tamperward 2.18.0 → 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 +50 -3
- package/dist/cli/index.js +181 -81
- 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
|
@@ -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) {
|
|
@@ -10894,10 +10902,24 @@ function renderStageDiagnostics(out2, stage, result) {
|
|
|
10894
10902
|
}
|
|
10895
10903
|
function runVerify(opts) {
|
|
10896
10904
|
const cwd = opts.cwd ?? process.cwd();
|
|
10897
|
-
const out2 = (
|
|
10898
|
-
|
|
10899
|
-
|
|
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
|
+
}
|
|
10900
10919
|
return 2;
|
|
10920
|
+
};
|
|
10921
|
+
if (opts.invalid) {
|
|
10922
|
+
return cannotVerify("INVALID_ARGUMENTS", opts.invalid);
|
|
10901
10923
|
}
|
|
10902
10924
|
let policy;
|
|
10903
10925
|
try {
|
|
@@ -10905,24 +10927,24 @@ function runVerify(opts) {
|
|
|
10905
10927
|
else if (opts.base) policy = loadPolicyAt(resolveBase(opts.base, cwd), cwd) ?? defaultPolicy();
|
|
10906
10928
|
else policy = loadPolicy(cwd);
|
|
10907
10929
|
} catch (e) {
|
|
10908
|
-
|
|
10909
|
-
return 2;
|
|
10930
|
+
return cannotVerify("POLICY_ERROR", `cannot load policy (${e instanceof Error ? e.message : String(e)})`);
|
|
10910
10931
|
}
|
|
10911
10932
|
if (opts.requireAncestor) {
|
|
10912
10933
|
const requested = git2(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
|
|
10913
10934
|
if (!baseIsAncestorOfHead(requested, cwd)) {
|
|
10914
|
-
|
|
10915
|
-
|
|
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 }
|
|
10916
10939
|
);
|
|
10917
|
-
return 2;
|
|
10918
10940
|
}
|
|
10919
10941
|
}
|
|
10920
10942
|
const cmd = opts.cmd ?? policy.verify?.command;
|
|
10921
10943
|
if (!cmd) {
|
|
10922
|
-
|
|
10923
|
-
|
|
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"
|
|
10924
10947
|
);
|
|
10925
|
-
return 2;
|
|
10926
10948
|
}
|
|
10927
10949
|
const budget = opts.budget ?? policy.verify?.budget ?? 300;
|
|
10928
10950
|
const verifierBackend = opts.verifierBackend ?? prepareVerifierBackend(policy.verify);
|
|
@@ -10930,6 +10952,7 @@ function runVerify(opts) {
|
|
|
10930
10952
|
if (!verifierBackend.available) {
|
|
10931
10953
|
if (opts.json) {
|
|
10932
10954
|
out2(JSON.stringify({
|
|
10955
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
10933
10956
|
verdict: "CANNOT_VERIFY",
|
|
10934
10957
|
reason: "VERIFIER_BACKEND_UNAVAILABLE",
|
|
10935
10958
|
verifier_backend: backendReport(),
|
|
@@ -10945,6 +10968,7 @@ function runVerify(opts) {
|
|
|
10945
10968
|
if (!isolated && !localVerifierShell(process.platform, cmd)) {
|
|
10946
10969
|
if (opts.json) {
|
|
10947
10970
|
out2(JSON.stringify({
|
|
10971
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
10948
10972
|
verdict: "CANNOT_VERIFY",
|
|
10949
10973
|
reason: "LOCAL_VERIFIER_UNSUPPORTED_PLATFORM",
|
|
10950
10974
|
platform: process.platform,
|
|
@@ -10967,6 +10991,7 @@ function runVerify(opts) {
|
|
|
10967
10991
|
if (dependencyEnvironment?.status === "unattestable" && !opts.allowDepDrift) {
|
|
10968
10992
|
if (opts.json) {
|
|
10969
10993
|
out2(JSON.stringify({
|
|
10994
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
10970
10995
|
verdict: "CANNOT_VERIFY",
|
|
10971
10996
|
reason: "DEPENDENCY_ENVIRONMENT_UNATTESTABLE",
|
|
10972
10997
|
verifier_backend: backendReport(),
|
|
@@ -10986,8 +11011,7 @@ function runVerify(opts) {
|
|
|
10986
11011
|
try {
|
|
10987
11012
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
10988
11013
|
} catch {
|
|
10989
|
-
|
|
10990
|
-
return 2;
|
|
11014
|
+
return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
|
|
10991
11015
|
}
|
|
10992
11016
|
const visRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-vis-"));
|
|
10993
11017
|
const visDir = join16(visRoot, "t");
|
|
@@ -11004,17 +11028,22 @@ function runVerify(opts) {
|
|
|
11004
11028
|
) ?? checkDeps() : { ok: true };
|
|
11005
11029
|
if (!dependencyAtEntry.ok && !opts.allowDepDrift) {
|
|
11006
11030
|
cleanup([visRoot]);
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
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
|
+
);
|
|
11010
11036
|
}
|
|
11011
11037
|
try {
|
|
11012
11038
|
mkdirSync6(visDir);
|
|
11013
11039
|
materialize(cwd, visDir, frozenNodeModules);
|
|
11014
11040
|
} catch (e) {
|
|
11015
11041
|
cleanup([visRoot]);
|
|
11016
|
-
|
|
11017
|
-
|
|
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
|
+
);
|
|
11018
11047
|
}
|
|
11019
11048
|
const visible = runStage(visDir);
|
|
11020
11049
|
if (visible.failure === "backend" || visible.failure === "resource") {
|
|
@@ -11022,6 +11051,7 @@ function runVerify(opts) {
|
|
|
11022
11051
|
const exhausted = visible.failure === "resource";
|
|
11023
11052
|
if (opts.json) {
|
|
11024
11053
|
out2(JSON.stringify({
|
|
11054
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
11025
11055
|
verdict: "CANNOT_VERIFY",
|
|
11026
11056
|
reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
11027
11057
|
stage: "visible",
|
|
@@ -11042,16 +11072,20 @@ function runVerify(opts) {
|
|
|
11042
11072
|
}
|
|
11043
11073
|
if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
|
|
11044
11074
|
cleanup([visRoot]);
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
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
|
+
);
|
|
11048
11080
|
}
|
|
11049
11081
|
const dependencyAfterVisible = checkDeps();
|
|
11050
11082
|
if (!dependencyAfterVisible.ok && !opts.allowDepDrift) {
|
|
11051
11083
|
cleanup([visRoot]);
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
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
|
+
);
|
|
11055
11089
|
}
|
|
11056
11090
|
const priRoot = mkdtempSync3(join16(tmpdir3(), "tw-verify-pri-"));
|
|
11057
11091
|
const priDir = join16(priRoot, "t");
|
|
@@ -11070,8 +11104,11 @@ function runVerify(opts) {
|
|
|
11070
11104
|
));
|
|
11071
11105
|
} catch (e) {
|
|
11072
11106
|
cleanup([visRoot, priRoot]);
|
|
11073
|
-
|
|
11074
|
-
|
|
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
|
+
);
|
|
11075
11112
|
}
|
|
11076
11113
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
11077
11114
|
const pristine = runStage(priDir);
|
|
@@ -11080,6 +11117,7 @@ function runVerify(opts) {
|
|
|
11080
11117
|
const exhausted = pristine.failure === "resource";
|
|
11081
11118
|
if (opts.json) {
|
|
11082
11119
|
out2(JSON.stringify({
|
|
11120
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
11083
11121
|
verdict: "CANNOT_VERIFY",
|
|
11084
11122
|
reason: exhausted ? "VERIFIER_RESOURCE_EXHAUSTED" : "VERIFIER_BACKEND_RUNTIME_FAILURE",
|
|
11085
11123
|
stage: "pristine",
|
|
@@ -11108,13 +11146,14 @@ function runVerify(opts) {
|
|
|
11108
11146
|
const depsMoved = !dependencyAfterPristine.ok && !opts.allowDepDrift;
|
|
11109
11147
|
cleanup([visRoot, priRoot]);
|
|
11110
11148
|
if (overlayMoved || treeMoved || depsMoved) {
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
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
|
+
});
|
|
11118
11157
|
}
|
|
11119
11158
|
let verdict2;
|
|
11120
11159
|
let code2;
|
|
@@ -11136,6 +11175,7 @@ function runVerify(opts) {
|
|
|
11136
11175
|
if (opts.json) {
|
|
11137
11176
|
out2(
|
|
11138
11177
|
JSON.stringify({
|
|
11178
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
11139
11179
|
verdict: verdict2,
|
|
11140
11180
|
base,
|
|
11141
11181
|
command: cmd,
|
|
@@ -11299,8 +11339,9 @@ var AGENT_SUPERVISOR = String.raw`
|
|
|
11299
11339
|
const { spawn, spawnSync } = require('node:child_process');
|
|
11300
11340
|
const fs = require('node:fs');
|
|
11301
11341
|
|
|
11302
|
-
const [resultFile, budgetRaw, command, ...args] = process.argv.slice(1);
|
|
11342
|
+
const [resultFile, budgetRaw, machineRaw, command, ...args] = process.argv.slice(1);
|
|
11303
11343
|
const budgetMs = budgetRaw === '' ? null : Number(budgetRaw);
|
|
11344
|
+
const machineMode = machineRaw === '1';
|
|
11304
11345
|
let child;
|
|
11305
11346
|
let timedOut = false;
|
|
11306
11347
|
let finished = false;
|
|
@@ -11326,7 +11367,9 @@ function killTree(pid) {
|
|
|
11326
11367
|
|
|
11327
11368
|
try {
|
|
11328
11369
|
child = spawn(command, args, {
|
|
11329
|
-
|
|
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',
|
|
11330
11373
|
detached: process.platform !== 'win32',
|
|
11331
11374
|
});
|
|
11332
11375
|
} catch (e) {
|
|
@@ -11363,8 +11406,9 @@ child.once('exit', (code, signal) => finish(code, signal));
|
|
|
11363
11406
|
var LINUX_SUBREAPER_SUPERVISOR = String.raw`
|
|
11364
11407
|
import ctypes, json, os, signal, subprocess, sys, time
|
|
11365
11408
|
|
|
11366
|
-
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:]
|
|
11367
11410
|
budget = None if budget_raw == "" else float(budget_raw)
|
|
11411
|
+
machine_mode = machine_raw == "1"
|
|
11368
11412
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
11369
11413
|
PR_SET_DUMPABLE = 4
|
|
11370
11414
|
PR_SET_CHILD_SUBREAPER = 36
|
|
@@ -11482,6 +11526,10 @@ try:
|
|
|
11482
11526
|
cwd=agent_cwd,
|
|
11483
11527
|
env=agent_env,
|
|
11484
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,
|
|
11485
11533
|
)
|
|
11486
11534
|
except Exception as exc:
|
|
11487
11535
|
# No candidate process exists, so the lifecycle domain is trivially empty.
|
|
@@ -11511,7 +11559,7 @@ write_result({
|
|
|
11511
11559
|
})
|
|
11512
11560
|
raise SystemExit(0)
|
|
11513
11561
|
`;
|
|
11514
|
-
function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode) {
|
|
11562
|
+
function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
|
|
11515
11563
|
const stateDir = mkdtempSync4(join17(tmpdir4(), "tw-agent-supervisor-"));
|
|
11516
11564
|
const resultFile = join17(stateDir, "result.json");
|
|
11517
11565
|
const agentEnvFile = join17(stateDir, "agent-env.json");
|
|
@@ -11544,6 +11592,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
|
|
|
11544
11592
|
cwd,
|
|
11545
11593
|
budgetSecs === void 0 ? "" : String(budgetSecs),
|
|
11546
11594
|
lifecycleTestMode ?? "",
|
|
11595
|
+
machineMode ? "1" : "0",
|
|
11547
11596
|
...argv2
|
|
11548
11597
|
];
|
|
11549
11598
|
supervisorCwd = "/";
|
|
@@ -11555,6 +11604,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
|
|
|
11555
11604
|
AGENT_SUPERVISOR,
|
|
11556
11605
|
resultFile,
|
|
11557
11606
|
budgetSecs === void 0 ? "" : String(budgetSecs * 1e3),
|
|
11607
|
+
machineMode ? "1" : "0",
|
|
11558
11608
|
...argv2
|
|
11559
11609
|
];
|
|
11560
11610
|
}
|
|
@@ -11674,8 +11724,7 @@ function stopObserverProcess(observer) {
|
|
|
11674
11724
|
}
|
|
11675
11725
|
const deadline = Date.now() + 2e3;
|
|
11676
11726
|
while (Date.now() < deadline) {
|
|
11677
|
-
|
|
11678
|
-
if (health.health?.state === "stopped" || !pidAlive2(pid)) break;
|
|
11727
|
+
if (!pidAlive2(pid)) break;
|
|
11679
11728
|
waitMs(25);
|
|
11680
11729
|
}
|
|
11681
11730
|
if (pidAlive2(pid)) {
|
|
@@ -11688,22 +11737,22 @@ function stopObserverProcess(observer) {
|
|
|
11688
11737
|
observer.finished = true;
|
|
11689
11738
|
return beforeStop;
|
|
11690
11739
|
}
|
|
11691
|
-
function observerSummary(observer, telemetry) {
|
|
11740
|
+
function observerSummary(observer, telemetry, write = out) {
|
|
11692
11741
|
const h = telemetry.health;
|
|
11693
11742
|
const counts = h ? `${h.event_count} event(s), ${h.dropped_events} dropped, ${h.error_count} error(s)` : "no valid health record";
|
|
11694
11743
|
const reason = telemetry.reason ? `; ${telemetry.reason}` : "";
|
|
11695
|
-
|
|
11744
|
+
write(
|
|
11696
11745
|
`tamperward run \u2014 transient observer: ${telemetry.state} (advisory; ${counts}; log ${observer.log}${reason})`
|
|
11697
11746
|
);
|
|
11698
11747
|
}
|
|
11699
|
-
function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
11700
|
-
observerSummary(observer, telemetry);
|
|
11748
|
+
function collectObserverFindings(observer, telemetry, cwd, base, head, policy, write = out) {
|
|
11749
|
+
observerSummary(observer, telemetry, write);
|
|
11701
11750
|
const strict = process.env.TAMPERWARD_TRANSIENT === "block";
|
|
11702
11751
|
const drained = drainEvents(observer.log, 0);
|
|
11703
11752
|
const { events } = drained;
|
|
11704
11753
|
if (!drained.complete) {
|
|
11705
11754
|
const issue = drained.issue ?? "read-stalled";
|
|
11706
|
-
|
|
11755
|
+
write(
|
|
11707
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.")
|
|
11708
11757
|
);
|
|
11709
11758
|
if (strict) return { blocking: true };
|
|
@@ -11719,7 +11768,7 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
|
11719
11768
|
changes.filter((change) => change.kind === "file").map((change) => change.path)
|
|
11720
11769
|
);
|
|
11721
11770
|
} catch (e) {
|
|
11722
|
-
|
|
11771
|
+
write(
|
|
11723
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." : "")
|
|
11724
11773
|
);
|
|
11725
11774
|
return { blocking: strict };
|
|
@@ -11730,16 +11779,16 @@ function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
|
11730
11779
|
};
|
|
11731
11780
|
const findings = transientFindings(events, persistent, policy, finalHash);
|
|
11732
11781
|
for (const finding of findings) {
|
|
11733
|
-
|
|
11782
|
+
write(
|
|
11734
11783
|
`[observer] ${finding.severity === "block" ? "BLOCK" : "warn"} ${finding.rule}${finding.file ? " " + finding.file : ""}: ${finding.message}`
|
|
11735
11784
|
);
|
|
11736
11785
|
}
|
|
11737
11786
|
return { blocking: findings.some((finding) => finding.severity === "block") };
|
|
11738
11787
|
}
|
|
11739
|
-
function finishObserverAdvisory(observer) {
|
|
11788
|
+
function finishObserverAdvisory(observer, write = out) {
|
|
11740
11789
|
if (!observer || observer.finished) return;
|
|
11741
11790
|
const telemetry = stopObserverProcess(observer);
|
|
11742
|
-
observerSummary(observer, telemetry);
|
|
11791
|
+
observerSummary(observer, telemetry, write);
|
|
11743
11792
|
}
|
|
11744
11793
|
function git3(args, cwd) {
|
|
11745
11794
|
return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
@@ -11798,6 +11847,8 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
11798
11847
|
}
|
|
11799
11848
|
function runEnvelope(opts) {
|
|
11800
11849
|
const cwd = resolve7(opts.cwd ?? process.cwd());
|
|
11850
|
+
const say = opts.json ? (_s) => {
|
|
11851
|
+
} : out;
|
|
11801
11852
|
if (!opts.argv.length) {
|
|
11802
11853
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
11803
11854
|
return 2;
|
|
@@ -11848,8 +11899,8 @@ function runEnvelope(opts) {
|
|
|
11848
11899
|
return 2;
|
|
11849
11900
|
}
|
|
11850
11901
|
const verifierBackend = prepareVerifierBackend(frozenPolicy.verify);
|
|
11851
|
-
|
|
11852
|
-
|
|
11902
|
+
say(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
11903
|
+
say(`tamperward run \u2014 verifier backend: ${verifierBackendSummary(verifierBackend)}`);
|
|
11853
11904
|
if (!verifierBackend.available) {
|
|
11854
11905
|
err("tamperward run: the requested verifier backend cannot be established \u2014");
|
|
11855
11906
|
err(`${verifierBackend.reason ?? "unknown backend failure"}. Failing closed before the agent starts.`);
|
|
@@ -11865,7 +11916,7 @@ function runEnvelope(opts) {
|
|
|
11865
11916
|
}
|
|
11866
11917
|
const dependencyEnvironment = verifierBackend.kind === "local" ? discoverDependencyEnvironment(cwd, frozenCmd) : null;
|
|
11867
11918
|
if (dependencyEnvironment) {
|
|
11868
|
-
|
|
11919
|
+
say(`tamperward run \u2014 dependency environment: ${dependencyEnvironmentSummary(dependencyEnvironment)}`);
|
|
11869
11920
|
if (dependencyEnvironment.status === "unattestable" && !opts.allowDepDrift) {
|
|
11870
11921
|
err("tamperward run: the verifier dependency environment cannot be attested \u2014");
|
|
11871
11922
|
err(`${dependencyEnvironment.reason ?? "unknown dependency environment"}. Failing closed before the agent starts.`);
|
|
@@ -11873,11 +11924,11 @@ function runEnvelope(opts) {
|
|
|
11873
11924
|
return 2;
|
|
11874
11925
|
}
|
|
11875
11926
|
} else {
|
|
11876
|
-
|
|
11927
|
+
say(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
|
|
11877
11928
|
}
|
|
11878
11929
|
const observer = opts.observeTransients ? startSupervisedObserver(cwd, base, opts.observerEntry) : null;
|
|
11879
11930
|
if (!observer) {
|
|
11880
|
-
|
|
11931
|
+
say("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
|
|
11881
11932
|
}
|
|
11882
11933
|
const rewritesBefore = objectRewriteState(cwd);
|
|
11883
11934
|
const spawnTicks = nowTicks();
|
|
@@ -11886,16 +11937,38 @@ function runEnvelope(opts) {
|
|
|
11886
11937
|
cwd,
|
|
11887
11938
|
opts.agentBudget,
|
|
11888
11939
|
opts.linuxPythonCandidates,
|
|
11889
|
-
opts.lifecycleTestMode
|
|
11940
|
+
opts.lifecycleTestMode,
|
|
11941
|
+
opts.json === true
|
|
11890
11942
|
);
|
|
11891
11943
|
const agentExit = agentRun.exit;
|
|
11892
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
|
+
};
|
|
11893
11963
|
if (agentRun.failure && !agentTimedOut) {
|
|
11894
11964
|
err(`tamperward run: agent runtime failed to start/report cleanly (${agentRun.failure}).`);
|
|
11895
11965
|
}
|
|
11896
|
-
if (
|
|
11897
|
-
err("tamperward run: the
|
|
11898
|
-
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
|
+
});
|
|
11899
11972
|
return 2;
|
|
11900
11973
|
}
|
|
11901
11974
|
if (agentTimedOut) {
|
|
@@ -11908,9 +11981,10 @@ function runEnvelope(opts) {
|
|
|
11908
11981
|
err("tamperward run: git object-replacement state changed during this run \u2014 a replace ref, graft");
|
|
11909
11982
|
err("or shallow boundary now redirects what the trusted base resolves to. Nothing below could be");
|
|
11910
11983
|
err("read against the anchor we started from. That is the finding.");
|
|
11911
|
-
|
|
11984
|
+
say(`
|
|
11912
11985
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
11913
|
-
finishObserverAdvisory(observer);
|
|
11986
|
+
finishObserverAdvisory(observer, say);
|
|
11987
|
+
emitRunJson("OBJECT_REWRITE", 1);
|
|
11914
11988
|
return 1;
|
|
11915
11989
|
}
|
|
11916
11990
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
@@ -11918,36 +11992,41 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
|
|
|
11918
11992
|
if (isAncestor.status !== 0) {
|
|
11919
11993
|
err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
11920
11994
|
err("the agent rewrote history out from under the anchor. That is the finding.");
|
|
11921
|
-
|
|
11995
|
+
say(`
|
|
11922
11996
|
tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
11923
|
-
finishObserverAdvisory(observer);
|
|
11997
|
+
finishObserverAdvisory(observer, say);
|
|
11998
|
+
emitRunJson("HISTORY_REWRITE", 1, { head });
|
|
11924
11999
|
return 1;
|
|
11925
12000
|
}
|
|
11926
12001
|
const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
|
|
11927
12002
|
const fpBefore = treeFingerprint(cwd, protectedIgnored);
|
|
11928
12003
|
let diffCode = 0;
|
|
11929
12004
|
if (head !== base) {
|
|
11930
|
-
|
|
12005
|
+
say(`
|
|
11931
12006
|
[envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
|
|
11932
|
-
diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy });
|
|
12007
|
+
diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy, silent: opts.json });
|
|
11933
12008
|
}
|
|
11934
|
-
|
|
11935
|
-
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 });
|
|
11936
12011
|
const dependencyBeforeVerificationAttestation = dependencyEnvironment ? attestDependencyEnvironment(cwd, dependencyEnvironment) : void 0;
|
|
11937
12012
|
const dependencyBeforeVerification = dependencyBeforeVerificationAttestation?.check ?? { ok: true };
|
|
11938
12013
|
if (!dependencyBeforeVerification.ok) {
|
|
11939
12014
|
err("tamperward run: the frozen dependency environment changed before verification began \u2014");
|
|
11940
12015
|
err(`${dependencyBeforeVerification.reason ?? "dependency identity changed"}.`);
|
|
11941
12016
|
if (!opts.allowDepDrift) {
|
|
11942
|
-
|
|
12017
|
+
say(`
|
|
11943
12018
|
tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
|
|
11944
|
-
finishObserverAdvisory(observer);
|
|
12019
|
+
finishObserverAdvisory(observer, say);
|
|
12020
|
+
emitRunJson("DEPENDENCY_DRIFT", 1, {
|
|
12021
|
+
head,
|
|
12022
|
+
checks: { diff: diffCode, worktree: workCode }
|
|
12023
|
+
});
|
|
11945
12024
|
return 1;
|
|
11946
12025
|
}
|
|
11947
12026
|
err("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
11948
12027
|
}
|
|
11949
12028
|
let verifierFinalDependencyAttestation;
|
|
11950
|
-
|
|
12029
|
+
say("\n[envelope] pristine verification against the trusted base:");
|
|
11951
12030
|
const verifyCode = runVerify({
|
|
11952
12031
|
cwd,
|
|
11953
12032
|
base,
|
|
@@ -11962,7 +12041,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
11962
12041
|
}
|
|
11963
12042
|
} : {},
|
|
11964
12043
|
allowDepDrift: opts.allowDepDrift,
|
|
11965
|
-
verifierBackend
|
|
12044
|
+
verifierBackend,
|
|
12045
|
+
silent: opts.json
|
|
11966
12046
|
});
|
|
11967
12047
|
if (opts.settle && opts.settle > 0) {
|
|
11968
12048
|
waitForSettleSync(opts.settle);
|
|
@@ -11975,7 +12055,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
11975
12055
|
const dependencyAfter = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
11976
12056
|
if (dependencyEnvironment && process.env.TAMPERWARD_DIAGNOSTICS === "1") {
|
|
11977
12057
|
const metrics = dependencyEnvironmentDiagnostics(dependencyEnvironment);
|
|
11978
|
-
|
|
12058
|
+
say(
|
|
11979
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"}`
|
|
11980
12060
|
);
|
|
11981
12061
|
}
|
|
@@ -12003,7 +12083,8 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
12003
12083
|
cwd,
|
|
12004
12084
|
base,
|
|
12005
12085
|
head,
|
|
12006
|
-
frozenPolicy
|
|
12086
|
+
frozenPolicy,
|
|
12087
|
+
say
|
|
12007
12088
|
);
|
|
12008
12089
|
observerBlocked = observed.blocking;
|
|
12009
12090
|
}
|
|
@@ -12012,10 +12093,25 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
12012
12093
|
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent || observerBlocked;
|
|
12013
12094
|
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
|
12014
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";
|
|
12015
12097
|
const agentSummary = agentTimedOut ? `AGENT_TIMEOUT (${opts.agentBudget}s; exit 124)` : `agent exit ${agentExit}`;
|
|
12016
|
-
|
|
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(`
|
|
12017
12112
|
tamperward run \u2014 ${agentSummary}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
|
|
12018
|
-
|
|
12113
|
+
}
|
|
12114
|
+
return exitCode;
|
|
12019
12115
|
}
|
|
12020
12116
|
function parseRun(args) {
|
|
12021
12117
|
const o = { argv: [] };
|
|
@@ -12027,6 +12123,7 @@ function parseRun(args) {
|
|
|
12027
12123
|
} else if (a === "--base") o.base = args[++i];
|
|
12028
12124
|
else if (a === "--cmd") o.cmd = args[++i];
|
|
12029
12125
|
else if (a === "--budget") o.budget = Number(args[++i]);
|
|
12126
|
+
else if (a === "--json") o.json = true;
|
|
12030
12127
|
else if (a === "--agent-budget") o.agentBudget = Number(args[++i]);
|
|
12031
12128
|
else if (a === "--allow-dirty") o.allowDirty = true;
|
|
12032
12129
|
else if (a === "--settle") o.settle = Number(args[++i]);
|
|
@@ -12073,14 +12170,14 @@ function lifecyclePlatformCheck(platform = process.platform, linuxPython = platf
|
|
|
12073
12170
|
var VERIFY_COMMAND = /\btamperward(?:@\S+)?\s+verify\b/;
|
|
12074
12171
|
function err2(message, opts, id = "doctor", checks = []) {
|
|
12075
12172
|
if (opts?.json) {
|
|
12076
|
-
const report2 = {
|
|
12173
|
+
const report2 = machineOutput({
|
|
12077
12174
|
command: "doctor",
|
|
12078
12175
|
authoritative: false,
|
|
12079
12176
|
checks: [
|
|
12080
12177
|
...checks.filter((check2) => check2.id !== id),
|
|
12081
12178
|
{ id, state: "BROKEN", detail: message }
|
|
12082
12179
|
]
|
|
12083
|
-
};
|
|
12180
|
+
});
|
|
12084
12181
|
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
12085
12182
|
} else {
|
|
12086
12183
|
process.stderr.write(`tamperward doctor: ${message}
|
|
@@ -12293,7 +12390,7 @@ function observerCheck(cwd) {
|
|
|
12293
12390
|
function emitReport(opts, checks) {
|
|
12294
12391
|
const authoritative = !checks.some((x) => x.state === "BROKEN");
|
|
12295
12392
|
if (opts.json) {
|
|
12296
|
-
const report2 = { command: "doctor", authoritative, checks };
|
|
12393
|
+
const report2 = machineOutput({ command: "doctor", authoritative, checks });
|
|
12297
12394
|
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
12298
12395
|
return;
|
|
12299
12396
|
}
|
|
@@ -13149,7 +13246,7 @@ function validateCliArgs(cmd, args) {
|
|
|
13149
13246
|
const prefix = args.slice(0, delimiter3);
|
|
13150
13247
|
const command = args.slice(delimiter3 + 1);
|
|
13151
13248
|
const parsed = validateFlatArgs(prefix, {
|
|
13152
|
-
flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients"],
|
|
13249
|
+
flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients", "--json"],
|
|
13153
13250
|
values: {
|
|
13154
13251
|
"--base": "string",
|
|
13155
13252
|
"--cmd": "string",
|
|
@@ -13250,7 +13347,8 @@ Formats:
|
|
|
13250
13347
|
github GitHub Actions view: one inline annotation per finding (so it shows up
|
|
13251
13348
|
on the line in "Files changed") plus a job-summary table on the run
|
|
13252
13349
|
page, alongside the same text output in the log.
|
|
13253
|
-
json
|
|
13350
|
+
json versioned machine-readable output. Top-level schema_version identifies
|
|
13351
|
+
the public JSON-schema major; published schemas live under schemas/.
|
|
13254
13352
|
auto github when GITHUB_ACTIONS=true, otherwise text.
|
|
13255
13353
|
tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
|
|
13256
13354
|
tamperward sweep claude Stop sweep (re-scan the turn's working tree)
|
|
@@ -13279,11 +13377,13 @@ Formats:
|
|
|
13279
13377
|
[--budget S] [--agent-budget S] untrusted, then re-adjudicate the tree it
|
|
13280
13378
|
[--allow-dirty] [--settle S] left (policy over base...HEAD and the
|
|
13281
13379
|
[--allow-dep-drift] [--cwd D] worktree, plus verify).
|
|
13282
|
-
[--observe-transients]
|
|
13380
|
+
[--observe-transients] [--json] --observe-transients starts a session-
|
|
13283
13381
|
scoped observer before the agent and
|
|
13284
13382
|
reports temporal evidence/health after it.
|
|
13285
13383
|
--agent-budget bounds the agent runtime
|
|
13286
13384
|
itself and still adjudicates after timeout.
|
|
13385
|
+
--json emits one final versioned run verdict
|
|
13386
|
+
document on stdout after agent adjudication.
|
|
13287
13387
|
Exit: agent's
|
|
13288
13388
|
code when clean; 124 on clean AGENT_TIMEOUT;
|
|
13289
13389
|
1 on any blocking finding/masked failure;
|