tamperward 2.17.1 → 2.19.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 +78 -3
- package/dist/cli/index.js +612 -93
- package/package.json +4 -2
- package/schemas/check-v1.schema.json +104 -0
- package/schemas/doctor-v1.schema.json +95 -0
- package/schemas/run-v1.schema.json +508 -0
- package/schemas/verify-v1.schema.json +606 -0
package/dist/cli/index.js
CHANGED
|
@@ -2010,8 +2010,8 @@ function numericOf(e) {
|
|
|
2010
2010
|
}
|
|
2011
2011
|
return void 0;
|
|
2012
2012
|
}
|
|
2013
|
-
function metricsOf(expr,
|
|
2014
|
-
const obj =
|
|
2013
|
+
function metricsOf(expr, resolve10) {
|
|
2014
|
+
const obj = resolve10(expr);
|
|
2015
2015
|
if (!ts3.isObjectLiteralExpression(obj)) return void 0;
|
|
2016
2016
|
const out2 = { values: {}, opaque: false };
|
|
2017
2017
|
let any = false;
|
|
@@ -2025,7 +2025,7 @@ function metricsOf(expr, resolve9) {
|
|
|
2025
2025
|
const k = keyName(p.name);
|
|
2026
2026
|
if (k === null || !isMetric(k)) continue;
|
|
2027
2027
|
any = true;
|
|
2028
|
-
const n = numericOf(
|
|
2028
|
+
const n = numericOf(resolve10(p.initializer));
|
|
2029
2029
|
if (n === void 0) out2.opaque = true;
|
|
2030
2030
|
else out2.values[k] = n;
|
|
2031
2031
|
}
|
|
@@ -2071,17 +2071,17 @@ function parseThresholds(src) {
|
|
|
2071
2071
|
const res = { paths: /* @__PURE__ */ new Map(), present: false, opaque: false };
|
|
2072
2072
|
try {
|
|
2073
2073
|
const sf = sourceOf(src);
|
|
2074
|
-
const
|
|
2074
|
+
const resolve10 = resolverFor(sf);
|
|
2075
2075
|
const visit = (node) => {
|
|
2076
2076
|
if (ts3.isPropertyAssignment(node)) {
|
|
2077
2077
|
const key2 = keyName(node.name);
|
|
2078
2078
|
if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
|
|
2079
2079
|
res.present = true;
|
|
2080
|
-
const init =
|
|
2080
|
+
const init = resolve10(node.initializer);
|
|
2081
2081
|
if (!ts3.isObjectLiteralExpression(init)) {
|
|
2082
2082
|
res.opaque = true;
|
|
2083
2083
|
} else {
|
|
2084
|
-
const flat = metricsOf(init,
|
|
2084
|
+
const flat = metricsOf(init, resolve10);
|
|
2085
2085
|
if (flat) res.global = merge(res.global, flat);
|
|
2086
2086
|
for (const p of init.properties) {
|
|
2087
2087
|
if (ts3.isSpreadAssignment(p)) {
|
|
@@ -2095,7 +2095,7 @@ function parseThresholds(src) {
|
|
|
2095
2095
|
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
2096
2096
|
continue;
|
|
2097
2097
|
}
|
|
2098
|
-
const m = metricsOf(p.initializer,
|
|
2098
|
+
const m = metricsOf(p.initializer, resolve10);
|
|
2099
2099
|
if (!m) continue;
|
|
2100
2100
|
if (k === "global") res.global = merge(res.global, m);
|
|
2101
2101
|
else res.paths.set(norm(k), m);
|
|
@@ -2128,18 +2128,18 @@ function parseLists(src) {
|
|
|
2128
2128
|
const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
|
|
2129
2129
|
try {
|
|
2130
2130
|
const sf = sourceOf(src);
|
|
2131
|
-
const
|
|
2131
|
+
const resolve10 = resolverFor(sf);
|
|
2132
2132
|
const visit = (node) => {
|
|
2133
2133
|
if (ts3.isPropertyAssignment(node)) {
|
|
2134
2134
|
const k = keyName(node.name);
|
|
2135
2135
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
2136
2136
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
2137
|
-
const { items, opaque } = literals(
|
|
2137
|
+
const { items, opaque } = literals(resolve10(node.initializer));
|
|
2138
2138
|
res.collectFrom = items;
|
|
2139
2139
|
res.collectKey = k === "include" ? "coverage.include" : k;
|
|
2140
2140
|
if (opaque) res.opaqueCollect = true;
|
|
2141
2141
|
} else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
|
|
2142
|
-
const { items, opaque } = literals(
|
|
2142
|
+
const { items, opaque } = literals(resolve10(node.initializer));
|
|
2143
2143
|
res.ignore.push(...items);
|
|
2144
2144
|
if (opaque) res.opaqueIgnore = true;
|
|
2145
2145
|
}
|
|
@@ -7084,6 +7084,12 @@ function oobFromEnv(env = process.env) {
|
|
|
7084
7084
|
return (env.TAMPERWARD_OOB_SIGNOFF ?? "").split(",").map((s) => s.trim().replace(/^tamperward:allow:/, "")).filter(Boolean);
|
|
7085
7085
|
}
|
|
7086
7086
|
|
|
7087
|
+
// src/machine-output.ts
|
|
7088
|
+
var MACHINE_SCHEMA_VERSION = 1;
|
|
7089
|
+
function machineOutput(payload) {
|
|
7090
|
+
return { ...payload, schema_version: MACHINE_SCHEMA_VERSION };
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7087
7093
|
// src/cli/render/text.ts
|
|
7088
7094
|
var ESC = "\x1B";
|
|
7089
7095
|
var RESET = `${ESC}[0m`;
|
|
@@ -7303,7 +7309,7 @@ function report(input) {
|
|
|
7303
7309
|
JSON.stringify(
|
|
7304
7310
|
// `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
|
|
7305
7311
|
// is additive, so an existing consumer reading only the old keys is unaffected.
|
|
7306
|
-
{ findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } },
|
|
7312
|
+
machineOutput({ findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } }),
|
|
7307
7313
|
null,
|
|
7308
7314
|
2
|
|
7309
7315
|
) + "\n"
|
|
@@ -7379,7 +7385,9 @@ function check(opts) {
|
|
|
7379
7385
|
process.stderr.write(`tamperward: ${cleared.length} blocking finding(s) cleared by ${how}: ${cleared.map((f) => f.rule + (f.file ? `(${f.file})` : "")).join(", ")}
|
|
7380
7386
|
`);
|
|
7381
7387
|
}
|
|
7382
|
-
|
|
7388
|
+
if (!opts.silent) {
|
|
7389
|
+
report({ findings, scanned: changes.length, ignoredFiles, json: opts.json, format: opts.format });
|
|
7390
|
+
}
|
|
7383
7391
|
return hasBlocking(findings) ? 1 : 0;
|
|
7384
7392
|
}
|
|
7385
7393
|
function runCheck(opts) {
|
|
@@ -10695,6 +10703,17 @@ function verifierInputs(cmd, atBase, policy) {
|
|
|
10695
10703
|
}
|
|
10696
10704
|
return picked;
|
|
10697
10705
|
}
|
|
10706
|
+
function verifierCoveredInputs(cmd, atBase, policy) {
|
|
10707
|
+
const verifierOwned = verifierInputs(cmd, atBase, policy);
|
|
10708
|
+
const verifierGlobs = policy.verify?.inputs ?? [];
|
|
10709
|
+
const covered = /* @__PURE__ */ new Set();
|
|
10710
|
+
for (const path of atBase) {
|
|
10711
|
+
if (OVERLAY_CLASSES.some((category) => isProtected(path, policy, category)) || matchesAny(path, VERIFICATION_SURFACE) || verifierOwned.has(path) || verifierGlobs.length > 0 && matchesAny(path, verifierGlobs)) {
|
|
10712
|
+
covered.add(path);
|
|
10713
|
+
}
|
|
10714
|
+
}
|
|
10715
|
+
return covered;
|
|
10716
|
+
}
|
|
10698
10717
|
function baseEntries(base, cwd) {
|
|
10699
10718
|
return git2(["ls-tree", "-r", "-z", base], cwd).split("\0").filter(Boolean).map((rec) => {
|
|
10700
10719
|
const tab = rec.indexOf(" ");
|
|
@@ -10722,9 +10741,12 @@ function overlayDigest(dest, paths) {
|
|
|
10722
10741
|
function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
10723
10742
|
const entries = baseEntries(base, cwd);
|
|
10724
10743
|
const atBase = entries.map((e) => e.path);
|
|
10725
|
-
const verifierOwned = verifierInputs(cmd, atBase, policy);
|
|
10726
10744
|
const verifierGlobs = policy.verify?.inputs ?? [];
|
|
10727
|
-
const
|
|
10745
|
+
const coveredBaseInputs = verifierCoveredInputs(cmd, atBase, policy);
|
|
10746
|
+
const isOverlay = (p) => coveredBaseInputs.has(p) || // Candidate-added files do not exist in atBase and therefore cannot appear
|
|
10747
|
+
// in the precomputed set. They still must be removed when they land on a
|
|
10748
|
+
// protected class or the built-in verification surface.
|
|
10749
|
+
OVERLAY_CLASSES.some((category) => isProtected(p, policy, category)) || matchesAny(p, VERIFICATION_SURFACE) || verifierGlobs.length > 0 && matchesAny(p, verifierGlobs);
|
|
10728
10750
|
const restored = [];
|
|
10729
10751
|
const restoredLinks = [];
|
|
10730
10752
|
const baseProtected = /* @__PURE__ */ new Set();
|
|
@@ -10880,10 +10902,24 @@ function renderStageDiagnostics(out2, stage, result) {
|
|
|
10880
10902
|
}
|
|
10881
10903
|
function runVerify(opts) {
|
|
10882
10904
|
const cwd = opts.cwd ?? process.cwd();
|
|
10883
|
-
const out2 = (
|
|
10884
|
-
|
|
10885
|
-
|
|
10905
|
+
const out2 = opts.silent ? (_s) => {
|
|
10906
|
+
} : (s) => void process.stdout.write(s + "\n");
|
|
10907
|
+
const cannotVerify = (reason, detail, extra = {}) => {
|
|
10908
|
+
if (opts.json) {
|
|
10909
|
+
out2(JSON.stringify({
|
|
10910
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
10911
|
+
verdict: "CANNOT_VERIFY",
|
|
10912
|
+
reason,
|
|
10913
|
+
...detail ? { detail } : {},
|
|
10914
|
+
...extra
|
|
10915
|
+
}));
|
|
10916
|
+
} else if (detail) {
|
|
10917
|
+
out2(`verify: ${detail} \u2014 failing closed`);
|
|
10918
|
+
}
|
|
10886
10919
|
return 2;
|
|
10920
|
+
};
|
|
10921
|
+
if (opts.invalid) {
|
|
10922
|
+
return cannotVerify("INVALID_ARGUMENTS", opts.invalid);
|
|
10887
10923
|
}
|
|
10888
10924
|
let policy;
|
|
10889
10925
|
try {
|
|
@@ -10891,24 +10927,24 @@ function runVerify(opts) {
|
|
|
10891
10927
|
else if (opts.base) policy = loadPolicyAt(resolveBase(opts.base, cwd), cwd) ?? defaultPolicy();
|
|
10892
10928
|
else policy = loadPolicy(cwd);
|
|
10893
10929
|
} catch (e) {
|
|
10894
|
-
|
|
10895
|
-
return 2;
|
|
10930
|
+
return cannotVerify("POLICY_ERROR", `cannot load policy (${e instanceof Error ? e.message : String(e)})`);
|
|
10896
10931
|
}
|
|
10897
10932
|
if (opts.requireAncestor) {
|
|
10898
10933
|
const requested = git2(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
|
|
10899
10934
|
if (!baseIsAncestorOfHead(requested, cwd)) {
|
|
10900
|
-
|
|
10901
|
-
|
|
10935
|
+
return cannotVerify(
|
|
10936
|
+
"BASE_NOT_ANCESTOR",
|
|
10937
|
+
`--require-ancestor: ${requested.slice(0, 10)} is not an ancestor of HEAD; the anchor would silently resolve to an older commit`,
|
|
10938
|
+
{ base: requested }
|
|
10902
10939
|
);
|
|
10903
|
-
return 2;
|
|
10904
10940
|
}
|
|
10905
10941
|
}
|
|
10906
10942
|
const cmd = opts.cmd ?? policy.verify?.command;
|
|
10907
10943
|
if (!cmd) {
|
|
10908
|
-
|
|
10909
|
-
|
|
10944
|
+
return cannotVerify(
|
|
10945
|
+
"NO_SUITE_COMMAND",
|
|
10946
|
+
opts.base ? "no suite command in the policy at the trusted base; add it at the base or pass --cmd explicitly" : "no suite command; set policy verify.command or pass --cmd"
|
|
10910
10947
|
);
|
|
10911
|
-
return 2;
|
|
10912
10948
|
}
|
|
10913
10949
|
const budget = opts.budget ?? policy.verify?.budget ?? 300;
|
|
10914
10950
|
const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
|
|
@@ -10916,6 +10952,7 @@ function runVerify(opts) {
|
|
|
10916
10952
|
if (!verifierBackend.available) {
|
|
10917
10953
|
if (opts.json) {
|
|
10918
10954
|
out2(JSON.stringify({
|
|
10955
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
10919
10956
|
verdict: "CANNOT_VERIFY",
|
|
10920
10957
|
reason: "VERIFIER_BACKEND_UNAVAILABLE",
|
|
10921
10958
|
verifier_backend: backendReport(),
|
|
@@ -10931,6 +10968,7 @@ function runVerify(opts) {
|
|
|
10931
10968
|
if (!isolated && !localVerifierShell(process.platform, cmd)) {
|
|
10932
10969
|
if (opts.json) {
|
|
10933
10970
|
out2(JSON.stringify({
|
|
10971
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
10934
10972
|
verdict: "CANNOT_VERIFY",
|
|
10935
10973
|
reason: "LOCAL_VERIFIER_UNSUPPORTED_PLATFORM",
|
|
10936
10974
|
platform: process.platform,
|
|
@@ -10953,6 +10991,7 @@ function runVerify(opts) {
|
|
|
10953
10991
|
if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
|
|
10954
10992
|
if (opts.json) {
|
|
10955
10993
|
out2(JSON.stringify({
|
|
10994
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
10956
10995
|
verdict: "CANNOT_VERIFY",
|
|
10957
10996
|
reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
|
|
10958
10997
|
verifier_backend: backendReport(),
|
|
@@ -10972,8 +11011,7 @@ function runVerify(opts) {
|
|
|
10972
11011
|
try {
|
|
10973
11012
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
10974
11013
|
} catch {
|
|
10975
|
-
|
|
10976
|
-
return 2;
|
|
11014
|
+
return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
|
|
10977
11015
|
}
|
|
10978
11016
|
const visRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-vis-"));
|
|
10979
11017
|
const visDir = join16(visRoot, "t");
|
|
@@ -10990,17 +11028,22 @@ function runVerify(opts) {
|
|
|
10990
11028
|
) ?? checkDeps() : { ok: true };
|
|
10991
11029
|
if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
|
|
10992
11030
|
cleanup([visRoot]);
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
11031
|
+
return cannotVerify(
|
|
11032
|
+
"DEPENDENCY_DRIFT",
|
|
11033
|
+
dependencyAtEntry.reason ?? "dependency identity changed before the visible suite ran",
|
|
11034
|
+
{ dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
|
|
11035
|
+
);
|
|
10996
11036
|
}
|
|
10997
11037
|
try {
|
|
10998
11038
|
mkdirSync6(visDir);
|
|
10999
11039
|
materialize(cwd, visDir, frozenNodeModules);
|
|
11000
11040
|
} catch (e) {
|
|
11001
11041
|
cleanup([visRoot]);
|
|
11002
|
-
|
|
11003
|
-
|
|
11042
|
+
return cannotVerify(
|
|
11043
|
+
"MATERIALIZATION_FAILED",
|
|
11044
|
+
`could not materialize (${e instanceof Error ? e.message : String(e)})`,
|
|
11045
|
+
{ verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
|
|
11046
|
+
);
|
|
11004
11047
|
}
|
|
11005
11048
|
const visible = runStage(visDir);
|
|
11006
11049
|
if (visible.failure === "backend" || visible.failure === "resource") {
|
|
@@ -11008,6 +11051,7 @@ function runVerify(opts) {
|
|
|
11008
11051
|
const exhausted = visible.failure === "resource";
|
|
11009
11052
|
if (opts.json) {
|
|
11010
11053
|
out2(JSON.stringify({
|
|
11054
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
11011
11055
|
verdict: "CANNOT_VERIFY",
|
|
11012
11056
|
reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
11013
11057
|
stage: "visible",
|
|
@@ -11028,16 +11072,20 @@ function runVerify(opts) {
|
|
|
11028
11072
|
}
|
|
11029
11073
|
if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
|
|
11030
11074
|
cleanup([visRoot]);
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
|
|
11075
|
+
return cannotVerify(
|
|
11076
|
+
"WORKTREE_CHANGED",
|
|
11077
|
+
"the working tree changed while the visible suite was running",
|
|
11078
|
+
{ stage: "visible", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
|
|
11079
|
+
);
|
|
11034
11080
|
}
|
|
11035
11081
|
const dependencyAfterVisible = checkDeps();
|
|
11036
11082
|
if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
|
|
11037
11083
|
cleanup([visRoot]);
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11084
|
+
return cannotVerify(
|
|
11085
|
+
"DEPENDENCY_DRIFT",
|
|
11086
|
+
dependencyAfterVisible.reason ?? "dependency identity changed while the visible suite was running",
|
|
11087
|
+
{ stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
|
|
11088
|
+
);
|
|
11041
11089
|
}
|
|
11042
11090
|
const priRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-pri-"));
|
|
11043
11091
|
const priDir = join16(priRoot, "t");
|
|
@@ -11056,8 +11104,11 @@ function runVerify(opts) {
|
|
|
11056
11104
|
));
|
|
11057
11105
|
} catch (e) {
|
|
11058
11106
|
cleanup([visRoot, priRoot]);
|
|
11059
|
-
|
|
11060
|
-
|
|
11107
|
+
return cannotVerify(
|
|
11108
|
+
"MATERIALIZATION_FAILED",
|
|
11109
|
+
`could not materialize pristine copy (${e instanceof Error ? e.message : String(e)})`,
|
|
11110
|
+
{ stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
|
|
11111
|
+
);
|
|
11061
11112
|
}
|
|
11062
11113
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
11063
11114
|
const pristine = runStage(priDir);
|
|
@@ -11066,6 +11117,7 @@ function runVerify(opts) {
|
|
|
11066
11117
|
const exhausted = pristine.failure === "resource";
|
|
11067
11118
|
if (opts.json) {
|
|
11068
11119
|
out2(JSON.stringify({
|
|
11120
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
11069
11121
|
verdict: "CANNOT_VERIFY",
|
|
11070
11122
|
reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
11071
11123
|
stage: "pristine",
|
|
@@ -11094,13 +11146,14 @@ function runVerify(opts) {
|
|
|
11094
11146
|
const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
|
|
11095
11147
|
cleanup([visRoot, priRoot]);
|
|
11096
11148
|
if (overlayMoved || treeMoved || depsMoved) {
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11149
|
+
const reason = overlayMoved ? "PRISTINE_INTEGRITY_CHANGED" : treeMoved ? "WORKTREE_CHANGED" : "DEPENDENCY_DRIFT";
|
|
11150
|
+
const detail = overlayMoved ? "a restored file in the pristine copy changed while the pristine suite was running" : treeMoved ? "the working tree changed while the pristine suite was running" : dependencyAfterPristine.reason ?? "the frozen dependency environment changed while the pristine suite was running";
|
|
11151
|
+
return cannotVerify(reason, detail, {
|
|
11152
|
+
stage: "pristine",
|
|
11153
|
+
verifier_backend: backendReport(),
|
|
11154
|
+
dependency_environment: dependencyReport(),
|
|
11155
|
+
oracle_assurance: oracleAssuranceReport()
|
|
11156
|
+
});
|
|
11104
11157
|
}
|
|
11105
11158
|
let verdict2;
|
|
11106
11159
|
let code2;
|
|
@@ -11122,6 +11175,7 @@ function runVerify(opts) {
|
|
|
11122
11175
|
if (opts.json) {
|
|
11123
11176
|
out2(
|
|
11124
11177
|
JSON.stringify({
|
|
11178
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
11125
11179
|
verdict: verdict2,
|
|
11126
11180
|
base,
|
|
11127
11181
|
command: cmd,
|
|
@@ -11285,8 +11339,9 @@ var AGENT_SUPERVISOR = String.raw`
|
|
|
11285
11339
|
const { spawn, spawnSync } = require('node:child_process');
|
|
11286
11340
|
const fs = require('node:fs');
|
|
11287
11341
|
|
|
11288
|
-
const [resultFile, budgetRaw, command, ...args] = process.argv.slice(1);
|
|
11342
|
+
const [resultFile, budgetRaw, machineRaw, command, ...args] = process.argv.slice(1);
|
|
11289
11343
|
const budgetMs = budgetRaw === '' ? null : Number(budgetRaw);
|
|
11344
|
+
const machineMode = machineRaw === '1';
|
|
11290
11345
|
let child;
|
|
11291
11346
|
let timedOut = false;
|
|
11292
11347
|
let finished = false;
|
|
@@ -11312,7 +11367,9 @@ function killTree(pid) {
|
|
|
11312
11367
|
|
|
11313
11368
|
try {
|
|
11314
11369
|
child = spawn(command, args, {
|
|
11315
|
-
|
|
11370
|
+
// Machine mode owns stdout for the final envelope document. Preserve agent
|
|
11371
|
+
// diagnostics by routing both child streams to the parent's stderr.
|
|
11372
|
+
stdio: machineMode ? ['inherit', 2, 2] : 'inherit',
|
|
11316
11373
|
detached: process.platform !== 'win32',
|
|
11317
11374
|
});
|
|
11318
11375
|
} catch (e) {
|
|
@@ -11349,8 +11406,9 @@ child.once('exit', (code, signal) => finish(code, signal));
|
|
|
11349
11406
|
var LINUX_SUBREAPER_SUPERVISOR = String.raw`
|
|
11350
11407
|
import ctypes, json, os, signal, subprocess, sys, time
|
|
11351
11408
|
|
|
11352
|
-
result_file, agent_env_file, agent_cwd, budget_raw, test_mode, command, *args = sys.argv[1:]
|
|
11409
|
+
result_file, agent_env_file, agent_cwd, budget_raw, test_mode, machine_raw, command, *args = sys.argv[1:]
|
|
11353
11410
|
budget = None if budget_raw == "" else float(budget_raw)
|
|
11411
|
+
machine_mode = machine_raw == "1"
|
|
11354
11412
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
11355
11413
|
PR_SET_DUMPABLE = 4
|
|
11356
11414
|
PR_SET_CHILD_SUBREAPER = 36
|
|
@@ -11468,6 +11526,10 @@ try:
|
|
|
11468
11526
|
cwd=agent_cwd,
|
|
11469
11527
|
env=agent_env,
|
|
11470
11528
|
start_new_session=True,
|
|
11529
|
+
# In machine mode stdout is reserved for TamperWard's one final JSON
|
|
11530
|
+
# document. Agent stdout is retained as diagnostics on stderr.
|
|
11531
|
+
stdout=sys.stderr if machine_mode else None,
|
|
11532
|
+
stderr=None,
|
|
11471
11533
|
)
|
|
11472
11534
|
except Exception as exc:
|
|
11473
11535
|
# No candidate process exists, so the lifecycle domain is trivially empty.
|
|
@@ -11497,7 +11559,7 @@ write_result({
|
|
|
11497
11559
|
})
|
|
11498
11560
|
raise SystemExit(0)
|
|
11499
11561
|
`;
|
|
11500
|
-
function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode) {
|
|
11562
|
+
function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
|
|
11501
11563
|
const stateDir = mkdtempSync4(join17(tmpdir4(), "tw-agent-supervisor-"));
|
|
11502
11564
|
const resultFile = join17(stateDir, "result.json");
|
|
11503
11565
|
const agentEnvFile = join17(stateDir, "agent-env.json");
|
|
@@ -11530,6 +11592,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
|
|
|
11530
11592
|
cwd,
|
|
11531
11593
|
budgetSecs === void 0 ? "" : String(budgetSecs),
|
|
11532
11594
|
lifecycleTestMode ?? "",
|
|
11595
|
+
machineMode ? "1" : "0",
|
|
11533
11596
|
...argv2
|
|
11534
11597
|
];
|
|
11535
11598
|
supervisorCwd = "/";
|
|
@@ -11541,6 +11604,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
|
|
|
11541
11604
|
AGENT_SUPERVISOR,
|
|
11542
11605
|
resultFile,
|
|
11543
11606
|
budgetSecs === void 0 ? "" : String(budgetSecs * 1e3),
|
|
11607
|
+
machineMode ? "1" : "0",
|
|
11544
11608
|
...argv2
|
|
11545
11609
|
];
|
|
11546
11610
|
}
|
|
@@ -11660,8 +11724,7 @@ function stopObserverProcess(observer) {
|
|
|
11660
11724
|
}
|
|
11661
11725
|
const deadline = Date.now() + 2e3;
|
|
11662
11726
|
while (Date.now() < deadline) {
|
|
11663
|
-
|
|
11664
|
-
if (health.health?.state === "stopped" || !pidAlive2(pid)) break;
|
|
11727
|
+
if (!pidAlive2(pid)) break;
|
|
11665
11728
|
waitMs(25);
|
|
11666
11729
|
}
|
|
11667
11730
|
if (pidAlive2(pid)) {
|
|
@@ -11674,22 +11737,22 @@ function stopObserverProcess(observer) {
|
|
|
11674
11737
|
observer.finished = true;
|
|
11675
11738
|
return beforeStop;
|
|
11676
11739
|
}
|
|
11677
|
-
function observerSummary(observer, telemetry) {
|
|
11740
|
+
function observerSummary(observer, telemetry, write = out) {
|
|
11678
11741
|
const h = telemetry.health;
|
|
11679
11742
|
const counts = h ? `${h.event_count} event(s), ${h.dropped_events} dropped, ${h.error_count} error(s)` : "no valid health record";
|
|
11680
11743
|
const reason = telemetry.reason ? `; ${telemetry.reason}` : "";
|
|
11681
|
-
|
|
11744
|
+
write(
|
|
11682
11745
|
`tamperward run \u2014 transient observer: ${telemetry.state} (advisory; ${counts}; log ${observer.log}${reason})`
|
|
11683
11746
|
);
|
|
11684
11747
|
}
|
|
11685
|
-
function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
11686
|
-
observerSummary(observer, telemetry);
|
|
11748
|
+
function collectObserverFindings(observer, telemetry, cwd, base, head, policy, write = out) {
|
|
11749
|
+
observerSummary(observer, telemetry, write);
|
|
11687
11750
|
const strict = process.env.TAMPERWARD_TRANSIENT === "block";
|
|
11688
11751
|
const drained = drainEvents(observer.log, 0);
|
|
11689
11752
|
const { events } = drained;
|
|
11690
11753
|
if (!drained.complete) {
|
|
11691
11754
|
const issue = drained.issue ?? "read-stalled";
|
|
11692
|
-
|
|
11755
|
+
write(
|
|
11693
11756
|
`tamperward run \u2014 transient observer: telemetry was not fully classified (${issue}; ${drained.bytesRead}/${MAX_EVENT_SWEEP_BYTES} bytes read). ` + (strict ? "Strict transient policy fails closed." : "Observer telemetry remains advisory, so this degrades evidence but does not independently fail the envelope.")
|
|
11694
11757
|
);
|
|
11695
11758
|
if (strict) return { blocking: true };
|
|
@@ -11705,7 +11768,7 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
|
11705
11768
|
changes.filter((change) => change.kind === "file").map((change) => change.path)
|
|
11706
11769
|
);
|
|
11707
11770
|
} catch (e) {
|
|
11708
|
-
|
|
11771
|
+
write(
|
|
11709
11772
|
`tamperward run \u2014 transient observer: recorded ${events.length} event(s), but could not classify them against the final diff (${e instanceof Error ? e.message : String(e)}).` + (strict ? " Strict transient policy fails closed." : "")
|
|
11710
11773
|
);
|
|
11711
11774
|
return { blocking: strict };
|
|
@@ -11716,16 +11779,16 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
|
11716
11779
|
};
|
|
11717
11780
|
const findings = transientFindings(events, persistent, policy, finalHash);
|
|
11718
11781
|
for (const finding of findings) {
|
|
11719
|
-
|
|
11782
|
+
write(
|
|
11720
11783
|
`[observer] ${finding.severity === "block" ? "BLOCK" : "warn"} ${finding.rule}${finding.file ? " " + finding.file : ""}: ${finding.message}`
|
|
11721
11784
|
);
|
|
11722
11785
|
}
|
|
11723
11786
|
return { blocking: findings.some((finding) => finding.severity === "block") };
|
|
11724
11787
|
}
|
|
11725
|
-
function finishObserverAdvisory(observer) {
|
|
11788
|
+
function finishObserverAdvisory(observer, write = out) {
|
|
11726
11789
|
if (!observer || observer.finished) return;
|
|
11727
11790
|
const telemetry = stopObserverProcess(observer);
|
|
11728
|
-
observerSummary(observer, telemetry);
|
|
11791
|
+
observerSummary(observer, telemetry, write);
|
|
11729
11792
|
}
|
|
11730
11793
|
function git3(args, cwd) {
|
|
11731
11794
|
return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
@@ -11784,6 +11847,8 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
11784
11847
|
}
|
|
11785
11848
|
function runEnvelope(opts) {
|
|
11786
11849
|
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
11850
|
+
const say = opts.json ? (_s) => {
|
|
11851
|
+
} : out;
|
|
11787
11852
|
if (!opts.argv.length) {
|
|
11788
11853
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
11789
11854
|
return 2;
|
|
@@ -11834,8 +11899,8 @@ function runEnvelope(opts) {
|
|
|
11834
11899
|
return 2;
|
|
11835
11900
|
}
|
|
11836
11901
|
const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
|
|
11837
|
-
|
|
11838
|
-
|
|
11902
|
+
say(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
11903
|
+
say(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
|
|
11839
11904
|
if (!verifierBackend.available) {
|
|
11840
11905
|
err("tamperward run: the requested verifier backend cannot be established \u2014");
|
|
11841
11906
|
err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
|
|
@@ -11851,7 +11916,7 @@ function runEnvelope(opts) {
|
|
|
11851
11916
|
}
|
|
11852
11917
|
const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
|
|
11853
11918
|
if (dependencyEnvironment) {
|
|
11854
|
-
|
|
11919
|
+
say(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
|
|
11855
11920
|
if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
|
|
11856
11921
|
err("tamperward run: the verifier dependency environment cannot be attested \u2014");
|
|
11857
11922
|
err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
|
|
@@ -11859,11 +11924,11 @@ function runEnvelope(opts) {
|
|
|
11859
11924
|
return 2;
|
|
11860
11925
|
}
|
|
11861
11926
|
} else {
|
|
11862
|
-
|
|
11927
|
+
say(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
|
|
11863
11928
|
}
|
|
11864
11929
|
const observer = opts.observeTransients ? startSupervisedObserver(cwd, base, opts.observerEntry) : null;
|
|
11865
11930
|
if (!observer) {
|
|
11866
|
-
|
|
11931
|
+
say("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
|
|
11867
11932
|
}
|
|
11868
11933
|
const rewritesBefore = objectRewriteState(cwd);
|
|
11869
11934
|
const spawnTicks = nowTicks();
|
|
@@ -11872,16 +11937,38 @@ function runEnvelope(opts) {
|
|
|
11872
11937
|
cwd,
|
|
11873
11938
|
opts.agentBudget,
|
|
11874
11939
|
opts.linuxPythonCandidates,
|
|
11875
|
-
opts.lifecycleTestMode
|
|
11940
|
+
opts.lifecycleTestMode,
|
|
11941
|
+
opts.json === true
|
|
11876
11942
|
);
|
|
11877
11943
|
const agentExit = agentRun.exit;
|
|
11878
11944
|
const agentTimedOut = agentRun.timedOut;
|
|
11945
|
+
const emitRunJson = (verdict3, exitCode2, extra = {}, complete = false) => {
|
|
11946
|
+
if (!opts.json) return;
|
|
11947
|
+
out(JSON.stringify(machineOutput({
|
|
11948
|
+
verdict: verdict3,
|
|
11949
|
+
exit_code: exitCode2,
|
|
11950
|
+
complete,
|
|
11951
|
+
base,
|
|
11952
|
+
agent: {
|
|
11953
|
+
exit_code: agentExit,
|
|
11954
|
+
timed_out: agentTimedOut,
|
|
11955
|
+
lifecycle_owned: agentRun.lifecycleOwned,
|
|
11956
|
+
...opts.agentBudget !== void 0 ? { budget_secs: opts.agentBudget } : {}
|
|
11957
|
+
},
|
|
11958
|
+
verifier_backend: verifierBackendReport(verifierBackend),
|
|
11959
|
+
dependency_environment: dependencyEnvironment ? dependencyEnvironmentReport(dependencyEnvironment) : { status: "verifier-owned", roots: [], image: verifierBackend.image },
|
|
11960
|
+
...extra
|
|
11961
|
+
})));
|
|
11962
|
+
};
|
|
11879
11963
|
if (agentRun.failure && !agentTimedOut) {
|
|
11880
11964
|
err(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}).`);
|
|
11881
11965
|
}
|
|
11882
|
-
if (
|
|
11883
|
-
err("tamperward run: the
|
|
11884
|
-
finishObserverAdvisory(observer);
|
|
11966
|
+
if (!agentRun.lifecycleOwned) {
|
|
11967
|
+
err("tamperward run: the agent lifecycle boundary was not established/drained \u2014 failing closed before adjudication.");
|
|
11968
|
+
finishObserverAdvisory(observer, say);
|
|
11969
|
+
emitRunJson("CANNOT_ADJUDICATE", 2, {
|
|
11970
|
+
reason: "AGENT_LIFECYCLE_NOT_OWNED"
|
|
11971
|
+
});
|
|
11885
11972
|
return 2;
|
|
11886
11973
|
}
|
|
11887
11974
|
if (agentTimedOut) {
|
|
@@ -11894,9 +11981,10 @@ function runEnvelope(opts) {
|
|
|
11894
11981
|
err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
|
|
11895
11982
|
err("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
|
|
11896
11983
|
err("read against the anchor we started from. That is the finding.");
|
|
11897
|
-
|
|
11984
|
+
say(`
|
|
11898
11985
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
11899
|
-
finishObserverAdvisory(observer);
|
|
11986
|
+
finishObserverAdvisory(observer, say);
|
|
11987
|
+
emitRunJson("OBJECT_REWRITE", 1);
|
|
11900
11988
|
return 1;
|
|
11901
11989
|
}
|
|
11902
11990
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
@@ -11904,36 +11992,41 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
|
|
|
11904
11992
|
if (isAncestor.status !== 0) {
|
|
11905
11993
|
err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
11906
11994
|
err("the agent rewrote history out from under the anchor. That is the finding.");
|
|
11907
|
-
|
|
11995
|
+
say(`
|
|
11908
11996
|
tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
11909
|
-
finishObserverAdvisory(observer);
|
|
11997
|
+
finishObserverAdvisory(observer, say);
|
|
11998
|
+
emitRunJson("HISTORY_REWRITE", 1, { head });
|
|
11910
11999
|
return 1;
|
|
11911
12000
|
}
|
|
11912
12001
|
const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
|
|
11913
12002
|
const fpBefore = treeFingerprint(cwd, protectedIgnored);
|
|
11914
12003
|
let diffCode = 0;
|
|
11915
12004
|
if (head !== base) {
|
|
11916
|
-
|
|
12005
|
+
say(`
|
|
11917
12006
|
[envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
|
|
11918
|
-
diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy });
|
|
12007
|
+
diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy, silent: opts.json });
|
|
11919
12008
|
}
|
|
11920
|
-
|
|
11921
|
-
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
|
|
12009
|
+
say("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
|
|
12010
|
+
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true, silent: opts.json });
|
|
11922
12011
|
const dependencyBeforeVerificationAttestation = dependencyEnvironment ? attestDependencyEnvironment(cwd, dependencyEnvironment) : void 0;
|
|
11923
12012
|
const dependencyBeforeVerification = dependencyBeforeVerificationAttestation?.check ?? { ok: true };
|
|
11924
12013
|
if (!dependencyBeforeVerification.ok) {
|
|
11925
12014
|
err("tamperward run: the frozen dependency environment changed before verification began \u2014");
|
|
11926
12015
|
err(`${dependencyBeforeVerification.reason ?? "dependency identity changed"}.`);
|
|
11927
12016
|
if (!opts.allowDepDrift) {
|
|
11928
|
-
|
|
12017
|
+
say(`
|
|
11929
12018
|
tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
|
|
11930
|
-
finishObserverAdvisory(observer);
|
|
12019
|
+
finishObserverAdvisory(observer, say);
|
|
12020
|
+
emitRunJson("DEPENDENCY_DRIFT", 1, {
|
|
12021
|
+
head,
|
|
12022
|
+
checks: { diff: diffCode, worktree: workCode }
|
|
12023
|
+
});
|
|
11931
12024
|
return 1;
|
|
11932
12025
|
}
|
|
11933
12026
|
err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
11934
12027
|
}
|
|
11935
12028
|
let verifierFinalDependencyAttestation;
|
|
11936
|
-
|
|
12029
|
+
say("\n[envelope] pristine verification against the trusted base:");
|
|
11937
12030
|
const verifyCode = runVerify({
|
|
11938
12031
|
cwd,
|
|
11939
12032
|
base,
|
|
@@ -11948,7 +12041,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
11948
12041
|
}
|
|
11949
12042
|
} : {},
|
|
11950
12043
|
allowDepDrift: opts.allowDepDrift,
|
|
11951
|
-
verifierBackend
|
|
12044
|
+
verifierBackend,
|
|
12045
|
+
silent: opts.json
|
|
11952
12046
|
});
|
|
11953
12047
|
if (opts.settle && opts.settle > 0) {
|
|
11954
12048
|
waitForSettleSync(opts.settle);
|
|
@@ -11961,7 +12055,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
11961
12055
|
const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
11962
12056
|
if (dependencyEnvironment && process.env.TAMPERWARD_DIAGNOSTICS === "1") {
|
|
11963
12057
|
const metrics = dependencyEnvironmentDiagnostics(dependencyEnvironment);
|
|
11964
|
-
|
|
12058
|
+
say(
|
|
11965
12059
|
`tamperward run \u2014 dependency attestation diagnostics: full_snapshots=${metrics.fullSnapshots} reused_snapshots=${metrics.reusedSnapshots} total_ms=${Number(metrics.totalMs.toFixed(3))} verifier_final_attestation=${verifierFinalDependencyAttestation ? "yes" : "no"} run_lifecycle_owned=${agentRun.lifecycleOwned ? "yes" : "no"} entry_reuse=${canReuseAdjacentDependencyAttestation(agentRun.lifecycleOwned) ? "yes" : "no"}`
|
|
11966
12060
|
);
|
|
11967
12061
|
}
|
|
@@ -11989,7 +12083,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
11989
12083
|
cwd,
|
|
11990
12084
|
base,
|
|
11991
12085
|
head,
|
|
11992
|
-
frozenPolicy
|
|
12086
|
+
frozenPolicy,
|
|
12087
|
+
say
|
|
11993
12088
|
);
|
|
11994
12089
|
observerBlocked = observed.blocking;
|
|
11995
12090
|
}
|
|
@@ -11998,10 +12093,25 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
11998
12093
|
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent || observerBlocked;
|
|
11999
12094
|
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
|
12000
12095
|
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : rewrote ? "OBJECT_REWRITE \u2014 the base the checks read is not the base we anchored to" : depsDrifted ? "DEPENDENCY_DRIFT \u2014 the suite runner changed under the envelope" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : observerBlocked ? "TRANSIENT_OBSERVER_BLOCK \u2014 observed temporal weakening under strict transient policy" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentTimedOut ? "AGENT_TIMEOUT \u2014 runtime budget expired; post-timeout enforcement clean" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
12096
|
+
const machineVerdict = enforcement === 2 ? "CANNOT_ADJUDICATE" : rewrote ? "OBJECT_REWRITE" : depsDrifted ? "DEPENDENCY_DRIFT" : notQuiescent ? "NOT_QUIESCENT" : observerBlocked ? "TRANSIENT_OBSERVER_BLOCK" : enforcement === 1 ? "ENFORCEMENT_FAILED" : agentTimedOut ? "AGENT_TIMEOUT" : agentExit !== 0 ? "AGENT_FAILED" : "VERIFIED";
|
|
12001
12097
|
const agentSummary = agentTimedOut ? `AGENT_TIMEOUT (${opts.agentBudget}s; exit 124)` : `agent exit ${agentExit}`;
|
|
12002
|
-
|
|
12098
|
+
const exitCode = enforcement !== 0 ? enforcement : agentTimedOut ? 124 : agentExit;
|
|
12099
|
+
if (opts.json) {
|
|
12100
|
+
const cannotReason = enforcement !== 2 ? null : verifyCode === 2 ? "VERIFY_CANNOT_VERIFY" : diffCode === 2 ? "CHECK_DIFF_UNJUDGEABLE" : "CHECK_WORKTREE_UNJUDGEABLE";
|
|
12101
|
+
emitRunJson(machineVerdict, exitCode, {
|
|
12102
|
+
head,
|
|
12103
|
+
checks: { diff: diffCode, worktree: workCode, verify: verifyCode },
|
|
12104
|
+
observer: {
|
|
12105
|
+
enabled: Boolean(opts.observeTransients),
|
|
12106
|
+
blocking: observerBlocked
|
|
12107
|
+
},
|
|
12108
|
+
...cannotReason ? { reason: cannotReason } : {}
|
|
12109
|
+
}, true);
|
|
12110
|
+
} else {
|
|
12111
|
+
say(`
|
|
12003
12112
|
tamperward run \u2014 ${agentSummary}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
|
|
12004
|
-
|
|
12113
|
+
}
|
|
12114
|
+
return exitCode;
|
|
12005
12115
|
}
|
|
12006
12116
|
function parseRun(args) {
|
|
12007
12117
|
const o = { argv: [] };
|
|
@@ -12013,6 +12123,7 @@ function parseRun(args) {
|
|
|
12013
12123
|
} else if (a === "--base") o.base = args[++i];
|
|
12014
12124
|
else if (a === "--cmd") o.cmd = args[++i];
|
|
12015
12125
|
else if (a === "--budget") o.budget = Number(args[++i]);
|
|
12126
|
+
else if (a === "--json") o.json = true;
|
|
12016
12127
|
else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
|
|
12017
12128
|
else if (a === "--allow-dirty") o.allowDirty = true;
|
|
12018
12129
|
else if (a === "--settle") o.settle = Number(args[++i]);
|
|
@@ -12059,14 +12170,14 @@ function lifecyclePlatformCheck(platform = process.platform, linuxPython = platf
|
|
|
12059
12170
|
var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
|
|
12060
12171
|
function err2(message, opts, id = "doctor", checks = []) {
|
|
12061
12172
|
if (opts?.json) {
|
|
12062
|
-
const report2 = {
|
|
12173
|
+
const report2 = machineOutput({
|
|
12063
12174
|
command: "doctor",
|
|
12064
12175
|
authoritative: false,
|
|
12065
12176
|
checks: [
|
|
12066
12177
|
...checks.filter((check2) => check2.id !== id),
|
|
12067
12178
|
{ id, state: "BROKEN", detail: message }
|
|
12068
12179
|
]
|
|
12069
|
-
};
|
|
12180
|
+
});
|
|
12070
12181
|
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
12071
12182
|
} else {
|
|
12072
12183
|
process.stderr.write(`tamperward doctor: ${message}
|
|
@@ -12279,7 +12390,7 @@ function observerCheck(cwd) {
|
|
|
12279
12390
|
function emitReport(opts, checks) {
|
|
12280
12391
|
const authoritative = !checks.some((x) => x.state === "BROKEN");
|
|
12281
12392
|
if (opts.json) {
|
|
12282
|
-
const report2 = { command: "doctor", authoritative, checks };
|
|
12393
|
+
const report2 = machineOutput({ command: "doctor", authoritative, checks });
|
|
12283
12394
|
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
12284
12395
|
return;
|
|
12285
12396
|
}
|
|
@@ -12627,6 +12738,384 @@ function runDoctor(opts = {}) {
|
|
|
12627
12738
|
return 0;
|
|
12628
12739
|
}
|
|
12629
12740
|
|
|
12741
|
+
// src/cli/trace-verify.ts
|
|
12742
|
+
import { execFileSync as execFileSync11, spawnSync as spawnSync5 } from "node:child_process";
|
|
12743
|
+
import {
|
|
12744
|
+
existsSync as existsSync13,
|
|
12745
|
+
mkdtempSync as mkdtempSync5,
|
|
12746
|
+
readFileSync as readFileSync18,
|
|
12747
|
+
readdirSync as readdirSync6,
|
|
12748
|
+
realpathSync as realpathSync7,
|
|
12749
|
+
rmSync as rmSync6,
|
|
12750
|
+
symlinkSync as symlinkSync2
|
|
12751
|
+
} from "node:fs";
|
|
12752
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
12753
|
+
import { isAbsolute as isAbsolute11, join as join19, relative as relative6, resolve as resolve9, sep as sep3 } from "node:path";
|
|
12754
|
+
var TRACE_ROOT = "/__tamperward_trusted_base__";
|
|
12755
|
+
function unescapeStraceString(s) {
|
|
12756
|
+
return s.replace(/\\([0-7]{1,3})/g, (_m, oct) => String.fromCharCode(parseInt(oct, 8))).replace(/\\x([0-9a-fA-F]{2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
12757
|
+
}
|
|
12758
|
+
function parseStraceFileAccess(raw) {
|
|
12759
|
+
const out2 = [];
|
|
12760
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12761
|
+
for (const line of raw.split("\n")) {
|
|
12762
|
+
if (!line.trim() || /\)\s+=\s+-1\b/.test(line)) continue;
|
|
12763
|
+
const call = line.match(/\b(execveat|execve|openat2|openat|open|newfstatat|fstatat|statx|lstat|stat|access|readlinkat|readlink)\s*\(/);
|
|
12764
|
+
if (!call) continue;
|
|
12765
|
+
const afterCall = line.slice((call.index ?? 0) + call[0].length);
|
|
12766
|
+
const quoted = afterCall.match(/"((?:\\.|[^"])*)"/);
|
|
12767
|
+
if (!quoted) continue;
|
|
12768
|
+
const path = unescapeStraceString(quoted[1]);
|
|
12769
|
+
if (!path) continue;
|
|
12770
|
+
const access = call[1].startsWith("execve") ? "exec" : "read";
|
|
12771
|
+
const key2 = `${access}\0${path}`;
|
|
12772
|
+
if (seen.has(key2)) continue;
|
|
12773
|
+
seen.add(key2);
|
|
12774
|
+
out2.push({ path, access });
|
|
12775
|
+
}
|
|
12776
|
+
return out2;
|
|
12777
|
+
}
|
|
12778
|
+
function inside3(root, path) {
|
|
12779
|
+
const rel = relative6(resolve9(root), resolve9(path));
|
|
12780
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute11(rel);
|
|
12781
|
+
}
|
|
12782
|
+
function normalRepoPath(root, raw) {
|
|
12783
|
+
const abs2 = isAbsolute11(raw) ? resolve9(raw) : resolve9(root, raw);
|
|
12784
|
+
if (!inside3(root, abs2)) return null;
|
|
12785
|
+
const rel = relative6(root, abs2).split(sep3).join("/");
|
|
12786
|
+
return rel && rel !== ".." ? rel : null;
|
|
12787
|
+
}
|
|
12788
|
+
var CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
12789
|
+
"package.json",
|
|
12790
|
+
"pytest.ini",
|
|
12791
|
+
".pytest.ini",
|
|
12792
|
+
"setup.cfg",
|
|
12793
|
+
"setup.py",
|
|
12794
|
+
"tox.ini",
|
|
12795
|
+
"pyproject.toml",
|
|
12796
|
+
".npmrc",
|
|
12797
|
+
".yarnrc",
|
|
12798
|
+
".yarnrc.yml",
|
|
12799
|
+
"pnpm-workspace.yaml",
|
|
12800
|
+
".pnpmfile.cjs",
|
|
12801
|
+
".pnp.cjs",
|
|
12802
|
+
".pnp.js",
|
|
12803
|
+
".pnp.loader.mjs",
|
|
12804
|
+
".nvmrc",
|
|
12805
|
+
".node-version",
|
|
12806
|
+
".python-version",
|
|
12807
|
+
".gitattributes",
|
|
12808
|
+
".rspec",
|
|
12809
|
+
"phpunit.xml",
|
|
12810
|
+
"phpunit.xml.dist"
|
|
12811
|
+
]);
|
|
12812
|
+
function likelyConfig(path) {
|
|
12813
|
+
const base = path.split("/").at(-1) ?? path;
|
|
12814
|
+
return CONFIG_BASENAMES.has(base) || /(?:^|\/)(?:config|configs)\//.test(path) || /^\.env(?:\.|$)/.test(base) || /^(?:jest|vitest|playwright|cypress|vite|babel|karma|ava)\.config\./.test(base) || /^tsconfig(?:\..+)?\.json$/.test(base) || /^\.?(?:babelrc|swcrc|nycrc|c8rc)/.test(base) || /(?:^|\/)conftest\.py$/.test(path);
|
|
12815
|
+
}
|
|
12816
|
+
function sortAccesses(values) {
|
|
12817
|
+
return [...values].sort((a, b) => a.localeCompare(b));
|
|
12818
|
+
}
|
|
12819
|
+
function summarizeTraceRuns(opts) {
|
|
12820
|
+
const repo = /* @__PURE__ */ new Map();
|
|
12821
|
+
const external = /* @__PURE__ */ new Map();
|
|
12822
|
+
opts.runs.forEach((run, index) => {
|
|
12823
|
+
const perRun = /* @__PURE__ */ new Set();
|
|
12824
|
+
for (const item of run) {
|
|
12825
|
+
const rel = normalRepoPath(opts.root, item.path);
|
|
12826
|
+
const isTracked = rel !== null && opts.tracked.has(rel);
|
|
12827
|
+
const keyPath = isTracked ? rel : rel === null ? resolve9(opts.root, item.path) : null;
|
|
12828
|
+
if (keyPath === null) continue;
|
|
12829
|
+
const target = isTracked ? repo : external;
|
|
12830
|
+
const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
|
|
12831
|
+
let acc2 = target.get(keyPath);
|
|
12832
|
+
if (!acc2) {
|
|
12833
|
+
acc2 = { accesses: /* @__PURE__ */ new Set(), runs: /* @__PURE__ */ new Set() };
|
|
12834
|
+
target.set(keyPath, acc2);
|
|
12835
|
+
}
|
|
12836
|
+
acc2.accesses.add(item.access);
|
|
12837
|
+
if (!perRun.has(key2)) {
|
|
12838
|
+
acc2.runs.add(index);
|
|
12839
|
+
perRun.add(key2);
|
|
12840
|
+
}
|
|
12841
|
+
}
|
|
12842
|
+
});
|
|
12843
|
+
const totalRuns = opts.runs.length;
|
|
12844
|
+
const repository_inputs = [...repo.entries()].map(([path, acc2]) => ({
|
|
12845
|
+
path,
|
|
12846
|
+
accesses: sortAccesses(acc2.accesses),
|
|
12847
|
+
config: likelyConfig(path),
|
|
12848
|
+
covered: opts.covered.has(path),
|
|
12849
|
+
observed_runs: acc2.runs.size,
|
|
12850
|
+
dynamic: acc2.runs.size < totalRuns
|
|
12851
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
12852
|
+
const external_inputs = [...external.entries()].map(([path, acc2]) => ({
|
|
12853
|
+
path,
|
|
12854
|
+
accesses: sortAccesses(acc2.accesses),
|
|
12855
|
+
observed_runs: acc2.runs.size,
|
|
12856
|
+
dynamic: acc2.runs.size < totalRuns
|
|
12857
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
12858
|
+
const uncovered_repository_inputs = repository_inputs.filter((x) => !x.covered).map((x) => x.path);
|
|
12859
|
+
return {
|
|
12860
|
+
repository_inputs,
|
|
12861
|
+
external_inputs,
|
|
12862
|
+
uncovered_repository_inputs,
|
|
12863
|
+
// Exact paths are intentionally conservative. A maintainer may widen them to
|
|
12864
|
+
// reviewed globs, but TamperWard never invents a broader trust surface.
|
|
12865
|
+
suggested_verify_inputs: [...uncovered_repository_inputs]
|
|
12866
|
+
};
|
|
12867
|
+
}
|
|
12868
|
+
function git4(args, cwd) {
|
|
12869
|
+
return execFileSync11("git", args, {
|
|
12870
|
+
cwd,
|
|
12871
|
+
encoding: "utf8",
|
|
12872
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
12873
|
+
env: trustedGitEnv(),
|
|
12874
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
12875
|
+
});
|
|
12876
|
+
}
|
|
12877
|
+
function commandExists(command) {
|
|
12878
|
+
const r = spawnSync5(command, ["--version"], { stdio: "ignore" });
|
|
12879
|
+
return !r.error && r.status === 0;
|
|
12880
|
+
}
|
|
12881
|
+
function trackedAt(base, cwd) {
|
|
12882
|
+
return git4(["ls-tree", "-r", "-z", "--name-only", base], cwd).split("\0").filter(Boolean);
|
|
12883
|
+
}
|
|
12884
|
+
function materializeBase(base, cwd, dest) {
|
|
12885
|
+
const archive = spawnSync5("git", ["archive", "--format=tar", base], {
|
|
12886
|
+
cwd,
|
|
12887
|
+
env: trustedGitEnv(),
|
|
12888
|
+
maxBuffer: 256 * 1024 * 1024
|
|
12889
|
+
});
|
|
12890
|
+
if (archive.error || archive.status !== 0 || !archive.stdout) {
|
|
12891
|
+
throw new Error(
|
|
12892
|
+
`git archive failed${archive.stderr?.length ? `: ${String(archive.stderr).trim()}` : ""}`
|
|
12893
|
+
);
|
|
12894
|
+
}
|
|
12895
|
+
const untar = spawnSync5("tar", ["-xf", "-", "-C", dest], {
|
|
12896
|
+
input: archive.stdout,
|
|
12897
|
+
maxBuffer: 256 * 1024 * 1024
|
|
12898
|
+
});
|
|
12899
|
+
if (untar.error || untar.status !== 0) {
|
|
12900
|
+
throw new Error(
|
|
12901
|
+
`tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
|
|
12902
|
+
);
|
|
12903
|
+
}
|
|
12904
|
+
const hostModules = join19(cwd, "node_modules");
|
|
12905
|
+
const tracedModules = join19(dest, "node_modules");
|
|
12906
|
+
if (existsSync13(hostModules) && !existsSync13(tracedModules)) {
|
|
12907
|
+
symlinkSync2(resolve9(hostModules), tracedModules, "dir");
|
|
12908
|
+
}
|
|
12909
|
+
}
|
|
12910
|
+
function traceFiles(prefix) {
|
|
12911
|
+
const dir = resolve9(prefix, "..");
|
|
12912
|
+
const base = prefix.split(sep3).at(-1);
|
|
12913
|
+
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join19(dir, name)).sort();
|
|
12914
|
+
}
|
|
12915
|
+
function rewriteTraceRoot(accesses, actualRoot) {
|
|
12916
|
+
return accesses.map((item) => {
|
|
12917
|
+
const abs2 = isAbsolute11(item.path) ? resolve9(item.path) : resolve9(actualRoot, item.path);
|
|
12918
|
+
if (!inside3(actualRoot, abs2)) return { ...item, path: abs2 };
|
|
12919
|
+
const rel = relative6(actualRoot, abs2).split(sep3).join("/");
|
|
12920
|
+
try {
|
|
12921
|
+
const real = resolve9(realpathSync7(abs2));
|
|
12922
|
+
if (!inside3(actualRoot, real)) return { ...item, path: real };
|
|
12923
|
+
} catch {
|
|
12924
|
+
}
|
|
12925
|
+
return { ...item, path: join19(TRACE_ROOT, ...rel.split("/")) };
|
|
12926
|
+
});
|
|
12927
|
+
}
|
|
12928
|
+
function runOneTrace(cwd, base, command, budget) {
|
|
12929
|
+
const root = mkdtempSync5(join19(tmpdir5(), "tw-trace-base-"));
|
|
12930
|
+
const traceDir = mkdtempSync5(join19(tmpdir5(), "tw-trace-log-"));
|
|
12931
|
+
try {
|
|
12932
|
+
materializeBase(base, cwd, root);
|
|
12933
|
+
const prefix = join19(traceDir, "trace");
|
|
12934
|
+
const traced = spawnSync5(
|
|
12935
|
+
"strace",
|
|
12936
|
+
[
|
|
12937
|
+
"-ff",
|
|
12938
|
+
"-qq",
|
|
12939
|
+
"-s",
|
|
12940
|
+
"4096",
|
|
12941
|
+
"-e",
|
|
12942
|
+
"trace=%file",
|
|
12943
|
+
"-o",
|
|
12944
|
+
prefix,
|
|
12945
|
+
"--",
|
|
12946
|
+
"timeout",
|
|
12947
|
+
"--signal=TERM",
|
|
12948
|
+
"--kill-after=2s",
|
|
12949
|
+
`${budget}s`,
|
|
12950
|
+
"sh",
|
|
12951
|
+
"-c",
|
|
12952
|
+
command
|
|
12953
|
+
],
|
|
12954
|
+
{
|
|
12955
|
+
cwd: root,
|
|
12956
|
+
env: process.env,
|
|
12957
|
+
encoding: "utf8",
|
|
12958
|
+
maxBuffer: 64 * 1024 * 1024
|
|
12959
|
+
}
|
|
12960
|
+
);
|
|
12961
|
+
if (traced.error) throw traced.error;
|
|
12962
|
+
const logs = traceFiles(prefix);
|
|
12963
|
+
if (logs.length === 0) {
|
|
12964
|
+
throw new Error("strace produced no trace files");
|
|
12965
|
+
}
|
|
12966
|
+
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync18(path, "utf8")));
|
|
12967
|
+
return {
|
|
12968
|
+
exit: traced.status ?? 1,
|
|
12969
|
+
accesses: rewriteTraceRoot(accesses, root)
|
|
12970
|
+
};
|
|
12971
|
+
} finally {
|
|
12972
|
+
rmSync6(root, { recursive: true, force: true });
|
|
12973
|
+
rmSync6(traceDir, { recursive: true, force: true });
|
|
12974
|
+
}
|
|
12975
|
+
}
|
|
12976
|
+
function renderText2(report2) {
|
|
12977
|
+
const out2 = (s = "") => void process.stdout.write(s + "\n");
|
|
12978
|
+
out2("tamperward trace-verify \u2014 ADVISORY observation, not an enforcement verdict");
|
|
12979
|
+
out2(`trusted base: ${report2.base}`);
|
|
12980
|
+
out2(`command: ${report2.command}`);
|
|
12981
|
+
out2(`runs: ${report2.runs_completed}/${report2.runs_requested}; exits: ${report2.run_exits.join(", ")}`);
|
|
12982
|
+
out2();
|
|
12983
|
+
out2("repository inputs observed:");
|
|
12984
|
+
if (!report2.repository_inputs.length) out2(" (none)");
|
|
12985
|
+
for (const item of report2.repository_inputs) {
|
|
12986
|
+
const flags = [
|
|
12987
|
+
item.covered ? "covered" : "UNCOVERED",
|
|
12988
|
+
item.config ? "config" : null,
|
|
12989
|
+
item.dynamic ? `dynamic ${item.observed_runs}/${report2.runs_completed}` : null,
|
|
12990
|
+
item.accesses.join("+")
|
|
12991
|
+
].filter(Boolean).join(", ");
|
|
12992
|
+
out2(` ${item.path} [${flags}]`);
|
|
12993
|
+
}
|
|
12994
|
+
out2();
|
|
12995
|
+
out2("external dependency/runtime paths observed:");
|
|
12996
|
+
if (!report2.external_inputs.length) out2(" (none)");
|
|
12997
|
+
for (const item of report2.external_inputs) {
|
|
12998
|
+
out2(
|
|
12999
|
+
` ${item.path} [${item.accesses.join("+")}${item.dynamic ? `, dynamic ${item.observed_runs}/${report2.runs_completed}` : ""}]`
|
|
13000
|
+
);
|
|
13001
|
+
}
|
|
13002
|
+
out2();
|
|
13003
|
+
out2("candidate verify.inputs entries for HUMAN REVIEW:");
|
|
13004
|
+
if (!report2.suggested_verify_inputs.length) out2(" (none \u2014 all observed tracked inputs are already covered)");
|
|
13005
|
+
for (const path of report2.suggested_verify_inputs) out2(` - ${JSON.stringify(path)}`);
|
|
13006
|
+
out2();
|
|
13007
|
+
for (const note of report2.notes) out2(`note: ${note}`);
|
|
13008
|
+
}
|
|
13009
|
+
function runTraceVerify(opts = {}) {
|
|
13010
|
+
const cwd = resolve9(opts.cwd ?? process.cwd());
|
|
13011
|
+
if (process.platform !== "linux") {
|
|
13012
|
+
process.stderr.write("tamperward trace-verify: Linux only in this release; unsupported platform \u2014 no parity is implied.\n");
|
|
13013
|
+
return 2;
|
|
13014
|
+
}
|
|
13015
|
+
if (!commandExists("strace")) {
|
|
13016
|
+
process.stderr.write("tamperward trace-verify: strace is required on Linux but was not found.\n");
|
|
13017
|
+
return 2;
|
|
13018
|
+
}
|
|
13019
|
+
if (!commandExists("tar")) {
|
|
13020
|
+
process.stderr.write("tamperward trace-verify: tar is required to materialize the trusted base but was not found.\n");
|
|
13021
|
+
return 2;
|
|
13022
|
+
}
|
|
13023
|
+
if (!commandExists("timeout")) {
|
|
13024
|
+
process.stderr.write("tamperward trace-verify: GNU timeout is required to bound traced verifier runs but was not found.\n");
|
|
13025
|
+
return 2;
|
|
13026
|
+
}
|
|
13027
|
+
const runs = opts.runs ?? 2;
|
|
13028
|
+
if (!Number.isInteger(runs) || runs <= 0) {
|
|
13029
|
+
process.stderr.write(`tamperward trace-verify: --runs needs a positive integer (got ${JSON.stringify(opts.runs)}).
|
|
13030
|
+
`);
|
|
13031
|
+
return 2;
|
|
13032
|
+
}
|
|
13033
|
+
const requestedBase = opts.base ?? "HEAD";
|
|
13034
|
+
let base;
|
|
13035
|
+
try {
|
|
13036
|
+
const baseArg = assertRev(requestedBase);
|
|
13037
|
+
base = git4(["rev-parse", "--verify", `${baseArg}^{commit}`], cwd).trim();
|
|
13038
|
+
} catch (e) {
|
|
13039
|
+
process.stderr.write(`tamperward trace-verify: cannot resolve trusted base ${JSON.stringify(requestedBase)} (${e instanceof Error ? e.message : String(e)}).
|
|
13040
|
+
`);
|
|
13041
|
+
return 2;
|
|
13042
|
+
}
|
|
13043
|
+
let policy;
|
|
13044
|
+
try {
|
|
13045
|
+
policy = loadPolicyAt(base, cwd) ?? defaultPolicy();
|
|
13046
|
+
} catch (e) {
|
|
13047
|
+
process.stderr.write(`tamperward trace-verify: cannot load policy at trusted base (${e instanceof Error ? e.message : String(e)}).
|
|
13048
|
+
`);
|
|
13049
|
+
return 2;
|
|
13050
|
+
}
|
|
13051
|
+
const command = opts.cmd ?? policy.verify?.command;
|
|
13052
|
+
if (!command) {
|
|
13053
|
+
process.stderr.write("tamperward trace-verify: no verifier command; set verify.command at the base or pass --cmd.\n");
|
|
13054
|
+
return 2;
|
|
13055
|
+
}
|
|
13056
|
+
const budget = opts.budget ?? policy.verify?.budget ?? 300;
|
|
13057
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
13058
|
+
process.stderr.write(`tamperward trace-verify: --budget needs a positive number of seconds (got ${JSON.stringify(budget)}).
|
|
13059
|
+
`);
|
|
13060
|
+
return 2;
|
|
13061
|
+
}
|
|
13062
|
+
const atBase = trackedAt(base, cwd);
|
|
13063
|
+
const tracked = new Set(atBase);
|
|
13064
|
+
const covered = verifierCoveredInputs(command, atBase, policy);
|
|
13065
|
+
const observed = [];
|
|
13066
|
+
const runExits = [];
|
|
13067
|
+
try {
|
|
13068
|
+
for (let i = 0; i < runs; i++) {
|
|
13069
|
+
const result = runOneTrace(cwd, base, command, budget);
|
|
13070
|
+
observed.push(result.accesses);
|
|
13071
|
+
runExits.push(result.exit);
|
|
13072
|
+
}
|
|
13073
|
+
} catch (e) {
|
|
13074
|
+
process.stderr.write(`tamperward trace-verify: tracing failed (${e instanceof Error ? e.message : String(e)}).
|
|
13075
|
+
`);
|
|
13076
|
+
return 2;
|
|
13077
|
+
}
|
|
13078
|
+
const summary = summarizeTraceRuns({
|
|
13079
|
+
root: TRACE_ROOT,
|
|
13080
|
+
runs: observed,
|
|
13081
|
+
tracked,
|
|
13082
|
+
covered
|
|
13083
|
+
});
|
|
13084
|
+
const report2 = {
|
|
13085
|
+
advisory: true,
|
|
13086
|
+
platform: process.platform,
|
|
13087
|
+
base,
|
|
13088
|
+
command,
|
|
13089
|
+
runs_requested: runs,
|
|
13090
|
+
runs_completed: observed.length,
|
|
13091
|
+
run_exits: runExits,
|
|
13092
|
+
trace_complete: runExits.every((code2) => code2 === 0),
|
|
13093
|
+
...summary,
|
|
13094
|
+
notes: [
|
|
13095
|
+
"Observed reads are evidence from these runs, not proof that an unobserved path can never be read.",
|
|
13096
|
+
"Dynamic paths are those observed in fewer than all repeated traces.",
|
|
13097
|
+
"Suggestions are exact tracked paths only; TamperWard never edits .tamperward.yml or widens a glob automatically.",
|
|
13098
|
+
"Review every suggestion before adding it to verify.inputs."
|
|
13099
|
+
]
|
|
13100
|
+
};
|
|
13101
|
+
if (opts.json) process.stdout.write(JSON.stringify(report2) + "\n");
|
|
13102
|
+
else renderText2(report2);
|
|
13103
|
+
return report2.trace_complete ? 0 : 1;
|
|
13104
|
+
}
|
|
13105
|
+
function parseTraceVerify(args) {
|
|
13106
|
+
const out2 = {};
|
|
13107
|
+
for (let i = 0; i < args.length; i++) {
|
|
13108
|
+
const a = args[i];
|
|
13109
|
+
if (a === "--json") out2.json = true;
|
|
13110
|
+
else if (a === "--base") out2.base = args[++i];
|
|
13111
|
+
else if (a === "--cmd") out2.cmd = args[++i];
|
|
13112
|
+
else if (a === "--cwd") out2.cwd = args[++i];
|
|
13113
|
+
else if (a === "--budget") out2.budget = Number(args[++i]);
|
|
13114
|
+
else if (a === "--runs") out2.runs = Number(args[++i]);
|
|
13115
|
+
}
|
|
13116
|
+
return out2;
|
|
13117
|
+
}
|
|
13118
|
+
|
|
12630
13119
|
// src/cli/main.ts
|
|
12631
13120
|
function parseAllow(args) {
|
|
12632
13121
|
const o = {};
|
|
@@ -12715,6 +13204,11 @@ function validateFlatArgs(args, grammar) {
|
|
|
12715
13204
|
if (!Number.isFinite(n) || n <= 0) {
|
|
12716
13205
|
return { error: `${a} needs a positive number (got "${v}")`, seen, positionals };
|
|
12717
13206
|
}
|
|
13207
|
+
} else if (rule === "positive-integer") {
|
|
13208
|
+
const n = Number(v);
|
|
13209
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
13210
|
+
return { error: `${a} needs a positive integer (got "${v}")`, seen, positionals };
|
|
13211
|
+
}
|
|
12718
13212
|
} else if (rule === "non-negative") {
|
|
12719
13213
|
const n = Number(v);
|
|
12720
13214
|
if (!Number.isFinite(n) || n < 0) {
|
|
@@ -12752,7 +13246,7 @@ function validateCliArgs(cmd, args) {
|
|
|
12752
13246
|
const prefix = args.slice(0, delimiter3);
|
|
12753
13247
|
const command = args.slice(delimiter3 + 1);
|
|
12754
13248
|
const parsed = validateFlatArgs(prefix, {
|
|
12755
|
-
flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients"],
|
|
13249
|
+
flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients", "--json"],
|
|
12756
13250
|
values: {
|
|
12757
13251
|
"--base": "string",
|
|
12758
13252
|
"--cmd": "string",
|
|
@@ -12817,6 +13311,18 @@ function validateCliArgs(cmd, args) {
|
|
|
12817
13311
|
}
|
|
12818
13312
|
}).error;
|
|
12819
13313
|
}
|
|
13314
|
+
if (cmd === "trace-verify") {
|
|
13315
|
+
return validateFlatArgs(args, {
|
|
13316
|
+
flags: ["--json"],
|
|
13317
|
+
values: {
|
|
13318
|
+
"--base": "string",
|
|
13319
|
+
"--cmd": "string",
|
|
13320
|
+
"--cwd": "string",
|
|
13321
|
+
"--budget": "positive",
|
|
13322
|
+
"--runs": "positive-integer"
|
|
13323
|
+
}
|
|
13324
|
+
}).error;
|
|
13325
|
+
}
|
|
12820
13326
|
if (cmd === "watch") {
|
|
12821
13327
|
return validateFlatArgs(args, {
|
|
12822
13328
|
values: { "--dir": "string", "--log": "string", "--base": "string" }
|
|
@@ -12841,7 +13347,8 @@ Formats:
|
|
|
12841
13347
|
github GitHub Actions view: one inline annotation per finding (so it shows up
|
|
12842
13348
|
on the line in "Files changed") plus a job-summary table on the run
|
|
12843
13349
|
page, alongside the same text output in the log.
|
|
12844
|
-
json
|
|
13350
|
+
json versioned machine-readable output. Top-level schema_version identifies
|
|
13351
|
+
the public JSON-schema major; published schemas live under schemas/.
|
|
12845
13352
|
auto github when GITHUB_ACTIONS=true, otherwise text.
|
|
12846
13353
|
tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
|
|
12847
13354
|
tamperward sweep claude Stop sweep (re-scan the turn's working tree)
|
|
@@ -12857,16 +13364,26 @@ Formats:
|
|
|
12857
13364
|
(exit 1, or 0 under an out-of-band
|
|
12858
13365
|
verify@<head-sha> approval); cannot-verify
|
|
12859
13366
|
fails closed (2)
|
|
13367
|
+
tamperward trace-verify [--base R] advisory Linux verifier-input discovery:
|
|
13368
|
+
[--cmd C] [--budget S] [--runs N] trace a trusted/known-good base with
|
|
13369
|
+
[--json] [--cwd D] strace, union repeated observations,
|
|
13370
|
+
mark dynamic/config/external reads,
|
|
13371
|
+
and suggest uncovered exact paths for
|
|
13372
|
+
HUMAN review as verify.inputs. Never
|
|
13373
|
+
edits policy; unsupported platforms
|
|
13374
|
+
fail explicitly rather than claiming parity.
|
|
12860
13375
|
tamperward run [opts] -- <agent cmd...> enforcement envelope: record the trusted
|
|
12861
13376
|
[--base R] [--cmd C] base, run the agent, treat its exit as
|
|
12862
13377
|
[--budget S] [--agent-budget S] untrusted, then re-adjudicate the tree it
|
|
12863
13378
|
[--allow-dirty] [--settle S] left (policy over base...HEAD and the
|
|
12864
13379
|
[--allow-dep-drift] [--cwd D] worktree, plus verify).
|
|
12865
|
-
[--observe-transients]
|
|
13380
|
+
[--observe-transients] [--json] --observe-transients starts a session-
|
|
12866
13381
|
scoped observer before the agent and
|
|
12867
13382
|
reports temporal evidence/health after it.
|
|
12868
13383
|
--agent-budget bounds the agent runtime
|
|
12869
13384
|
itself and still adjudicates after timeout.
|
|
13385
|
+
--json emits one final versioned run verdict
|
|
13386
|
+
document on stdout after agent adjudication.
|
|
12870
13387
|
Exit: agent's
|
|
12871
13388
|
code when clean; 124 on clean AGENT_TIMEOUT;
|
|
12872
13389
|
1 on any blocking finding/masked failure;
|
|
@@ -12930,6 +13447,8 @@ function main(argv2) {
|
|
|
12930
13447
|
return runWatch(rest);
|
|
12931
13448
|
case "verify":
|
|
12932
13449
|
return runVerify(parseVerify(rest));
|
|
13450
|
+
case "trace-verify":
|
|
13451
|
+
return runTraceVerify(parseTraceVerify(rest));
|
|
12933
13452
|
case "run":
|
|
12934
13453
|
return runEnvelope({
|
|
12935
13454
|
...parseRun(rest),
|