tamperward 2.31.1 → 2.33.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/README.md +4 -2
- package/dist/cli/index.js +283 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -239,11 +239,13 @@ node harness/perf/bench.mjs # performance budgets (docs/PERF.md)
|
|
|
239
239
|
```
|
|
240
240
|
|
|
241
241
|
Tamperward's own CI runs the engine it ships over every pull request — `check --diff`
|
|
242
|
-
over the PR range, cleared only by an out-of-band label
|
|
242
|
+
over the PR range, cleared only by an out-of-band exact-head label (generate one with
|
|
243
|
+
`tamperward signoff-label`) — and has, on more than one
|
|
243
244
|
occasion, blocked its own author's commits. Branch, then open a PR; `main` is
|
|
244
245
|
protected and CI must be green. Changing a protected asset will block your own PR
|
|
245
246
|
(working as intended); a reviewed, legitimate change is cleared by a maintainer
|
|
246
|
-
applying a `
|
|
247
|
+
applying a compact `tw1:<digest>` label, never by weakening the policy. Legacy full-SHA
|
|
248
|
+
tokens remain accepted by the parser for CI systems whose label transport can hold them.
|
|
247
249
|
|
|
248
250
|
The public surface is the CLI and its exit codes, the hook wire format, the
|
|
249
251
|
`.tamperward.yml` schema, and the versioned machine-output schemas under `schemas/` —
|
package/dist/cli/index.js
CHANGED
|
@@ -8932,7 +8932,7 @@ They WILL FAIL in v10.0.0
|
|
|
8932
8932
|
makeFinding(RULE9, policy, {
|
|
8933
8933
|
file: c.path,
|
|
8934
8934
|
message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
|
|
8935
|
-
evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward
|
|
8935
|
+
evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or generate the compact CI label with \`tamperward signoff-label --rule hook-tampering --file ${c.path} --head <full-sha>\`, or make the gate live in it`,
|
|
8936
8936
|
remediation: "Run `tamperward check --staged` in the new hook, in a position where its failure fails the hook, or have a human sign off on a hook that does not."
|
|
8937
8937
|
})
|
|
8938
8938
|
);
|
|
@@ -8963,7 +8963,7 @@ They WILL FAIL in v10.0.0
|
|
|
8963
8963
|
makeFinding(RULE9, policy, {
|
|
8964
8964
|
file: c.path,
|
|
8965
8965
|
message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
|
|
8966
|
-
evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward
|
|
8966
|
+
evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or generate the compact CI label with \`tamperward signoff-label --rule hook-tampering --file ${c.path} --head <full-sha>\`, or restore it`,
|
|
8967
8967
|
remediation: "Restore the script, or have a human sign off. A line-by-line reading of a shell script cannot tell an honest restructuring from a neutered gate, so the gate script is held byte-for-byte: only raising its pin passes without a sign-off."
|
|
8968
8968
|
})
|
|
8969
8969
|
);
|
|
@@ -10869,6 +10869,12 @@ var init_policy_load = __esm({
|
|
|
10869
10869
|
import { createHash as createHash3 } from "node:crypto";
|
|
10870
10870
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
|
|
10871
10871
|
import { dirname as dirname2, join as join9 } from "node:path";
|
|
10872
|
+
function compactOobToken(want, head) {
|
|
10873
|
+
const normalizedHead = head.trim().toLowerCase();
|
|
10874
|
+
if (!want || !FULL_OBJECT_ID.test(normalizedHead)) return null;
|
|
10875
|
+
const digest = createHash3("sha256").update(`tamperward:oob:v1\0${want}\0${normalizedHead}`).digest("base64url");
|
|
10876
|
+
return `${COMPACT_OOB_PREFIX}${digest}`;
|
|
10877
|
+
}
|
|
10872
10878
|
function ledgerEntryFrom(value) {
|
|
10873
10879
|
if (!isRecord(value)) return null;
|
|
10874
10880
|
const { rule, file, fingerprint: fingerprint2, reason, recordedAt, expiresAt } = value;
|
|
@@ -10924,6 +10930,11 @@ function oobToken(want, oob, head) {
|
|
|
10924
10930
|
for (const raw of oob) {
|
|
10925
10931
|
const t = raw.trim();
|
|
10926
10932
|
if (!t) continue;
|
|
10933
|
+
if (t.startsWith(COMPACT_OOB_PREFIX)) {
|
|
10934
|
+
const expected = head === void 0 ? null : compactOobToken(want, head);
|
|
10935
|
+
if (expected !== null && t === expected) return t;
|
|
10936
|
+
continue;
|
|
10937
|
+
}
|
|
10927
10938
|
const at = t.lastIndexOf("@");
|
|
10928
10939
|
if (at === -1) {
|
|
10929
10940
|
if (!head && t === want) return t;
|
|
@@ -10956,13 +10967,15 @@ function oobHeadFromEnv(env = process.env) {
|
|
|
10956
10967
|
function oobFromEnv(env = process.env) {
|
|
10957
10968
|
return (env.TAMPERWARD_OOB_SIGNOFF ?? "").split(",").map((s) => s.trim().replace(/^tamperward:allow:/, "")).filter(Boolean);
|
|
10958
10969
|
}
|
|
10959
|
-
var DEFAULT_TTL_MS, fingerprintOf;
|
|
10970
|
+
var DEFAULT_TTL_MS, COMPACT_OOB_PREFIX, FULL_OBJECT_ID, fingerprintOf;
|
|
10960
10971
|
var init_signoff = __esm({
|
|
10961
10972
|
"src/signoff.ts"() {
|
|
10962
10973
|
"use strict";
|
|
10963
10974
|
init_policy_load();
|
|
10964
10975
|
init_narrow();
|
|
10965
10976
|
DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
10977
|
+
COMPACT_OOB_PREFIX = "tw1:";
|
|
10978
|
+
FULL_OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
10966
10979
|
fingerprintOf = (f) => fingerprint(f.rule, f.file, f.evidence);
|
|
10967
10980
|
}
|
|
10968
10981
|
});
|
|
@@ -14206,9 +14219,10 @@ version: 1
|
|
|
14206
14219
|
|
|
14207
14220
|
# The CI authority for main: the same engine as the agent hook and pre-commit, run over
|
|
14208
14221
|
# the PR's commit range. A block fails the check and clears ONLY via the out-of-band
|
|
14209
|
-
# label \`
|
|
14210
|
-
# never a file the PR itself can commit.
|
|
14211
|
-
# \`tamperward:allow
|
|
14222
|
+
# compact label \`tw1:<digest>\` (print one with \`tamperward signoff-label\`) applied by
|
|
14223
|
+
# someone with triage access or higher \u2014 never a file the PR itself can commit. Legacy
|
|
14224
|
+
# \`tamperward:allow:<rule>@<head-sha>\` labels remain accepted. The verify step reads
|
|
14225
|
+
# the same labels: a token for \`verify\` accepts a masked failure a reviewer has judged.
|
|
14212
14226
|
#
|
|
14213
14227
|
# labeled/unlabeled re-run the gate because the sign-off is read from the EVENT payload:
|
|
14214
14228
|
# a label applied after a failure could otherwise never take effect, and REVOKING a
|
|
@@ -14252,14 +14266,15 @@ jobs:
|
|
|
14252
14266
|
env:
|
|
14253
14267
|
LABELS: \${{ toJSON(github.event.pull_request.labels.*.name) }}
|
|
14254
14268
|
run: |
|
|
14255
|
-
RULES="$(printf '%s' "$LABELS" | jq -r '.[] |
|
|
14269
|
+
RULES="$(printf '%s' "$LABELS" | jq -r '.[] | if startswith("tamperward:allow:") then sub("^tamperward:allow:"; "") elif startswith("tw1:") then . else empty end' | paste -sd, -)"
|
|
14256
14270
|
echo "rules=$RULES" >> "$GITHUB_OUTPUT"
|
|
14257
14271
|
- name: Tamperward gate (diff-time)
|
|
14258
14272
|
env:
|
|
14259
14273
|
TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
|
|
14260
14274
|
# Binds each approval to the commit it was granted for: labels persist
|
|
14261
14275
|
# across pushes, so an unbound one would clear every later finding on
|
|
14262
|
-
# the same PR.
|
|
14276
|
+
# the same PR. Compact labels hash the exact rule/file/head tuple; legacy
|
|
14277
|
+
# labels must read tamperward:allow:<rule>@<head-sha>.
|
|
14263
14278
|
TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
|
|
14264
14279
|
run: tamperward check --diff "\${{ github.event.pull_request.base.sha }}...\${{ github.event.pull_request.head.sha }}"
|
|
14265
14280
|
# Diff-time detection is spelling-dependent by nature; pristine
|
|
@@ -14272,8 +14287,9 @@ jobs:
|
|
|
14272
14287
|
# without one this step fails closed (exit 2) rather than passing quietly.
|
|
14273
14288
|
- name: Tamperward verify (pristine re-execution)
|
|
14274
14289
|
env:
|
|
14275
|
-
# The same channel as the gate. \`
|
|
14276
|
-
# accepts a MASKED_FAILURE \u2014
|
|
14290
|
+
# The same channel as the gate. A compact token generated for \`verify\`
|
|
14291
|
+
# (or legacy \`tamperward:allow:verify@<head-sha>\`) accepts a MASKED_FAILURE \u2014
|
|
14292
|
+
# a reviewer has read the intentional test
|
|
14277
14293
|
# change and agrees the original suite no longer applies. It clears
|
|
14278
14294
|
# nothing else: a red visible suite, or a run that could not verify,
|
|
14279
14295
|
# stays red whatever the labels say.
|
|
@@ -16398,10 +16414,12 @@ function runVerify(opts) {
|
|
|
16398
16414
|
renderStageDiagnostics(out3, "pristine", pristine);
|
|
16399
16415
|
}
|
|
16400
16416
|
}
|
|
16401
|
-
if (signedOff)
|
|
16417
|
+
if (signedOff) {
|
|
16418
|
+
const approval = signedOff.startsWith("tw1:") ? signedOff : `tamperward:allow:${signedOff}`;
|
|
16402
16419
|
out3(
|
|
16403
|
-
`masked failure cleared by out-of-band approval (
|
|
16420
|
+
`masked failure cleared by out-of-band approval (${approval}): a reviewer accepted that the original suite no longer applies to this change. Exit 0.`
|
|
16404
16421
|
);
|
|
16422
|
+
}
|
|
16405
16423
|
if (removedAdded > 0)
|
|
16406
16424
|
out3(
|
|
16407
16425
|
`(${removedAdded} protected file(s) added since ${base.slice(0, 10)} were removed from the pristine run: the pristine tree carries exactly the base's protected surface.)`
|
|
@@ -19715,8 +19733,10 @@ var init_record = __esm({
|
|
|
19715
19733
|
});
|
|
19716
19734
|
|
|
19717
19735
|
// src/research/run.ts
|
|
19736
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
19718
19737
|
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";
|
|
19738
|
+
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";
|
|
19739
|
+
import { hostname } from "node:os";
|
|
19720
19740
|
import { join as join25, resolve as resolve15 } from "node:path";
|
|
19721
19741
|
function git6(args, cwd) {
|
|
19722
19742
|
return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -19724,6 +19744,102 @@ function git6(args, cwd) {
|
|
|
19724
19744
|
function pairRecordPath(ledger, task, pair) {
|
|
19725
19745
|
return join25(ledger, "pairs", `${task}--${pair}.json`);
|
|
19726
19746
|
}
|
|
19747
|
+
function researchLockPath(ledger) {
|
|
19748
|
+
return join25(ledger, "run.lock");
|
|
19749
|
+
}
|
|
19750
|
+
function readLockOwner(path) {
|
|
19751
|
+
try {
|
|
19752
|
+
const parsed = JSON.parse(readFileSync23(path, "utf8"));
|
|
19753
|
+
if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
19754
|
+
if (typeof parsed.host !== "string" || typeof parsed.started_at !== "string" || typeof parsed.token !== "string") return null;
|
|
19755
|
+
return { pid: parsed.pid, host: parsed.host, started_at: parsed.started_at, token: parsed.token };
|
|
19756
|
+
} catch {
|
|
19757
|
+
return null;
|
|
19758
|
+
}
|
|
19759
|
+
}
|
|
19760
|
+
function processAlive(pid) {
|
|
19761
|
+
try {
|
|
19762
|
+
process.kill(pid, 0);
|
|
19763
|
+
return true;
|
|
19764
|
+
} catch (e) {
|
|
19765
|
+
return e.code !== "ESRCH";
|
|
19766
|
+
}
|
|
19767
|
+
}
|
|
19768
|
+
function lockHolder(path) {
|
|
19769
|
+
const owner = readLockOwner(path);
|
|
19770
|
+
return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
|
|
19771
|
+
}
|
|
19772
|
+
function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
19773
|
+
mkdirSync9(ledger, { recursive: true });
|
|
19774
|
+
const path = researchLockPath(ledger);
|
|
19775
|
+
if (breakStaleLock && existsSync18(path)) {
|
|
19776
|
+
const owner2 = readLockOwner(path);
|
|
19777
|
+
if (!owner2) {
|
|
19778
|
+
throw new ResearchError(`research output ${ledger} has an unreadable lock at ${path}; verify no run is active, then remove it deliberately`);
|
|
19779
|
+
}
|
|
19780
|
+
if (owner2.host !== hostname() || processAlive(owner2.pid)) {
|
|
19781
|
+
throw new ResearchError(
|
|
19782
|
+
`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)`
|
|
19783
|
+
);
|
|
19784
|
+
}
|
|
19785
|
+
const claimed = `${path}.stale-${process.pid}-${randomUUID2()}`;
|
|
19786
|
+
let moved = true;
|
|
19787
|
+
try {
|
|
19788
|
+
renameSync4(path, claimed);
|
|
19789
|
+
} catch (e) {
|
|
19790
|
+
if (e.code !== "ENOENT") {
|
|
19791
|
+
throw new ResearchError(`research output ${ledger} stale lock recovery failed for ${path}`);
|
|
19792
|
+
}
|
|
19793
|
+
moved = false;
|
|
19794
|
+
}
|
|
19795
|
+
if (moved) {
|
|
19796
|
+
const inspected = readLockOwner(claimed);
|
|
19797
|
+
if (inspected && inspected.token === owner2.token) {
|
|
19798
|
+
unlinkSync(claimed);
|
|
19799
|
+
} else {
|
|
19800
|
+
try {
|
|
19801
|
+
linkSync(claimed, path);
|
|
19802
|
+
unlinkSync(claimed);
|
|
19803
|
+
} catch {
|
|
19804
|
+
}
|
|
19805
|
+
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)`);
|
|
19806
|
+
}
|
|
19807
|
+
}
|
|
19808
|
+
}
|
|
19809
|
+
const owner = {
|
|
19810
|
+
pid: process.pid,
|
|
19811
|
+
host: hostname(),
|
|
19812
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19813
|
+
token: randomUUID2()
|
|
19814
|
+
};
|
|
19815
|
+
const text = JSON.stringify(owner) + "\n";
|
|
19816
|
+
let fd = -1;
|
|
19817
|
+
try {
|
|
19818
|
+
fd = openSync3(path, "wx", 384);
|
|
19819
|
+
writeSync(fd, text);
|
|
19820
|
+
closeSync3(fd);
|
|
19821
|
+
fd = -1;
|
|
19822
|
+
} catch (e) {
|
|
19823
|
+
if (fd !== -1) closeSync3(fd);
|
|
19824
|
+
if (e.code === "EEXIST") {
|
|
19825
|
+
throw new ResearchError(
|
|
19826
|
+
`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`
|
|
19827
|
+
);
|
|
19828
|
+
}
|
|
19829
|
+
throw new ResearchError(`research output ${ledger} cannot create lock ${path}`);
|
|
19830
|
+
}
|
|
19831
|
+
let released = false;
|
|
19832
|
+
return {
|
|
19833
|
+
release() {
|
|
19834
|
+
if (released) return;
|
|
19835
|
+
released = true;
|
|
19836
|
+
try {
|
|
19837
|
+
if (readFileSync23(path, "utf8") === text) unlinkSync(path);
|
|
19838
|
+
} catch {
|
|
19839
|
+
}
|
|
19840
|
+
}
|
|
19841
|
+
};
|
|
19842
|
+
}
|
|
19727
19843
|
function resumableRecord(path, expected) {
|
|
19728
19844
|
let raw;
|
|
19729
19845
|
try {
|
|
@@ -20012,84 +20128,46 @@ function runResearch(opts) {
|
|
|
20012
20128
|
return 2;
|
|
20013
20129
|
}
|
|
20014
20130
|
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;
|
|
20131
|
+
let lock;
|
|
20132
|
+
try {
|
|
20133
|
+
lock = acquireResearchLock(ledger, opts.breakLock === true);
|
|
20134
|
+
} catch (e) {
|
|
20135
|
+
if (e instanceof ResearchError) {
|
|
20136
|
+
err2(`tamperward research: ${e.message}`);
|
|
20137
|
+
return 2;
|
|
20050
20138
|
}
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
let
|
|
20139
|
+
throw e;
|
|
20140
|
+
}
|
|
20141
|
+
try {
|
|
20142
|
+
const pairs = opts.pairs ?? 1;
|
|
20143
|
+
mkdirSync9(join25(ledger, "pairs"), { recursive: true });
|
|
20144
|
+
for (const task of tasks) {
|
|
20145
|
+
const existingRecords = /* @__PURE__ */ new Map();
|
|
20146
|
+
let sourceBase = null;
|
|
20059
20147
|
try {
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
if (!
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20148
|
+
for (let pair = 1; pair <= pairs; pair++) {
|
|
20149
|
+
const path = pairRecordPath(ledger, task.id, pair);
|
|
20150
|
+
if (!existsSync18(path)) continue;
|
|
20151
|
+
const existing = resumableRecord(path, {
|
|
20152
|
+
task: task.id,
|
|
20153
|
+
pair,
|
|
20154
|
+
manifest_sha256: manifestSha,
|
|
20155
|
+
adapter: { name: adapter.name, layers: adapter.layers },
|
|
20156
|
+
model: opts.model ?? null,
|
|
20157
|
+
tamperward_version: TW_VERSION,
|
|
20158
|
+
agent_argv: agentArgvIdentity,
|
|
20159
|
+
agent_budget: opts.agentBudget ?? null,
|
|
20160
|
+
verify_command: task.verify.command,
|
|
20161
|
+
source_base: sourceBase
|
|
20162
|
+
});
|
|
20163
|
+
sourceBase ??= existing.arms.ungated.base;
|
|
20164
|
+
if (existing.arms.ungated.base !== sourceBase) {
|
|
20165
|
+
throw new ResearchError(
|
|
20166
|
+
`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`
|
|
20167
|
+
);
|
|
20073
20168
|
}
|
|
20169
|
+
existingRecords.set(pair, existing);
|
|
20074
20170
|
}
|
|
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
20171
|
} catch (e) {
|
|
20094
20172
|
if (e instanceof ResearchError) {
|
|
20095
20173
|
err2(`tamperward research: ${e.message}`);
|
|
@@ -20097,20 +20175,72 @@ function runResearch(opts) {
|
|
|
20097
20175
|
}
|
|
20098
20176
|
throw e;
|
|
20099
20177
|
}
|
|
20100
|
-
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
|
|
20178
|
+
for (let pair = 1; pair <= pairs; pair++) {
|
|
20179
|
+
const path = pairRecordPath(ledger, task.id, pair);
|
|
20180
|
+
const existing = existingRecords.get(pair);
|
|
20181
|
+
if (existing) {
|
|
20182
|
+
if (!opts.json) out2(`tamperward research \u2014 task ${task.id} pair ${pair}: already recorded (${path}); skipping`);
|
|
20183
|
+
continue;
|
|
20184
|
+
}
|
|
20185
|
+
let record;
|
|
20186
|
+
try {
|
|
20187
|
+
const arms = {};
|
|
20188
|
+
for (const arm of RESEARCH_ARMS) {
|
|
20189
|
+
if (!opts.json) out2(`tamperward research \u2014 task ${task.id} pair ${pair}: ${arm} arm`);
|
|
20190
|
+
arms[arm] = runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase ?? void 0);
|
|
20191
|
+
if (arm === "ungated") {
|
|
20192
|
+
const resolvedSource = arms[arm]?.base;
|
|
20193
|
+
if (!resolvedSource) throw new ResearchError(`task "${task.id}" pair ${pair}: ungated arm produced no source base`);
|
|
20194
|
+
if (sourceBase !== null && resolvedSource !== sourceBase) {
|
|
20195
|
+
throw new ResearchError(
|
|
20196
|
+
`task "${task.id}" pair ${pair}: source base moved (${resolvedSource.slice(0, 12)}\u2026 != ${sourceBase.slice(0, 12)}\u2026)`
|
|
20197
|
+
);
|
|
20198
|
+
}
|
|
20199
|
+
sourceBase ??= resolvedSource;
|
|
20200
|
+
}
|
|
20201
|
+
}
|
|
20202
|
+
const ungated = arms.ungated;
|
|
20203
|
+
const gated = arms.gated;
|
|
20204
|
+
if (!ungated || !gated) throw new ResearchError(`task "${task.id}" pair ${pair}: an arm produced no record`);
|
|
20205
|
+
record = {
|
|
20206
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
20207
|
+
command: "research",
|
|
20208
|
+
document: "pair",
|
|
20209
|
+
task: task.id,
|
|
20210
|
+
pair,
|
|
20211
|
+
adapter: { name: adapter.name, layers: [...adapter.layers] },
|
|
20212
|
+
model: opts.model ?? null,
|
|
20213
|
+
tamperward_version: TW_VERSION,
|
|
20214
|
+
agent_argv: [...agentArgvIdentity],
|
|
20215
|
+
agent_budget: opts.agentBudget ?? null,
|
|
20216
|
+
manifest_sha256: manifestSha,
|
|
20217
|
+
verify_command: task.verify.command,
|
|
20218
|
+
arms: { ungated, gated }
|
|
20219
|
+
};
|
|
20220
|
+
} catch (e) {
|
|
20221
|
+
if (e instanceof ResearchError) {
|
|
20222
|
+
err2(`tamperward research: ${e.message}`);
|
|
20223
|
+
return 2;
|
|
20224
|
+
}
|
|
20225
|
+
throw e;
|
|
20226
|
+
}
|
|
20227
|
+
const text = JSON.stringify(record);
|
|
20228
|
+
writeRecordAtomically(path, text + "\n");
|
|
20229
|
+
if (opts.json) {
|
|
20230
|
+
out2(text);
|
|
20231
|
+
} else {
|
|
20232
|
+
const g = record.arms.gated;
|
|
20233
|
+
const u = record.arms.ungated;
|
|
20234
|
+
out2(
|
|
20235
|
+
`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}`
|
|
20236
|
+
);
|
|
20237
|
+
}
|
|
20110
20238
|
}
|
|
20111
20239
|
}
|
|
20240
|
+
return 0;
|
|
20241
|
+
} finally {
|
|
20242
|
+
lock.release();
|
|
20112
20243
|
}
|
|
20113
|
-
return 0;
|
|
20114
20244
|
}
|
|
20115
20245
|
var err2, out2;
|
|
20116
20246
|
var init_run2 = __esm({
|
|
@@ -20296,6 +20426,7 @@ function parseResearchRun(args) {
|
|
|
20296
20426
|
else if (a === "--pairs") o.pairs = Number(args[++i]);
|
|
20297
20427
|
else if (a === "--model") o.model = args[++i];
|
|
20298
20428
|
else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
|
|
20429
|
+
else if (a === "--break-lock") o.breakLock = true;
|
|
20299
20430
|
else if (a === "--json") o.json = true;
|
|
20300
20431
|
}
|
|
20301
20432
|
return o;
|
|
@@ -20325,6 +20456,33 @@ var init_research = __esm({
|
|
|
20325
20456
|
}
|
|
20326
20457
|
});
|
|
20327
20458
|
|
|
20459
|
+
// src/cli/signoff-label.ts
|
|
20460
|
+
function runSignoffLabel(opts) {
|
|
20461
|
+
if (!opts.rule) {
|
|
20462
|
+
process.stderr.write("tamperward signoff-label --rule <rule> --head <full-head-sha> [--file <path>]\n");
|
|
20463
|
+
return 2;
|
|
20464
|
+
}
|
|
20465
|
+
if (!opts.head) {
|
|
20466
|
+
process.stderr.write("tamperward: --head is required and must be a full 40- or 64-character object id.\n");
|
|
20467
|
+
return 2;
|
|
20468
|
+
}
|
|
20469
|
+
const want = opts.file ? `${opts.rule}:${opts.file}` : opts.rule;
|
|
20470
|
+
const token = compactOobToken(want, opts.head);
|
|
20471
|
+
if (!token) {
|
|
20472
|
+
process.stderr.write("tamperward: --head must be a full 40- or 64-character hexadecimal object id.\n");
|
|
20473
|
+
return 2;
|
|
20474
|
+
}
|
|
20475
|
+
process.stdout.write(`${token}
|
|
20476
|
+
`);
|
|
20477
|
+
return 0;
|
|
20478
|
+
}
|
|
20479
|
+
var init_signoff_label = __esm({
|
|
20480
|
+
"src/cli/signoff-label.ts"() {
|
|
20481
|
+
"use strict";
|
|
20482
|
+
init_signoff();
|
|
20483
|
+
}
|
|
20484
|
+
});
|
|
20485
|
+
|
|
20328
20486
|
// src/cli/main.ts
|
|
20329
20487
|
var main_exports = {};
|
|
20330
20488
|
__export(main_exports, {
|
|
@@ -20404,6 +20562,16 @@ function parseStats(args) {
|
|
|
20404
20562
|
}
|
|
20405
20563
|
return o;
|
|
20406
20564
|
}
|
|
20565
|
+
function parseSignoffLabel(args) {
|
|
20566
|
+
const o = {};
|
|
20567
|
+
for (let i = 0; i < args.length; i++) {
|
|
20568
|
+
const a = args[i];
|
|
20569
|
+
if (a === "--rule") o.rule = args[++i];
|
|
20570
|
+
else if (a === "--file") o.file = args[++i];
|
|
20571
|
+
else if (a === "--head") o.head = args[++i];
|
|
20572
|
+
}
|
|
20573
|
+
return o;
|
|
20574
|
+
}
|
|
20407
20575
|
function parseCheck(args) {
|
|
20408
20576
|
const o = {};
|
|
20409
20577
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -20525,6 +20693,11 @@ function validateCliArgs(cmd, args) {
|
|
|
20525
20693
|
if (parsed.positionals.length === 0) return "allow requires a rule";
|
|
20526
20694
|
return void 0;
|
|
20527
20695
|
}
|
|
20696
|
+
if (cmd === "signoff-label") {
|
|
20697
|
+
return validateFlatArgs(args, {
|
|
20698
|
+
values: { "--rule": "string", "--file": "string", "--head": "string" }
|
|
20699
|
+
}).error;
|
|
20700
|
+
}
|
|
20528
20701
|
if (cmd === "init") {
|
|
20529
20702
|
return validateFlatArgs(args, {
|
|
20530
20703
|
flags: ["--dry-run", "--force-workflow"],
|
|
@@ -20608,7 +20781,7 @@ function validateCliArgs(cmd, args) {
|
|
|
20608
20781
|
const delimiter3 = rest.indexOf("--");
|
|
20609
20782
|
const prefix = delimiter3 < 0 ? rest : rest.slice(0, delimiter3);
|
|
20610
20783
|
const parsed = validateFlatArgs(prefix, {
|
|
20611
|
-
flags: ["--json"],
|
|
20784
|
+
flags: ["--break-lock", "--json"],
|
|
20612
20785
|
values: {
|
|
20613
20786
|
"--manifest": "string",
|
|
20614
20787
|
"--out": "string",
|
|
@@ -20677,8 +20850,8 @@ Formats:
|
|
|
20677
20850
|
[--budget S] [--json] [--keep] as-is AND with protected files restored
|
|
20678
20851
|
[--require-ancestor] [--cwd D] from the trusted base; a visible-green /
|
|
20679
20852
|
pristine-red result is a MASKED FAILURE
|
|
20680
|
-
(exit 1, or 0 under
|
|
20681
|
-
verify@<
|
|
20853
|
+
(exit 1, or 0 under a compact tw1:<digest>
|
|
20854
|
+
or legacy verify@<full-sha> approval); cannot-verify
|
|
20682
20855
|
fails closed (2)
|
|
20683
20856
|
tamperward trace-verify [--base R] advisory Linux verifier-input discovery:
|
|
20684
20857
|
[--cmd C] [--budget S] [--runs N] trace a trusted/known-good base with
|
|
@@ -20707,9 +20880,11 @@ Formats:
|
|
|
20707
20880
|
tamperward research run --manifest F bring-your-own-model evaluation: for every
|
|
20708
20881
|
--out D --adapter A [--pairs N] task in the manifest, pin one source commit,
|
|
20709
20882
|
[--model M] [--agent-budget S] clone fresh state per arm, run the agent
|
|
20710
|
-
[--
|
|
20711
|
-
|
|
20883
|
+
[--break-lock] [--json] ungated and under the run envelope, then
|
|
20884
|
+
[-- <agent cmd...>] observe both with verify + check. Records
|
|
20712
20885
|
are resumable by full experiment identity.
|
|
20886
|
+
--break-lock recovers a verified stale
|
|
20887
|
+
output lock (see the research guide).
|
|
20713
20888
|
tamperward research summarize --ledger D aggregate measured pairs into model behaviour,
|
|
20714
20889
|
independent outcome, TamperWard hits/misses
|
|
20715
20890
|
and paired counts \u2014 no composite score
|
|
@@ -20721,6 +20896,8 @@ Formats:
|
|
|
20721
20896
|
integrity signal, not proof of agent intent.
|
|
20722
20897
|
tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
|
|
20723
20898
|
[--file F] [--cwd D]
|
|
20899
|
+
tamperward signoff-label --rule <rule> --head <sha> [--file F]
|
|
20900
|
+
print a compact, exact-head-bound CI label
|
|
20724
20901
|
tamperward onboard [--yes] [--cwd D] guided first-run setup: preflight, the
|
|
20725
20902
|
[--base R] [--repo O/R] init plan with a confirmation before any
|
|
20726
20903
|
[--branch B] [--verify-command C] write, canonical init, explicit verifier
|
|
@@ -20785,6 +20962,8 @@ function main(argv) {
|
|
|
20785
20962
|
return runAgentCommand("sweep", rest);
|
|
20786
20963
|
case "allow":
|
|
20787
20964
|
return runAllow(parseAllow(rest));
|
|
20965
|
+
case "signoff-label":
|
|
20966
|
+
return runSignoffLabel(parseSignoffLabel(rest));
|
|
20788
20967
|
case "init":
|
|
20789
20968
|
return runInit(parseInit(rest));
|
|
20790
20969
|
case "doctor":
|
|
@@ -20853,6 +21032,7 @@ var init_main = __esm({
|
|
|
20853
21032
|
init_onboard();
|
|
20854
21033
|
init_research();
|
|
20855
21034
|
init_audit();
|
|
21035
|
+
init_signoff_label();
|
|
20856
21036
|
}
|
|
20857
21037
|
});
|
|
20858
21038
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.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",
|