tamperward 2.11.3 → 2.12.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 +12 -0
- package/dist/cli/index.js +309 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,6 +199,16 @@ are involved here, and they are not the same thing.
|
|
|
199
199
|
its policy, verification surface and verifier command and budget from that same
|
|
200
200
|
trusted base — so neither step's verdict is governed by the candidate.
|
|
201
201
|
|
|
202
|
+
Since **2.12.0**, the generated job uses GitHub Actions' 360-minute outer timeout and
|
|
203
|
+
runs `tamperward doctor` before verification. The doctor reads the **trusted base**
|
|
204
|
+
policy, finds the job(s) that actually invoke `tamperward verify`, and requires enough
|
|
205
|
+
static outer time for both independently budgeted stages plus a 60-minute authority
|
|
206
|
+
reserve. With the generated 360-minute job that means budgets up to 9,000 seconds per
|
|
207
|
+
stage fit. Larger positive finite policy budgets remain valid — there is no schema cap —
|
|
208
|
+
but this generated GitHub-hosted authority refuses deterministically and tells you how
|
|
209
|
+
much outer time is required, so a custom runner/workflow can provide it instead of
|
|
210
|
+
GitHub killing verification mid-verdict.
|
|
211
|
+
|
|
202
212
|
Legitimate exceptions are out-of-band PR labels, `tamperward:allow:<rule>@<head-sha>`.
|
|
203
213
|
The workflow passes the head SHA to both steps through `TAMPERWARD_OOB_HEAD`, so an
|
|
204
214
|
approval is bound to the exact commit it was granted for and a new push invalidates it.
|
|
@@ -321,6 +331,7 @@ commands ignore what they do not know.
|
|
|
321
331
|
| --- | --- |
|
|
322
332
|
| `check` | one view — `--staged` · `--worktree` · `--diff <base>...<head>` — plus `--format text\|json\|github\|auto` (default `auto`) · `--json` (alias for `--format json`) · `--cwd <dir>` |
|
|
323
333
|
| `verify` | `--base <rev>` (default `HEAD`) · `--cmd <suite command>` · `--budget <seconds>` · `--json` · `--keep` (keep the two materialised copies and report their paths) · `--require-ancestor` (refuse a base that is not an ancestor of `HEAD`) · `--cwd <dir>` |
|
|
334
|
+
| `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>` (default `.github/workflows/tamperward.yml`) · `--cwd <dir>` — validates every job that runs `tamperward verify` has a static outer timeout covering visible + pristine + authority reserve |
|
|
324
335
|
| `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
|
|
325
336
|
| `allow` | `<rule>` · `--file <path>` · `--reason "<why>"` (required) · `--cwd <dir>` |
|
|
326
337
|
| `init` | `--cwd <dir>` · `--dry-run` · `--force-workflow` |
|
|
@@ -333,6 +344,7 @@ commands ignore what they do not know.
|
|
|
333
344
|
| --- | --- | --- | --- | --- |
|
|
334
345
|
| `check` | no blocking finding | at least one blocking finding | cannot evaluate: policy parse error, malformed `--diff` range, no view given, not a git repository, or an unresolvable revision — any failure the gate cannot recover from is one clean `tamperward: …` line on stderr at exit 2, never a stack trace at exit 1 | — |
|
|
335
346
|
| `verify` | `VERIFIED` — visible and pristine both green; or a `MASKED_FAILURE` cleared by an out-of-band `verify@<head-sha>` approval | `MASKED_FAILURE` (visible green, pristine red) or `SUITE_RED` | cannot verify, failing closed: no suite command, unresolvable base, `--require-ancestor` refused, budget exceeded, or the working or dependency tree moved during the run | — |
|
|
347
|
+
| `doctor` | configured verify job(s) have sufficient static outer time for the trusted policy | — | missing/invalid workflow, no verify job, missing/malformed/insufficient timeout, or trusted policy cannot be loaded | — |
|
|
336
348
|
| `run` | enforcement clean and the agent exited 0 — another non-zero agent exit is passed through unchanged | any blocking finding or masked failure, including a non-quiescent process after timeout | cannot adjudicate: dirty start, policy error, verify cannot run | `AGENT_TIMEOUT`: `--agent-budget` expired and post-timeout enforcement was clean |
|
|
337
349
|
| `hook claude` / `sweep claude` | always — a deny is JSON on stdout at exit 0, never exit 2 | — | only for an unsupported agent name | — |
|
|
338
350
|
| `allow` | sign-off recorded | — | no rule or `--reason`, not a git repo, or no current blocking finding to sign off | — |
|
package/dist/cli/index.js
CHANGED
|
@@ -674,9 +674,9 @@ function git(args, cwd) {
|
|
|
674
674
|
env: trustedGitEnv()
|
|
675
675
|
});
|
|
676
676
|
} catch (e) {
|
|
677
|
-
const
|
|
678
|
-
const detail = String(
|
|
679
|
-
throw new Error(detail ? `git ${args[0]}: ${detail}` :
|
|
677
|
+
const err3 = e;
|
|
678
|
+
const detail = String(err3.stderr ?? "").trim();
|
|
679
|
+
throw new Error(detail ? `git ${args[0]}: ${detail}` : err3.message ?? `git ${args[0]} failed`);
|
|
680
680
|
}
|
|
681
681
|
}
|
|
682
682
|
function blobAt(rev, path, cwd) {
|
|
@@ -864,11 +864,11 @@ function synthFileChange(displayPath, before, after) {
|
|
|
864
864
|
maxBuffer: 256 * 1024 * 1024
|
|
865
865
|
});
|
|
866
866
|
} catch (e) {
|
|
867
|
-
const
|
|
868
|
-
if (
|
|
869
|
-
throw new Error(`cannot diff the incoming edit to ${displayPath}: ${
|
|
867
|
+
const err3 = e;
|
|
868
|
+
if (err3.status !== 1) {
|
|
869
|
+
throw new Error(`cannot diff the incoming edit to ${displayPath}: ${err3.code ?? err3.message ?? String(e)}`);
|
|
870
870
|
}
|
|
871
|
-
raw =
|
|
871
|
+
raw = err3.stdout ? String(err3.stdout) : "";
|
|
872
872
|
}
|
|
873
873
|
} finally {
|
|
874
874
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -1585,7 +1585,7 @@ function branchExists(name, ctx) {
|
|
|
1585
1585
|
}
|
|
1586
1586
|
function revIsHead(rev, ctx) {
|
|
1587
1587
|
if (!ctx?.cwd || rev.startsWith("-")) return null;
|
|
1588
|
-
const
|
|
1588
|
+
const parse4 = (r) => {
|
|
1589
1589
|
try {
|
|
1590
1590
|
return execFileSync4("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${r}^{commit}`], {
|
|
1591
1591
|
cwd: ctx.cwd,
|
|
@@ -1596,8 +1596,8 @@ function revIsHead(rev, ctx) {
|
|
|
1596
1596
|
return null;
|
|
1597
1597
|
}
|
|
1598
1598
|
};
|
|
1599
|
-
const head =
|
|
1600
|
-
const target =
|
|
1599
|
+
const head = parse4("HEAD");
|
|
1600
|
+
const target = parse4(rev);
|
|
1601
1601
|
if (!head || !target) return null;
|
|
1602
1602
|
return head === target;
|
|
1603
1603
|
}
|
|
@@ -1651,8 +1651,8 @@ function numericOf(e) {
|
|
|
1651
1651
|
}
|
|
1652
1652
|
return void 0;
|
|
1653
1653
|
}
|
|
1654
|
-
function metricsOf(expr,
|
|
1655
|
-
const obj =
|
|
1654
|
+
function metricsOf(expr, resolve9) {
|
|
1655
|
+
const obj = resolve9(expr);
|
|
1656
1656
|
if (!ts2.isObjectLiteralExpression(obj)) return void 0;
|
|
1657
1657
|
const out2 = { values: {}, opaque: false };
|
|
1658
1658
|
let any = false;
|
|
@@ -1666,7 +1666,7 @@ function metricsOf(expr, resolve8) {
|
|
|
1666
1666
|
const k = keyName(p.name);
|
|
1667
1667
|
if (k === null || !isMetric(k)) continue;
|
|
1668
1668
|
any = true;
|
|
1669
|
-
const n = numericOf(
|
|
1669
|
+
const n = numericOf(resolve9(p.initializer));
|
|
1670
1670
|
if (n === void 0) out2.opaque = true;
|
|
1671
1671
|
else out2.values[k] = n;
|
|
1672
1672
|
}
|
|
@@ -1712,17 +1712,17 @@ function parseThresholds(src) {
|
|
|
1712
1712
|
const res = { paths: /* @__PURE__ */ new Map(), present: false, opaque: false };
|
|
1713
1713
|
try {
|
|
1714
1714
|
const sf = sourceOf(src);
|
|
1715
|
-
const
|
|
1715
|
+
const resolve9 = resolverFor(sf);
|
|
1716
1716
|
const visit = (node) => {
|
|
1717
1717
|
if (ts2.isPropertyAssignment(node)) {
|
|
1718
1718
|
const key2 = keyName(node.name);
|
|
1719
1719
|
if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
|
|
1720
1720
|
res.present = true;
|
|
1721
|
-
const init =
|
|
1721
|
+
const init = resolve9(node.initializer);
|
|
1722
1722
|
if (!ts2.isObjectLiteralExpression(init)) {
|
|
1723
1723
|
res.opaque = true;
|
|
1724
1724
|
} else {
|
|
1725
|
-
const flat = metricsOf(init,
|
|
1725
|
+
const flat = metricsOf(init, resolve9);
|
|
1726
1726
|
if (flat) res.global = merge(res.global, flat);
|
|
1727
1727
|
for (const p of init.properties) {
|
|
1728
1728
|
if (ts2.isSpreadAssignment(p)) {
|
|
@@ -1736,7 +1736,7 @@ function parseThresholds(src) {
|
|
|
1736
1736
|
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
1737
1737
|
continue;
|
|
1738
1738
|
}
|
|
1739
|
-
const m = metricsOf(p.initializer,
|
|
1739
|
+
const m = metricsOf(p.initializer, resolve9);
|
|
1740
1740
|
if (!m) continue;
|
|
1741
1741
|
if (k === "global") res.global = merge(res.global, m);
|
|
1742
1742
|
else res.paths.set(norm(k), m);
|
|
@@ -1769,18 +1769,18 @@ function parseLists(src) {
|
|
|
1769
1769
|
const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
|
|
1770
1770
|
try {
|
|
1771
1771
|
const sf = sourceOf(src);
|
|
1772
|
-
const
|
|
1772
|
+
const resolve9 = resolverFor(sf);
|
|
1773
1773
|
const visit = (node) => {
|
|
1774
1774
|
if (ts2.isPropertyAssignment(node)) {
|
|
1775
1775
|
const k = keyName(node.name);
|
|
1776
1776
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
1777
1777
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
1778
|
-
const { items, opaque } = literals(
|
|
1778
|
+
const { items, opaque } = literals(resolve9(node.initializer));
|
|
1779
1779
|
res.collectFrom = items;
|
|
1780
1780
|
res.collectKey = k === "include" ? "coverage.include" : k;
|
|
1781
1781
|
if (opaque) res.opaqueCollect = true;
|
|
1782
1782
|
} else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
|
|
1783
|
-
const { items, opaque } = literals(
|
|
1783
|
+
const { items, opaque } = literals(resolve9(node.initializer));
|
|
1784
1784
|
res.ignore.push(...items);
|
|
1785
1785
|
if (opaque) res.opaqueIgnore = true;
|
|
1786
1786
|
}
|
|
@@ -7600,6 +7600,36 @@ import { execFileSync as execFileSync6 } from "node:child_process";
|
|
|
7600
7600
|
import { createHash as createHash6 } from "node:crypto";
|
|
7601
7601
|
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
|
|
7602
7602
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7603
|
+
|
|
7604
|
+
// src/verifier-limits.ts
|
|
7605
|
+
var VERIFIER_STAGE_COUNT = 2;
|
|
7606
|
+
var GENERATED_CI_TIMEOUT_MINUTES = 360;
|
|
7607
|
+
var GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS = 60 * 60;
|
|
7608
|
+
function assertEnvelopeConstants() {
|
|
7609
|
+
if (!Number.isInteger(VERIFIER_STAGE_COUNT) || VERIFIER_STAGE_COUNT < 1) {
|
|
7610
|
+
throw new Error("TamperWard verifier envelope: stage count must be a positive integer");
|
|
7611
|
+
}
|
|
7612
|
+
if (!Number.isFinite(GENERATED_CI_TIMEOUT_MINUTES) || GENERATED_CI_TIMEOUT_MINUTES <= 0) {
|
|
7613
|
+
throw new Error("TamperWard verifier envelope: generated CI timeout must be positive");
|
|
7614
|
+
}
|
|
7615
|
+
if (!Number.isFinite(GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS) || GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS < 0) {
|
|
7616
|
+
throw new Error("TamperWard verifier envelope: overhead reserve cannot be negative");
|
|
7617
|
+
}
|
|
7618
|
+
if (GENERATED_CI_TIMEOUT_MINUTES * 60 <= GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS) {
|
|
7619
|
+
throw new Error(
|
|
7620
|
+
"TamperWard verifier envelope: generated CI timeout leaves no time for verifier stages"
|
|
7621
|
+
);
|
|
7622
|
+
}
|
|
7623
|
+
}
|
|
7624
|
+
assertEnvelopeConstants();
|
|
7625
|
+
function requiredVerifierAuthoritySeconds(stageBudgetSecs) {
|
|
7626
|
+
if (!Number.isFinite(stageBudgetSecs) || stageBudgetSecs <= 0) {
|
|
7627
|
+
throw new Error("verifier stage budget must be a positive finite number");
|
|
7628
|
+
}
|
|
7629
|
+
return VERIFIER_STAGE_COUNT * stageBudgetSecs + GENERATED_CI_MIN_OVERHEAD_RESERVE_SECS;
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7632
|
+
// src/cli/init.ts
|
|
7603
7633
|
var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
7604
7634
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7605
7635
|
function gitConfig(cwd, key2) {
|
|
@@ -7690,7 +7720,7 @@ permissions:
|
|
|
7690
7720
|
jobs:
|
|
7691
7721
|
tamperward:
|
|
7692
7722
|
runs-on: ubuntu-latest
|
|
7693
|
-
timeout-minutes:
|
|
7723
|
+
timeout-minutes: ${GENERATED_CI_TIMEOUT_MINUTES}
|
|
7694
7724
|
steps:
|
|
7695
7725
|
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
|
7696
7726
|
with:
|
|
@@ -7708,6 +7738,12 @@ jobs:
|
|
|
7708
7738
|
# Candidate code runs later in this job. Do not leave the checkout
|
|
7709
7739
|
# token in .git/config where a verifier command can read/reuse it.
|
|
7710
7740
|
persist-credentials: false
|
|
7741
|
+
- name: Tamperward doctor (CI verifier envelope)
|
|
7742
|
+
# Validate the outer GitHub job budget against the TRUSTED base policy
|
|
7743
|
+
# before starting visible/pristine execution. Large policy budgets remain
|
|
7744
|
+
# valid for custom runners; this generated authority refuses if its host
|
|
7745
|
+
# cannot accommodate both stages plus cleanup/reporting reserve.
|
|
7746
|
+
run: tamperward doctor --base "\${{ github.event.pull_request.base.sha }}" --workflow .github/workflows/tamperward.yml
|
|
7711
7747
|
- name: Resolve out-of-band sign-off from PR labels
|
|
7712
7748
|
id: oob
|
|
7713
7749
|
env:
|
|
@@ -8190,12 +8226,139 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8190
8226
|
return 0;
|
|
8191
8227
|
}
|
|
8192
8228
|
|
|
8229
|
+
// src/cli/doctor.ts
|
|
8230
|
+
import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync as readdirSync3 } from "node:fs";
|
|
8231
|
+
import { join as join13, resolve as resolve4 } from "node:path";
|
|
8232
|
+
import { parse as parse3 } from "yaml";
|
|
8233
|
+
var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
|
|
8234
|
+
function err(message) {
|
|
8235
|
+
process.stderr.write(`tamperward doctor: ${message}
|
|
8236
|
+
`);
|
|
8237
|
+
return 2;
|
|
8238
|
+
}
|
|
8239
|
+
function policyFor(opts, cwd) {
|
|
8240
|
+
if (!opts.base) return loadPolicy(cwd);
|
|
8241
|
+
const atBase = loadPolicyAt(opts.base, cwd);
|
|
8242
|
+
return atBase ?? defaultPolicy();
|
|
8243
|
+
}
|
|
8244
|
+
function asMapping(value) {
|
|
8245
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
8246
|
+
}
|
|
8247
|
+
function verifyJobs(doc) {
|
|
8248
|
+
const root = asMapping(doc);
|
|
8249
|
+
const jobs = asMapping(root?.jobs);
|
|
8250
|
+
if (!jobs) return [];
|
|
8251
|
+
const found = [];
|
|
8252
|
+
for (const [name, raw] of Object.entries(jobs)) {
|
|
8253
|
+
const job = asMapping(raw);
|
|
8254
|
+
if (!job || !Array.isArray(job.steps)) continue;
|
|
8255
|
+
const hasVerify = job.steps.some((step) => {
|
|
8256
|
+
const item = asMapping(step);
|
|
8257
|
+
return typeof item?.run === "string" && VERIFY_COMMAND.test(item.run);
|
|
8258
|
+
});
|
|
8259
|
+
if (hasVerify) found.push({ name, job });
|
|
8260
|
+
}
|
|
8261
|
+
return found;
|
|
8262
|
+
}
|
|
8263
|
+
function runDoctor(opts = {}) {
|
|
8264
|
+
const cwd = resolve4(opts.cwd ?? process.cwd());
|
|
8265
|
+
let policy;
|
|
8266
|
+
try {
|
|
8267
|
+
policy = policyFor(opts, cwd);
|
|
8268
|
+
} catch (e) {
|
|
8269
|
+
return err(e instanceof PolicyError || e instanceof Error ? e.message : String(e));
|
|
8270
|
+
}
|
|
8271
|
+
if (!policy.verify?.command) {
|
|
8272
|
+
return err("trusted policy has no verify.command; generated CI cannot verify this repository");
|
|
8273
|
+
}
|
|
8274
|
+
const workflowRels = [];
|
|
8275
|
+
if (opts.workflow) {
|
|
8276
|
+
workflowRels.push(opts.workflow);
|
|
8277
|
+
} else {
|
|
8278
|
+
const workflowDirRel = ".github/workflows";
|
|
8279
|
+
const workflowDir = resolve4(cwd, workflowDirRel);
|
|
8280
|
+
if (!existsSync10(workflowDir)) {
|
|
8281
|
+
return err(`${workflowDirRel}: workflow directory does not exist`);
|
|
8282
|
+
}
|
|
8283
|
+
let entries;
|
|
8284
|
+
try {
|
|
8285
|
+
entries = readdirSync3(workflowDir).filter((name) => /\.ya?ml$/i.test(name)).sort();
|
|
8286
|
+
} catch (e) {
|
|
8287
|
+
return err(
|
|
8288
|
+
`${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
|
|
8289
|
+
);
|
|
8290
|
+
}
|
|
8291
|
+
for (const name of entries) workflowRels.push(join13(workflowDirRel, name));
|
|
8292
|
+
}
|
|
8293
|
+
const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
|
|
8294
|
+
const requiredMinutes = Math.ceil(requiredSecs / 60);
|
|
8295
|
+
let verifyJobCount = 0;
|
|
8296
|
+
for (const workflowRel of workflowRels) {
|
|
8297
|
+
const workflowPath = resolve4(cwd, workflowRel);
|
|
8298
|
+
if (!existsSync10(workflowPath)) {
|
|
8299
|
+
return err(`${workflowRel}: workflow does not exist`);
|
|
8300
|
+
}
|
|
8301
|
+
let doc;
|
|
8302
|
+
try {
|
|
8303
|
+
doc = parse3(readFileSync13(workflowPath, "utf8"));
|
|
8304
|
+
} catch (e) {
|
|
8305
|
+
return err(
|
|
8306
|
+
`${workflowRel}: workflow is not valid YAML (${e instanceof Error ? e.message : String(e)})`
|
|
8307
|
+
);
|
|
8308
|
+
}
|
|
8309
|
+
const jobs = verifyJobs(doc);
|
|
8310
|
+
verifyJobCount += jobs.length;
|
|
8311
|
+
for (const { name, job } of jobs) {
|
|
8312
|
+
const timeout = job["timeout-minutes"];
|
|
8313
|
+
if (timeout === void 0 || timeout === null) {
|
|
8314
|
+
return err(
|
|
8315
|
+
`${workflowRel} job "${name}": timeout-minutes is missing; requires at least ${requiredMinutes} minutes`
|
|
8316
|
+
);
|
|
8317
|
+
}
|
|
8318
|
+
if (typeof timeout === "string" && timeout.includes("${{")) {
|
|
8319
|
+
return err(
|
|
8320
|
+
`${workflowRel} job "${name}": timeout-minutes must be a static numeric value; requires at least ${requiredMinutes} minutes`
|
|
8321
|
+
);
|
|
8322
|
+
}
|
|
8323
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
|
|
8324
|
+
return err(
|
|
8325
|
+
`${workflowRel} job "${name}": timeout-minutes must be numeric; got ${JSON.stringify(timeout)}`
|
|
8326
|
+
);
|
|
8327
|
+
}
|
|
8328
|
+
if (timeout <= 0) {
|
|
8329
|
+
return err(
|
|
8330
|
+
`${workflowRel} job "${name}": timeout-minutes must be positive; got ${timeout}`
|
|
8331
|
+
);
|
|
8332
|
+
}
|
|
8333
|
+
if (!Number.isInteger(timeout)) {
|
|
8334
|
+
return err(
|
|
8335
|
+
`${workflowRel} job "${name}": timeout-minutes must be a whole number; got ${timeout}`
|
|
8336
|
+
);
|
|
8337
|
+
}
|
|
8338
|
+
if (timeout < requiredMinutes) {
|
|
8339
|
+
return err(
|
|
8340
|
+
`${workflowRel} job "${name}": timeout-minutes ${timeout} is too small; trusted verify.budget ${policy.verify.budget}s requires at least ${requiredMinutes} minutes for visible + pristine + authority overhead`
|
|
8341
|
+
);
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
if (verifyJobCount === 0) {
|
|
8346
|
+
const scope = opts.workflow ?? ".github/workflows/*.yml|*.yaml";
|
|
8347
|
+
return err(`${scope}: no job contains a tamperward verify step`);
|
|
8348
|
+
}
|
|
8349
|
+
process.stdout.write(
|
|
8350
|
+
`tamperward doctor: CI verifier envelope OK \u2014 ${verifyJobCount} verify job(s), trusted budget ${policy.verify.budget}s/stage, requires >=${requiredMinutes}m outer timeout.
|
|
8351
|
+
`
|
|
8352
|
+
);
|
|
8353
|
+
return 0;
|
|
8354
|
+
}
|
|
8355
|
+
|
|
8193
8356
|
// src/cli/verify.ts
|
|
8194
8357
|
import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "node:child_process";
|
|
8195
8358
|
import { createHash as createHash10 } from "node:crypto";
|
|
8196
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as
|
|
8359
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8197
8360
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8198
|
-
import { dirname as dirname7, isAbsolute as isAbsolute10, join as
|
|
8361
|
+
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join16, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
|
|
8199
8362
|
|
|
8200
8363
|
// src/fingerprint.ts
|
|
8201
8364
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
@@ -8226,10 +8389,10 @@ function treeFingerprint(cwd, keepIgnored) {
|
|
|
8226
8389
|
// src/dependency-env.ts
|
|
8227
8390
|
import { createHash as createHash8 } from "node:crypto";
|
|
8228
8391
|
import {
|
|
8229
|
-
existsSync as
|
|
8392
|
+
existsSync as existsSync11,
|
|
8230
8393
|
lstatSync as lstatSync3,
|
|
8231
|
-
readdirSync as
|
|
8232
|
-
readFileSync as
|
|
8394
|
+
readdirSync as readdirSync4,
|
|
8395
|
+
readFileSync as readFileSync14,
|
|
8233
8396
|
readlinkSync as readlinkSync2,
|
|
8234
8397
|
realpathSync as realpathSync3,
|
|
8235
8398
|
statSync as statSync4
|
|
@@ -8239,15 +8402,15 @@ import {
|
|
|
8239
8402
|
delimiter,
|
|
8240
8403
|
dirname as dirname5,
|
|
8241
8404
|
isAbsolute as isAbsolute9,
|
|
8242
|
-
join as
|
|
8405
|
+
join as join14,
|
|
8243
8406
|
relative as relative4,
|
|
8244
|
-
resolve as
|
|
8407
|
+
resolve as resolve5,
|
|
8245
8408
|
sep
|
|
8246
8409
|
} from "node:path";
|
|
8247
8410
|
var Unattestable = class extends Error {
|
|
8248
8411
|
};
|
|
8249
8412
|
function inside(root, candidate) {
|
|
8250
|
-
const rel = relative4(
|
|
8413
|
+
const rel = relative4(resolve5(root), resolve5(candidate));
|
|
8251
8414
|
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
|
|
8252
8415
|
}
|
|
8253
8416
|
function modeKind(path) {
|
|
@@ -8286,7 +8449,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
8286
8449
|
st = statSync4(path);
|
|
8287
8450
|
if (!st.isFile()) throw new Error("not a regular file");
|
|
8288
8451
|
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
8289
|
-
h.update(
|
|
8452
|
+
h.update(readFileSync14(path));
|
|
8290
8453
|
h.update("\0");
|
|
8291
8454
|
} catch (e) {
|
|
8292
8455
|
throw new Unattestable(
|
|
@@ -8300,14 +8463,14 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
8300
8463
|
visited.add(realDir);
|
|
8301
8464
|
let entries;
|
|
8302
8465
|
try {
|
|
8303
|
-
entries =
|
|
8466
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
8304
8467
|
} catch (e) {
|
|
8305
8468
|
throw new Unattestable(
|
|
8306
8469
|
"dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
8307
8470
|
);
|
|
8308
8471
|
}
|
|
8309
8472
|
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
8310
|
-
const path =
|
|
8473
|
+
const path = join14(dir, entry.name);
|
|
8311
8474
|
const rel = namespace ? namespace + "/" + entry.name : entry.name;
|
|
8312
8475
|
let ident;
|
|
8313
8476
|
try {
|
|
@@ -8320,7 +8483,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
8320
8483
|
h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
|
|
8321
8484
|
if (ident.kind === "file") {
|
|
8322
8485
|
try {
|
|
8323
|
-
h.update(
|
|
8486
|
+
h.update(readFileSync14(path));
|
|
8324
8487
|
h.update("\0");
|
|
8325
8488
|
} catch (e) {
|
|
8326
8489
|
throw new Unattestable(
|
|
@@ -8379,7 +8542,7 @@ function snapshot(roots, probes, cwd) {
|
|
|
8379
8542
|
const h = createHash8("sha256");
|
|
8380
8543
|
for (const probe of probes) {
|
|
8381
8544
|
h.update(
|
|
8382
|
-
"probe\0" + probe.kind + "\0" + probe.path + "\0" + (
|
|
8545
|
+
"probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync11(probe.path) ? "present" : "absent") + "\0"
|
|
8383
8546
|
);
|
|
8384
8547
|
}
|
|
8385
8548
|
for (const root of roots) fingerprintRoot(root, cwd, h);
|
|
@@ -8390,7 +8553,7 @@ function freezeRoot(kind, path) {
|
|
|
8390
8553
|
if (!statSync4(path).isDirectory()) {
|
|
8391
8554
|
throw new Error("not a directory");
|
|
8392
8555
|
}
|
|
8393
|
-
return { kind, path:
|
|
8556
|
+
return { kind, path: resolve5(path), realPath: realpathSync3(path) };
|
|
8394
8557
|
} catch (e) {
|
|
8395
8558
|
throw new Unattestable(
|
|
8396
8559
|
kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
@@ -8426,17 +8589,17 @@ function firstExecutable(command) {
|
|
|
8426
8589
|
function resolveExecutable(token, cwd, env) {
|
|
8427
8590
|
if (!token) return null;
|
|
8428
8591
|
if (token.includes("/") || token.includes("\\")) {
|
|
8429
|
-
const path = isAbsolute9(token) ? token :
|
|
8592
|
+
const path = isAbsolute9(token) ? token : resolve5(cwd, token);
|
|
8430
8593
|
try {
|
|
8431
|
-
return
|
|
8594
|
+
return existsSync11(path) ? realpathSync3(path) : null;
|
|
8432
8595
|
} catch {
|
|
8433
8596
|
return null;
|
|
8434
8597
|
}
|
|
8435
8598
|
}
|
|
8436
8599
|
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
8437
|
-
const candidate =
|
|
8600
|
+
const candidate = resolve5(entry, token);
|
|
8438
8601
|
try {
|
|
8439
|
-
if (
|
|
8602
|
+
if (existsSync11(candidate) && statSync4(candidate).isFile()) return realpathSync3(candidate);
|
|
8440
8603
|
} catch {
|
|
8441
8604
|
}
|
|
8442
8605
|
}
|
|
@@ -8453,13 +8616,13 @@ function looksPython(command, executable, token) {
|
|
|
8453
8616
|
}
|
|
8454
8617
|
function inferredVenvFromToken(token, cwd) {
|
|
8455
8618
|
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
8456
|
-
const path = isAbsolute9(token) ? token :
|
|
8619
|
+
const path = isAbsolute9(token) ? token : resolve5(cwd, token);
|
|
8457
8620
|
if (!pythonName(basename3(path))) return null;
|
|
8458
8621
|
const parent = dirname5(path);
|
|
8459
8622
|
const parentName = basename3(parent).toLowerCase();
|
|
8460
8623
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
8461
8624
|
const root = dirname5(parent);
|
|
8462
|
-
return
|
|
8625
|
+
return existsSync11(join14(root, "pyvenv.cfg")) ? root : null;
|
|
8463
8626
|
}
|
|
8464
8627
|
function unsupportedEcosystem(command, token) {
|
|
8465
8628
|
const name = token ? basename3(token).toLowerCase() : "";
|
|
@@ -8475,13 +8638,13 @@ function inferredVenv(executable) {
|
|
|
8475
8638
|
const parentName = basename3(parent).toLowerCase();
|
|
8476
8639
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
8477
8640
|
const root = dirname5(parent);
|
|
8478
|
-
return
|
|
8641
|
+
return existsSync11(join14(root, "pyvenv.cfg")) ? root : null;
|
|
8479
8642
|
}
|
|
8480
8643
|
function venvProblem(root) {
|
|
8481
|
-
const cfg =
|
|
8482
|
-
if (!
|
|
8644
|
+
const cfg = join14(root, "pyvenv.cfg");
|
|
8645
|
+
if (!existsSync11(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
|
|
8483
8646
|
try {
|
|
8484
|
-
const body =
|
|
8647
|
+
const body = readFileSync14(cfg, "utf8");
|
|
8485
8648
|
if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
|
|
8486
8649
|
return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
|
|
8487
8650
|
}
|
|
@@ -8491,7 +8654,7 @@ function venvProblem(root) {
|
|
|
8491
8654
|
return null;
|
|
8492
8655
|
}
|
|
8493
8656
|
function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
8494
|
-
const cwd =
|
|
8657
|
+
const cwd = resolve5(cwdInput);
|
|
8495
8658
|
const roots = [];
|
|
8496
8659
|
const probes = [];
|
|
8497
8660
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8504,8 +8667,8 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
|
8504
8667
|
}
|
|
8505
8668
|
};
|
|
8506
8669
|
try {
|
|
8507
|
-
const nodeModules =
|
|
8508
|
-
if (
|
|
8670
|
+
const nodeModules = join14(cwd, "node_modules");
|
|
8671
|
+
if (existsSync11(nodeModules)) addRoot("node_modules", nodeModules);
|
|
8509
8672
|
else probes.push({ kind: "node_modules", path: nodeModules });
|
|
8510
8673
|
const token = firstExecutable(command);
|
|
8511
8674
|
const executable = resolveExecutable(token, cwd, env);
|
|
@@ -8514,11 +8677,11 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
|
8514
8677
|
if (!isAbsolute9(env.VIRTUAL_ENV)) {
|
|
8515
8678
|
throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
|
|
8516
8679
|
}
|
|
8517
|
-
venv =
|
|
8680
|
+
venv = resolve5(env.VIRTUAL_ENV);
|
|
8518
8681
|
} else {
|
|
8519
8682
|
venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
|
|
8520
|
-
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) &&
|
|
8521
|
-
venv =
|
|
8683
|
+
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync11(join14(cwd, ".venv", "pyvenv.cfg"))) {
|
|
8684
|
+
venv = join14(cwd, ".venv");
|
|
8522
8685
|
}
|
|
8523
8686
|
}
|
|
8524
8687
|
if (venv) {
|
|
@@ -8550,7 +8713,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
|
|
|
8550
8713
|
return { ok: false, reason: descriptor.reason ?? "dependency environment is not attestable" };
|
|
8551
8714
|
}
|
|
8552
8715
|
try {
|
|
8553
|
-
const fingerprint2 = snapshot(descriptor.roots, descriptor.probes,
|
|
8716
|
+
const fingerprint2 = snapshot(descriptor.roots, descriptor.probes, resolve5(cwdInput));
|
|
8554
8717
|
if (fingerprint2 !== descriptor.fingerprint) {
|
|
8555
8718
|
return { ok: false, fingerprint: fingerprint2, reason: "dependency environment fingerprint changed" };
|
|
8556
8719
|
}
|
|
@@ -8579,8 +8742,8 @@ function dependencyEnvironmentSummary(descriptor) {
|
|
|
8579
8742
|
// src/verifier-backend.ts
|
|
8580
8743
|
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
8581
8744
|
import { spawnSync } from "node:child_process";
|
|
8582
|
-
import { accessSync, constants as constants2, readFileSync as
|
|
8583
|
-
import { delimiter as delimiter2, dirname as dirname6, join as
|
|
8745
|
+
import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
|
|
8746
|
+
import { delimiter as delimiter2, dirname as dirname6, join as join15, resolve as resolve6 } from "node:path";
|
|
8584
8747
|
var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
|
|
8585
8748
|
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
8586
8749
|
// Docker interprets memory-swap == memory as no additional swap allowance.
|
|
@@ -8609,7 +8772,7 @@ function writableByCaller(path) {
|
|
|
8609
8772
|
}
|
|
8610
8773
|
}
|
|
8611
8774
|
function replaceableByCaller(path) {
|
|
8612
|
-
let current =
|
|
8775
|
+
let current = resolve6(path);
|
|
8613
8776
|
if (writableByCaller(current)) return true;
|
|
8614
8777
|
current = dirname6(current);
|
|
8615
8778
|
while (true) {
|
|
@@ -8623,7 +8786,7 @@ function replaceableByCaller(path) {
|
|
|
8623
8786
|
function findExecutable(name) {
|
|
8624
8787
|
for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
|
|
8625
8788
|
if (!raw) continue;
|
|
8626
|
-
const candidate =
|
|
8789
|
+
const candidate = join15(raw, name);
|
|
8627
8790
|
try {
|
|
8628
8791
|
accessSync(candidate, constants2.X_OK);
|
|
8629
8792
|
return realpathSync4(candidate);
|
|
@@ -8633,7 +8796,7 @@ function findExecutable(name) {
|
|
|
8633
8796
|
return null;
|
|
8634
8797
|
}
|
|
8635
8798
|
function fileSha256(path) {
|
|
8636
|
-
return createHash9("sha256").update(
|
|
8799
|
+
return createHash9("sha256").update(readFileSync15(path)).digest("hex");
|
|
8637
8800
|
}
|
|
8638
8801
|
function engineClientEnv() {
|
|
8639
8802
|
return {
|
|
@@ -8955,7 +9118,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
8955
9118
|
const args = containerRunArgs({
|
|
8956
9119
|
image: backend.image,
|
|
8957
9120
|
name,
|
|
8958
|
-
workspace:
|
|
9121
|
+
workspace: resolve6(dir),
|
|
8959
9122
|
command,
|
|
8960
9123
|
uid,
|
|
8961
9124
|
gid,
|
|
@@ -9124,7 +9287,7 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
9124
9287
|
return r.status === 0;
|
|
9125
9288
|
}
|
|
9126
9289
|
function inside2(root, path) {
|
|
9127
|
-
const rel = relative5(
|
|
9290
|
+
const rel = relative5(resolve7(root), resolve7(path));
|
|
9128
9291
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
|
|
9129
9292
|
}
|
|
9130
9293
|
function linkParts(path) {
|
|
@@ -9141,7 +9304,7 @@ function rootedLinkTarget(target) {
|
|
|
9141
9304
|
return isAbsolute10(target) || driveRelativeLinkTarget(target);
|
|
9142
9305
|
}
|
|
9143
9306
|
function safeSymlink(target, out2, root, label) {
|
|
9144
|
-
if (rootedLinkTarget(target) || !inside2(root,
|
|
9307
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out2), target))) {
|
|
9145
9308
|
throw linkEscape(label, target);
|
|
9146
9309
|
}
|
|
9147
9310
|
rmSync3(out2, { force: true });
|
|
@@ -9167,7 +9330,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
9167
9330
|
continue;
|
|
9168
9331
|
}
|
|
9169
9332
|
const base = domain === "tree" ? root : dependencyRoot;
|
|
9170
|
-
const current =
|
|
9333
|
+
const current = join16(base, ...stack);
|
|
9171
9334
|
let st;
|
|
9172
9335
|
try {
|
|
9173
9336
|
st = lstatSync4(current);
|
|
@@ -9193,7 +9356,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
9193
9356
|
throw linkEscape(label, target);
|
|
9194
9357
|
}
|
|
9195
9358
|
stack = [];
|
|
9196
|
-
pending.unshift(...linkParts(relative5(dependencyRoot,
|
|
9359
|
+
pending.unshift(...linkParts(relative5(dependencyRoot, resolve7(next))));
|
|
9197
9360
|
} else {
|
|
9198
9361
|
pending.unshift(...linkParts(next));
|
|
9199
9362
|
}
|
|
@@ -9203,7 +9366,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
9203
9366
|
const parts = rel.split("/").slice(0, -1);
|
|
9204
9367
|
let current = cwd;
|
|
9205
9368
|
for (const part of parts) {
|
|
9206
|
-
current =
|
|
9369
|
+
current = join16(current, part);
|
|
9207
9370
|
let st;
|
|
9208
9371
|
try {
|
|
9209
9372
|
st = lstatSync4(current);
|
|
@@ -9219,7 +9382,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
9219
9382
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
9220
9383
|
const links = [];
|
|
9221
9384
|
for (const rel of listed) {
|
|
9222
|
-
const src =
|
|
9385
|
+
const src = join16(cwd, rel);
|
|
9223
9386
|
rejectLinkedParent(cwd, rel);
|
|
9224
9387
|
let st;
|
|
9225
9388
|
try {
|
|
@@ -9227,7 +9390,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
9227
9390
|
} catch {
|
|
9228
9391
|
continue;
|
|
9229
9392
|
}
|
|
9230
|
-
const out2 =
|
|
9393
|
+
const out2 = join16(dest, rel);
|
|
9231
9394
|
mkdirSync6(dirname7(out2), { recursive: true });
|
|
9232
9395
|
dropSymlink(out2);
|
|
9233
9396
|
if (st.isSymbolicLink()) {
|
|
@@ -9244,7 +9407,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
9244
9407
|
for (const link of links) {
|
|
9245
9408
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
9246
9409
|
}
|
|
9247
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
9410
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join16(dest, "node_modules"), "dir");
|
|
9248
9411
|
}
|
|
9249
9412
|
function dropSymlink(p) {
|
|
9250
9413
|
try {
|
|
@@ -9277,11 +9440,11 @@ function overlayDigest(dest, paths) {
|
|
|
9277
9440
|
for (const rel of [...paths].sort()) {
|
|
9278
9441
|
h.update(rel);
|
|
9279
9442
|
h.update("\0");
|
|
9280
|
-
const p =
|
|
9443
|
+
const p = join16(dest, rel);
|
|
9281
9444
|
try {
|
|
9282
9445
|
const st = lstatSync4(p);
|
|
9283
9446
|
h.update(String(st.mode));
|
|
9284
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
9447
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
|
|
9285
9448
|
} catch {
|
|
9286
9449
|
h.update("<gone>");
|
|
9287
9450
|
}
|
|
@@ -9305,7 +9468,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
9305
9468
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
9306
9469
|
}
|
|
9307
9470
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
9308
|
-
const out2 =
|
|
9471
|
+
const out2 = join16(dest, e.path);
|
|
9309
9472
|
mkdirSync6(dirname7(out2), { recursive: true });
|
|
9310
9473
|
rmSync3(out2, { force: true });
|
|
9311
9474
|
if (e.mode === "120000") {
|
|
@@ -9322,7 +9485,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
9322
9485
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
9323
9486
|
for (const rel of inCopy) {
|
|
9324
9487
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
9325
|
-
rmSync3(
|
|
9488
|
+
rmSync3(join16(dest, rel), { force: true });
|
|
9326
9489
|
removed++;
|
|
9327
9490
|
}
|
|
9328
9491
|
for (const link of restoredLinks) {
|
|
@@ -9367,8 +9530,8 @@ function suiteEnv(scratch) {
|
|
|
9367
9530
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
9368
9531
|
env[k] = v;
|
|
9369
9532
|
}
|
|
9370
|
-
const userRc =
|
|
9371
|
-
const globalRc =
|
|
9533
|
+
const userRc = join16(scratch, "npmrc-user");
|
|
9534
|
+
const globalRc = join16(scratch, "npmrc-global");
|
|
9372
9535
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
9373
9536
|
env.npm_config_userconfig = userRc;
|
|
9374
9537
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -9378,8 +9541,8 @@ function suiteEnv(scratch) {
|
|
|
9378
9541
|
}
|
|
9379
9542
|
function runLocalSuite(dir, cmd, budgetSecs) {
|
|
9380
9543
|
const t0 = Date.now();
|
|
9381
|
-
const outDir = mkdtempSync2(
|
|
9382
|
-
const outFile =
|
|
9544
|
+
const outDir = mkdtempSync2(join16(tmpdir2(), "tw-verify-run-"));
|
|
9545
|
+
const outFile = join16(outDir, "result.json");
|
|
9383
9546
|
try {
|
|
9384
9547
|
spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
9385
9548
|
cwd: dir,
|
|
@@ -9392,7 +9555,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
9392
9555
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
9393
9556
|
let r;
|
|
9394
9557
|
try {
|
|
9395
|
-
r = JSON.parse(
|
|
9558
|
+
r = JSON.parse(readFileSync16(outFile, "utf8"));
|
|
9396
9559
|
} catch {
|
|
9397
9560
|
return { exit: null, secs };
|
|
9398
9561
|
}
|
|
@@ -9484,8 +9647,8 @@ function runVerify(opts) {
|
|
|
9484
9647
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
9485
9648
|
return 2;
|
|
9486
9649
|
}
|
|
9487
|
-
const visRoot = mkdtempSync2(
|
|
9488
|
-
const visDir =
|
|
9650
|
+
const visRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-vis-"));
|
|
9651
|
+
const visDir = join16(visRoot, "t");
|
|
9489
9652
|
const cleanup = (dirs) => {
|
|
9490
9653
|
if (opts.keep) return;
|
|
9491
9654
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
@@ -9542,8 +9705,8 @@ function runVerify(opts) {
|
|
|
9542
9705
|
out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
9543
9706
|
return 2;
|
|
9544
9707
|
}
|
|
9545
|
-
const priRoot = mkdtempSync2(
|
|
9546
|
-
const priDir =
|
|
9708
|
+
const priRoot = mkdtempSync2(join16(tmpdir2(), "tw-verify-pri-"));
|
|
9709
|
+
const priDir = join16(priRoot, "t");
|
|
9547
9710
|
let restored = [];
|
|
9548
9711
|
let removedAdded = 0;
|
|
9549
9712
|
try {
|
|
@@ -9694,11 +9857,11 @@ function parseVerify(args) {
|
|
|
9694
9857
|
|
|
9695
9858
|
// src/cli/run.ts
|
|
9696
9859
|
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
9697
|
-
import { mkdtempSync as mkdtempSync3, readdirSync as
|
|
9860
|
+
import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
|
|
9698
9861
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
9699
|
-
import { join as
|
|
9862
|
+
import { join as join17, resolve as resolve8 } from "node:path";
|
|
9700
9863
|
var out = (s) => process.stdout.write(s + "\n");
|
|
9701
|
-
var
|
|
9864
|
+
var err2 = (s) => process.stderr.write(s + "\n");
|
|
9702
9865
|
var AGENT_SUPERVISOR = String.raw`
|
|
9703
9866
|
const { spawn, spawnSync } = require('node:child_process');
|
|
9704
9867
|
const fs = require('node:fs');
|
|
@@ -9809,14 +9972,14 @@ function linuxDescendantPids(rootPid) {
|
|
|
9809
9972
|
const byParent = /* @__PURE__ */ new Map();
|
|
9810
9973
|
let names;
|
|
9811
9974
|
try {
|
|
9812
|
-
names =
|
|
9975
|
+
names = readdirSync5("/proc").filter((x) => /^\d+$/.test(x));
|
|
9813
9976
|
} catch {
|
|
9814
9977
|
return [];
|
|
9815
9978
|
}
|
|
9816
9979
|
for (const name of names) {
|
|
9817
9980
|
const pid = Number(name);
|
|
9818
9981
|
try {
|
|
9819
|
-
const stat =
|
|
9982
|
+
const stat = readFileSync17(`/proc/${name}/stat`, "utf8");
|
|
9820
9983
|
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
9821
9984
|
const ppid = Number(fields[1]);
|
|
9822
9985
|
if (!Number.isFinite(ppid)) continue;
|
|
@@ -9866,8 +10029,8 @@ function killAgentTree(pid) {
|
|
|
9866
10029
|
}
|
|
9867
10030
|
}
|
|
9868
10031
|
function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
9869
|
-
const stateDir = mkdtempSync3(
|
|
9870
|
-
const resultFile =
|
|
10032
|
+
const stateDir = mkdtempSync3(join17(tmpdir3(), "tw-agent-supervisor-"));
|
|
10033
|
+
const resultFile = join17(stateDir, "result.json");
|
|
9871
10034
|
let supervisorTimedOut = false;
|
|
9872
10035
|
try {
|
|
9873
10036
|
const supervisor = spawnSync3(
|
|
@@ -9885,7 +10048,7 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
|
9885
10048
|
supervisorTimedOut = Boolean(supervisor.error) && supervisor.error.code === "ETIMEDOUT";
|
|
9886
10049
|
let state = {};
|
|
9887
10050
|
try {
|
|
9888
|
-
state = JSON.parse(
|
|
10051
|
+
state = JSON.parse(readFileSync17(resultFile, "utf8"));
|
|
9889
10052
|
} catch {
|
|
9890
10053
|
}
|
|
9891
10054
|
if (supervisorTimedOut || state.timedOut) {
|
|
@@ -9920,7 +10083,7 @@ function git3(args, cwd) {
|
|
|
9920
10083
|
}
|
|
9921
10084
|
function nowTicks() {
|
|
9922
10085
|
try {
|
|
9923
|
-
return Math.floor(parseFloat(
|
|
10086
|
+
return Math.floor(parseFloat(readFileSync17("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
9924
10087
|
} catch {
|
|
9925
10088
|
return Number.POSITIVE_INFINITY;
|
|
9926
10089
|
}
|
|
@@ -9935,7 +10098,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9935
10098
|
}
|
|
9936
10099
|
let pids;
|
|
9937
10100
|
try {
|
|
9938
|
-
pids =
|
|
10101
|
+
pids = readdirSync5("/proc").filter((d) => /^\d+$/.test(d));
|
|
9939
10102
|
} catch {
|
|
9940
10103
|
return out2;
|
|
9941
10104
|
}
|
|
@@ -9943,7 +10106,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9943
10106
|
const n = Number(pid);
|
|
9944
10107
|
if (n === process.pid) continue;
|
|
9945
10108
|
try {
|
|
9946
|
-
const stat =
|
|
10109
|
+
const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
|
|
9947
10110
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
9948
10111
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
9949
10112
|
} catch {
|
|
@@ -9951,14 +10114,14 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9951
10114
|
}
|
|
9952
10115
|
const procLinks = [`/proc/${pid}/cwd`, `/proc/${pid}/exe`];
|
|
9953
10116
|
try {
|
|
9954
|
-
for (const fd of
|
|
10117
|
+
for (const fd of readdirSync5(`/proc/${pid}/fd`)) procLinks.push(`/proc/${pid}/fd/${fd}`);
|
|
9955
10118
|
} catch {
|
|
9956
10119
|
}
|
|
9957
10120
|
let holds = false;
|
|
9958
10121
|
for (const link of procLinks) {
|
|
9959
10122
|
try {
|
|
9960
10123
|
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
9961
|
-
const held = raw.startsWith("/") ?
|
|
10124
|
+
const held = raw.startsWith("/") ? resolve8(raw) : raw;
|
|
9962
10125
|
if (held === real || held.startsWith(real + "/")) {
|
|
9963
10126
|
holds = true;
|
|
9964
10127
|
break;
|
|
@@ -9971,13 +10134,13 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9971
10134
|
return out2;
|
|
9972
10135
|
}
|
|
9973
10136
|
function runEnvelope(opts) {
|
|
9974
|
-
const cwd =
|
|
10137
|
+
const cwd = resolve8(opts.cwd ?? process.cwd());
|
|
9975
10138
|
if (!opts.argv.length) {
|
|
9976
|
-
|
|
10139
|
+
err2("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
9977
10140
|
return 2;
|
|
9978
10141
|
}
|
|
9979
10142
|
if (opts.agentBudget !== void 0 && (!Number.isFinite(opts.agentBudget) || opts.agentBudget <= 0)) {
|
|
9980
|
-
|
|
10143
|
+
err2(
|
|
9981
10144
|
`tamperward run: --agent-budget needs a positive number of seconds (got ${JSON.stringify(opts.agentBudget)}) \u2014 failing closed.`
|
|
9982
10145
|
);
|
|
9983
10146
|
return 2;
|
|
@@ -9986,7 +10149,7 @@ function runEnvelope(opts) {
|
|
|
9986
10149
|
try {
|
|
9987
10150
|
base = git3(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
|
|
9988
10151
|
} catch {
|
|
9989
|
-
|
|
10152
|
+
err2(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
|
|
9990
10153
|
return 2;
|
|
9991
10154
|
}
|
|
9992
10155
|
let frozenPolicy;
|
|
@@ -9997,19 +10160,19 @@ function runEnvelope(opts) {
|
|
|
9997
10160
|
frozenCmd = opts.cmd ?? frozenPolicy.verify?.command;
|
|
9998
10161
|
frozenBudget = opts.budget ?? frozenPolicy.verify?.budget;
|
|
9999
10162
|
} catch (e) {
|
|
10000
|
-
|
|
10163
|
+
err2(`tamperward run: cannot load the entry-time policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed.`);
|
|
10001
10164
|
return 2;
|
|
10002
10165
|
}
|
|
10003
10166
|
if (!frozenCmd) {
|
|
10004
|
-
|
|
10005
|
-
|
|
10167
|
+
err2("tamperward run: no suite command at entry (policy `verify: { command: ... }` or --cmd) \u2014");
|
|
10168
|
+
err2("refusing to start: a verifier that appears after the agent runs would be agent-authored.");
|
|
10006
10169
|
return 2;
|
|
10007
10170
|
}
|
|
10008
10171
|
if (!opts.allowDirty) {
|
|
10009
10172
|
const dirty = git3(["status", "--porcelain"], cwd).trim();
|
|
10010
10173
|
if (dirty) {
|
|
10011
|
-
|
|
10012
|
-
|
|
10174
|
+
err2("tamperward run: working tree is dirty before the agent starts \u2014 the envelope cannot");
|
|
10175
|
+
err2("attribute changes. Commit or stash first, or pass --allow-dirty to own the risk.");
|
|
10013
10176
|
return 2;
|
|
10014
10177
|
}
|
|
10015
10178
|
}
|
|
@@ -10017,25 +10180,25 @@ function runEnvelope(opts) {
|
|
|
10017
10180
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
10018
10181
|
out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
|
|
10019
10182
|
if (!verifierBackend.available) {
|
|
10020
|
-
|
|
10021
|
-
|
|
10183
|
+
err2("tamperward run: the requested verifier backend cannot be established \u2014");
|
|
10184
|
+
err2(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
|
|
10022
10185
|
return 2;
|
|
10023
10186
|
}
|
|
10024
10187
|
if (verifierBackend.kind === "container") {
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10188
|
+
err2("tamperward run: container verification requires a frozen candidate handoff from an");
|
|
10189
|
+
err2("agent domain that cannot control the verifier engine. `run` executes the agent under");
|
|
10190
|
+
err2("this host identity, so it cannot honestly provide that separation. Failing closed");
|
|
10191
|
+
err2("before the agent starts; use standalone `tamperward verify` in trusted CI or after");
|
|
10192
|
+
err2("an externally isolated agent produces the frozen candidate artifact.");
|
|
10030
10193
|
return 2;
|
|
10031
10194
|
}
|
|
10032
10195
|
const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
|
|
10033
10196
|
if (dependencyEnvironment) {
|
|
10034
10197
|
out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
|
|
10035
10198
|
if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10199
|
+
err2("tamperward run: the verifier dependency environment cannot be attested \u2014");
|
|
10200
|
+
err2(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
|
|
10201
|
+
err2("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
|
|
10039
10202
|
return 2;
|
|
10040
10203
|
}
|
|
10041
10204
|
} else {
|
|
@@ -10055,37 +10218,37 @@ function runEnvelope(opts) {
|
|
|
10055
10218
|
const agentExit = agentRun.exit;
|
|
10056
10219
|
const agentTimedOut = agentRun.timedOut;
|
|
10057
10220
|
if (agentRun.failure && !agentTimedOut) {
|
|
10058
|
-
|
|
10221
|
+
err2(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}) \u2014 continuing to adjudicate the released tree.`);
|
|
10059
10222
|
}
|
|
10060
10223
|
if (agentTimedOut) {
|
|
10061
|
-
|
|
10224
|
+
err2(
|
|
10062
10225
|
`tamperward run: AGENT_TIMEOUT \u2014 wrapped agent exceeded ${opts.agentBudget}s; its owned process tree was terminated. Post-timeout adjudication continues.`
|
|
10063
10226
|
);
|
|
10064
10227
|
}
|
|
10065
10228
|
if (objectRewriteState(cwd) !== rewritesBefore) {
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10229
|
+
err2("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
|
|
10230
|
+
err2("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
|
|
10231
|
+
err2("read against the anchor we started from. That is the finding.");
|
|
10069
10232
|
out(`
|
|
10070
10233
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
10071
10234
|
return 1;
|
|
10072
10235
|
}
|
|
10073
10236
|
const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
10074
10237
|
if (!dependencyBeforeAdjudication.ok) {
|
|
10075
|
-
|
|
10076
|
-
|
|
10238
|
+
err2("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
|
|
10239
|
+
err2(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
|
|
10077
10240
|
if (!opts.allowDepDrift) {
|
|
10078
10241
|
out(`
|
|
10079
10242
|
tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
|
|
10080
10243
|
return 1;
|
|
10081
10244
|
}
|
|
10082
|
-
|
|
10245
|
+
err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
10083
10246
|
}
|
|
10084
10247
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
10085
10248
|
const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
|
|
10086
10249
|
if (isAncestor.status !== 0) {
|
|
10087
|
-
|
|
10088
|
-
|
|
10250
|
+
err2(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
10251
|
+
err2("the agent rewrote history out from under the anchor. That is the finding.");
|
|
10089
10252
|
out(`
|
|
10090
10253
|
tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
10091
10254
|
return 1;
|
|
@@ -10119,24 +10282,24 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
10119
10282
|
}
|
|
10120
10283
|
const mutatedDuringAdjudication = treeFingerprint(cwd, protectedIgnored) !== fpBefore;
|
|
10121
10284
|
if (mutatedDuringAdjudication) {
|
|
10122
|
-
|
|
10123
|
-
|
|
10285
|
+
err2("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
10286
|
+
err2("the verdict below would describe a tree that no longer exists.");
|
|
10124
10287
|
}
|
|
10125
10288
|
const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
10126
10289
|
const depsDrifted = Boolean(dependencyEnvironment) && !opts.allowDepDrift && !dependencyAfter.ok;
|
|
10127
10290
|
if (depsDrifted) {
|
|
10128
|
-
|
|
10129
|
-
|
|
10291
|
+
err2("tamperward run: the frozen dependency environment changed during this run \u2014");
|
|
10292
|
+
err2(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
|
|
10130
10293
|
}
|
|
10131
10294
|
const rewrote = objectRewriteState(cwd) !== rewritesBefore;
|
|
10132
10295
|
if (rewrote) {
|
|
10133
|
-
|
|
10134
|
-
|
|
10296
|
+
err2("tamperward run: git object-replacement state changed WHILE the tree was being adjudicated \u2014");
|
|
10297
|
+
err2("the checks above read the trusted base through an object layer that has since moved.");
|
|
10135
10298
|
}
|
|
10136
10299
|
const survivors = survivorsHoldingTree(cwd, spawnTicks);
|
|
10137
10300
|
if (survivors.length) {
|
|
10138
|
-
|
|
10139
|
-
|
|
10301
|
+
err2(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
|
|
10302
|
+
err2(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
|
|
10140
10303
|
}
|
|
10141
10304
|
const notQuiescent = mutatedDuringAdjudication || survivors.length > 0 || depsDrifted || rewrote;
|
|
10142
10305
|
const cannot = !notQuiescent && (diffCode === 2 || workCode === 2 || verifyCode === 2);
|
|
@@ -10202,6 +10365,16 @@ function parseInit(args) {
|
|
|
10202
10365
|
}
|
|
10203
10366
|
return o;
|
|
10204
10367
|
}
|
|
10368
|
+
function parseDoctor(args) {
|
|
10369
|
+
const o = {};
|
|
10370
|
+
for (let i = 0; i < args.length; i++) {
|
|
10371
|
+
const a = args[i];
|
|
10372
|
+
if (a === "--cwd") o.cwd = args[++i];
|
|
10373
|
+
else if (a === "--base") o.base = args[++i];
|
|
10374
|
+
else if (a === "--workflow") o.workflow = args[++i];
|
|
10375
|
+
}
|
|
10376
|
+
return o;
|
|
10377
|
+
}
|
|
10205
10378
|
function parseCheck(args) {
|
|
10206
10379
|
const o = {};
|
|
10207
10380
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -10276,6 +10449,11 @@ Formats:
|
|
|
10276
10449
|
every tool the gate must see.
|
|
10277
10450
|
--force-workflow replaces a workflow it
|
|
10278
10451
|
did not write, or one you have edited.
|
|
10452
|
+
tamperward doctor [--base R] validate the CI verifier's outer-time
|
|
10453
|
+
[--workflow F] [--cwd D] envelope against the trusted policy.
|
|
10454
|
+
Generated PR CI passes the base SHA so
|
|
10455
|
+
candidate policy edits cannot choose
|
|
10456
|
+
their own timing requirement.
|
|
10279
10457
|
|
|
10280
10458
|
Exit codes: 0 clean \xB7 1 a blocking finding (check), MASKED_FAILURE or SUITE_RED
|
|
10281
10459
|
(verify), any blocking finding or masked failure (run) \xB7 2 cannot
|
|
@@ -10300,6 +10478,8 @@ function main(argv2) {
|
|
|
10300
10478
|
return runAllow(parseAllow(rest));
|
|
10301
10479
|
case "init":
|
|
10302
10480
|
return runInit(parseInit(rest));
|
|
10481
|
+
case "doctor":
|
|
10482
|
+
return runDoctor(parseDoctor(rest));
|
|
10303
10483
|
case "verify":
|
|
10304
10484
|
return runVerify(parseVerify(rest));
|
|
10305
10485
|
case "run":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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",
|