tamperward 2.10.9 → 2.11.1
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 +31 -5
- package/dist/cli/index.js +613 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,8 +207,14 @@ request would need the verify label; the self-gate stays a diff-time gate.
|
|
|
207
207
|
|
|
208
208
|
This guarantee depends on a protected and immutable base, required status checks, a
|
|
209
209
|
pinned Tamperward version, and label permissions restricted to trusted humans.
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
The default local verifier is checkpointed same-host execution, not OS isolation.
|
|
211
|
+
An optional digest-pinned container backend isolates final verification with no network,
|
|
212
|
+
host dependency tree, HOME, temp, credential or socket sharing. Tamperward still is not a
|
|
213
|
+
semantic-correctness oracle: verifier output reports `oracle_assurance: suite-exit-only`
|
|
214
|
+
because candidate source still executes inside the configured suite process and can
|
|
215
|
+
terminate or interpose on that in-process oracle. `isolated-container` therefore means
|
|
216
|
+
execution-domain isolation, not semantic/oracle isolation. `tamperward run` also does
|
|
217
|
+
not pretend that a same-identity host agent is isolated from the Docker daemon.
|
|
212
218
|
|
|
213
219
|
Full assumptions and residual risks: [SPEC.md](./SPEC.md),
|
|
214
220
|
[SECURITY-ENVELOPE.md](./SECURITY-ENVELOPE.md), and the
|
|
@@ -246,11 +252,26 @@ verify:
|
|
|
246
252
|
command: npm test
|
|
247
253
|
budget: 300
|
|
248
254
|
inputs: ['scripts/**'] # what the command DELEGATES to
|
|
255
|
+
# Optional stronger final-verification boundary:
|
|
256
|
+
# backend: container
|
|
257
|
+
# image: ghcr.io/acme/verifier@sha256:<64-hex-digest>
|
|
249
258
|
```
|
|
250
259
|
|
|
260
|
+
`backend: local` is the default and is reported as `checkpointed-local`. With
|
|
261
|
+
`backend: container`, the image must be digest-pinned and already present on the fixed
|
|
262
|
+
local Docker daemon; Tamperward never pulls during adjudication. The materialised
|
|
263
|
+
candidate/pristine tree is mounted read-only, the image owns runtime/dependencies,
|
|
264
|
+
network is disabled, HOME/tmp are private, and optional suite output belongs in
|
|
265
|
+
`$TAMPERWARD_OUTPUT_DIR` (`/workspace-out`). Images that declare Dockerfile
|
|
266
|
+
`VOLUME` paths are refused: Docker mounts those paths writable even with
|
|
267
|
+
`--read-only`, which would undermine the immutable verifier-image boundary. Image
|
|
268
|
+
`ENTRYPOINT` is also overridden; the pinned image supplies the runtime/dependencies,
|
|
269
|
+
while the trusted policy's `verify.command` remains the command that is adjudicated.
|
|
270
|
+
|
|
251
271
|
That block is itself a guarded surface: changing the command, lowering the budget,
|
|
252
|
-
narrowing `inputs`,
|
|
253
|
-
agent can
|
|
272
|
+
narrowing `inputs`, removing `backend: container`, or changing its pinned image is
|
|
273
|
+
flagged as policy weakening — a verifier an agent can redirect is no verification at
|
|
274
|
+
all.
|
|
254
275
|
|
|
255
276
|
`inputs` names the files the command *executes*, so the pristine run gets the
|
|
256
277
|
base's copy of them too. A command token that names a file present at the base is
|
|
@@ -266,9 +287,14 @@ The four primitives:
|
|
|
266
287
|
npx tamperward check --staged # pre-commit view
|
|
267
288
|
npx tamperward check --diff "main...HEAD" # CI view over the PR's commit range
|
|
268
289
|
npx tamperward verify --base main # pristine-suite re-execution
|
|
269
|
-
npx tamperward run -- <agent command...> #
|
|
290
|
+
npx tamperward run -- <agent command...> # same-host envelope (local verifier only)
|
|
270
291
|
```
|
|
271
292
|
|
|
293
|
+
The isolated backend is a **frozen-artifact final verifier**. `tamperward run`
|
|
294
|
+
deliberately refuses `backend: container` before launching the agent because the agent
|
|
295
|
+
would share the host identity that controls Docker. Use isolated `tamperward verify`
|
|
296
|
+
from trusted CI, or after an externally isolated agent hands off the frozen candidate.
|
|
297
|
+
|
|
272
298
|
### CLI reference
|
|
273
299
|
|
|
274
300
|
Every flag below is what the command's parser actually reads (`src/cli/index.ts`,
|
package/dist/cli/index.js
CHANGED
|
@@ -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, resolve8) {
|
|
1655
|
+
const obj = resolve8(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, resolve7) {
|
|
|
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(resolve8(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 resolve8 = 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 = resolve8(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, resolve8);
|
|
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, resolve8);
|
|
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 resolve8 = 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(resolve8(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(resolve8(node.initializer));
|
|
1784
1784
|
res.ignore.push(...items);
|
|
1785
1785
|
if (opaque) res.opaqueIgnore = true;
|
|
1786
1786
|
}
|
|
@@ -3064,6 +3064,18 @@ function policyWeakening(before, after) {
|
|
|
3064
3064
|
`verify.inputs narrowed (removed ${droppedInputs.join(", ")}) \u2014 the pristine run would execute the candidate's copy of those files`
|
|
3065
3065
|
);
|
|
3066
3066
|
}
|
|
3067
|
+
const beforeBackend = bv.backend ?? "local";
|
|
3068
|
+
const afterBackend = av.backend ?? "local";
|
|
3069
|
+
if (beforeBackend === "container" && afterBackend !== "container") {
|
|
3070
|
+
reasons.push(
|
|
3071
|
+
`verify.backend lowered container \u2192 ${afterBackend} \u2014 final verification would leave the isolated trust domain`
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
if (beforeBackend === "container" && afterBackend === "container" && av.image !== bv.image) {
|
|
3075
|
+
reasons.push(
|
|
3076
|
+
`verify.image changed (${bv.image ?? "<missing>"} \u2192 ${av.image ?? "<missing>"}) \u2014 the trusted verifier image identity changed`
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3067
3079
|
}
|
|
3068
3080
|
}
|
|
3069
3081
|
return reasons;
|
|
@@ -6342,6 +6354,21 @@ function validate(r, where) {
|
|
|
6342
6354
|
bad(`verify.budget must be a positive number of seconds, got ${show(v.budget)}`);
|
|
6343
6355
|
}
|
|
6344
6356
|
if (v.inputs !== void 0 && !isStringList(v.inputs)) bad(`verify.inputs must be a list of globs, got ${show(v.inputs)}`);
|
|
6357
|
+
if (v.backend !== void 0 && v.backend !== "local" && v.backend !== "container") {
|
|
6358
|
+
bad(`verify.backend must be "local" or "container", got ${show(v.backend)}`);
|
|
6359
|
+
}
|
|
6360
|
+
if (v.image !== void 0 && typeof v.image !== "string") {
|
|
6361
|
+
bad(`verify.image must be an immutable container image reference, got ${show(v.image)}`);
|
|
6362
|
+
}
|
|
6363
|
+
const backend = v.backend ?? "local";
|
|
6364
|
+
if (backend === "container") {
|
|
6365
|
+
if (typeof v.image !== "string") bad('verify.image is required when verify.backend is "container"');
|
|
6366
|
+
if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(v.image)) {
|
|
6367
|
+
bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(v.image)}`);
|
|
6368
|
+
}
|
|
6369
|
+
} else if (v.image !== void 0) {
|
|
6370
|
+
bad('verify.image is only valid when verify.backend is "container"');
|
|
6371
|
+
}
|
|
6345
6372
|
}
|
|
6346
6373
|
}
|
|
6347
6374
|
function parsePolicy(raw, where = POLICY_FILE) {
|
|
@@ -6374,7 +6401,9 @@ function parsePolicy(raw, where = POLICY_FILE) {
|
|
|
6374
6401
|
// Rebuilding the block field-by-field means a field added later and
|
|
6375
6402
|
// not listed here is SILENTLY DROPPED — which is how `inputs` first
|
|
6376
6403
|
// shipped as a no-op that parsed, validated, and restored nothing.
|
|
6377
|
-
...Array.isArray(r.verify.inputs) ? { inputs: r.verify.inputs.map(String) } : {}
|
|
6404
|
+
...Array.isArray(r.verify.inputs) ? { inputs: r.verify.inputs.map(String) } : {},
|
|
6405
|
+
...r.verify.backend ? { backend: r.verify.backend } : {},
|
|
6406
|
+
...r.verify.image ? { image: r.verify.image } : {}
|
|
6378
6407
|
}
|
|
6379
6408
|
} : {}
|
|
6380
6409
|
};
|
|
@@ -8162,11 +8191,11 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
8162
8191
|
}
|
|
8163
8192
|
|
|
8164
8193
|
// src/cli/verify.ts
|
|
8165
|
-
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
8166
|
-
import { createHash as
|
|
8167
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as
|
|
8194
|
+
import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "node:child_process";
|
|
8195
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
8196
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8168
8197
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8169
|
-
import { dirname as
|
|
8198
|
+
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join15, relative as relative5, resolve as resolve6, sep as sep2 } from "node:path";
|
|
8170
8199
|
|
|
8171
8200
|
// src/fingerprint.ts
|
|
8172
8201
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
@@ -8547,7 +8576,429 @@ function dependencyEnvironmentSummary(descriptor) {
|
|
|
8547
8576
|
return "attested " + kinds + " (sha256 " + (descriptor.fingerprint?.slice(0, 12) ?? "unknown") + "...)";
|
|
8548
8577
|
}
|
|
8549
8578
|
|
|
8579
|
+
// src/verifier-backend.ts
|
|
8580
|
+
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
8581
|
+
import { spawnSync } from "node:child_process";
|
|
8582
|
+
import { accessSync, constants as constants2, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
|
|
8583
|
+
import { delimiter as delimiter2, dirname as dirname6, join as join14, resolve as resolve5 } from "node:path";
|
|
8584
|
+
var DIGEST_IMAGE = /^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i;
|
|
8585
|
+
var DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock";
|
|
8586
|
+
function identity() {
|
|
8587
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
8588
|
+
const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
|
|
8589
|
+
return { uid, groups };
|
|
8590
|
+
}
|
|
8591
|
+
function writableByCaller(path) {
|
|
8592
|
+
try {
|
|
8593
|
+
const st = statSync5(path);
|
|
8594
|
+
const { uid, groups } = identity();
|
|
8595
|
+
if (uid === 0) return true;
|
|
8596
|
+
if ((st.mode & 2) !== 0) return true;
|
|
8597
|
+
if (uid !== null && st.uid === uid && (st.mode & 128) !== 0) return true;
|
|
8598
|
+
if (groups.includes(st.gid) && (st.mode & 16) !== 0) return true;
|
|
8599
|
+
return false;
|
|
8600
|
+
} catch {
|
|
8601
|
+
return true;
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
function replaceableByCaller(path) {
|
|
8605
|
+
let current = resolve5(path);
|
|
8606
|
+
if (writableByCaller(current)) return true;
|
|
8607
|
+
current = dirname6(current);
|
|
8608
|
+
while (true) {
|
|
8609
|
+
if (writableByCaller(current)) return true;
|
|
8610
|
+
const parent = dirname6(current);
|
|
8611
|
+
if (parent === current) break;
|
|
8612
|
+
current = parent;
|
|
8613
|
+
}
|
|
8614
|
+
return false;
|
|
8615
|
+
}
|
|
8616
|
+
function findExecutable(name) {
|
|
8617
|
+
for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
|
|
8618
|
+
if (!raw) continue;
|
|
8619
|
+
const candidate = join14(raw, name);
|
|
8620
|
+
try {
|
|
8621
|
+
accessSync(candidate, constants2.X_OK);
|
|
8622
|
+
return realpathSync4(candidate);
|
|
8623
|
+
} catch {
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
return null;
|
|
8627
|
+
}
|
|
8628
|
+
function fileSha256(path) {
|
|
8629
|
+
return createHash9("sha256").update(readFileSync14(path)).digest("hex");
|
|
8630
|
+
}
|
|
8631
|
+
function engineClientEnv() {
|
|
8632
|
+
return {
|
|
8633
|
+
PATH: "/usr/bin:/bin",
|
|
8634
|
+
HOME: "/nonexistent",
|
|
8635
|
+
LANG: "C",
|
|
8636
|
+
LC_ALL: "C"
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
function dockerArgs(host, args) {
|
|
8640
|
+
return ["--host", host, ...args];
|
|
8641
|
+
}
|
|
8642
|
+
function dockerAvailable(enginePath, host) {
|
|
8643
|
+
try {
|
|
8644
|
+
const r = spawnSync(enginePath, dockerArgs(host, ["version"]), {
|
|
8645
|
+
stdio: "ignore",
|
|
8646
|
+
timeout: 5e3,
|
|
8647
|
+
killSignal: "SIGKILL",
|
|
8648
|
+
env: engineClientEnv()
|
|
8649
|
+
});
|
|
8650
|
+
return r.status === 0;
|
|
8651
|
+
} catch {
|
|
8652
|
+
return false;
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
function declaredImageVolumePaths(metadata) {
|
|
8656
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
8657
|
+
throw new Error("image metadata must be a Docker image-inspect object");
|
|
8658
|
+
}
|
|
8659
|
+
const config = metadata.Config;
|
|
8660
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
8661
|
+
throw new Error("image metadata Config must be an object");
|
|
8662
|
+
}
|
|
8663
|
+
const volumes = config.Volumes;
|
|
8664
|
+
if (volumes === void 0 || volumes === null) return [];
|
|
8665
|
+
if (typeof volumes !== "object" || Array.isArray(volumes)) {
|
|
8666
|
+
throw new Error("image metadata Config.Volumes must be a mapping or null");
|
|
8667
|
+
}
|
|
8668
|
+
return Object.keys(volumes).sort();
|
|
8669
|
+
}
|
|
8670
|
+
function inspectImageMetadata(enginePath, host, image) {
|
|
8671
|
+
try {
|
|
8672
|
+
const r = spawnSync(enginePath, dockerArgs(host, ["image", "inspect", image]), {
|
|
8673
|
+
encoding: "utf8",
|
|
8674
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8675
|
+
timeout: 1e4,
|
|
8676
|
+
killSignal: "SIGKILL",
|
|
8677
|
+
env: engineClientEnv()
|
|
8678
|
+
});
|
|
8679
|
+
if (r.error || r.status !== 0) {
|
|
8680
|
+
return {
|
|
8681
|
+
metadata: null,
|
|
8682
|
+
reason: "the digest-pinned verifier image is not present locally; TamperWard refuses to pull during adjudication"
|
|
8683
|
+
};
|
|
8684
|
+
}
|
|
8685
|
+
const parsed = JSON.parse(r.stdout ?? "");
|
|
8686
|
+
if (!Array.isArray(parsed) || parsed.length !== 1) {
|
|
8687
|
+
return { metadata: null, reason: "Docker returned malformed verifier image metadata" };
|
|
8688
|
+
}
|
|
8689
|
+
return { metadata: parsed[0] };
|
|
8690
|
+
} catch {
|
|
8691
|
+
return { metadata: null, reason: "Docker returned unreadable verifier image metadata" };
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
function engineStable(backend) {
|
|
8695
|
+
if (!backend.enginePath || !backend.engineSha256) return false;
|
|
8696
|
+
try {
|
|
8697
|
+
if (realpathSync4(backend.enginePath) !== backend.enginePath) return false;
|
|
8698
|
+
if (replaceableByCaller(backend.enginePath)) return false;
|
|
8699
|
+
return fileSha256(backend.enginePath) === backend.engineSha256;
|
|
8700
|
+
} catch {
|
|
8701
|
+
return false;
|
|
8702
|
+
}
|
|
8703
|
+
}
|
|
8704
|
+
function prepareVerifierBackend(verify) {
|
|
8705
|
+
const kind = verify?.backend ?? "local";
|
|
8706
|
+
if (kind === "local") {
|
|
8707
|
+
return { kind: "local", trust: "checkpointed-local", available: true };
|
|
8708
|
+
}
|
|
8709
|
+
const image = verify?.image;
|
|
8710
|
+
if (!image || !DIGEST_IMAGE.test(image)) {
|
|
8711
|
+
return {
|
|
8712
|
+
kind: "container",
|
|
8713
|
+
trust: "isolated-container",
|
|
8714
|
+
available: false,
|
|
8715
|
+
...image ? { image } : {},
|
|
8716
|
+
reason: "container verifier requires an immutable runtime-safe image reference name@sha256:<64 hex>"
|
|
8717
|
+
};
|
|
8718
|
+
}
|
|
8719
|
+
const forced = process.env.TAMPERWARD_CONTAINER_ENGINE;
|
|
8720
|
+
if (forced && forced !== "docker") {
|
|
8721
|
+
return {
|
|
8722
|
+
kind: "container",
|
|
8723
|
+
trust: "isolated-container",
|
|
8724
|
+
available: false,
|
|
8725
|
+
image,
|
|
8726
|
+
reason: `unsupported isolated verifier engine ${JSON.stringify(forced)}; Docker is the tested backend`
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
const enginePath = findExecutable("docker");
|
|
8730
|
+
if (!enginePath) {
|
|
8731
|
+
return {
|
|
8732
|
+
kind: "container",
|
|
8733
|
+
trust: "isolated-container",
|
|
8734
|
+
available: false,
|
|
8735
|
+
image,
|
|
8736
|
+
reason: "Docker executable was not found on the operator PATH"
|
|
8737
|
+
};
|
|
8738
|
+
}
|
|
8739
|
+
if (replaceableByCaller(enginePath)) {
|
|
8740
|
+
return {
|
|
8741
|
+
kind: "container",
|
|
8742
|
+
trust: "isolated-container",
|
|
8743
|
+
available: false,
|
|
8744
|
+
image,
|
|
8745
|
+
engine: "docker",
|
|
8746
|
+
enginePath,
|
|
8747
|
+
reason: "Docker engine executable is writable/replaceable by the caller identity; it cannot be verifier authority"
|
|
8748
|
+
};
|
|
8749
|
+
}
|
|
8750
|
+
const daemonHost = DEFAULT_DOCKER_HOST;
|
|
8751
|
+
if (!dockerAvailable(enginePath, daemonHost)) {
|
|
8752
|
+
return {
|
|
8753
|
+
kind: "container",
|
|
8754
|
+
trust: "isolated-container",
|
|
8755
|
+
available: false,
|
|
8756
|
+
image,
|
|
8757
|
+
engine: "docker",
|
|
8758
|
+
enginePath,
|
|
8759
|
+
daemonHost,
|
|
8760
|
+
reason: `Docker is unavailable at the fixed verifier endpoint ${daemonHost}`
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
const inspectedImage = inspectImageMetadata(enginePath, daemonHost, image);
|
|
8764
|
+
if (!inspectedImage.metadata) {
|
|
8765
|
+
return {
|
|
8766
|
+
kind: "container",
|
|
8767
|
+
trust: "isolated-container",
|
|
8768
|
+
available: false,
|
|
8769
|
+
image,
|
|
8770
|
+
engine: "docker",
|
|
8771
|
+
enginePath,
|
|
8772
|
+
daemonHost,
|
|
8773
|
+
reason: inspectedImage.reason ?? "Docker could not attest the verifier image metadata"
|
|
8774
|
+
};
|
|
8775
|
+
}
|
|
8776
|
+
let declaredVolumes;
|
|
8777
|
+
try {
|
|
8778
|
+
declaredVolumes = declaredImageVolumePaths(inspectedImage.metadata);
|
|
8779
|
+
} catch (e) {
|
|
8780
|
+
return {
|
|
8781
|
+
kind: "container",
|
|
8782
|
+
trust: "isolated-container",
|
|
8783
|
+
available: false,
|
|
8784
|
+
image,
|
|
8785
|
+
engine: "docker",
|
|
8786
|
+
enginePath,
|
|
8787
|
+
daemonHost,
|
|
8788
|
+
reason: `Docker verifier image metadata is invalid: ${e instanceof Error ? e.message : String(e)}`
|
|
8789
|
+
};
|
|
8790
|
+
}
|
|
8791
|
+
if (declaredVolumes.length > 0) {
|
|
8792
|
+
return {
|
|
8793
|
+
kind: "container",
|
|
8794
|
+
trust: "isolated-container",
|
|
8795
|
+
available: false,
|
|
8796
|
+
image,
|
|
8797
|
+
engine: "docker",
|
|
8798
|
+
enginePath,
|
|
8799
|
+
daemonHost,
|
|
8800
|
+
reason: `the verifier image declares writable Docker VOLUME path(s): ${declaredVolumes.join(", ")}; --read-only does not make image-declared volumes immutable`
|
|
8801
|
+
};
|
|
8802
|
+
}
|
|
8803
|
+
return {
|
|
8804
|
+
kind: "container",
|
|
8805
|
+
trust: "isolated-container",
|
|
8806
|
+
available: true,
|
|
8807
|
+
image,
|
|
8808
|
+
engine: "docker",
|
|
8809
|
+
enginePath,
|
|
8810
|
+
engineSha256: fileSha256(enginePath),
|
|
8811
|
+
daemonHost
|
|
8812
|
+
};
|
|
8813
|
+
}
|
|
8814
|
+
function verifierBackendReport(backend) {
|
|
8815
|
+
return {
|
|
8816
|
+
kind: backend.kind,
|
|
8817
|
+
trust: backend.trust,
|
|
8818
|
+
available: backend.available,
|
|
8819
|
+
...backend.image ? { image: backend.image } : {},
|
|
8820
|
+
...backend.engine ? { engine: backend.engine } : {},
|
|
8821
|
+
...backend.reason ? { reason: backend.reason } : {}
|
|
8822
|
+
};
|
|
8823
|
+
}
|
|
8824
|
+
function verifierBackendSummary(backend) {
|
|
8825
|
+
if (backend.kind === "local") {
|
|
8826
|
+
return "local (checkpointed same-host; weaker trust boundary)";
|
|
8827
|
+
}
|
|
8828
|
+
const identity2 = backend.image ?? "<missing image>";
|
|
8829
|
+
if (!backend.available) {
|
|
8830
|
+
return `container (isolated; unavailable: ${backend.reason ?? "unknown reason"}; image ${identity2})`;
|
|
8831
|
+
}
|
|
8832
|
+
return `container (isolated via Docker; image ${identity2})`;
|
|
8833
|
+
}
|
|
8834
|
+
function cleanupContainer(backend, name) {
|
|
8835
|
+
if (!backend.enginePath || !backend.daemonHost || !engineStable(backend)) return;
|
|
8836
|
+
try {
|
|
8837
|
+
spawnSync(backend.enginePath, dockerArgs(backend.daemonHost, ["rm", "-f", "-v", name]), {
|
|
8838
|
+
stdio: "ignore",
|
|
8839
|
+
timeout: 1e4,
|
|
8840
|
+
killSignal: "SIGKILL",
|
|
8841
|
+
env: engineClientEnv()
|
|
8842
|
+
});
|
|
8843
|
+
} catch {
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
function containerRunArgs(input) {
|
|
8847
|
+
return [
|
|
8848
|
+
"run",
|
|
8849
|
+
"--name",
|
|
8850
|
+
input.name,
|
|
8851
|
+
// Keep the stopped container until the trusted host has inspected .State.
|
|
8852
|
+
// Docker CLI exit codes overlap candidate exit codes; container metadata is
|
|
8853
|
+
// the authority that tells us whether the suite actually ran.
|
|
8854
|
+
"--pull",
|
|
8855
|
+
"never",
|
|
8856
|
+
"--network",
|
|
8857
|
+
"none",
|
|
8858
|
+
"--read-only",
|
|
8859
|
+
"--cap-drop",
|
|
8860
|
+
"ALL",
|
|
8861
|
+
"--security-opt",
|
|
8862
|
+
"no-new-privileges",
|
|
8863
|
+
"--pids-limit",
|
|
8864
|
+
"256",
|
|
8865
|
+
"--user",
|
|
8866
|
+
`${input.uid}:${input.gid}`,
|
|
8867
|
+
// Frozen candidate/pristine input is immutable in the verifier. A suite
|
|
8868
|
+
// needing outputs writes to /workspace-out, HOME or /tmp instead.
|
|
8869
|
+
"--mount",
|
|
8870
|
+
`type=bind,src=${input.workspace},dst=/workspace,ro`,
|
|
8871
|
+
"--tmpfs",
|
|
8872
|
+
"/tmp:rw,nosuid,nodev,mode=1777",
|
|
8873
|
+
"--tmpfs",
|
|
8874
|
+
`/home/tamperward:rw,nosuid,nodev,mode=700,uid=${input.uid},gid=${input.gid}`,
|
|
8875
|
+
"--tmpfs",
|
|
8876
|
+
`/workspace-out:rw,nosuid,nodev,mode=700,uid=${input.uid},gid=${input.gid}`,
|
|
8877
|
+
"--workdir",
|
|
8878
|
+
"/workspace",
|
|
8879
|
+
"--env",
|
|
8880
|
+
"HOME=/home/tamperward",
|
|
8881
|
+
"--env",
|
|
8882
|
+
"TMPDIR=/tmp",
|
|
8883
|
+
"--env",
|
|
8884
|
+
"CI=1",
|
|
8885
|
+
"--env",
|
|
8886
|
+
"TAMPERWARD_OUTPUT_DIR=/workspace-out",
|
|
8887
|
+
"--env",
|
|
8888
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
8889
|
+
// The image supplies immutable runtime/dependencies, not command policy.
|
|
8890
|
+
// Docker otherwise appends our command to an image ENTRYPOINT.
|
|
8891
|
+
"--entrypoint",
|
|
8892
|
+
"/bin/sh",
|
|
8893
|
+
input.image,
|
|
8894
|
+
"-c",
|
|
8895
|
+
input.command
|
|
8896
|
+
];
|
|
8897
|
+
}
|
|
8898
|
+
function runContainerStage(backend, dir, command, budgetSecs) {
|
|
8899
|
+
const t0 = Date.now();
|
|
8900
|
+
if (backend.kind !== "container" || !backend.available || !backend.image || !backend.enginePath || !backend.engineSha256 || !backend.daemonHost || !engineStable(backend)) {
|
|
8901
|
+
return {
|
|
8902
|
+
exit: null,
|
|
8903
|
+
secs: 0,
|
|
8904
|
+
failure: "backend",
|
|
8905
|
+
reason: "isolated verifier authority is incomplete or changed before stage execution"
|
|
8906
|
+
};
|
|
8907
|
+
}
|
|
8908
|
+
const name = `tamperward-verify-${process.pid}-${randomUUID().slice(0, 12)}`;
|
|
8909
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 65534;
|
|
8910
|
+
const gid = typeof process.getgid === "function" ? process.getgid() : 65534;
|
|
8911
|
+
const args = containerRunArgs({
|
|
8912
|
+
image: backend.image,
|
|
8913
|
+
name,
|
|
8914
|
+
workspace: resolve5(dir),
|
|
8915
|
+
command,
|
|
8916
|
+
uid,
|
|
8917
|
+
gid
|
|
8918
|
+
});
|
|
8919
|
+
try {
|
|
8920
|
+
const r = spawnSync(
|
|
8921
|
+
backend.enginePath,
|
|
8922
|
+
dockerArgs(backend.daemonHost, args),
|
|
8923
|
+
{
|
|
8924
|
+
stdio: "ignore",
|
|
8925
|
+
timeout: budgetSecs * 1e3,
|
|
8926
|
+
killSignal: "SIGKILL",
|
|
8927
|
+
env: engineClientEnv()
|
|
8928
|
+
}
|
|
8929
|
+
);
|
|
8930
|
+
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
8931
|
+
if (r.error) {
|
|
8932
|
+
cleanupContainer(backend, name);
|
|
8933
|
+
const code = r.error.code;
|
|
8934
|
+
return code === "ETIMEDOUT" ? { exit: null, secs, failure: "budget", reason: "verifier stage exceeded its budget" } : { exit: null, secs, failure: "backend", reason: `Docker client failed: ${r.error.message}` };
|
|
8935
|
+
}
|
|
8936
|
+
if (r.status === null) {
|
|
8937
|
+
cleanupContainer(backend, name);
|
|
8938
|
+
return {
|
|
8939
|
+
exit: null,
|
|
8940
|
+
secs,
|
|
8941
|
+
failure: "backend",
|
|
8942
|
+
reason: `Docker client did not return a status${r.signal ? ` (signal ${r.signal})` : ""}`
|
|
8943
|
+
};
|
|
8944
|
+
}
|
|
8945
|
+
if (!engineStable(backend)) {
|
|
8946
|
+
return {
|
|
8947
|
+
exit: null,
|
|
8948
|
+
secs,
|
|
8949
|
+
failure: "backend",
|
|
8950
|
+
reason: "Docker engine identity changed before verifier result inspection"
|
|
8951
|
+
};
|
|
8952
|
+
}
|
|
8953
|
+
const inspected = spawnSync(
|
|
8954
|
+
backend.enginePath,
|
|
8955
|
+
dockerArgs(backend.daemonHost, ["inspect", "--format", "{{json .State}}", name]),
|
|
8956
|
+
{
|
|
8957
|
+
encoding: "utf8",
|
|
8958
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8959
|
+
timeout: 1e4,
|
|
8960
|
+
killSignal: "SIGKILL",
|
|
8961
|
+
env: engineClientEnv()
|
|
8962
|
+
}
|
|
8963
|
+
);
|
|
8964
|
+
let state = null;
|
|
8965
|
+
if (!inspected.error && inspected.status === 0) {
|
|
8966
|
+
try {
|
|
8967
|
+
state = JSON.parse((inspected.stdout ?? "").trim());
|
|
8968
|
+
} catch {
|
|
8969
|
+
state = null;
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8972
|
+
cleanupContainer(backend, name);
|
|
8973
|
+
if (!state || state.Status !== "exited" || typeof state.ExitCode !== "number" || state.OOMKilled || (state.Error ?? "") !== "") {
|
|
8974
|
+
return {
|
|
8975
|
+
exit: null,
|
|
8976
|
+
secs,
|
|
8977
|
+
failure: "backend",
|
|
8978
|
+
reason: state?.OOMKilled ? "verifier container was OOM-killed" : state?.Error ? `verifier container runtime error: ${state.Error}` : "Docker did not provide a trustworthy exited-container state"
|
|
8979
|
+
};
|
|
8980
|
+
}
|
|
8981
|
+
return { exit: state.ExitCode, secs };
|
|
8982
|
+
} catch (e) {
|
|
8983
|
+
cleanupContainer(backend, name);
|
|
8984
|
+
return {
|
|
8985
|
+
exit: null,
|
|
8986
|
+
secs: Math.round((Date.now() - t0) / 1e3),
|
|
8987
|
+
failure: "backend",
|
|
8988
|
+
reason: `Docker verifier execution failed: ${e instanceof Error ? e.message : String(e)}`
|
|
8989
|
+
};
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
|
|
8550
8993
|
// src/cli/verify.ts
|
|
8994
|
+
function oracleAssuranceReport() {
|
|
8995
|
+
return {
|
|
8996
|
+
level: "suite-exit-only",
|
|
8997
|
+
semantic_isolation: false,
|
|
8998
|
+
candidate_code_executes_in_oracle_process: true,
|
|
8999
|
+
limitation: "candidate source executes inside the configured suite process and may terminate or interpose on that in-process oracle; isolated-container is execution-domain isolation, not semantic/oracle isolation"
|
|
9000
|
+
};
|
|
9001
|
+
}
|
|
8551
9002
|
var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
|
|
8552
9003
|
var VERIFICATION_SURFACE = [
|
|
8553
9004
|
// Python / pytest — read from the rootdir, and conftest at any depth
|
|
@@ -8632,11 +9083,11 @@ function resolveBase(base, cwd) {
|
|
|
8632
9083
|
}
|
|
8633
9084
|
}
|
|
8634
9085
|
function baseIsAncestorOfHead(base, cwd) {
|
|
8635
|
-
const r =
|
|
9086
|
+
const r = spawnSync2("git", ["merge-base", "--is-ancestor", base, "HEAD"], { cwd, env: trustedGitEnv() });
|
|
8636
9087
|
return r.status === 0;
|
|
8637
9088
|
}
|
|
8638
9089
|
function inside2(root, path) {
|
|
8639
|
-
const rel = relative5(
|
|
9090
|
+
const rel = relative5(resolve6(root), resolve6(path));
|
|
8640
9091
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute10(rel);
|
|
8641
9092
|
}
|
|
8642
9093
|
function linkParts(path) {
|
|
@@ -8653,7 +9104,7 @@ function rootedLinkTarget(target) {
|
|
|
8653
9104
|
return isAbsolute10(target) || driveRelativeLinkTarget(target);
|
|
8654
9105
|
}
|
|
8655
9106
|
function safeSymlink(target, out2, root, label) {
|
|
8656
|
-
if (rootedLinkTarget(target) || !inside2(root,
|
|
9107
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve6(dirname7(out2), target))) {
|
|
8657
9108
|
throw linkEscape(label, target);
|
|
8658
9109
|
}
|
|
8659
9110
|
rmSync3(out2, { force: true });
|
|
@@ -8661,7 +9112,7 @@ function safeSymlink(target, out2, root, label) {
|
|
|
8661
9112
|
}
|
|
8662
9113
|
function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
8663
9114
|
let domain = "tree";
|
|
8664
|
-
let stack = linkParts(relative5(root,
|
|
9115
|
+
let stack = linkParts(relative5(root, dirname7(out2)));
|
|
8665
9116
|
const pending = linkParts(target);
|
|
8666
9117
|
const seen = /* @__PURE__ */ new Set();
|
|
8667
9118
|
let hops = 0;
|
|
@@ -8679,7 +9130,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8679
9130
|
continue;
|
|
8680
9131
|
}
|
|
8681
9132
|
const base = domain === "tree" ? root : dependencyRoot;
|
|
8682
|
-
const current =
|
|
9133
|
+
const current = join15(base, ...stack);
|
|
8683
9134
|
let st;
|
|
8684
9135
|
try {
|
|
8685
9136
|
st = lstatSync4(current);
|
|
@@ -8705,7 +9156,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
8705
9156
|
throw linkEscape(label, target);
|
|
8706
9157
|
}
|
|
8707
9158
|
stack = [];
|
|
8708
|
-
pending.unshift(...linkParts(relative5(dependencyRoot,
|
|
9159
|
+
pending.unshift(...linkParts(relative5(dependencyRoot, resolve6(next))));
|
|
8709
9160
|
} else {
|
|
8710
9161
|
pending.unshift(...linkParts(next));
|
|
8711
9162
|
}
|
|
@@ -8715,7 +9166,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
8715
9166
|
const parts = rel.split("/").slice(0, -1);
|
|
8716
9167
|
let current = cwd;
|
|
8717
9168
|
for (const part of parts) {
|
|
8718
|
-
current =
|
|
9169
|
+
current = join15(current, part);
|
|
8719
9170
|
let st;
|
|
8720
9171
|
try {
|
|
8721
9172
|
st = lstatSync4(current);
|
|
@@ -8731,7 +9182,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
8731
9182
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8732
9183
|
const links = [];
|
|
8733
9184
|
for (const rel of listed) {
|
|
8734
|
-
const src =
|
|
9185
|
+
const src = join15(cwd, rel);
|
|
8735
9186
|
rejectLinkedParent(cwd, rel);
|
|
8736
9187
|
let st;
|
|
8737
9188
|
try {
|
|
@@ -8739,8 +9190,8 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
8739
9190
|
} catch {
|
|
8740
9191
|
continue;
|
|
8741
9192
|
}
|
|
8742
|
-
const out2 =
|
|
8743
|
-
mkdirSync6(
|
|
9193
|
+
const out2 = join15(dest, rel);
|
|
9194
|
+
mkdirSync6(dirname7(out2), { recursive: true });
|
|
8744
9195
|
dropSymlink(out2);
|
|
8745
9196
|
if (st.isSymbolicLink()) {
|
|
8746
9197
|
const target = readlinkSync3(src);
|
|
@@ -8756,7 +9207,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
8756
9207
|
for (const link of links) {
|
|
8757
9208
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
8758
9209
|
}
|
|
8759
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
9210
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join15(dest, "node_modules"), "dir");
|
|
8760
9211
|
}
|
|
8761
9212
|
function dropSymlink(p) {
|
|
8762
9213
|
try {
|
|
@@ -8785,15 +9236,15 @@ function baseEntries(base, cwd) {
|
|
|
8785
9236
|
});
|
|
8786
9237
|
}
|
|
8787
9238
|
function overlayDigest(dest, paths) {
|
|
8788
|
-
const h =
|
|
9239
|
+
const h = createHash10("sha256");
|
|
8789
9240
|
for (const rel of [...paths].sort()) {
|
|
8790
9241
|
h.update(rel);
|
|
8791
9242
|
h.update("\0");
|
|
8792
|
-
const p =
|
|
9243
|
+
const p = join15(dest, rel);
|
|
8793
9244
|
try {
|
|
8794
9245
|
const st = lstatSync4(p);
|
|
8795
9246
|
h.update(String(st.mode));
|
|
8796
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
9247
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync15(p));
|
|
8797
9248
|
} catch {
|
|
8798
9249
|
h.update("<gone>");
|
|
8799
9250
|
}
|
|
@@ -8817,8 +9268,8 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
8817
9268
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
8818
9269
|
}
|
|
8819
9270
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
8820
|
-
const out2 =
|
|
8821
|
-
mkdirSync6(
|
|
9271
|
+
const out2 = join15(dest, e.path);
|
|
9272
|
+
mkdirSync6(dirname7(out2), { recursive: true });
|
|
8822
9273
|
rmSync3(out2, { force: true });
|
|
8823
9274
|
if (e.mode === "120000") {
|
|
8824
9275
|
const target = content.toString("utf8");
|
|
@@ -8834,7 +9285,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
8834
9285
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8835
9286
|
for (const rel of inCopy) {
|
|
8836
9287
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
8837
|
-
rmSync3(
|
|
9288
|
+
rmSync3(join15(dest, rel), { force: true });
|
|
8838
9289
|
removed++;
|
|
8839
9290
|
}
|
|
8840
9291
|
for (const link of restoredLinks) {
|
|
@@ -8879,8 +9330,8 @@ function suiteEnv(scratch) {
|
|
|
8879
9330
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
8880
9331
|
env[k] = v;
|
|
8881
9332
|
}
|
|
8882
|
-
const userRc =
|
|
8883
|
-
const globalRc =
|
|
9333
|
+
const userRc = join15(scratch, "npmrc-user");
|
|
9334
|
+
const globalRc = join15(scratch, "npmrc-global");
|
|
8884
9335
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
8885
9336
|
env.npm_config_userconfig = userRc;
|
|
8886
9337
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -8888,12 +9339,12 @@ function suiteEnv(scratch) {
|
|
|
8888
9339
|
env.YARN_IGNORE_PATH = "1";
|
|
8889
9340
|
return env;
|
|
8890
9341
|
}
|
|
8891
|
-
function
|
|
9342
|
+
function runLocalSuite(dir, cmd, budgetSecs) {
|
|
8892
9343
|
const t0 = Date.now();
|
|
8893
|
-
const outDir = mkdtempSync2(
|
|
8894
|
-
const outFile =
|
|
9344
|
+
const outDir = mkdtempSync2(join15(tmpdir2(), "tw-verify-run-"));
|
|
9345
|
+
const outFile = join15(outDir, "result.json");
|
|
8895
9346
|
try {
|
|
8896
|
-
|
|
9347
|
+
spawnSync2(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
8897
9348
|
cwd: dir,
|
|
8898
9349
|
env: suiteEnv(outDir),
|
|
8899
9350
|
stdio: "ignore",
|
|
@@ -8904,7 +9355,7 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
8904
9355
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
8905
9356
|
let r;
|
|
8906
9357
|
try {
|
|
8907
|
-
r = JSON.parse(
|
|
9358
|
+
r = JSON.parse(readFileSync15(outFile, "utf8"));
|
|
8908
9359
|
} catch {
|
|
8909
9360
|
return { exit: null, secs };
|
|
8910
9361
|
}
|
|
@@ -8948,14 +9399,37 @@ function runVerify(opts) {
|
|
|
8948
9399
|
return 2;
|
|
8949
9400
|
}
|
|
8950
9401
|
const budget = opts.budget ?? policy.verify?.budget ?? 300;
|
|
8951
|
-
const
|
|
8952
|
-
const
|
|
8953
|
-
if (
|
|
9402
|
+
const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
|
|
9403
|
+
const backendReport = () => verifierBackendReport(verifierBackend);
|
|
9404
|
+
if (!verifierBackend.available) {
|
|
9405
|
+
if (opts.json) {
|
|
9406
|
+
out2(JSON.stringify({
|
|
9407
|
+
verdict: "CANNOT_VERIFY",
|
|
9408
|
+
reason: "VERIFIER_BACKEND_UNAVAILABLE",
|
|
9409
|
+
verifier_backend: backendReport(),
|
|
9410
|
+
oracle_assurance: oracleAssuranceReport()
|
|
9411
|
+
}));
|
|
9412
|
+
} else {
|
|
9413
|
+
out2("verify: isolated verifier backend is unavailable \u2014 failing closed");
|
|
9414
|
+
out2("verify: " + (verifierBackend.reason ?? "unknown backend failure"));
|
|
9415
|
+
}
|
|
9416
|
+
return 2;
|
|
9417
|
+
}
|
|
9418
|
+
const isolated = verifierBackend.kind === "container";
|
|
9419
|
+
const dependencyEnvironment = isolated ? null : opts.dependencyEnvironment ?? discoverDependencyEnvironment(cwd, cmd);
|
|
9420
|
+
const dependencyReport = () => dependencyEnvironment ? dependencyEnvironmentReport(dependencyEnvironment) : {
|
|
9421
|
+
status: "verifier-owned",
|
|
9422
|
+
roots: [],
|
|
9423
|
+
image: verifierBackend.image
|
|
9424
|
+
};
|
|
9425
|
+
if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
|
|
8954
9426
|
if (opts.json) {
|
|
8955
9427
|
out2(JSON.stringify({
|
|
8956
9428
|
verdict: "CANNOT_VERIFY",
|
|
8957
9429
|
reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
|
|
8958
|
-
|
|
9430
|
+
verifier_backend: backendReport(),
|
|
9431
|
+
dependency_environment: dependencyReport(),
|
|
9432
|
+
oracle_assurance: oracleAssuranceReport()
|
|
8959
9433
|
}));
|
|
8960
9434
|
} else {
|
|
8961
9435
|
out2("verify: dependency environment is not attestable \u2014 failing closed");
|
|
@@ -8963,7 +9437,9 @@ function runVerify(opts) {
|
|
|
8963
9437
|
}
|
|
8964
9438
|
return 2;
|
|
8965
9439
|
}
|
|
8966
|
-
const frozenNodeModules = dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null;
|
|
9440
|
+
const frozenNodeModules = dependencyEnvironment ? dependencyEnvironment.roots.find((root) => root.kind === "node_modules")?.realPath ?? null : null;
|
|
9441
|
+
const checkDeps = () => dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
9442
|
+
const runStage = (dir) => isolated ? runContainerStage(verifierBackend, dir, cmd, budget) : runLocalSuite(dir, cmd, budget);
|
|
8967
9443
|
let base;
|
|
8968
9444
|
try {
|
|
8969
9445
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
@@ -8971,15 +9447,15 @@ function runVerify(opts) {
|
|
|
8971
9447
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
8972
9448
|
return 2;
|
|
8973
9449
|
}
|
|
8974
|
-
const visRoot = mkdtempSync2(
|
|
8975
|
-
const visDir =
|
|
9450
|
+
const visRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-vis-"));
|
|
9451
|
+
const visDir = join15(visRoot, "t");
|
|
8976
9452
|
const cleanup = (dirs) => {
|
|
8977
9453
|
if (opts.keep) return;
|
|
8978
9454
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
8979
9455
|
};
|
|
8980
9456
|
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
8981
9457
|
const treeBefore = treeFingerprint(cwd, protectedIgnored);
|
|
8982
|
-
const dependencyAtEntry =
|
|
9458
|
+
const dependencyAtEntry = checkDeps();
|
|
8983
9459
|
if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
|
|
8984
9460
|
cleanup([visRoot]);
|
|
8985
9461
|
out2("verify: the frozen dependency environment changed before the visible suite ran \u2014");
|
|
@@ -8994,22 +9470,39 @@ function runVerify(opts) {
|
|
|
8994
9470
|
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
8995
9471
|
return 2;
|
|
8996
9472
|
}
|
|
8997
|
-
const visible =
|
|
9473
|
+
const visible = runStage(visDir);
|
|
9474
|
+
if (visible.failure === "backend") {
|
|
9475
|
+
cleanup([visRoot]);
|
|
9476
|
+
if (opts.json) {
|
|
9477
|
+
out2(JSON.stringify({
|
|
9478
|
+
verdict: "CANNOT_VERIFY",
|
|
9479
|
+
reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
9480
|
+
stage: "visible",
|
|
9481
|
+
detail: visible.reason,
|
|
9482
|
+
verifier_backend: backendReport(),
|
|
9483
|
+
oracle_assurance: oracleAssuranceReport()
|
|
9484
|
+
}));
|
|
9485
|
+
} else {
|
|
9486
|
+
out2("verify: isolated verifier backend failed while running the visible stage \u2014 failing closed");
|
|
9487
|
+
if (visible.reason) out2("verify: " + visible.reason);
|
|
9488
|
+
}
|
|
9489
|
+
return 2;
|
|
9490
|
+
}
|
|
8998
9491
|
if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
|
|
8999
9492
|
cleanup([visRoot]);
|
|
9000
9493
|
out2("verify: the working tree changed while the visible suite was running \u2014 the pristine copy");
|
|
9001
9494
|
out2("would be materialised from a tree the candidate just edited. Failing closed, not open.");
|
|
9002
9495
|
return 2;
|
|
9003
9496
|
}
|
|
9004
|
-
const dependencyAfterVisible =
|
|
9497
|
+
const dependencyAfterVisible = checkDeps();
|
|
9005
9498
|
if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
|
|
9006
9499
|
cleanup([visRoot]);
|
|
9007
9500
|
out2("verify: the frozen dependency environment changed while the visible suite was running \u2014");
|
|
9008
9501
|
out2((dependencyAfterVisible.reason ?? "dependency identity changed") + ". Failing closed, not open.");
|
|
9009
9502
|
return 2;
|
|
9010
9503
|
}
|
|
9011
|
-
const priRoot = mkdtempSync2(
|
|
9012
|
-
const priDir =
|
|
9504
|
+
const priRoot = mkdtempSync2(join15(tmpdir2(), "tw-verify-pri-"));
|
|
9505
|
+
const priDir = join15(priRoot, "t");
|
|
9013
9506
|
let restored = [];
|
|
9014
9507
|
let removedAdded = 0;
|
|
9015
9508
|
try {
|
|
@@ -9029,10 +9522,27 @@ function runVerify(opts) {
|
|
|
9029
9522
|
return 2;
|
|
9030
9523
|
}
|
|
9031
9524
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
9032
|
-
const pristine =
|
|
9525
|
+
const pristine = runStage(priDir);
|
|
9526
|
+
if (pristine.failure === "backend") {
|
|
9527
|
+
cleanup([visRoot, priRoot]);
|
|
9528
|
+
if (opts.json) {
|
|
9529
|
+
out2(JSON.stringify({
|
|
9530
|
+
verdict: "CANNOT_VERIFY",
|
|
9531
|
+
reason: "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
9532
|
+
stage: "pristine",
|
|
9533
|
+
detail: pristine.reason,
|
|
9534
|
+
verifier_backend: backendReport(),
|
|
9535
|
+
oracle_assurance: oracleAssuranceReport()
|
|
9536
|
+
}));
|
|
9537
|
+
} else {
|
|
9538
|
+
out2("verify: isolated verifier backend failed while running the pristine stage \u2014 failing closed");
|
|
9539
|
+
if (pristine.reason) out2("verify: " + pristine.reason);
|
|
9540
|
+
}
|
|
9541
|
+
return 2;
|
|
9542
|
+
}
|
|
9033
9543
|
const overlayMoved = overlayDigest(priDir, restored) !== overlayBefore;
|
|
9034
9544
|
const treeMoved = treeFingerprint(cwd, protectedIgnored) !== treeBefore;
|
|
9035
|
-
const dependencyAfterPristine =
|
|
9545
|
+
const dependencyAfterPristine = checkDeps();
|
|
9036
9546
|
const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
|
|
9037
9547
|
cleanup([visRoot, priRoot]);
|
|
9038
9548
|
if (overlayMoved || treeMoved || depsMoved) {
|
|
@@ -9072,7 +9582,9 @@ function runVerify(opts) {
|
|
|
9072
9582
|
pristine: { exit: pristine.exit, secs: pristine.secs },
|
|
9073
9583
|
protected_restored: restored.length,
|
|
9074
9584
|
added_protected_removed: removedAdded,
|
|
9585
|
+
verifier_backend: backendReport(),
|
|
9075
9586
|
dependency_environment: dependencyReport(),
|
|
9587
|
+
oracle_assurance: oracleAssuranceReport(),
|
|
9076
9588
|
...signedOff ? { oob_signoff: signedOff } : {},
|
|
9077
9589
|
...opts.keep ? { visible_dir: visDir, pristine_dir: priDir } : {}
|
|
9078
9590
|
})
|
|
@@ -9085,7 +9597,13 @@ function runVerify(opts) {
|
|
|
9085
9597
|
BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
|
|
9086
9598
|
};
|
|
9087
9599
|
out2(`tamperward verify \u2014 ${lines[verdict2]}`);
|
|
9088
|
-
out2(`
|
|
9600
|
+
out2(`verifier backend: ${verifierBackendSummary(verifierBackend)}`);
|
|
9601
|
+
out2(
|
|
9602
|
+
"oracle assurance: suite-exit-only (candidate source executes inside the suite process; execution-domain isolation is not semantic/oracle isolation)"
|
|
9603
|
+
);
|
|
9604
|
+
out2(
|
|
9605
|
+
dependencyEnvironment ? `dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}` + (opts.allowDepDrift && dependencyEnvironment.status === "unattestable" ? " (operator override)" : "") : `dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`
|
|
9606
|
+
);
|
|
9089
9607
|
if (signedOff)
|
|
9090
9608
|
out2(
|
|
9091
9609
|
`masked failure cleared by out-of-band approval (tamperward:allow:${signedOff}): a reviewer accepted that the original suite no longer applies to this change. Exit 0.`
|
|
@@ -9130,9 +9648,9 @@ function parseVerify(args) {
|
|
|
9130
9648
|
}
|
|
9131
9649
|
|
|
9132
9650
|
// src/cli/run.ts
|
|
9133
|
-
import { execFileSync as execFileSync9, spawnSync as
|
|
9134
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
9135
|
-
import { resolve as
|
|
9651
|
+
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
9652
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync16, readlinkSync as readlinkSync4, realpathSync as realpathSync6 } from "node:fs";
|
|
9653
|
+
import { resolve as resolve7 } from "node:path";
|
|
9136
9654
|
var out = (s) => process.stdout.write(s + "\n");
|
|
9137
9655
|
var err = (s) => process.stderr.write(s + "\n");
|
|
9138
9656
|
function git3(args, cwd) {
|
|
@@ -9140,7 +9658,7 @@ function git3(args, cwd) {
|
|
|
9140
9658
|
}
|
|
9141
9659
|
function nowTicks() {
|
|
9142
9660
|
try {
|
|
9143
|
-
return Math.floor(parseFloat(
|
|
9661
|
+
return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
9144
9662
|
} catch {
|
|
9145
9663
|
return Number.POSITIVE_INFINITY;
|
|
9146
9664
|
}
|
|
@@ -9149,7 +9667,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9149
9667
|
const out2 = [];
|
|
9150
9668
|
let real;
|
|
9151
9669
|
try {
|
|
9152
|
-
real =
|
|
9670
|
+
real = realpathSync6(cwd);
|
|
9153
9671
|
} catch {
|
|
9154
9672
|
return out2;
|
|
9155
9673
|
}
|
|
@@ -9163,7 +9681,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9163
9681
|
const n = Number(pid);
|
|
9164
9682
|
if (n === process.pid) continue;
|
|
9165
9683
|
try {
|
|
9166
|
-
const stat =
|
|
9684
|
+
const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
|
|
9167
9685
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
9168
9686
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
9169
9687
|
} catch {
|
|
@@ -9178,7 +9696,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9178
9696
|
for (const link of procLinks) {
|
|
9179
9697
|
try {
|
|
9180
9698
|
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
9181
|
-
const held = raw.startsWith("/") ?
|
|
9699
|
+
const held = raw.startsWith("/") ? resolve7(raw) : raw;
|
|
9182
9700
|
if (held === real || held.startsWith(real + "/")) {
|
|
9183
9701
|
holds = true;
|
|
9184
9702
|
break;
|
|
@@ -9191,7 +9709,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
9191
9709
|
return out2;
|
|
9192
9710
|
}
|
|
9193
9711
|
function runEnvelope(opts) {
|
|
9194
|
-
const cwd =
|
|
9712
|
+
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
9195
9713
|
if (!opts.argv.length) {
|
|
9196
9714
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
9197
9715
|
return 2;
|
|
@@ -9227,18 +9745,37 @@ function runEnvelope(opts) {
|
|
|
9227
9745
|
return 2;
|
|
9228
9746
|
}
|
|
9229
9747
|
}
|
|
9230
|
-
const
|
|
9748
|
+
const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
|
|
9231
9749
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
9232
|
-
out(`tamperward run \u2014
|
|
9233
|
-
if (
|
|
9234
|
-
err("tamperward run: the verifier
|
|
9235
|
-
err(`${
|
|
9236
|
-
|
|
9750
|
+
out(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
|
|
9751
|
+
if (!verifierBackend.available) {
|
|
9752
|
+
err("tamperward run: the requested verifier backend cannot be established \u2014");
|
|
9753
|
+
err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
|
|
9754
|
+
return 2;
|
|
9755
|
+
}
|
|
9756
|
+
if (verifierBackend.kind === "container") {
|
|
9757
|
+
err("tamperward run: container verification requires a frozen candidate handoff from an");
|
|
9758
|
+
err("agent domain that cannot control the verifier engine. `run` executes the agent under");
|
|
9759
|
+
err("this host identity, so it cannot honestly provide that separation. Failing closed");
|
|
9760
|
+
err("before the agent starts; use standalone `tamperward verify` in trusted CI or after");
|
|
9761
|
+
err("an externally isolated agent produces the frozen candidate artifact.");
|
|
9237
9762
|
return 2;
|
|
9238
9763
|
}
|
|
9764
|
+
const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
|
|
9765
|
+
if (dependencyEnvironment) {
|
|
9766
|
+
out(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
|
|
9767
|
+
if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
|
|
9768
|
+
err("tamperward run: the verifier dependency environment cannot be attested \u2014");
|
|
9769
|
+
err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
|
|
9770
|
+
err("(Pass --allow-dep-drift only if you explicitly own this dependency-integrity risk.)");
|
|
9771
|
+
return 2;
|
|
9772
|
+
}
|
|
9773
|
+
} else {
|
|
9774
|
+
out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
|
|
9775
|
+
}
|
|
9239
9776
|
const rewritesBefore = objectRewriteState(cwd);
|
|
9240
9777
|
const spawnTicks = nowTicks();
|
|
9241
|
-
const agent =
|
|
9778
|
+
const agent = spawnSync3(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
9242
9779
|
const agentExit = agent.status ?? 1;
|
|
9243
9780
|
if (objectRewriteState(cwd) !== rewritesBefore) {
|
|
9244
9781
|
err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
|
|
@@ -9248,7 +9785,7 @@ function runEnvelope(opts) {
|
|
|
9248
9785
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
9249
9786
|
return 1;
|
|
9250
9787
|
}
|
|
9251
|
-
const dependencyBeforeAdjudication = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9788
|
+
const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
9252
9789
|
if (!dependencyBeforeAdjudication.ok) {
|
|
9253
9790
|
err("tamperward run: the frozen dependency environment changed before adjudication began \u2014");
|
|
9254
9791
|
err(`${dependencyBeforeAdjudication.reason ?? "dependency identity changed"}.`);
|
|
@@ -9260,7 +9797,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
9260
9797
|
err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
9261
9798
|
}
|
|
9262
9799
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
9263
|
-
const isAncestor =
|
|
9800
|
+
const isAncestor = spawnSync3("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
|
|
9264
9801
|
if (isAncestor.status !== 0) {
|
|
9265
9802
|
err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
9266
9803
|
err("the agent rewrote history out from under the anchor. That is the finding.");
|
|
@@ -9285,8 +9822,9 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
9285
9822
|
cmd: frozenCmd,
|
|
9286
9823
|
budget: frozenBudget,
|
|
9287
9824
|
policyOverride: frozenPolicy,
|
|
9288
|
-
dependencyEnvironment,
|
|
9289
|
-
allowDepDrift: opts.allowDepDrift
|
|
9825
|
+
...dependencyEnvironment ? { dependencyEnvironment } : {},
|
|
9826
|
+
allowDepDrift: opts.allowDepDrift,
|
|
9827
|
+
verifierBackend
|
|
9290
9828
|
});
|
|
9291
9829
|
if (opts.settle && opts.settle > 0) {
|
|
9292
9830
|
try {
|
|
@@ -9299,8 +9837,8 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
9299
9837
|
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
9300
9838
|
err("the verdict below would describe a tree that no longer exists.");
|
|
9301
9839
|
}
|
|
9302
|
-
const dependencyAfter = checkDependencyEnvironment(cwd, dependencyEnvironment);
|
|
9303
|
-
const depsDrifted = !opts.allowDepDrift && !dependencyAfter.ok;
|
|
9840
|
+
const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
9841
|
+
const depsDrifted = Boolean(dependencyEnvironment) && !opts.allowDepDrift && !dependencyAfter.ok;
|
|
9304
9842
|
if (depsDrifted) {
|
|
9305
9843
|
err("tamperward run: the frozen dependency environment changed during this run \u2014");
|
|
9306
9844
|
err(`${dependencyAfter.reason ?? "dependency identity changed"}. Both verification runs depend on it.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.1",
|
|
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",
|