tamperward 2.32.0 → 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 +79 -14
- 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.)`
|
|
@@ -20438,6 +20456,33 @@ var init_research = __esm({
|
|
|
20438
20456
|
}
|
|
20439
20457
|
});
|
|
20440
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
|
+
|
|
20441
20486
|
// src/cli/main.ts
|
|
20442
20487
|
var main_exports = {};
|
|
20443
20488
|
__export(main_exports, {
|
|
@@ -20517,6 +20562,16 @@ function parseStats(args) {
|
|
|
20517
20562
|
}
|
|
20518
20563
|
return o;
|
|
20519
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
|
+
}
|
|
20520
20575
|
function parseCheck(args) {
|
|
20521
20576
|
const o = {};
|
|
20522
20577
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -20638,6 +20693,11 @@ function validateCliArgs(cmd, args) {
|
|
|
20638
20693
|
if (parsed.positionals.length === 0) return "allow requires a rule";
|
|
20639
20694
|
return void 0;
|
|
20640
20695
|
}
|
|
20696
|
+
if (cmd === "signoff-label") {
|
|
20697
|
+
return validateFlatArgs(args, {
|
|
20698
|
+
values: { "--rule": "string", "--file": "string", "--head": "string" }
|
|
20699
|
+
}).error;
|
|
20700
|
+
}
|
|
20641
20701
|
if (cmd === "init") {
|
|
20642
20702
|
return validateFlatArgs(args, {
|
|
20643
20703
|
flags: ["--dry-run", "--force-workflow"],
|
|
@@ -20790,8 +20850,8 @@ Formats:
|
|
|
20790
20850
|
[--budget S] [--json] [--keep] as-is AND with protected files restored
|
|
20791
20851
|
[--require-ancestor] [--cwd D] from the trusted base; a visible-green /
|
|
20792
20852
|
pristine-red result is a MASKED FAILURE
|
|
20793
|
-
(exit 1, or 0 under
|
|
20794
|
-
verify@<
|
|
20853
|
+
(exit 1, or 0 under a compact tw1:<digest>
|
|
20854
|
+
or legacy verify@<full-sha> approval); cannot-verify
|
|
20795
20855
|
fails closed (2)
|
|
20796
20856
|
tamperward trace-verify [--base R] advisory Linux verifier-input discovery:
|
|
20797
20857
|
[--cmd C] [--budget S] [--runs N] trace a trusted/known-good base with
|
|
@@ -20836,6 +20896,8 @@ Formats:
|
|
|
20836
20896
|
integrity signal, not proof of agent intent.
|
|
20837
20897
|
tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
|
|
20838
20898
|
[--file F] [--cwd D]
|
|
20899
|
+
tamperward signoff-label --rule <rule> --head <sha> [--file F]
|
|
20900
|
+
print a compact, exact-head-bound CI label
|
|
20839
20901
|
tamperward onboard [--yes] [--cwd D] guided first-run setup: preflight, the
|
|
20840
20902
|
[--base R] [--repo O/R] init plan with a confirmation before any
|
|
20841
20903
|
[--branch B] [--verify-command C] write, canonical init, explicit verifier
|
|
@@ -20900,6 +20962,8 @@ function main(argv) {
|
|
|
20900
20962
|
return runAgentCommand("sweep", rest);
|
|
20901
20963
|
case "allow":
|
|
20902
20964
|
return runAllow(parseAllow(rest));
|
|
20965
|
+
case "signoff-label":
|
|
20966
|
+
return runSignoffLabel(parseSignoffLabel(rest));
|
|
20903
20967
|
case "init":
|
|
20904
20968
|
return runInit(parseInit(rest));
|
|
20905
20969
|
case "doctor":
|
|
@@ -20968,6 +21032,7 @@ var init_main = __esm({
|
|
|
20968
21032
|
init_onboard();
|
|
20969
21033
|
init_research();
|
|
20970
21034
|
init_audit();
|
|
21035
|
+
init_signoff_label();
|
|
20971
21036
|
}
|
|
20972
21037
|
});
|
|
20973
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",
|