tamperward 2.31.0 → 2.32.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 +214 -94
  2. package/package.json +4 -2
package/dist/cli/index.js CHANGED
@@ -12476,7 +12476,7 @@ function verdict(blocks2, kind, audit) {
12476
12476
  });
12477
12477
  }
12478
12478
  const reason = formatDenial(blocks2);
12479
- return { exitCode: 0, stdout: denyWire(reason, kind) };
12479
+ return { exitCode: 0, stdout: denyWire(reason, kind), findings: blocks2 };
12480
12480
  }
12481
12481
  function fileChanges(changes) {
12482
12482
  return changes.filter((c) => c.kind === "file");
@@ -19275,6 +19275,9 @@ function taskEnv(task) {
19275
19275
  ...task.model !== void 0 ? { TAMPERWARD_RESEARCH_MODEL: task.model } : {}
19276
19276
  };
19277
19277
  }
19278
+ function substitutePlaceholders(template, task) {
19279
+ return template.replace(PLACEHOLDER_PATTERN, (token) => PLACEHOLDERS[token](task));
19280
+ }
19278
19281
  function normalizeCommandArgv(argv, cwd = process.cwd()) {
19279
19282
  if (argv.length === 0) {
19280
19283
  throw new ResearchError('the command adapter needs an agent command after "--"');
@@ -19288,9 +19291,7 @@ function commandAdapter(argv, cwd = process.cwd()) {
19288
19291
  name: "command",
19289
19292
  layers: ["envelope"],
19290
19293
  launch(task) {
19291
- const substituted = anchored.map(
19292
- (arg) => Object.entries(PLACEHOLDERS).reduce((acc2, [token, value]) => acc2.split(token).join(value(task)), arg)
19293
- );
19294
+ const substituted = anchored.map((arg) => substitutePlaceholders(arg, task));
19294
19295
  return { argv: substituted, env: taskEnv(task) };
19295
19296
  }
19296
19297
  };
@@ -19319,7 +19320,7 @@ function resolveAdapter(name, argv, model) {
19319
19320
  if (name === "claude-code") return claudeCodeAdapter(model);
19320
19321
  throw new ResearchError(`unknown adapter "${name}" (built-in adapters: ${BUILTIN_ADAPTERS.join(", ")})`);
19321
19322
  }
19322
- var ADAPTER_LAYERS, RESEARCH_ARMS, BUILTIN_ADAPTERS, ResearchError, PLACEHOLDERS;
19323
+ var ADAPTER_LAYERS, RESEARCH_ARMS, BUILTIN_ADAPTERS, ResearchError, PLACEHOLDERS, PLACEHOLDER_PATTERN;
19323
19324
  var init_adapter = __esm({
19324
19325
  "src/research/adapter.ts"() {
19325
19326
  "use strict";
@@ -19337,6 +19338,10 @@ var init_adapter = __esm({
19337
19338
  "{arm}": (t) => t.arm,
19338
19339
  "{model}": (t) => t.model ?? ""
19339
19340
  };
19341
+ PLACEHOLDER_PATTERN = new RegExp(
19342
+ Object.keys(PLACEHOLDERS).map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
19343
+ "g"
19344
+ );
19340
19345
  }
19341
19346
  });
19342
19347
 
@@ -19710,8 +19715,10 @@ var init_record = __esm({
19710
19715
  });
19711
19716
 
19712
19717
  // src/research/run.ts
19718
+ import { randomUUID as randomUUID2 } from "node:crypto";
19713
19719
  import { execFileSync as execFileSync13 } from "node:child_process";
19714
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
19720
+ import { closeSync as closeSync3, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync9, openSync as openSync3, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync11, unlinkSync, writeFileSync as writeFileSync11, writeSync } from "node:fs";
19721
+ import { hostname } from "node:os";
19715
19722
  import { join as join25, resolve as resolve15 } from "node:path";
19716
19723
  function git6(args, cwd) {
19717
19724
  return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
@@ -19719,6 +19726,102 @@ function git6(args, cwd) {
19719
19726
  function pairRecordPath(ledger, task, pair) {
19720
19727
  return join25(ledger, "pairs", `${task}--${pair}.json`);
19721
19728
  }
19729
+ function researchLockPath(ledger) {
19730
+ return join25(ledger, "run.lock");
19731
+ }
19732
+ function readLockOwner(path) {
19733
+ try {
19734
+ const parsed = JSON.parse(readFileSync23(path, "utf8"));
19735
+ if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
19736
+ if (typeof parsed.host !== "string" || typeof parsed.started_at !== "string" || typeof parsed.token !== "string") return null;
19737
+ return { pid: parsed.pid, host: parsed.host, started_at: parsed.started_at, token: parsed.token };
19738
+ } catch {
19739
+ return null;
19740
+ }
19741
+ }
19742
+ function processAlive(pid) {
19743
+ try {
19744
+ process.kill(pid, 0);
19745
+ return true;
19746
+ } catch (e) {
19747
+ return e.code !== "ESRCH";
19748
+ }
19749
+ }
19750
+ function lockHolder(path) {
19751
+ const owner = readLockOwner(path);
19752
+ return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
19753
+ }
19754
+ function acquireResearchLock(ledger, breakStaleLock = false) {
19755
+ mkdirSync9(ledger, { recursive: true });
19756
+ const path = researchLockPath(ledger);
19757
+ if (breakStaleLock && existsSync18(path)) {
19758
+ const owner2 = readLockOwner(path);
19759
+ if (!owner2) {
19760
+ throw new ResearchError(`research output ${ledger} has an unreadable lock at ${path}; verify no run is active, then remove it deliberately`);
19761
+ }
19762
+ if (owner2.host !== hostname() || processAlive(owner2.pid)) {
19763
+ throw new ResearchError(
19764
+ `research output ${ledger} is still locked by ${lockHolder(path)}; refusing to break an active lock (a reused pid looks alive: if that owner is certainly gone, remove ${path} deliberately)`
19765
+ );
19766
+ }
19767
+ const claimed = `${path}.stale-${process.pid}-${randomUUID2()}`;
19768
+ let moved = true;
19769
+ try {
19770
+ renameSync4(path, claimed);
19771
+ } catch (e) {
19772
+ if (e.code !== "ENOENT") {
19773
+ throw new ResearchError(`research output ${ledger} stale lock recovery failed for ${path}`);
19774
+ }
19775
+ moved = false;
19776
+ }
19777
+ if (moved) {
19778
+ const inspected = readLockOwner(claimed);
19779
+ if (inspected && inspected.token === owner2.token) {
19780
+ unlinkSync(claimed);
19781
+ } else {
19782
+ try {
19783
+ linkSync(claimed, path);
19784
+ unlinkSync(claimed);
19785
+ } catch {
19786
+ }
19787
+ throw new ResearchError(`research output ${ledger}: the lock at ${path} changed while it was being broken; refusing (rerun --break-lock only once no run is active)`);
19788
+ }
19789
+ }
19790
+ }
19791
+ const owner = {
19792
+ pid: process.pid,
19793
+ host: hostname(),
19794
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
19795
+ token: randomUUID2()
19796
+ };
19797
+ const text = JSON.stringify(owner) + "\n";
19798
+ let fd = -1;
19799
+ try {
19800
+ fd = openSync3(path, "wx", 384);
19801
+ writeSync(fd, text);
19802
+ closeSync3(fd);
19803
+ fd = -1;
19804
+ } catch (e) {
19805
+ if (fd !== -1) closeSync3(fd);
19806
+ if (e.code === "EEXIST") {
19807
+ throw new ResearchError(
19808
+ `research output ${ledger} is already locked by ${lockHolder(path)}; refusing concurrent writers. Verify the owner is not running, then remove ${path} deliberately or rerun with --break-lock`
19809
+ );
19810
+ }
19811
+ throw new ResearchError(`research output ${ledger} cannot create lock ${path}`);
19812
+ }
19813
+ let released = false;
19814
+ return {
19815
+ release() {
19816
+ if (released) return;
19817
+ released = true;
19818
+ try {
19819
+ if (readFileSync23(path, "utf8") === text) unlinkSync(path);
19820
+ } catch {
19821
+ }
19822
+ }
19823
+ };
19824
+ }
19722
19825
  function resumableRecord(path, expected) {
19723
19826
  let raw;
19724
19827
  try {
@@ -20007,84 +20110,46 @@ function runResearch(opts) {
20007
20110
  return 2;
20008
20111
  }
20009
20112
  const ledger = resolve15(opts.out);
20010
- const pairs = opts.pairs ?? 1;
20011
- mkdirSync9(join25(ledger, "pairs"), { recursive: true });
20012
- for (const task of tasks) {
20013
- const existingRecords = /* @__PURE__ */ new Map();
20014
- let sourceBase = null;
20015
- try {
20016
- for (let pair = 1; pair <= pairs; pair++) {
20017
- const path = pairRecordPath(ledger, task.id, pair);
20018
- if (!existsSync18(path)) continue;
20019
- const existing = resumableRecord(path, {
20020
- task: task.id,
20021
- pair,
20022
- manifest_sha256: manifestSha,
20023
- adapter: { name: adapter.name, layers: adapter.layers },
20024
- model: opts.model ?? null,
20025
- tamperward_version: TW_VERSION,
20026
- agent_argv: agentArgvIdentity,
20027
- agent_budget: opts.agentBudget ?? null,
20028
- verify_command: task.verify.command,
20029
- source_base: sourceBase
20030
- });
20031
- sourceBase ??= existing.arms.ungated.base;
20032
- if (existing.arms.ungated.base !== sourceBase) {
20033
- throw new ResearchError(
20034
- `ledger record ${path} belongs to a different source commit (${existing.arms.ungated.base.slice(0, 12)}\u2026 != ${sourceBase.slice(0, 12)}\u2026); use a new --out, or remove it deliberately`
20035
- );
20036
- }
20037
- existingRecords.set(pair, existing);
20038
- }
20039
- } catch (e) {
20040
- if (e instanceof ResearchError) {
20041
- err2(`tamperward research: ${e.message}`);
20042
- return 2;
20043
- }
20044
- throw e;
20113
+ let lock;
20114
+ try {
20115
+ lock = acquireResearchLock(ledger, opts.breakLock === true);
20116
+ } catch (e) {
20117
+ if (e instanceof ResearchError) {
20118
+ err2(`tamperward research: ${e.message}`);
20119
+ return 2;
20045
20120
  }
20046
- for (let pair = 1; pair <= pairs; pair++) {
20047
- const path = pairRecordPath(ledger, task.id, pair);
20048
- const existing = existingRecords.get(pair);
20049
- if (existing) {
20050
- if (!opts.json) out2(`tamperward research \u2014 task ${task.id} pair ${pair}: already recorded (${path}); skipping`);
20051
- continue;
20052
- }
20053
- let record;
20121
+ throw e;
20122
+ }
20123
+ try {
20124
+ const pairs = opts.pairs ?? 1;
20125
+ mkdirSync9(join25(ledger, "pairs"), { recursive: true });
20126
+ for (const task of tasks) {
20127
+ const existingRecords = /* @__PURE__ */ new Map();
20128
+ let sourceBase = null;
20054
20129
  try {
20055
- const arms = {};
20056
- for (const arm of RESEARCH_ARMS) {
20057
- if (!opts.json) out2(`tamperward research \u2014 task ${task.id} pair ${pair}: ${arm} arm`);
20058
- arms[arm] = runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase ?? void 0);
20059
- if (arm === "ungated") {
20060
- const resolvedSource = arms[arm]?.base;
20061
- if (!resolvedSource) throw new ResearchError(`task "${task.id}" pair ${pair}: ungated arm produced no source base`);
20062
- if (sourceBase !== null && resolvedSource !== sourceBase) {
20063
- throw new ResearchError(
20064
- `task "${task.id}" pair ${pair}: source base moved (${resolvedSource.slice(0, 12)}\u2026 != ${sourceBase.slice(0, 12)}\u2026)`
20065
- );
20066
- }
20067
- sourceBase ??= resolvedSource;
20130
+ for (let pair = 1; pair <= pairs; pair++) {
20131
+ const path = pairRecordPath(ledger, task.id, pair);
20132
+ if (!existsSync18(path)) continue;
20133
+ const existing = resumableRecord(path, {
20134
+ task: task.id,
20135
+ pair,
20136
+ manifest_sha256: manifestSha,
20137
+ adapter: { name: adapter.name, layers: adapter.layers },
20138
+ model: opts.model ?? null,
20139
+ tamperward_version: TW_VERSION,
20140
+ agent_argv: agentArgvIdentity,
20141
+ agent_budget: opts.agentBudget ?? null,
20142
+ verify_command: task.verify.command,
20143
+ source_base: sourceBase
20144
+ });
20145
+ sourceBase ??= existing.arms.ungated.base;
20146
+ if (existing.arms.ungated.base !== sourceBase) {
20147
+ throw new ResearchError(
20148
+ `ledger record ${path} belongs to a different source commit (${existing.arms.ungated.base.slice(0, 12)}\u2026 != ${sourceBase.slice(0, 12)}\u2026); use a new --out, or remove it deliberately`
20149
+ );
20068
20150
  }
20151
+ existingRecords.set(pair, existing);
20069
20152
  }
20070
- const ungated = arms.ungated;
20071
- const gated = arms.gated;
20072
- if (!ungated || !gated) throw new ResearchError(`task "${task.id}" pair ${pair}: an arm produced no record`);
20073
- record = {
20074
- schema_version: MACHINE_SCHEMA_VERSION,
20075
- command: "research",
20076
- document: "pair",
20077
- task: task.id,
20078
- pair,
20079
- adapter: { name: adapter.name, layers: [...adapter.layers] },
20080
- model: opts.model ?? null,
20081
- tamperward_version: TW_VERSION,
20082
- agent_argv: [...agentArgvIdentity],
20083
- agent_budget: opts.agentBudget ?? null,
20084
- manifest_sha256: manifestSha,
20085
- verify_command: task.verify.command,
20086
- arms: { ungated, gated }
20087
- };
20088
20153
  } catch (e) {
20089
20154
  if (e instanceof ResearchError) {
20090
20155
  err2(`tamperward research: ${e.message}`);
@@ -20092,20 +20157,72 @@ function runResearch(opts) {
20092
20157
  }
20093
20158
  throw e;
20094
20159
  }
20095
- const text = JSON.stringify(record);
20096
- writeRecordAtomically(path, text + "\n");
20097
- if (opts.json) {
20098
- out2(text);
20099
- } else {
20100
- const g = record.arms.gated;
20101
- const u = record.arms.ungated;
20102
- out2(
20103
- `tamperward research \u2014 task ${task.id} pair ${pair}: ungated ${u.outcome.verify_verdict} (masked=${u.outcome.masked_failure}, surviving=${u.outcome.surviving_protected_mutations}); gated ${g.outcome.verify_verdict} (masked=${g.outcome.masked_failure}, surviving=${g.outcome.surviving_protected_mutations}), tamperward ${g.treatment?.verdict ?? "n/a"} \u2192 ${g.treatment?.disposition ?? "n/a"}` + (u.measured && g.measured ? "" : `; UNMEASURABLE (${[u.unmeasurable, g.unmeasurable].filter(Boolean).join(" / ")})`) + `; recorded ${path}`
20104
- );
20160
+ for (let pair = 1; pair <= pairs; pair++) {
20161
+ const path = pairRecordPath(ledger, task.id, pair);
20162
+ const existing = existingRecords.get(pair);
20163
+ if (existing) {
20164
+ if (!opts.json) out2(`tamperward research \u2014 task ${task.id} pair ${pair}: already recorded (${path}); skipping`);
20165
+ continue;
20166
+ }
20167
+ let record;
20168
+ try {
20169
+ const arms = {};
20170
+ for (const arm of RESEARCH_ARMS) {
20171
+ if (!opts.json) out2(`tamperward research \u2014 task ${task.id} pair ${pair}: ${arm} arm`);
20172
+ arms[arm] = runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase ?? void 0);
20173
+ if (arm === "ungated") {
20174
+ const resolvedSource = arms[arm]?.base;
20175
+ if (!resolvedSource) throw new ResearchError(`task "${task.id}" pair ${pair}: ungated arm produced no source base`);
20176
+ if (sourceBase !== null && resolvedSource !== sourceBase) {
20177
+ throw new ResearchError(
20178
+ `task "${task.id}" pair ${pair}: source base moved (${resolvedSource.slice(0, 12)}\u2026 != ${sourceBase.slice(0, 12)}\u2026)`
20179
+ );
20180
+ }
20181
+ sourceBase ??= resolvedSource;
20182
+ }
20183
+ }
20184
+ const ungated = arms.ungated;
20185
+ const gated = arms.gated;
20186
+ if (!ungated || !gated) throw new ResearchError(`task "${task.id}" pair ${pair}: an arm produced no record`);
20187
+ record = {
20188
+ schema_version: MACHINE_SCHEMA_VERSION,
20189
+ command: "research",
20190
+ document: "pair",
20191
+ task: task.id,
20192
+ pair,
20193
+ adapter: { name: adapter.name, layers: [...adapter.layers] },
20194
+ model: opts.model ?? null,
20195
+ tamperward_version: TW_VERSION,
20196
+ agent_argv: [...agentArgvIdentity],
20197
+ agent_budget: opts.agentBudget ?? null,
20198
+ manifest_sha256: manifestSha,
20199
+ verify_command: task.verify.command,
20200
+ arms: { ungated, gated }
20201
+ };
20202
+ } catch (e) {
20203
+ if (e instanceof ResearchError) {
20204
+ err2(`tamperward research: ${e.message}`);
20205
+ return 2;
20206
+ }
20207
+ throw e;
20208
+ }
20209
+ const text = JSON.stringify(record);
20210
+ writeRecordAtomically(path, text + "\n");
20211
+ if (opts.json) {
20212
+ out2(text);
20213
+ } else {
20214
+ const g = record.arms.gated;
20215
+ const u = record.arms.ungated;
20216
+ out2(
20217
+ `tamperward research \u2014 task ${task.id} pair ${pair}: ungated ${u.outcome.verify_verdict} (masked=${u.outcome.masked_failure}, surviving=${u.outcome.surviving_protected_mutations}); gated ${g.outcome.verify_verdict} (masked=${g.outcome.masked_failure}, surviving=${g.outcome.surviving_protected_mutations}), tamperward ${g.treatment?.verdict ?? "n/a"} \u2192 ${g.treatment?.disposition ?? "n/a"}` + (u.measured && g.measured ? "" : `; UNMEASURABLE (${[u.unmeasurable, g.unmeasurable].filter(Boolean).join(" / ")})`) + `; recorded ${path}`
20218
+ );
20219
+ }
20105
20220
  }
20106
20221
  }
20222
+ return 0;
20223
+ } finally {
20224
+ lock.release();
20107
20225
  }
20108
- return 0;
20109
20226
  }
20110
20227
  var err2, out2;
20111
20228
  var init_run2 = __esm({
@@ -20291,6 +20408,7 @@ function parseResearchRun(args) {
20291
20408
  else if (a === "--pairs") o.pairs = Number(args[++i]);
20292
20409
  else if (a === "--model") o.model = args[++i];
20293
20410
  else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
20411
+ else if (a === "--break-lock") o.breakLock = true;
20294
20412
  else if (a === "--json") o.json = true;
20295
20413
  }
20296
20414
  return o;
@@ -20603,7 +20721,7 @@ function validateCliArgs(cmd, args) {
20603
20721
  const delimiter3 = rest.indexOf("--");
20604
20722
  const prefix = delimiter3 < 0 ? rest : rest.slice(0, delimiter3);
20605
20723
  const parsed = validateFlatArgs(prefix, {
20606
- flags: ["--json"],
20724
+ flags: ["--break-lock", "--json"],
20607
20725
  values: {
20608
20726
  "--manifest": "string",
20609
20727
  "--out": "string",
@@ -20702,9 +20820,11 @@ Formats:
20702
20820
  tamperward research run --manifest F bring-your-own-model evaluation: for every
20703
20821
  --out D --adapter A [--pairs N] task in the manifest, pin one source commit,
20704
20822
  [--model M] [--agent-budget S] clone fresh state per arm, run the agent
20705
- [--json] [-- <agent cmd...>] ungated and under the run envelope, then
20706
- observe both with verify + check. Records
20823
+ [--break-lock] [--json] ungated and under the run envelope, then
20824
+ [-- <agent cmd...>] observe both with verify + check. Records
20707
20825
  are resumable by full experiment identity.
20826
+ --break-lock recovers a verified stale
20827
+ output lock (see the research guide).
20708
20828
  tamperward research summarize --ledger D aggregate measured pairs into model behaviour,
20709
20829
  independent outcome, TamperWard hits/misses
20710
20830
  and paired counts \u2014 no composite score
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.31.0",
3
+ "version": "2.32.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",
@@ -48,7 +48,9 @@
48
48
  "test:watch": "vitest",
49
49
  "test:perf-smoke": "vitest run --config vitest.perf-smoke.config.ts --no-file-parallelism",
50
50
  "check": "node dist/cli/index.js check",
51
- "probe:codex-runtime": "node harness/adapters/codex-probe.mjs"
51
+ "probe:codex-runtime": "node harness/adapters/codex-probe.mjs",
52
+ "probe:copilot-runtime": "node harness/adapters/copilot-probe.mjs",
53
+ "spike:copilot-sdk": "node harness/adapters/copilot-sdk-spike.mjs"
52
54
  },
53
55
  "dependencies": {
54
56
  "picomatch": "^4.0.2",