tamperward 2.10.5 → 2.10.6

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 +122 -12
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -8149,9 +8149,9 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
8149
8149
  // src/cli/verify.ts
8150
8150
  import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
8151
8151
  import { createHash as createHash8 } from "node:crypto";
8152
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, rmSync as rmSync3, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
8152
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, realpathSync as realpathSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
8153
8153
  import { tmpdir as tmpdir2 } from "node:os";
8154
- import { dirname as dirname5, join as join14, resolve as resolve4 } from "node:path";
8154
+ import { dirname as dirname5, isAbsolute as isAbsolute9, join as join14, relative as relative4, resolve as resolve4, sep } from "node:path";
8155
8155
 
8156
8156
  // src/fingerprint.ts
8157
8157
  import { execFileSync as execFileSync7 } from "node:child_process";
@@ -8308,29 +8308,131 @@ function baseIsAncestorOfHead(base, cwd) {
8308
8308
  const r = spawnSync("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
8309
8309
  return r.status === 0;
8310
8310
  }
8311
+ function inside(root, path) {
8312
+ const rel = relative4(resolve4(root), resolve4(path));
8313
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute9(rel);
8314
+ }
8315
+ function linkParts(path) {
8316
+ const parts = sep === "\\" ? path.split(/[\\/]+/) : path.split("/");
8317
+ return parts.filter((part) => part !== "" && part !== ".");
8318
+ }
8319
+ function linkEscape(label, target) {
8320
+ return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
8321
+ }
8322
+ function driveRelativeLinkTarget(target) {
8323
+ return sep === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
8324
+ }
8325
+ function rootedLinkTarget(target) {
8326
+ return isAbsolute9(target) || driveRelativeLinkTarget(target);
8327
+ }
8328
+ function safeSymlink(target, out2, root, label) {
8329
+ if (rootedLinkTarget(target) || !inside(root, resolve4(dirname5(out2), target))) {
8330
+ throw linkEscape(label, target);
8331
+ }
8332
+ rmSync3(out2, { force: true });
8333
+ symlinkSync(target, out2);
8334
+ }
8335
+ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
8336
+ let domain = "tree";
8337
+ let stack = linkParts(relative4(root, dirname5(out2)));
8338
+ const pending = linkParts(target);
8339
+ const seen = /* @__PURE__ */ new Set();
8340
+ let hops = 0;
8341
+ while (pending.length) {
8342
+ const part = pending.shift();
8343
+ if (part === "..") {
8344
+ if (!stack.length) throw linkEscape(label, target);
8345
+ stack.pop();
8346
+ continue;
8347
+ }
8348
+ stack.push(part);
8349
+ if (domain === "tree" && dependencyRoot && stack.length === 1 && stack[0] === "node_modules") {
8350
+ domain = "deps";
8351
+ stack = [];
8352
+ continue;
8353
+ }
8354
+ const base = domain === "tree" ? root : dependencyRoot;
8355
+ const current = join14(base, ...stack);
8356
+ let st;
8357
+ try {
8358
+ st = lstatSync3(current);
8359
+ } catch {
8360
+ continue;
8361
+ }
8362
+ if (!st.isSymbolicLink()) continue;
8363
+ if (++hops > 128) {
8364
+ throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8365
+ }
8366
+ const key2 = `${domain}:${current}:${pending.join(sep)}`;
8367
+ if (seen.has(key2)) {
8368
+ throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
8369
+ }
8370
+ seen.add(key2);
8371
+ const next = readlinkSync3(current);
8372
+ stack.pop();
8373
+ if (driveRelativeLinkTarget(next)) {
8374
+ throw linkEscape(label, target);
8375
+ }
8376
+ if (isAbsolute9(next)) {
8377
+ if (domain === "tree" || !dependencyRoot || !inside(dependencyRoot, next)) {
8378
+ throw linkEscape(label, target);
8379
+ }
8380
+ stack = [];
8381
+ pending.unshift(...linkParts(relative4(dependencyRoot, resolve4(next))));
8382
+ } else {
8383
+ pending.unshift(...linkParts(next));
8384
+ }
8385
+ }
8386
+ }
8387
+ function rejectLinkedParent(cwd, rel) {
8388
+ const parts = rel.split("/").slice(0, -1);
8389
+ let current = cwd;
8390
+ for (const part of parts) {
8391
+ current = join14(current, part);
8392
+ let st;
8393
+ try {
8394
+ st = lstatSync3(current);
8395
+ } catch {
8396
+ continue;
8397
+ }
8398
+ if (st.isSymbolicLink()) {
8399
+ throw new Error(`${rel} has a symlinked parent directory (${relative4(cwd, current)})`);
8400
+ }
8401
+ }
8402
+ }
8311
8403
  function materialize(cwd, dest) {
8312
8404
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
8405
+ const links = [];
8313
8406
  for (const rel of listed) {
8314
8407
  const src = join14(cwd, rel);
8408
+ rejectLinkedParent(cwd, rel);
8315
8409
  let st;
8316
8410
  try {
8317
- st = statSync5(src);
8411
+ st = lstatSync3(src);
8318
8412
  } catch {
8319
8413
  continue;
8320
8414
  }
8321
- if (!st.isFile()) continue;
8322
8415
  const out2 = join14(dest, rel);
8323
8416
  mkdirSync6(dirname5(out2), { recursive: true });
8324
8417
  dropSymlink(out2);
8325
- if (lstatSync3(src).isSymbolicLink()) {
8326
- writeFileSync6(out2, readFileSync14(src));
8327
- } else {
8418
+ if (st.isSymbolicLink()) {
8419
+ const target = readlinkSync3(src);
8420
+ safeSymlink(target, out2, dest, rel);
8421
+ links.push({ rel, out: out2, target });
8422
+ } else if (st.isFile()) {
8328
8423
  cpSync(src, out2, { dereference: true });
8424
+ chmodSync2(out2, st.mode);
8425
+ } else {
8426
+ throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
8329
8427
  }
8330
- chmodSync2(out2, st.mode);
8331
8428
  }
8332
8429
  const nm = resolve4(cwd, "node_modules");
8333
- if (existsSync10(nm)) symlinkSync(nm, join14(dest, "node_modules"), "dir");
8430
+ let dependencyRoot = null;
8431
+ if (existsSync10(nm)) dependencyRoot = realpathSync3(nm);
8432
+ for (const link of links) {
8433
+ validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
8434
+ }
8435
+ if (dependencyRoot) symlinkSync(nm, join14(dest, "node_modules"), "dir");
8334
8436
  }
8335
8437
  function dropSymlink(p) {
8336
8438
  try {
@@ -8382,6 +8484,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8382
8484
  const verifierGlobs = policy.verify?.inputs ?? [];
8383
8485
  const isOverlay = (p) => OVERLAY_CLASSES.some((c) => isProtected(p, policy, c)) || matchesAny(p, VERIFICATION_SURFACE) || verifierOwned.has(p) || verifierGlobs.length > 0 && matchesAny(p, verifierGlobs);
8384
8486
  const restored = [];
8487
+ const restoredLinks = [];
8385
8488
  const baseProtected = /* @__PURE__ */ new Set();
8386
8489
  for (const e of entries) {
8387
8490
  if (!isOverlay(e.path)) continue;
@@ -8394,7 +8497,9 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8394
8497
  mkdirSync6(dirname5(out2), { recursive: true });
8395
8498
  rmSync3(out2, { force: true });
8396
8499
  if (e.mode === "120000") {
8397
- symlinkSync(content.toString("utf8"), out2);
8500
+ const target = content.toString("utf8");
8501
+ safeSymlink(target, out2, dest, `the base's ${e.path}`);
8502
+ restoredLinks.push({ path: e.path, out: out2, target });
8398
8503
  } else {
8399
8504
  writeFileSync6(out2, content);
8400
8505
  chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
@@ -8408,6 +8513,11 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
8408
8513
  rmSync3(join14(dest, rel), { force: true });
8409
8514
  removed++;
8410
8515
  }
8516
+ const nm = resolve4(cwd, "node_modules");
8517
+ const dependencyRoot = existsSync10(nm) ? realpathSync3(nm) : null;
8518
+ for (const link of restoredLinks) {
8519
+ validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, `the base's ${link.path}`);
8520
+ }
8411
8521
  return { restored, removed };
8412
8522
  }
8413
8523
  var SUPERVISOR = `
@@ -8666,7 +8776,7 @@ function parseVerify(args) {
8666
8776
 
8667
8777
  // src/cli/run.ts
8668
8778
  import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
8669
- import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync3 } from "node:fs";
8779
+ import { readdirSync as readdirSync4, readFileSync as readFileSync15, readlinkSync as readlinkSync4, realpathSync as realpathSync4 } from "node:fs";
8670
8780
  import { resolve as resolve5 } from "node:path";
8671
8781
  var out = (s) => process.stdout.write(s + "\n");
8672
8782
  var err = (s) => process.stderr.write(s + "\n");
@@ -8684,7 +8794,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
8684
8794
  const out2 = [];
8685
8795
  let real;
8686
8796
  try {
8687
- real = realpathSync3(cwd);
8797
+ real = realpathSync4(cwd);
8688
8798
  } catch {
8689
8799
  return out2;
8690
8800
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.10.5",
3
+ "version": "2.10.6",
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",