tamperward 2.31.1 → 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.
- package/dist/cli/index.js +204 -89
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -19715,8 +19715,10 @@ var init_record = __esm({
|
|
|
19715
19715
|
});
|
|
19716
19716
|
|
|
19717
19717
|
// src/research/run.ts
|
|
19718
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
19718
19719
|
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
19719
|
-
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";
|
|
19720
19722
|
import { join as join25, resolve as resolve15 } from "node:path";
|
|
19721
19723
|
function git6(args, cwd) {
|
|
19722
19724
|
return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -19724,6 +19726,102 @@ function git6(args, cwd) {
|
|
|
19724
19726
|
function pairRecordPath(ledger, task, pair) {
|
|
19725
19727
|
return join25(ledger, "pairs", `${task}--${pair}.json`);
|
|
19726
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
|
+
}
|
|
19727
19825
|
function resumableRecord(path, expected) {
|
|
19728
19826
|
let raw;
|
|
19729
19827
|
try {
|
|
@@ -20012,84 +20110,46 @@ function runResearch(opts) {
|
|
|
20012
20110
|
return 2;
|
|
20013
20111
|
}
|
|
20014
20112
|
const ledger = resolve15(opts.out);
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
const path = pairRecordPath(ledger, task.id, pair);
|
|
20023
|
-
if (!existsSync18(path)) continue;
|
|
20024
|
-
const existing = resumableRecord(path, {
|
|
20025
|
-
task: task.id,
|
|
20026
|
-
pair,
|
|
20027
|
-
manifest_sha256: manifestSha,
|
|
20028
|
-
adapter: { name: adapter.name, layers: adapter.layers },
|
|
20029
|
-
model: opts.model ?? null,
|
|
20030
|
-
tamperward_version: TW_VERSION,
|
|
20031
|
-
agent_argv: agentArgvIdentity,
|
|
20032
|
-
agent_budget: opts.agentBudget ?? null,
|
|
20033
|
-
verify_command: task.verify.command,
|
|
20034
|
-
source_base: sourceBase
|
|
20035
|
-
});
|
|
20036
|
-
sourceBase ??= existing.arms.ungated.base;
|
|
20037
|
-
if (existing.arms.ungated.base !== sourceBase) {
|
|
20038
|
-
throw new ResearchError(
|
|
20039
|
-
`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`
|
|
20040
|
-
);
|
|
20041
|
-
}
|
|
20042
|
-
existingRecords.set(pair, existing);
|
|
20043
|
-
}
|
|
20044
|
-
} catch (e) {
|
|
20045
|
-
if (e instanceof ResearchError) {
|
|
20046
|
-
err2(`tamperward research: ${e.message}`);
|
|
20047
|
-
return 2;
|
|
20048
|
-
}
|
|
20049
|
-
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;
|
|
20050
20120
|
}
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
let
|
|
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;
|
|
20059
20129
|
try {
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
if (!
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
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
|
+
);
|
|
20073
20150
|
}
|
|
20151
|
+
existingRecords.set(pair, existing);
|
|
20074
20152
|
}
|
|
20075
|
-
const ungated = arms.ungated;
|
|
20076
|
-
const gated = arms.gated;
|
|
20077
|
-
if (!ungated || !gated) throw new ResearchError(`task "${task.id}" pair ${pair}: an arm produced no record`);
|
|
20078
|
-
record = {
|
|
20079
|
-
schema_version: MACHINE_SCHEMA_VERSION,
|
|
20080
|
-
command: "research",
|
|
20081
|
-
document: "pair",
|
|
20082
|
-
task: task.id,
|
|
20083
|
-
pair,
|
|
20084
|
-
adapter: { name: adapter.name, layers: [...adapter.layers] },
|
|
20085
|
-
model: opts.model ?? null,
|
|
20086
|
-
tamperward_version: TW_VERSION,
|
|
20087
|
-
agent_argv: [...agentArgvIdentity],
|
|
20088
|
-
agent_budget: opts.agentBudget ?? null,
|
|
20089
|
-
manifest_sha256: manifestSha,
|
|
20090
|
-
verify_command: task.verify.command,
|
|
20091
|
-
arms: { ungated, gated }
|
|
20092
|
-
};
|
|
20093
20153
|
} catch (e) {
|
|
20094
20154
|
if (e instanceof ResearchError) {
|
|
20095
20155
|
err2(`tamperward research: ${e.message}`);
|
|
@@ -20097,20 +20157,72 @@ function runResearch(opts) {
|
|
|
20097
20157
|
}
|
|
20098
20158
|
throw e;
|
|
20099
20159
|
}
|
|
20100
|
-
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
|
|
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
|
+
}
|
|
20110
20220
|
}
|
|
20111
20221
|
}
|
|
20222
|
+
return 0;
|
|
20223
|
+
} finally {
|
|
20224
|
+
lock.release();
|
|
20112
20225
|
}
|
|
20113
|
-
return 0;
|
|
20114
20226
|
}
|
|
20115
20227
|
var err2, out2;
|
|
20116
20228
|
var init_run2 = __esm({
|
|
@@ -20296,6 +20408,7 @@ function parseResearchRun(args) {
|
|
|
20296
20408
|
else if (a === "--pairs") o.pairs = Number(args[++i]);
|
|
20297
20409
|
else if (a === "--model") o.model = args[++i];
|
|
20298
20410
|
else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
|
|
20411
|
+
else if (a === "--break-lock") o.breakLock = true;
|
|
20299
20412
|
else if (a === "--json") o.json = true;
|
|
20300
20413
|
}
|
|
20301
20414
|
return o;
|
|
@@ -20608,7 +20721,7 @@ function validateCliArgs(cmd, args) {
|
|
|
20608
20721
|
const delimiter3 = rest.indexOf("--");
|
|
20609
20722
|
const prefix = delimiter3 < 0 ? rest : rest.slice(0, delimiter3);
|
|
20610
20723
|
const parsed = validateFlatArgs(prefix, {
|
|
20611
|
-
flags: ["--json"],
|
|
20724
|
+
flags: ["--break-lock", "--json"],
|
|
20612
20725
|
values: {
|
|
20613
20726
|
"--manifest": "string",
|
|
20614
20727
|
"--out": "string",
|
|
@@ -20707,9 +20820,11 @@ Formats:
|
|
|
20707
20820
|
tamperward research run --manifest F bring-your-own-model evaluation: for every
|
|
20708
20821
|
--out D --adapter A [--pairs N] task in the manifest, pin one source commit,
|
|
20709
20822
|
[--model M] [--agent-budget S] clone fresh state per arm, run the agent
|
|
20710
|
-
[--
|
|
20711
|
-
|
|
20823
|
+
[--break-lock] [--json] ungated and under the run envelope, then
|
|
20824
|
+
[-- <agent cmd...>] observe both with verify + check. Records
|
|
20712
20825
|
are resumable by full experiment identity.
|
|
20826
|
+
--break-lock recovers a verified stale
|
|
20827
|
+
output lock (see the research guide).
|
|
20713
20828
|
tamperward research summarize --ledger D aggregate measured pairs into model behaviour,
|
|
20714
20829
|
independent outcome, TamperWard hits/misses
|
|
20715
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.
|
|
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",
|