tamperward 2.37.1 → 2.38.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/dist/cli/index.js +1147 -190
- package/package.json +1 -1
- package/schemas/audit-v1.schema.json +1 -1
- package/schemas/check-v1.schema.json +1 -1
- package/schemas/doctor-v1.schema.json +1 -1
- package/schemas/receipt-v1.schema.json +90 -0
- package/schemas/reconcile-v1.schema.json +211 -0
- package/schemas/research-stdio-v1.schema.json +1 -1
- package/schemas/research-v1.schema.json +1 -1
- package/schemas/run-v1.schema.json +1 -1
- package/schemas/runtime-qualification-v1.schema.json +1 -1
- package/schemas/stats-v1.schema.json +1 -1
- package/schemas/status-v1.schema.json +1 -1
- package/schemas/verify-v1.schema.json +5 -1
package/dist/cli/index.js
CHANGED
|
@@ -5300,13 +5300,13 @@ function merge(a, b) {
|
|
|
5300
5300
|
function literals(e) {
|
|
5301
5301
|
if (ts.isStringLiteral(e)) return { items: [e.text], opaque: false };
|
|
5302
5302
|
if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
5303
|
-
const
|
|
5303
|
+
const items2 = [];
|
|
5304
5304
|
let opaque = false;
|
|
5305
5305
|
for (const el of e.elements) {
|
|
5306
|
-
if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el))
|
|
5306
|
+
if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) items2.push(el.text);
|
|
5307
5307
|
else opaque = true;
|
|
5308
5308
|
}
|
|
5309
|
-
return { items, opaque };
|
|
5309
|
+
return { items: items2, opaque };
|
|
5310
5310
|
}
|
|
5311
5311
|
function parseLists(src) {
|
|
5312
5312
|
const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], ignoreRe: [], opaqueCollect: false, opaqueIgnore: false };
|
|
@@ -5319,13 +5319,13 @@ function parseLists(src) {
|
|
|
5319
5319
|
const k = keyName(node.name);
|
|
5320
5320
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
5321
5321
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
5322
|
-
const { items, opaque } = literals(resolve23(node.initializer));
|
|
5323
|
-
res.collectFrom =
|
|
5322
|
+
const { items: items2, opaque } = literals(resolve23(node.initializer));
|
|
5323
|
+
res.collectFrom = items2;
|
|
5324
5324
|
res.collectKey = k === "include" ? "coverage.include" : k;
|
|
5325
5325
|
if (opaque) res.opaqueCollect = true;
|
|
5326
5326
|
} else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
|
|
5327
|
-
const { items, opaque } = literals(resolve23(node.initializer));
|
|
5328
|
-
(k === "exclude" ? res.ignore : res.ignoreRe).push(...
|
|
5327
|
+
const { items: items2, opaque } = literals(resolve23(node.initializer));
|
|
5328
|
+
(k === "exclude" ? res.ignore : res.ignoreRe).push(...items2);
|
|
5329
5329
|
if (opaque) res.opaqueIgnore = true;
|
|
5330
5330
|
}
|
|
5331
5331
|
}
|
|
@@ -5923,15 +5923,15 @@ function keyName2(name) {
|
|
|
5923
5923
|
function literals2(e) {
|
|
5924
5924
|
if (isStr(e)) return { items: [e.text], opaque: false };
|
|
5925
5925
|
if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
5926
|
-
const
|
|
5926
|
+
const items2 = [];
|
|
5927
5927
|
let opaque = false;
|
|
5928
5928
|
for (const el of e.elements) {
|
|
5929
|
-
if (isStr(el))
|
|
5929
|
+
if (isStr(el)) items2.push(el.text);
|
|
5930
5930
|
else if (ts.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
|
|
5931
|
-
|
|
5931
|
+
items2.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5932
5932
|
} else opaque = true;
|
|
5933
5933
|
}
|
|
5934
|
-
return { items, opaque };
|
|
5934
|
+
return { items: items2, opaque };
|
|
5935
5935
|
}
|
|
5936
5936
|
function ownerKey2(node) {
|
|
5937
5937
|
const obj = node.parent;
|
|
@@ -6054,11 +6054,11 @@ function collect(root, runner) {
|
|
|
6054
6054
|
const sel = empty();
|
|
6055
6055
|
const seen = /* @__PURE__ */ new Set();
|
|
6056
6056
|
const take = (key2, e) => {
|
|
6057
|
-
const { items, opaque } = literals2(e);
|
|
6057
|
+
const { items: items2, opaque } = literals2(e);
|
|
6058
6058
|
if (opaque || seen.has(key2)) sel.opaque = true;
|
|
6059
6059
|
seen.add(key2);
|
|
6060
6060
|
sel.present = true;
|
|
6061
|
-
return
|
|
6061
|
+
return items2.map(stripRootDir);
|
|
6062
6062
|
};
|
|
6063
6063
|
const ignores = (key2, e) => {
|
|
6064
6064
|
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
@@ -10701,10 +10701,10 @@ function validate(r, where) {
|
|
|
10701
10701
|
throw new PolicyError(`${where}: ${msg}`);
|
|
10702
10702
|
}
|
|
10703
10703
|
const show = (v) => JSON.stringify(v) ?? String(v);
|
|
10704
|
-
function refuseNegated(
|
|
10704
|
+
function refuseNegated(field2, list3) {
|
|
10705
10705
|
const neg = list3.find(isNegatedGlob);
|
|
10706
10706
|
if (neg !== void 0) {
|
|
10707
|
-
bad2(`${
|
|
10707
|
+
bad2(`${field2}: negated glob ${show(neg)} is not allowed \u2014 a "!" pattern matches every path, so it would ${field2 === "ignore" ? "ignore every file" : field2.startsWith("protected.") ? `put every file in ${field2}` : "blind the rule on every file"}; list the paths to include instead`);
|
|
10708
10708
|
}
|
|
10709
10709
|
}
|
|
10710
10710
|
const out3 = {};
|
|
@@ -14395,6 +14395,10 @@ jobs:
|
|
|
14395
14395
|
# Requires a \`verify:\` block in .tamperward.yml naming the suite command;
|
|
14396
14396
|
# without one this step fails closed (exit 2) rather than passing quietly.
|
|
14397
14397
|
- name: Tamperward verify (pristine re-execution)
|
|
14398
|
+
# bash so \`set -o pipefail\` propagates verify's exit through the tee \u2014 the
|
|
14399
|
+
# default GitHub shell does not enable pipefail, and tee's own 0 would
|
|
14400
|
+
# otherwise mask a red verify.
|
|
14401
|
+
shell: bash
|
|
14398
14402
|
env:
|
|
14399
14403
|
# The same channel as the gate. A compact token generated for \`verify\`
|
|
14400
14404
|
# (or legacy \`tamperward:allow:verify@<head-sha>\`) accepts a MASKED_FAILURE \u2014
|
|
@@ -14404,7 +14408,53 @@ jobs:
|
|
|
14404
14408
|
# stays red whatever the labels say.
|
|
14405
14409
|
TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
|
|
14406
14410
|
TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
|
|
14407
|
-
|
|
14411
|
+
# --json to a file for the reconciliation step below; the tee keeps the
|
|
14412
|
+
# verdict visible in the log. This step remains the ENFORCEMENT authority:
|
|
14413
|
+
# its own exit gates the job.
|
|
14414
|
+
run: tamperward verify --require-ancestor --base "\${{ github.event.pull_request.base.sha }}" --json | tee "$RUNNER_TEMP/tw-verify.json"
|
|
14415
|
+
# EVIDENCE, not authority (#601). CI has now rerun the canonical verification
|
|
14416
|
+
# itself; this step reconciles a claimed LOCAL verification receipt against
|
|
14417
|
+
# THAT result and writes a job summary separating the local claim, the CI
|
|
14418
|
+
# result and their agreement/divergence. A stale/mismatched/tampered/missing/
|
|
14419
|
+
# unknown-schema receipt can NEVER promote a CI result \u2014 the reconciled
|
|
14420
|
+
# verdict is CI's own (consumed from the verify --json above), recomputed from
|
|
14421
|
+
# trusted inputs. Set TAMPERWARD_RECEIPT (e.g. from a download-artifact step)
|
|
14422
|
+
# to a receipt exported by \`tamperward receipt export\` on the BRANCH TIP;
|
|
14423
|
+
# with none, reconciliation reports NO_CLAIM and CI's verdict stands.
|
|
14424
|
+
# \`if: always()\` re-asserts CI's verdict even when the verify step failed.
|
|
14425
|
+
#
|
|
14426
|
+
# The reconcile computes CI's candidate identity from pull_request.head.sha \u2014
|
|
14427
|
+
# the branch tip a developer verifies \u2014 NOT this job's merge-ref HEAD, so a
|
|
14428
|
+
# genuine receipt's \`head\`/\`tree\` line up (a receipt binds the branch tip;
|
|
14429
|
+
# the merge ref is a synthetic commit that no receipt can match). A linked
|
|
14430
|
+
# worktree reuses this checkout's object store (base, head and the merge
|
|
14431
|
+
# commit are all present from fetch-depth: 0), so merge-base resolves without
|
|
14432
|
+
# a second fetch. reconcile with --ci-result runs NO candidate code here \u2014 it
|
|
14433
|
+
# only computes identity (git/filesystem reads) and consumes the verdict the
|
|
14434
|
+
# verify step already produced. The enforcement authority remains the verify
|
|
14435
|
+
# step, which runs over the MERGE result as before.
|
|
14436
|
+
- name: Tamperward receipt reconciliation (evidence)
|
|
14437
|
+
if: always()
|
|
14438
|
+
shell: bash
|
|
14439
|
+
env:
|
|
14440
|
+
# The reconcile step's exit re-asserts CI's verdict, and a MASKED_FAILURE
|
|
14441
|
+
# a reviewer has signed off must exit 0 here just as the verify step does.
|
|
14442
|
+
# reconcile recomputes that sign-off from this SAME trusted channel \u2014 never
|
|
14443
|
+
# from the --ci-result document \u2014 so a forged oob_signoff field inside the
|
|
14444
|
+
# file it validates can never flip the exit (#601 re-review).
|
|
14445
|
+
TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
|
|
14446
|
+
TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
|
|
14447
|
+
run: |
|
|
14448
|
+
set -euo pipefail
|
|
14449
|
+
HEAD_TREE="$RUNNER_TEMP/tw-head"
|
|
14450
|
+
rm -rf "$HEAD_TREE"
|
|
14451
|
+
git worktree add --detach "$HEAD_TREE" "\${{ github.event.pull_request.head.sha }}"
|
|
14452
|
+
args=(receipt reconcile --require-ancestor --cwd "$HEAD_TREE" --base "\${{ github.event.pull_request.base.sha }}" --ci-result "$RUNNER_TEMP/tw-verify.json")
|
|
14453
|
+
if [ -n "\${TAMPERWARD_RECEIPT:-}" ] && [ -f "\${TAMPERWARD_RECEIPT:-}" ]; then
|
|
14454
|
+
args+=(--receipt "$TAMPERWARD_RECEIPT")
|
|
14455
|
+
fi
|
|
14456
|
+
tamperward "\${args[@]}"
|
|
14457
|
+
git worktree remove --force "$HEAD_TREE" || true
|
|
14408
14458
|
`;
|
|
14409
14459
|
WORKFLOW_MARK = "# tamperward:generated";
|
|
14410
14460
|
WORKFLOW_MARK_RE = /^# tamperward:generated v(\S+) sha256:([0-9a-f]+)\n/m;
|
|
@@ -15752,6 +15802,14 @@ function resolveBaseRev(baseRef, cwd) {
|
|
|
15752
15802
|
return rev2;
|
|
15753
15803
|
}
|
|
15754
15804
|
}
|
|
15805
|
+
function resolveConcreteCommit(cwd, ref) {
|
|
15806
|
+
try {
|
|
15807
|
+
assertRev(ref);
|
|
15808
|
+
return git2(["rev-parse", "--verify", `${ref}^{commit}`], repoRoot(cwd)).trim();
|
|
15809
|
+
} catch {
|
|
15810
|
+
return null;
|
|
15811
|
+
}
|
|
15812
|
+
}
|
|
15755
15813
|
function canonicalJson(value) {
|
|
15756
15814
|
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
15757
15815
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
@@ -16001,6 +16059,15 @@ function firstMismatch(recorded, live) {
|
|
|
16001
16059
|
}
|
|
16002
16060
|
return null;
|
|
16003
16061
|
}
|
|
16062
|
+
function firstCandidateIdentityMismatch(recorded, live) {
|
|
16063
|
+
for (const input of CANDIDATE_MISMATCH_PRIORITY) {
|
|
16064
|
+
if (recorded[input] !== live[input]) return input;
|
|
16065
|
+
}
|
|
16066
|
+
return null;
|
|
16067
|
+
}
|
|
16068
|
+
function environmentInputDivergences(recorded, live) {
|
|
16069
|
+
return ENVIRONMENT_INPUTS.filter((input) => recorded[input] !== live[input]);
|
|
16070
|
+
}
|
|
16004
16071
|
function evaluateVerificationState(cwd) {
|
|
16005
16072
|
if (readVerifyingMarker(cwd)) {
|
|
16006
16073
|
return { state: "VERIFYING", reason: "a verification is in progress" };
|
|
@@ -16047,7 +16114,7 @@ function evaluateVerificationState(cwd) {
|
|
|
16047
16114
|
record
|
|
16048
16115
|
};
|
|
16049
16116
|
}
|
|
16050
|
-
var VERIFICATION_STATE_SCHEMA_VERSION, BINDING_INPUTS, STALE_REASON, MISMATCH_PRIORITY;
|
|
16117
|
+
var VERIFICATION_STATE_SCHEMA_VERSION, BINDING_INPUTS, STALE_REASON, MISMATCH_PRIORITY, CANDIDATE_IDENTITY_INPUTS, ENVIRONMENT_INPUTS, CANDIDATE_IDENTITY_SET, CANDIDATE_MISMATCH_PRIORITY;
|
|
16051
16118
|
var init_verification_state = __esm({
|
|
16052
16119
|
"src/verification-state.ts"() {
|
|
16053
16120
|
"use strict";
|
|
@@ -16090,15 +16157,257 @@ var init_verification_state = __esm({
|
|
|
16090
16157
|
"policy",
|
|
16091
16158
|
"tree"
|
|
16092
16159
|
];
|
|
16160
|
+
CANDIDATE_IDENTITY_INPUTS = [
|
|
16161
|
+
"tree",
|
|
16162
|
+
"head",
|
|
16163
|
+
"base",
|
|
16164
|
+
"policy",
|
|
16165
|
+
"verifier",
|
|
16166
|
+
"surface"
|
|
16167
|
+
];
|
|
16168
|
+
ENVIRONMENT_INPUTS = ["intervention", "dependencies"];
|
|
16169
|
+
CANDIDATE_IDENTITY_SET = new Set(CANDIDATE_IDENTITY_INPUTS);
|
|
16170
|
+
CANDIDATE_MISMATCH_PRIORITY = MISMATCH_PRIORITY.filter(
|
|
16171
|
+
(i) => CANDIDATE_IDENTITY_SET.has(i)
|
|
16172
|
+
);
|
|
16173
|
+
}
|
|
16174
|
+
});
|
|
16175
|
+
|
|
16176
|
+
// src/verification-receipt.ts
|
|
16177
|
+
import { createHash as createHash13 } from "node:crypto";
|
|
16178
|
+
import { mkdirSync as mkdirSync9, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
|
|
16179
|
+
import { dirname as dirname9, join as join20 } from "node:path";
|
|
16180
|
+
function isRecord5(v) {
|
|
16181
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16182
|
+
}
|
|
16183
|
+
function canonicalJson2(value) {
|
|
16184
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
16185
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson2).join(",")}]`;
|
|
16186
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
16187
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson2(v)}`).join(",")}}`;
|
|
16188
|
+
}
|
|
16189
|
+
function receiptEvidenceDigest(fields2) {
|
|
16190
|
+
const canonical2 = {
|
|
16191
|
+
schema_version: fields2.schema_version,
|
|
16192
|
+
verdict: fields2.verdict,
|
|
16193
|
+
binding: BINDING_INPUTS.reduce((acc2, k) => {
|
|
16194
|
+
acc2[k] = fields2.binding[k];
|
|
16195
|
+
return acc2;
|
|
16196
|
+
}, {}),
|
|
16197
|
+
stages: fields2.stages
|
|
16198
|
+
};
|
|
16199
|
+
return "sha256:" + createHash13("sha256").update(canonicalJson2(canonical2)).digest("hex");
|
|
16200
|
+
}
|
|
16201
|
+
function receiptFromRecord(record) {
|
|
16202
|
+
const stages = { candidate: "PASS", pristine: "PASS", integrity: "CLEAN" };
|
|
16203
|
+
const core = {
|
|
16204
|
+
schema_version: RECEIPT_SCHEMA_VERSION,
|
|
16205
|
+
verdict: "VERIFIED",
|
|
16206
|
+
binding: record.binding,
|
|
16207
|
+
stages
|
|
16208
|
+
};
|
|
16209
|
+
return {
|
|
16210
|
+
...core,
|
|
16211
|
+
tw_version: record.tw_version,
|
|
16212
|
+
verified_at: record.verified_at,
|
|
16213
|
+
evidence_digest: receiptEvidenceDigest(core)
|
|
16214
|
+
};
|
|
16215
|
+
}
|
|
16216
|
+
function receiptPath(cwd) {
|
|
16217
|
+
const ctx = repoContext(cwd);
|
|
16218
|
+
return ctx ? join20(ctx.gitDir, "tamperward", "verification-receipt.json") : null;
|
|
16219
|
+
}
|
|
16220
|
+
function storeReceipt(cwd, receipt) {
|
|
16221
|
+
const path = receiptPath(cwd);
|
|
16222
|
+
if (!path) return false;
|
|
16223
|
+
try {
|
|
16224
|
+
mkdirSync9(dirname9(path), { recursive: true });
|
|
16225
|
+
writeFileSync10(path, JSON.stringify(receipt) + "\n");
|
|
16226
|
+
return true;
|
|
16227
|
+
} catch {
|
|
16228
|
+
return false;
|
|
16229
|
+
}
|
|
16230
|
+
}
|
|
16231
|
+
function removeStoredReceipt(cwd) {
|
|
16232
|
+
const path = receiptPath(cwd);
|
|
16233
|
+
if (!path) return;
|
|
16234
|
+
try {
|
|
16235
|
+
rmSync8(path, { force: true });
|
|
16236
|
+
} catch {
|
|
16237
|
+
}
|
|
16238
|
+
}
|
|
16239
|
+
function isOneOf(list3, v) {
|
|
16240
|
+
return typeof v === "string" && list3.includes(v);
|
|
16241
|
+
}
|
|
16242
|
+
function isStages(v) {
|
|
16243
|
+
return isRecord5(v) && isOneOf(RECEIPT_STAGE_RESULTS, v.candidate) && isOneOf(RECEIPT_STAGE_RESULTS, v.pristine) && isOneOf(RECEIPT_INTEGRITY_RESULTS, v.integrity);
|
|
16244
|
+
}
|
|
16245
|
+
function isBinding2(v) {
|
|
16246
|
+
if (!isRecord5(v)) return false;
|
|
16247
|
+
return BINDING_INPUTS.every((k) => {
|
|
16248
|
+
const val = v[k];
|
|
16249
|
+
return typeof val === "string" && val.length > 0;
|
|
16250
|
+
});
|
|
16251
|
+
}
|
|
16252
|
+
function classifyReceipt(value) {
|
|
16253
|
+
if (value === null || value === void 0) {
|
|
16254
|
+
return { disposition: "ABSENT", detail: "no local verification receipt was provided" };
|
|
16255
|
+
}
|
|
16256
|
+
if (!isRecord5(value)) {
|
|
16257
|
+
return { disposition: "UNKNOWN_SCHEMA", detail: "receipt is not a JSON object" };
|
|
16258
|
+
}
|
|
16259
|
+
if (value.schema_version !== RECEIPT_SCHEMA_VERSION) {
|
|
16260
|
+
return {
|
|
16261
|
+
disposition: "UNKNOWN_SCHEMA",
|
|
16262
|
+
detail: `unrecognised receipt schema_version ${JSON.stringify(value.schema_version)} (this build understands ${RECEIPT_SCHEMA_VERSION})`
|
|
16263
|
+
};
|
|
16264
|
+
}
|
|
16265
|
+
if (value.verdict !== "VERIFIED") {
|
|
16266
|
+
return { disposition: "MALFORMED", detail: "receipt verdict is not VERIFIED" };
|
|
16267
|
+
}
|
|
16268
|
+
if (typeof value.tw_version !== "string" || typeof value.verified_at !== "string") {
|
|
16269
|
+
return { disposition: "MALFORMED", detail: "receipt is missing tw_version or verified_at" };
|
|
16270
|
+
}
|
|
16271
|
+
if (!isBinding2(value.binding)) {
|
|
16272
|
+
return { disposition: "MALFORMED", detail: "receipt binding is incomplete or malformed" };
|
|
16273
|
+
}
|
|
16274
|
+
if (!isStages(value.stages)) {
|
|
16275
|
+
return { disposition: "MALFORMED", detail: "receipt stages are incomplete or malformed" };
|
|
16276
|
+
}
|
|
16277
|
+
if (value.stages.candidate !== "PASS" || value.stages.pristine !== "PASS" || value.stages.integrity !== "CLEAN") {
|
|
16278
|
+
return {
|
|
16279
|
+
disposition: "MALFORMED",
|
|
16280
|
+
detail: "receipt verdict is VERIFIED but a stage result is not a clean pass"
|
|
16281
|
+
};
|
|
16282
|
+
}
|
|
16283
|
+
if (typeof value.evidence_digest !== "string") {
|
|
16284
|
+
return { disposition: "MALFORMED", detail: "receipt evidence_digest is missing" };
|
|
16285
|
+
}
|
|
16286
|
+
const b = value.binding;
|
|
16287
|
+
const binding = {
|
|
16288
|
+
tree: b.tree,
|
|
16289
|
+
head: b.head,
|
|
16290
|
+
base: b.base,
|
|
16291
|
+
policy: b.policy,
|
|
16292
|
+
verifier: b.verifier,
|
|
16293
|
+
surface: b.surface,
|
|
16294
|
+
intervention: b.intervention,
|
|
16295
|
+
dependencies: b.dependencies
|
|
16296
|
+
};
|
|
16297
|
+
const stages = {
|
|
16298
|
+
candidate: value.stages.candidate,
|
|
16299
|
+
pristine: value.stages.pristine,
|
|
16300
|
+
integrity: value.stages.integrity
|
|
16301
|
+
};
|
|
16302
|
+
const expected = receiptEvidenceDigest({
|
|
16303
|
+
schema_version: RECEIPT_SCHEMA_VERSION,
|
|
16304
|
+
verdict: "VERIFIED",
|
|
16305
|
+
binding,
|
|
16306
|
+
stages
|
|
16307
|
+
});
|
|
16308
|
+
if (expected !== value.evidence_digest) {
|
|
16309
|
+
return { disposition: "MALFORMED", detail: "receipt evidence_digest does not match its own contents (tampered)" };
|
|
16310
|
+
}
|
|
16311
|
+
return {
|
|
16312
|
+
disposition: "PRESENT",
|
|
16313
|
+
receipt: {
|
|
16314
|
+
schema_version: RECEIPT_SCHEMA_VERSION,
|
|
16315
|
+
verdict: "VERIFIED",
|
|
16316
|
+
tw_version: value.tw_version,
|
|
16317
|
+
verified_at: value.verified_at,
|
|
16318
|
+
binding,
|
|
16319
|
+
stages,
|
|
16320
|
+
evidence_digest: value.evidence_digest
|
|
16321
|
+
}
|
|
16322
|
+
};
|
|
16323
|
+
}
|
|
16324
|
+
function reconcile(ci, claimed) {
|
|
16325
|
+
const base = {
|
|
16326
|
+
result: ci.verdict,
|
|
16327
|
+
// CI is the sole authority for the verdict — always.
|
|
16328
|
+
agreement: "NO_CLAIM",
|
|
16329
|
+
applicable: false,
|
|
16330
|
+
local: { disposition: claimed.disposition },
|
|
16331
|
+
ci: { verdict: ci.verdict },
|
|
16332
|
+
divergence: [],
|
|
16333
|
+
environment_divergence: []
|
|
16334
|
+
};
|
|
16335
|
+
if (claimed.disposition !== "PRESENT") {
|
|
16336
|
+
base.local.detail = claimed.detail;
|
|
16337
|
+
if (claimed.disposition === "ABSENT") {
|
|
16338
|
+
base.agreement = "NO_CLAIM";
|
|
16339
|
+
} else {
|
|
16340
|
+
base.agreement = "NON_APPLICABLE";
|
|
16341
|
+
base.divergence.push(`local receipt ${claimed.disposition === "UNKNOWN_SCHEMA" ? "has an unknown schema/version" : "is malformed or tampered"} \u2014 ignored as evidence`);
|
|
16342
|
+
}
|
|
16343
|
+
return base;
|
|
16344
|
+
}
|
|
16345
|
+
const receipt = claimed.receipt;
|
|
16346
|
+
base.local.verdict = receipt.verdict;
|
|
16347
|
+
base.local.verified_at = receipt.verified_at;
|
|
16348
|
+
if (!ci.binding) {
|
|
16349
|
+
base.agreement = "NON_APPLICABLE";
|
|
16350
|
+
base.divergence.push(`CI could not independently compute the candidate identity${ci.binding_error ? ` (${ci.binding_error})` : ""}; the receipt cannot be applied`);
|
|
16351
|
+
return base;
|
|
16352
|
+
}
|
|
16353
|
+
base.environment_divergence = environmentInputDivergences(receipt.binding, ci.binding);
|
|
16354
|
+
const mismatch = firstCandidateIdentityMismatch(receipt.binding, ci.binding);
|
|
16355
|
+
if (mismatch) {
|
|
16356
|
+
base.agreement = "NON_APPLICABLE";
|
|
16357
|
+
base.mismatched_input = mismatch;
|
|
16358
|
+
base.divergence.push(`receipt binds a different \`${mismatch}\` than CI computed \u2014 the receipt describes another state`);
|
|
16359
|
+
return base;
|
|
16360
|
+
}
|
|
16361
|
+
if (ci.adjudicated_tree === void 0) {
|
|
16362
|
+
base.agreement = "NON_APPLICABLE";
|
|
16363
|
+
base.mismatched_input = "tree";
|
|
16364
|
+
base.divergence.push(
|
|
16365
|
+
"CI verify result predates adjudicated-tree reporting \u2014 cannot confirm CI ran this tree; treating as non-applicable"
|
|
16366
|
+
);
|
|
16367
|
+
return base;
|
|
16368
|
+
}
|
|
16369
|
+
if (ci.adjudicated_tree !== receipt.binding.tree) {
|
|
16370
|
+
base.agreement = "NON_APPLICABLE";
|
|
16371
|
+
base.mismatched_input = "tree";
|
|
16372
|
+
base.divergence.push(
|
|
16373
|
+
"CI adjudicated the merge result, not this tree \u2014 bring the branch up to date to reconcile"
|
|
16374
|
+
);
|
|
16375
|
+
return base;
|
|
16376
|
+
}
|
|
16377
|
+
for (const env of base.environment_divergence) {
|
|
16378
|
+
base.divergence.push(`informational: ${STALE_REASON[env]} (machine-local; not load-bearing for cross-machine applicability)`);
|
|
16379
|
+
}
|
|
16380
|
+
base.applicable = true;
|
|
16381
|
+
if (ci.verdict === "VERIFIED") {
|
|
16382
|
+
base.agreement = "AGREE";
|
|
16383
|
+
return base;
|
|
16384
|
+
}
|
|
16385
|
+
base.agreement = "DIVERGENCE";
|
|
16386
|
+
base.divergence.push(`local claim VERIFIED, CI ${ci.verdict}`);
|
|
16387
|
+
if (receipt.stages.pristine === "PASS" && ci.verdict === "MASKED_FAILURE") {
|
|
16388
|
+
base.divergence.push("local pristine: PASS / CI pristine: FAIL");
|
|
16389
|
+
}
|
|
16390
|
+
return base;
|
|
16391
|
+
}
|
|
16392
|
+
var RECEIPT_SCHEMA_VERSION, RECEIPT_STAGE_RESULTS, RECEIPT_INTEGRITY_RESULTS;
|
|
16393
|
+
var init_verification_receipt = __esm({
|
|
16394
|
+
"src/verification-receipt.ts"() {
|
|
16395
|
+
"use strict";
|
|
16396
|
+
init_repo_context();
|
|
16397
|
+
init_verification_state();
|
|
16398
|
+
init_machine_output();
|
|
16399
|
+
RECEIPT_SCHEMA_VERSION = 1;
|
|
16400
|
+
RECEIPT_STAGE_RESULTS = ["PASS", "FAIL"];
|
|
16401
|
+
RECEIPT_INTEGRITY_RESULTS = ["CLEAN", "CHANGED"];
|
|
16093
16402
|
}
|
|
16094
16403
|
});
|
|
16095
16404
|
|
|
16096
16405
|
// src/cli/verify.ts
|
|
16097
16406
|
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
16098
|
-
import { createHash as
|
|
16099
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as
|
|
16407
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
16408
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync9, symlinkSync, writeFileSync as writeFileSync11, existsSync as existsSync14 } from "node:fs";
|
|
16100
16409
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
16101
|
-
import { dirname as
|
|
16410
|
+
import { dirname as dirname10, isAbsolute as isAbsolute11, join as join21, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
|
|
16102
16411
|
function oracleAssuranceReport() {
|
|
16103
16412
|
return {
|
|
16104
16413
|
level: "suite-exit-only",
|
|
@@ -16162,15 +16471,15 @@ function rootedLinkTarget(target) {
|
|
|
16162
16471
|
return isAbsolute11(target) || driveRelativeLinkTarget(target);
|
|
16163
16472
|
}
|
|
16164
16473
|
function safeSymlink(target, out3, root, label) {
|
|
16165
|
-
if (rootedLinkTarget(target) || !inside2(root, resolve9(
|
|
16474
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve9(dirname10(out3), target))) {
|
|
16166
16475
|
throw linkEscape(label, target);
|
|
16167
16476
|
}
|
|
16168
|
-
|
|
16477
|
+
rmSync9(out3, { force: true });
|
|
16169
16478
|
symlinkSync(target, out3);
|
|
16170
16479
|
}
|
|
16171
16480
|
function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
16172
16481
|
let domain = "tree";
|
|
16173
|
-
let stack = linkParts(relative6(root,
|
|
16482
|
+
let stack = linkParts(relative6(root, dirname10(out3)));
|
|
16174
16483
|
const pending = linkParts(target);
|
|
16175
16484
|
const seen = /* @__PURE__ */ new Set();
|
|
16176
16485
|
let hops = 0;
|
|
@@ -16187,7 +16496,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
16187
16496
|
continue;
|
|
16188
16497
|
}
|
|
16189
16498
|
const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
|
|
16190
|
-
const current =
|
|
16499
|
+
const current = join21(base, ...stack);
|
|
16191
16500
|
let st;
|
|
16192
16501
|
try {
|
|
16193
16502
|
st = lstatSync9(current);
|
|
@@ -16229,7 +16538,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
16229
16538
|
const parts = rel.split("/").slice(0, -1);
|
|
16230
16539
|
let current = cwd;
|
|
16231
16540
|
for (const part of parts) {
|
|
16232
|
-
current =
|
|
16541
|
+
current = join21(current, part);
|
|
16233
16542
|
let st;
|
|
16234
16543
|
try {
|
|
16235
16544
|
st = lstatSync9(current);
|
|
@@ -16254,7 +16563,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
16254
16563
|
}
|
|
16255
16564
|
const links = [];
|
|
16256
16565
|
for (const rel of listed) {
|
|
16257
|
-
const src =
|
|
16566
|
+
const src = join21(cwd, rel);
|
|
16258
16567
|
rejectLinkedParent(cwd, rel);
|
|
16259
16568
|
let st;
|
|
16260
16569
|
try {
|
|
@@ -16262,8 +16571,8 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
16262
16571
|
} catch {
|
|
16263
16572
|
continue;
|
|
16264
16573
|
}
|
|
16265
|
-
const out3 =
|
|
16266
|
-
|
|
16574
|
+
const out3 = join21(dest, rel);
|
|
16575
|
+
mkdirSync10(dirname10(out3), { recursive: true });
|
|
16267
16576
|
dropSymlink(out3);
|
|
16268
16577
|
if (st.isSymbolicLink()) {
|
|
16269
16578
|
const target = readlinkSync3(src);
|
|
@@ -16282,19 +16591,19 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
16282
16591
|
for (const link of links) {
|
|
16283
16592
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
16284
16593
|
}
|
|
16285
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
16594
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join21(dest, "node_modules"), "dir");
|
|
16286
16595
|
}
|
|
16287
16596
|
function workingTreePaths(cwd) {
|
|
16288
16597
|
return git3(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
16289
16598
|
}
|
|
16290
16599
|
function probeFilesystemCaseSensitivity(dir) {
|
|
16291
|
-
const upper =
|
|
16292
|
-
const lower =
|
|
16600
|
+
const upper = join21(dir, ".tw-case-probe-A");
|
|
16601
|
+
const lower = join21(dir, ".tw-case-probe-a");
|
|
16293
16602
|
try {
|
|
16294
|
-
|
|
16603
|
+
writeFileSync11(upper, "", { flag: "wx" });
|
|
16295
16604
|
return !existsSync14(lower);
|
|
16296
16605
|
} finally {
|
|
16297
|
-
|
|
16606
|
+
rmSync9(upper, { force: true });
|
|
16298
16607
|
}
|
|
16299
16608
|
}
|
|
16300
16609
|
function foldCase(p) {
|
|
@@ -16312,7 +16621,7 @@ function caseFoldCollision(paths) {
|
|
|
16312
16621
|
}
|
|
16313
16622
|
function dropSymlink(p) {
|
|
16314
16623
|
try {
|
|
16315
|
-
if (lstatSync9(p).isSymbolicLink())
|
|
16624
|
+
if (lstatSync9(p).isSymbolicLink()) rmSync9(p, { force: true });
|
|
16316
16625
|
} catch {
|
|
16317
16626
|
}
|
|
16318
16627
|
}
|
|
@@ -16348,11 +16657,11 @@ function baseEntries(base, cwd) {
|
|
|
16348
16657
|
});
|
|
16349
16658
|
}
|
|
16350
16659
|
function overlayDigest(dest, paths) {
|
|
16351
|
-
const h =
|
|
16660
|
+
const h = createHash14("sha256");
|
|
16352
16661
|
for (const rel of [...paths].sort()) {
|
|
16353
16662
|
h.update(rel);
|
|
16354
16663
|
h.update("\0");
|
|
16355
|
-
const p =
|
|
16664
|
+
const p = join21(dest, rel);
|
|
16356
16665
|
try {
|
|
16357
16666
|
const st = lstatSync9(p);
|
|
16358
16667
|
h.update(String(st.mode));
|
|
@@ -16386,15 +16695,15 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
16386
16695
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
16387
16696
|
}
|
|
16388
16697
|
const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
16389
|
-
const out3 =
|
|
16390
|
-
|
|
16391
|
-
|
|
16698
|
+
const out3 = join21(dest, e.path);
|
|
16699
|
+
mkdirSync10(dirname10(out3), { recursive: true });
|
|
16700
|
+
rmSync9(out3, { force: true });
|
|
16392
16701
|
if (e.mode === "120000") {
|
|
16393
16702
|
const target = content.toString("utf8");
|
|
16394
16703
|
safeSymlink(target, out3, dest, `the base's ${e.path}`);
|
|
16395
16704
|
restoredLinks.push({ path: e.path, out: out3, target });
|
|
16396
16705
|
} else {
|
|
16397
|
-
|
|
16706
|
+
writeFileSync11(out3, content);
|
|
16398
16707
|
chmodSync2(out3, parseInt(e.mode.slice(-4), 8) & 511);
|
|
16399
16708
|
}
|
|
16400
16709
|
restored.push(e.path);
|
|
@@ -16403,7 +16712,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
16403
16712
|
const inCopy = workingTreePaths(cwd);
|
|
16404
16713
|
for (const rel of inCopy) {
|
|
16405
16714
|
if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
|
|
16406
|
-
|
|
16715
|
+
rmSync9(join21(dest, rel), { force: true });
|
|
16407
16716
|
removed++;
|
|
16408
16717
|
}
|
|
16409
16718
|
for (const link of restoredLinks) {
|
|
@@ -16417,9 +16726,9 @@ function suiteEnv(scratch) {
|
|
|
16417
16726
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
16418
16727
|
env[k] = v;
|
|
16419
16728
|
}
|
|
16420
|
-
const userRc =
|
|
16421
|
-
const globalRc =
|
|
16422
|
-
for (const f of [userRc, globalRc])
|
|
16729
|
+
const userRc = join21(scratch, "npmrc-user");
|
|
16730
|
+
const globalRc = join21(scratch, "npmrc-global");
|
|
16731
|
+
for (const f of [userRc, globalRc]) writeFileSync11(f, "", { mode: 292 });
|
|
16423
16732
|
env.npm_config_userconfig = userRc;
|
|
16424
16733
|
env.npm_config_globalconfig = globalRc;
|
|
16425
16734
|
env.npm_config_node_options = " ";
|
|
@@ -16450,7 +16759,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
16450
16759
|
reason: `checkpointed-local verifier is unsupported on ${process.platform}; no trusted local shell contract is defined`
|
|
16451
16760
|
};
|
|
16452
16761
|
}
|
|
16453
|
-
const scratch = mkdtempSync3(
|
|
16762
|
+
const scratch = mkdtempSync3(join21(tmpdir4(), "tw-verify-run-"));
|
|
16454
16763
|
try {
|
|
16455
16764
|
const r = runCapturedProcessSync(shell.executable, shell.args, {
|
|
16456
16765
|
cwd: dir,
|
|
@@ -16495,7 +16804,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
16495
16804
|
diagnostics: r.diagnostics
|
|
16496
16805
|
};
|
|
16497
16806
|
} finally {
|
|
16498
|
-
|
|
16807
|
+
rmSync9(scratch, { recursive: true, force: true });
|
|
16499
16808
|
}
|
|
16500
16809
|
}
|
|
16501
16810
|
function diagnosticsJson(diagnostics, includeTail) {
|
|
@@ -16696,17 +17005,17 @@ function runVerifyImpl(opts) {
|
|
|
16696
17005
|
} catch {
|
|
16697
17006
|
return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
|
|
16698
17007
|
}
|
|
16699
|
-
const visRoot = mkdtempSync3(
|
|
16700
|
-
const visDir =
|
|
17008
|
+
const visRoot = mkdtempSync3(join21(tmpdir4(), "tw-verify-vis-"));
|
|
17009
|
+
const visDir = join21(visRoot, "t");
|
|
16701
17010
|
const cleanup = (dirs) => {
|
|
16702
17011
|
if (opts.keep) return;
|
|
16703
|
-
for (const d of dirs)
|
|
17012
|
+
for (const d of dirs) rmSync9(d, { recursive: true, force: true });
|
|
16704
17013
|
};
|
|
16705
17014
|
filesystemCaseSensitive = (opts.probeCaseSensitivity ?? probeFilesystemCaseSensitivity)(visRoot);
|
|
16706
17015
|
if (!filesystemCaseSensitive) {
|
|
16707
17016
|
const present = (rel) => {
|
|
16708
17017
|
try {
|
|
16709
|
-
lstatSync9(
|
|
17018
|
+
lstatSync9(join21(cwd, rel));
|
|
16710
17019
|
return true;
|
|
16711
17020
|
} catch {
|
|
16712
17021
|
return false;
|
|
@@ -16738,7 +17047,7 @@ function runVerifyImpl(opts) {
|
|
|
16738
17047
|
);
|
|
16739
17048
|
}
|
|
16740
17049
|
try {
|
|
16741
|
-
|
|
17050
|
+
mkdirSync10(visDir);
|
|
16742
17051
|
materialize(cwd, visDir, frozenNodeModules);
|
|
16743
17052
|
} catch (e) {
|
|
16744
17053
|
cleanup([visRoot]);
|
|
@@ -16839,12 +17148,12 @@ function runVerifyImpl(opts) {
|
|
|
16839
17148
|
{ stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
|
|
16840
17149
|
);
|
|
16841
17150
|
}
|
|
16842
|
-
const priRoot = mkdtempSync3(
|
|
16843
|
-
const priDir =
|
|
17151
|
+
const priRoot = mkdtempSync3(join21(tmpdir4(), "tw-verify-pri-"));
|
|
17152
|
+
const priDir = join21(priRoot, "t");
|
|
16844
17153
|
let restored = [];
|
|
16845
17154
|
let removedAdded = 0;
|
|
16846
17155
|
try {
|
|
16847
|
-
|
|
17156
|
+
mkdirSync10(priDir);
|
|
16848
17157
|
materialize(cwd, priDir, frozenNodeModules);
|
|
16849
17158
|
({ restored, removed: removedAdded } = overlayPristine(
|
|
16850
17159
|
cwd,
|
|
@@ -16941,11 +17250,13 @@ function runVerifyImpl(opts) {
|
|
|
16941
17250
|
budget
|
|
16942
17251
|
};
|
|
16943
17252
|
recordVerification(cwd, inputs, treeBefore);
|
|
17253
|
+
const written = readVerificationRecord(cwd);
|
|
17254
|
+
if (written) storeReceipt(cwd, receiptFromRecord(written));
|
|
16944
17255
|
} catch {
|
|
16945
17256
|
}
|
|
16946
17257
|
} else {
|
|
16947
17258
|
try {
|
|
16948
|
-
invalidateVerificationRecordIfCurrent(cwd);
|
|
17259
|
+
if (invalidateVerificationRecordIfCurrent(cwd)) removeStoredReceipt(cwd);
|
|
16949
17260
|
} catch {
|
|
16950
17261
|
}
|
|
16951
17262
|
}
|
|
@@ -16957,6 +17268,15 @@ function runVerifyImpl(opts) {
|
|
|
16957
17268
|
schema_version: MACHINE_SCHEMA_VERSION,
|
|
16958
17269
|
verdict: verdict2,
|
|
16959
17270
|
base,
|
|
17271
|
+
// The candidate tree fingerprint this run actually ADJUDICATED — the same
|
|
17272
|
+
// identity `verify` binds into the #600 record (`treeBefore`). Additive
|
|
17273
|
+
// (verify-v1 is `additionalProperties: true`), it lets `receipt reconcile`
|
|
17274
|
+
// tell whether CI's verdict is about the receipt's tree or a different one:
|
|
17275
|
+
// in a `pull_request` run the enforcement verify runs over the MERGE result,
|
|
17276
|
+
// so when the branch is behind its base this differs from the branch-tip
|
|
17277
|
+
// tree a receipt binds, and reconcile must report NON_APPLICABLE rather than
|
|
17278
|
+
// attribute CI's verdict to a tree CI never ran (#601 re-review).
|
|
17279
|
+
adjudicated_tree: treeBefore,
|
|
16960
17280
|
command: cmd,
|
|
16961
17281
|
budget_secs: budget,
|
|
16962
17282
|
visible: stageJson(visible),
|
|
@@ -17055,6 +17375,7 @@ var init_verify = __esm({
|
|
|
17055
17375
|
init_machine_output();
|
|
17056
17376
|
init_signoff();
|
|
17057
17377
|
init_verification_state();
|
|
17378
|
+
init_verification_receipt();
|
|
17058
17379
|
init_suite_diagnostics();
|
|
17059
17380
|
init_text();
|
|
17060
17381
|
init_status();
|
|
@@ -17153,9 +17474,9 @@ var init_verify = __esm({
|
|
|
17153
17474
|
// src/cli/run.ts
|
|
17154
17475
|
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
17155
17476
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
17156
|
-
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as
|
|
17477
|
+
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync10, statSync as statSync5, writeFileSync as writeFileSync12 } from "node:fs";
|
|
17157
17478
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
17158
|
-
import { dirname as
|
|
17479
|
+
import { dirname as dirname11, join as join22, resolve as resolve10 } from "node:path";
|
|
17159
17480
|
function authoritativeRunLifecyclePlatform(platform = process.platform) {
|
|
17160
17481
|
return platform === "linux";
|
|
17161
17482
|
}
|
|
@@ -17192,7 +17513,7 @@ function writableByCaller2(path) {
|
|
|
17192
17513
|
return true;
|
|
17193
17514
|
} catch {
|
|
17194
17515
|
}
|
|
17195
|
-
const parent =
|
|
17516
|
+
const parent = dirname11(cur);
|
|
17196
17517
|
if (parent === cur) break;
|
|
17197
17518
|
cur = parent;
|
|
17198
17519
|
}
|
|
@@ -17231,9 +17552,9 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
|
|
|
17231
17552
|
};
|
|
17232
17553
|
}
|
|
17233
17554
|
function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
|
|
17234
|
-
const stateDir2 = mkdtempSync4(
|
|
17235
|
-
const resultFile =
|
|
17236
|
-
const agentEnvFile =
|
|
17555
|
+
const stateDir2 = mkdtempSync4(join22(tmpdir5(), "tw-agent-supervisor-"));
|
|
17556
|
+
const resultFile = join22(stateDir2, "result.json");
|
|
17557
|
+
const agentEnvFile = join22(stateDir2, "agent-env.json");
|
|
17237
17558
|
try {
|
|
17238
17559
|
const linux = process.platform === "linux";
|
|
17239
17560
|
let executable;
|
|
@@ -17250,7 +17571,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
17250
17571
|
failure: trustedPython.reason ?? "trusted Linux python3 interpreter is unavailable"
|
|
17251
17572
|
};
|
|
17252
17573
|
}
|
|
17253
|
-
|
|
17574
|
+
writeFileSync12(agentEnvFile, JSON.stringify(process.env), { mode: 384 });
|
|
17254
17575
|
executable = trustedPython.path;
|
|
17255
17576
|
args = [
|
|
17256
17577
|
"-I",
|
|
@@ -17332,7 +17653,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
17332
17653
|
...state.failure ? { failure: state.failure } : {}
|
|
17333
17654
|
};
|
|
17334
17655
|
} finally {
|
|
17335
|
-
|
|
17656
|
+
rmSync10(stateDir2, { recursive: true, force: true });
|
|
17336
17657
|
}
|
|
17337
17658
|
}
|
|
17338
17659
|
function waitMs(ms) {
|
|
@@ -17359,8 +17680,8 @@ function observerExited(pid) {
|
|
|
17359
17680
|
}
|
|
17360
17681
|
}
|
|
17361
17682
|
function supervisedObserverLog(cwd) {
|
|
17362
|
-
const gd = gitDir(cwd) ??
|
|
17363
|
-
return
|
|
17683
|
+
const gd = gitDir(cwd) ?? join22(cwd, ".git");
|
|
17684
|
+
return join22(
|
|
17364
17685
|
gd,
|
|
17365
17686
|
"tamperward",
|
|
17366
17687
|
`run-observer-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}.jsonl`
|
|
@@ -18070,7 +18391,7 @@ raise SystemExit(0)
|
|
|
18070
18391
|
// src/cli/doctor.ts
|
|
18071
18392
|
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
18072
18393
|
import { existsSync as existsSync15, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
|
|
18073
|
-
import { join as
|
|
18394
|
+
import { join as join23, resolve as resolve11 } from "node:path";
|
|
18074
18395
|
function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
|
|
18075
18396
|
if (platform === "linux") {
|
|
18076
18397
|
if (linuxPython?.path) {
|
|
@@ -18268,9 +18589,9 @@ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow =
|
|
|
18268
18589
|
addWiring("codeowners", "codeowners", true);
|
|
18269
18590
|
checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
|
|
18270
18591
|
const pins = [
|
|
18271
|
-
...pinsInFile(
|
|
18272
|
-
...pinsInFile(
|
|
18273
|
-
...pinsInFile(
|
|
18592
|
+
...pinsInFile(join23(cwd, ".claude", "settings.json")),
|
|
18593
|
+
...pinsInFile(join23(cwd, ".git", "hooks", "pre-commit")),
|
|
18594
|
+
...pinsInFile(join23(cwd, ".husky", "pre-commit"))
|
|
18274
18595
|
];
|
|
18275
18596
|
const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
|
|
18276
18597
|
const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
|
|
@@ -18566,7 +18887,7 @@ function diagnose(opts = {}) {
|
|
|
18566
18887
|
`${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
|
|
18567
18888
|
);
|
|
18568
18889
|
}
|
|
18569
|
-
for (const name of entries) workflowRels.push(
|
|
18890
|
+
for (const name of entries) workflowRels.push(join23(workflowDirRel, name));
|
|
18570
18891
|
}
|
|
18571
18892
|
const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
|
|
18572
18893
|
const requiredMinutes = Math.ceil(requiredSecs / 60);
|
|
@@ -18895,11 +19216,11 @@ import {
|
|
|
18895
19216
|
readFileSync as readFileSync19,
|
|
18896
19217
|
readdirSync as readdirSync6,
|
|
18897
19218
|
realpathSync as realpathSync9,
|
|
18898
|
-
rmSync as
|
|
19219
|
+
rmSync as rmSync11,
|
|
18899
19220
|
symlinkSync as symlinkSync2
|
|
18900
19221
|
} from "node:fs";
|
|
18901
19222
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
18902
|
-
import { isAbsolute as isAbsolute12, join as
|
|
19223
|
+
import { isAbsolute as isAbsolute12, join as join24, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
|
|
18903
19224
|
function unescapeStraceString(s) {
|
|
18904
19225
|
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, "\\");
|
|
18905
19226
|
}
|
|
@@ -19147,8 +19468,8 @@ function materializeBase(base, cwd, dest) {
|
|
|
19147
19468
|
`tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
|
|
19148
19469
|
);
|
|
19149
19470
|
}
|
|
19150
|
-
const hostModules =
|
|
19151
|
-
const tracedModules =
|
|
19471
|
+
const hostModules = join24(cwd, "node_modules");
|
|
19472
|
+
const tracedModules = join24(dest, "node_modules");
|
|
19152
19473
|
if (existsSync16(hostModules) && !existsSync16(tracedModules)) {
|
|
19153
19474
|
symlinkSync2(resolve12(hostModules), tracedModules, "dir");
|
|
19154
19475
|
}
|
|
@@ -19156,7 +19477,7 @@ function materializeBase(base, cwd, dest) {
|
|
|
19156
19477
|
function traceFiles(prefix) {
|
|
19157
19478
|
const dir = resolve12(prefix, "..");
|
|
19158
19479
|
const base = prefix.split(sep5).at(-1) ?? prefix;
|
|
19159
|
-
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) =>
|
|
19480
|
+
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join24(dir, name)).sort();
|
|
19160
19481
|
}
|
|
19161
19482
|
function rewriteTraceRoot(accesses, actualRoot) {
|
|
19162
19483
|
return accesses.map((item) => {
|
|
@@ -19171,13 +19492,13 @@ function rewriteTraceRoot(accesses, actualRoot) {
|
|
|
19171
19492
|
if (!inside3(actualRoot, real)) return { ...item, path: real };
|
|
19172
19493
|
} catch {
|
|
19173
19494
|
}
|
|
19174
|
-
return { ...item, path:
|
|
19495
|
+
return { ...item, path: join24(TRACE_ROOT, ...rel.split("/")) };
|
|
19175
19496
|
});
|
|
19176
19497
|
}
|
|
19177
19498
|
function straceOnce(root, command, budget) {
|
|
19178
|
-
const traceDir = mkdtempSync5(
|
|
19499
|
+
const traceDir = mkdtempSync5(join24(tmpdir6(), "tw-trace-log-"));
|
|
19179
19500
|
try {
|
|
19180
|
-
const prefix =
|
|
19501
|
+
const prefix = join24(traceDir, "trace");
|
|
19181
19502
|
const traced = spawnSync5(
|
|
19182
19503
|
"strace",
|
|
19183
19504
|
[
|
|
@@ -19217,26 +19538,26 @@ function straceOnce(root, command, budget) {
|
|
|
19217
19538
|
};
|
|
19218
19539
|
return { raw, accesses };
|
|
19219
19540
|
} finally {
|
|
19220
|
-
|
|
19541
|
+
rmSync11(traceDir, { recursive: true, force: true });
|
|
19221
19542
|
}
|
|
19222
19543
|
}
|
|
19223
19544
|
function runOneTrace(cwd, base, command, budget) {
|
|
19224
|
-
const root = mkdtempSync5(
|
|
19545
|
+
const root = mkdtempSync5(join24(tmpdir6(), "tw-trace-base-"));
|
|
19225
19546
|
try {
|
|
19226
19547
|
materializeBase(base, cwd, root);
|
|
19227
19548
|
const { raw, accesses } = straceOnce(root, command, budget);
|
|
19228
19549
|
return { outcome: classifyTraceRun(raw), accesses: rewriteTraceRoot(accesses, root) };
|
|
19229
19550
|
} finally {
|
|
19230
|
-
|
|
19551
|
+
rmSync11(root, { recursive: true, force: true });
|
|
19231
19552
|
}
|
|
19232
19553
|
}
|
|
19233
19554
|
function tracerPreflight(budget = 10) {
|
|
19234
|
-
const probeRoot = mkdtempSync5(
|
|
19555
|
+
const probeRoot = mkdtempSync5(join24(tmpdir6(), "tw-trace-probe-"));
|
|
19235
19556
|
try {
|
|
19236
19557
|
const { raw } = straceOnce(probeRoot, "true", Math.max(1, Math.min(budget, 10)));
|
|
19237
19558
|
return classifyTraceRun(raw);
|
|
19238
19559
|
} finally {
|
|
19239
|
-
|
|
19560
|
+
rmSync11(probeRoot, { recursive: true, force: true });
|
|
19240
19561
|
}
|
|
19241
19562
|
}
|
|
19242
19563
|
function renderText2(report2) {
|
|
@@ -19439,25 +19760,25 @@ var init_trace_verify = __esm({
|
|
|
19439
19760
|
|
|
19440
19761
|
// src/runtimes.ts
|
|
19441
19762
|
import { existsSync as existsSync17, readFileSync as readFileSync20 } from "node:fs";
|
|
19442
|
-
import { join as
|
|
19443
|
-
function
|
|
19763
|
+
import { join as join25 } from "node:path";
|
|
19764
|
+
function isRecord6(x) {
|
|
19444
19765
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
19445
19766
|
}
|
|
19446
19767
|
function hooksBeyondTamperward(hooks) {
|
|
19447
|
-
if (!
|
|
19768
|
+
if (!isRecord6(hooks)) return hooks != null;
|
|
19448
19769
|
for (const entries of Object.values(hooks)) {
|
|
19449
19770
|
if (!Array.isArray(entries)) {
|
|
19450
19771
|
if (entries != null) return true;
|
|
19451
19772
|
continue;
|
|
19452
19773
|
}
|
|
19453
19774
|
for (const entry of entries) {
|
|
19454
|
-
const hs =
|
|
19775
|
+
const hs = isRecord6(entry) ? entry.hooks : null;
|
|
19455
19776
|
if (!Array.isArray(hs)) {
|
|
19456
19777
|
if (hs != null) return true;
|
|
19457
19778
|
continue;
|
|
19458
19779
|
}
|
|
19459
19780
|
for (const h of hs) {
|
|
19460
|
-
const cmd =
|
|
19781
|
+
const cmd = isRecord6(h) ? String(h.command ?? "") : "";
|
|
19461
19782
|
if (!/tamperward/.test(cmd)) return true;
|
|
19462
19783
|
}
|
|
19463
19784
|
}
|
|
@@ -19465,7 +19786,7 @@ function hooksBeyondTamperward(hooks) {
|
|
|
19465
19786
|
return false;
|
|
19466
19787
|
}
|
|
19467
19788
|
function claudeSettingsBeyondTamperward(cwd) {
|
|
19468
|
-
const path =
|
|
19789
|
+
const path = join25(cwd, ".claude", "settings.json");
|
|
19469
19790
|
let raw;
|
|
19470
19791
|
try {
|
|
19471
19792
|
if (!existsSync17(path)) return false;
|
|
@@ -19479,7 +19800,7 @@ function claudeSettingsBeyondTamperward(cwd) {
|
|
|
19479
19800
|
} catch {
|
|
19480
19801
|
return raw.trim().length > 0;
|
|
19481
19802
|
}
|
|
19482
|
-
if (!
|
|
19803
|
+
if (!isRecord6(parsed)) return raw.trim().length > 0;
|
|
19483
19804
|
for (const [key2, value] of Object.entries(parsed)) {
|
|
19484
19805
|
if (key2 === "disableAllHooks") continue;
|
|
19485
19806
|
if (key2 === "hooks") {
|
|
@@ -19495,7 +19816,7 @@ function detectRuntimes(cwd) {
|
|
|
19495
19816
|
for (const rt of KNOWN_RUNTIMES) {
|
|
19496
19817
|
const marker = rt.markers.find((m) => {
|
|
19497
19818
|
try {
|
|
19498
|
-
return existsSync17(
|
|
19819
|
+
return existsSync17(join25(cwd, m));
|
|
19499
19820
|
} catch {
|
|
19500
19821
|
return false;
|
|
19501
19822
|
}
|
|
@@ -19576,9 +19897,9 @@ var init_runtimes = __esm({
|
|
|
19576
19897
|
|
|
19577
19898
|
// src/cli/onboard.ts
|
|
19578
19899
|
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
19579
|
-
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync10, rmSync as
|
|
19900
|
+
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync10, rmSync as rmSync12 } from "node:fs";
|
|
19580
19901
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
19581
|
-
import { join as
|
|
19902
|
+
import { join as join26, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
|
|
19582
19903
|
import { createInterface } from "node:readline";
|
|
19583
19904
|
import { parseDocument } from "yaml";
|
|
19584
19905
|
function readlineAsker(input, output) {
|
|
@@ -19643,7 +19964,7 @@ function explainVerdict(v, code2) {
|
|
|
19643
19964
|
}
|
|
19644
19965
|
}
|
|
19645
19966
|
function writeVerifyCommand(cwd, command) {
|
|
19646
|
-
const path =
|
|
19967
|
+
const path = join26(cwd, POLICY_FILE);
|
|
19647
19968
|
const kind = writeTargetKind(path);
|
|
19648
19969
|
if (kind === "symlink" || kind === "irregular") {
|
|
19649
19970
|
return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
|
|
@@ -19666,7 +19987,7 @@ function writeVerifyCommand(cwd, command) {
|
|
|
19666
19987
|
if (loaded2.verify?.command !== command) throw new Error("verify.command did not round-trip");
|
|
19667
19988
|
return null;
|
|
19668
19989
|
} catch (e) {
|
|
19669
|
-
if (original === null)
|
|
19990
|
+
if (original === null) rmSync12(path, { force: true });
|
|
19670
19991
|
else atomicReplaceFile(path, original, mode);
|
|
19671
19992
|
return `the updated ${POLICY_FILE} does not load (${errorMessage(e)}); restored the previous file`;
|
|
19672
19993
|
}
|
|
@@ -20036,13 +20357,13 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
20036
20357
|
}
|
|
20037
20358
|
const before = treeFingerprint(cwd);
|
|
20038
20359
|
out3(`working tree fingerprint before: ${before.slice(0, 16)}`);
|
|
20039
|
-
const wt = mkdtempSync6(
|
|
20360
|
+
const wt = mkdtempSync6(join26(tmpdir7(), "tw-onboard-demo-"));
|
|
20040
20361
|
try {
|
|
20041
20362
|
if (git6(cwd, ["worktree", "add", "--detach", wt, head]) === null) {
|
|
20042
20363
|
out3("Demo skipped: could not create a temporary worktree (git worktree add failed).");
|
|
20043
20364
|
return;
|
|
20044
20365
|
}
|
|
20045
|
-
const targetPath =
|
|
20366
|
+
const targetPath = join26(wt, target.path);
|
|
20046
20367
|
const targetStat = lstatSync10(targetPath, { throwIfNoEntry: false });
|
|
20047
20368
|
if (!targetStat || !targetStat.isFile() || targetStat.isSymbolicLink()) {
|
|
20048
20369
|
out3("Demo skipped: the selected test is not a regular file in the disposable worktree.");
|
|
@@ -20073,7 +20394,7 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
20073
20394
|
} finally {
|
|
20074
20395
|
git6(cwd, ["worktree", "remove", "--force", wt]);
|
|
20075
20396
|
git6(cwd, ["worktree", "prune"]);
|
|
20076
|
-
|
|
20397
|
+
rmSync12(wt, { recursive: true, force: true });
|
|
20077
20398
|
}
|
|
20078
20399
|
const after = treeFingerprint(cwd);
|
|
20079
20400
|
out3(`working tree fingerprint after: ${after.slice(0, 16)}`);
|
|
@@ -20260,9 +20581,9 @@ var init_capture = __esm({
|
|
|
20260
20581
|
});
|
|
20261
20582
|
|
|
20262
20583
|
// src/research/manifest.ts
|
|
20263
|
-
import { createHash as
|
|
20584
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
20264
20585
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
20265
|
-
import { dirname as
|
|
20586
|
+
import { dirname as dirname12, resolve as resolve15 } from "node:path";
|
|
20266
20587
|
function taskFrom(raw, index, manifestDir) {
|
|
20267
20588
|
const where = `task #${index + 1}`;
|
|
20268
20589
|
if (!isRecord(raw)) throw new ResearchError(`task manifest: ${where} is not an object`);
|
|
@@ -20312,13 +20633,13 @@ function readManifest(path) {
|
|
|
20312
20633
|
if (!Array.isArray(raw.tasks) || raw.tasks.length === 0) {
|
|
20313
20634
|
throw new ResearchError(`task manifest ${abs}: no tasks (tasks must be a non-empty array)`);
|
|
20314
20635
|
}
|
|
20315
|
-
const tasks = raw.tasks.map((t, i) => taskFrom(t, i,
|
|
20636
|
+
const tasks = raw.tasks.map((t, i) => taskFrom(t, i, dirname12(abs)));
|
|
20316
20637
|
const seen = /* @__PURE__ */ new Set();
|
|
20317
20638
|
for (const t of tasks) {
|
|
20318
20639
|
if (seen.has(t.id)) throw new ResearchError(`task manifest ${abs}: duplicate task id "${t.id}"`);
|
|
20319
20640
|
seen.add(t.id);
|
|
20320
20641
|
}
|
|
20321
|
-
return { path: abs, sha256:
|
|
20642
|
+
return { path: abs, sha256: createHash15("sha256").update(bytes).digest("hex"), tasks };
|
|
20322
20643
|
}
|
|
20323
20644
|
var MANIFEST_VERSION;
|
|
20324
20645
|
var init_manifest = __esm({
|
|
@@ -20577,7 +20898,52 @@ function pairRecordFrom(raw, where = "record") {
|
|
|
20577
20898
|
}
|
|
20578
20899
|
};
|
|
20579
20900
|
}
|
|
20580
|
-
|
|
20901
|
+
function onlyFields(raw, allowed, where) {
|
|
20902
|
+
if (!isRecord(raw)) return;
|
|
20903
|
+
for (const key2 of Object.keys(raw)) {
|
|
20904
|
+
if (!allowed.includes(key2)) bad(`${where}.${key2} is not a field of the v1 pair record; refusing to carry it as portable evidence`);
|
|
20905
|
+
}
|
|
20906
|
+
}
|
|
20907
|
+
function portableObject(raw, spec2, where) {
|
|
20908
|
+
if (!isRecord(raw)) bad(`${where} is not an object`);
|
|
20909
|
+
for (const [key2, value] of Object.entries(raw)) {
|
|
20910
|
+
const field2 = spec2.get(key2);
|
|
20911
|
+
const at = `${where}.${key2}`;
|
|
20912
|
+
if (field2 === void 0) bad(`${at} is not a field of the v1 run document; refusing to carry it as portable evidence`);
|
|
20913
|
+
if (field2 === "scalar") {
|
|
20914
|
+
if (value !== null && typeof value === "object") bad(`${at} is not a scalar; refusing to carry it as portable evidence`);
|
|
20915
|
+
} else if ("items" in field2) {
|
|
20916
|
+
if (!Array.isArray(value)) bad(`${at} is not an array`);
|
|
20917
|
+
value.forEach((item, i) => portableObject(item, field2.items, `${at}[${i}]`));
|
|
20918
|
+
} else {
|
|
20919
|
+
portableObject(value, field2.fields, at);
|
|
20920
|
+
}
|
|
20921
|
+
}
|
|
20922
|
+
}
|
|
20923
|
+
function portablePairRecordFrom(raw, where = "record") {
|
|
20924
|
+
onlyFields(raw, PAIR_FIELDS, where);
|
|
20925
|
+
if (isRecord(raw)) {
|
|
20926
|
+
onlyFields(raw.adapter, ADAPTER_FIELDS, `${where}.adapter`);
|
|
20927
|
+
onlyFields(raw.arms, ARMS_FIELDS, `${where}.arms`);
|
|
20928
|
+
if (isRecord(raw.arms)) {
|
|
20929
|
+
for (const arm of ARMS_FIELDS) {
|
|
20930
|
+
const trajectory = raw.arms[arm];
|
|
20931
|
+
const at = `${where}.arms.${arm}`;
|
|
20932
|
+
onlyFields(trajectory, TRAJECTORY_FIELDS, at);
|
|
20933
|
+
if (isRecord(trajectory)) {
|
|
20934
|
+
onlyFields(trajectory.agent, AGENT_FIELDS, `${at}.agent`);
|
|
20935
|
+
onlyFields(trajectory.treatment, TREATMENT_FIELDS, `${at}.treatment`);
|
|
20936
|
+
if (isRecord(trajectory.treatment) && trajectory.treatment.envelope !== null && trajectory.treatment.envelope !== void 0) {
|
|
20937
|
+
portableObject(trajectory.treatment.envelope, RUN_DOCUMENT_FIELDS, `${at}.treatment.envelope`);
|
|
20938
|
+
}
|
|
20939
|
+
onlyFields(trajectory.outcome, OUTCOME_FIELDS, `${at}.outcome`);
|
|
20940
|
+
}
|
|
20941
|
+
}
|
|
20942
|
+
}
|
|
20943
|
+
}
|
|
20944
|
+
return pairRecordFrom(raw, where);
|
|
20945
|
+
}
|
|
20946
|
+
var SHA_RE, SHA256_RE, PAIR_FIELDS, ADAPTER_FIELDS, ARMS_FIELDS, TRAJECTORY_FIELDS, AGENT_FIELDS, TREATMENT_FIELDS, OUTCOME_FIELDS, fields, items, RUN_DOCUMENT_FIELDS;
|
|
20581
20947
|
var init_record = __esm({
|
|
20582
20948
|
"src/research/record.ts"() {
|
|
20583
20949
|
"use strict";
|
|
@@ -20587,23 +20953,99 @@ var init_record = __esm({
|
|
|
20587
20953
|
init_derive();
|
|
20588
20954
|
SHA_RE = /^[0-9a-f]{40,64}$/;
|
|
20589
20955
|
SHA256_RE = /^[0-9a-f]{64}$/;
|
|
20956
|
+
PAIR_FIELDS = [
|
|
20957
|
+
"schema_version",
|
|
20958
|
+
"command",
|
|
20959
|
+
"document",
|
|
20960
|
+
"task",
|
|
20961
|
+
"pair",
|
|
20962
|
+
"adapter",
|
|
20963
|
+
"model",
|
|
20964
|
+
"tamperward_version",
|
|
20965
|
+
"agent_argv",
|
|
20966
|
+
"agent_budget",
|
|
20967
|
+
"manifest_sha256",
|
|
20968
|
+
"verify_command",
|
|
20969
|
+
"arms"
|
|
20970
|
+
];
|
|
20971
|
+
ADAPTER_FIELDS = ["name", "layers"];
|
|
20972
|
+
ARMS_FIELDS = ["ungated", "gated"];
|
|
20973
|
+
TRAJECTORY_FIELDS = [
|
|
20974
|
+
"arm",
|
|
20975
|
+
"workspace",
|
|
20976
|
+
"base",
|
|
20977
|
+
"head",
|
|
20978
|
+
"started_at",
|
|
20979
|
+
"finished_at",
|
|
20980
|
+
"agent",
|
|
20981
|
+
"treatment",
|
|
20982
|
+
"outcome",
|
|
20983
|
+
"released_green",
|
|
20984
|
+
"measured",
|
|
20985
|
+
"unmeasurable"
|
|
20986
|
+
];
|
|
20987
|
+
AGENT_FIELDS = ["exit_code", "signal", "timed_out", "failure"];
|
|
20988
|
+
TREATMENT_FIELDS = ["verdict", "exit_code", "complete", "disposition", "envelope"];
|
|
20989
|
+
OUTCOME_FIELDS = [
|
|
20990
|
+
"verify_verdict",
|
|
20991
|
+
"visible_exit",
|
|
20992
|
+
"pristine_exit",
|
|
20993
|
+
"visible_green",
|
|
20994
|
+
"pristine_green",
|
|
20995
|
+
"masked_failure",
|
|
20996
|
+
"surviving_protected_mutations",
|
|
20997
|
+
"warn_findings",
|
|
20998
|
+
"rules",
|
|
20999
|
+
"honest_completion"
|
|
21000
|
+
];
|
|
21001
|
+
fields = (entries) => ({ fields: new Map(entries) });
|
|
21002
|
+
items = (entries) => ({ items: new Map(entries) });
|
|
21003
|
+
RUN_DOCUMENT_FIELDS = /* @__PURE__ */ new Map([
|
|
21004
|
+
["schema_version", "scalar"],
|
|
21005
|
+
["verdict", "scalar"],
|
|
21006
|
+
["exit_code", "scalar"],
|
|
21007
|
+
["complete", "scalar"],
|
|
21008
|
+
["base", "scalar"],
|
|
21009
|
+
["head", "scalar"],
|
|
21010
|
+
["reason", "scalar"],
|
|
21011
|
+
["agent", fields([["exit_code", "scalar"], ["timed_out", "scalar"], ["lifecycle_owned", "scalar"], ["budget_secs", "scalar"]])],
|
|
21012
|
+
["checks", fields([["diff", "scalar"], ["worktree", "scalar"], ["verify", "scalar"]])],
|
|
21013
|
+
["verifier_backend", fields([
|
|
21014
|
+
["kind", "scalar"],
|
|
21015
|
+
["trust", "scalar"],
|
|
21016
|
+
["available", "scalar"],
|
|
21017
|
+
["image", "scalar"],
|
|
21018
|
+
["engine", "scalar"],
|
|
21019
|
+
["reason", "scalar"],
|
|
21020
|
+
["resources", fields([["memory_bytes", "scalar"], ["memory_swap_bytes", "scalar"], ["cpus", "scalar"], ["pids", "scalar"]])]
|
|
21021
|
+
])],
|
|
21022
|
+
["dependency_environment", fields([
|
|
21023
|
+
["status", "scalar"],
|
|
21024
|
+
["fingerprint", "scalar"],
|
|
21025
|
+
["reason", "scalar"],
|
|
21026
|
+
["image", "scalar"],
|
|
21027
|
+
["roots", items([["kind", "scalar"], ["path", "scalar"]])],
|
|
21028
|
+
["diagnostics", fields([["full_snapshots", "scalar"], ["reused_snapshots", "scalar"], ["total_ms", "scalar"]])]
|
|
21029
|
+
])],
|
|
21030
|
+
["observer", fields([["enabled", "scalar"], ["blocking", "scalar"]])]
|
|
21031
|
+
]);
|
|
20590
21032
|
}
|
|
20591
21033
|
});
|
|
20592
21034
|
|
|
20593
21035
|
// src/research/run.ts
|
|
20594
21036
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20595
21037
|
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
20596
|
-
import { closeSync as closeSync4, existsSync as existsSync18, linkSync, mkdirSync as
|
|
21038
|
+
import { closeSync as closeSync4, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync11, openSync as openSync4, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync13, unlinkSync, writeFileSync as writeFileSync13, writeSync } from "node:fs";
|
|
20597
21039
|
import { hostname } from "node:os";
|
|
20598
|
-
import { join as
|
|
21040
|
+
import { join as join27, resolve as resolve16 } from "node:path";
|
|
20599
21041
|
function git7(args, cwd) {
|
|
20600
21042
|
return execFileSync14("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
20601
21043
|
}
|
|
20602
21044
|
function pairRecordPath(ledger, task, pair) {
|
|
20603
|
-
return
|
|
21045
|
+
return join27(ledger, "pairs", `${task}--${pair}.json`);
|
|
20604
21046
|
}
|
|
20605
21047
|
function researchLockPath(ledger) {
|
|
20606
|
-
return
|
|
21048
|
+
return join27(ledger, "run.lock");
|
|
20607
21049
|
}
|
|
20608
21050
|
function readLockOwner(path) {
|
|
20609
21051
|
try {
|
|
@@ -20628,7 +21070,7 @@ function lockHolder(path) {
|
|
|
20628
21070
|
return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
|
|
20629
21071
|
}
|
|
20630
21072
|
function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
20631
|
-
|
|
21073
|
+
mkdirSync11(ledger, { recursive: true });
|
|
20632
21074
|
const path = researchLockPath(ledger);
|
|
20633
21075
|
if (breakStaleLock && existsSync18(path)) {
|
|
20634
21076
|
const owner2 = readLockOwner(path);
|
|
@@ -20738,16 +21180,16 @@ function resumableRecord(path, expected) {
|
|
|
20738
21180
|
}
|
|
20739
21181
|
function writeRecordAtomically(path, text) {
|
|
20740
21182
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
20741
|
-
|
|
21183
|
+
writeFileSync13(tmp, text);
|
|
20742
21184
|
renameSync4(tmp, path);
|
|
20743
21185
|
}
|
|
20744
21186
|
function cloneArgs(repo, ws3) {
|
|
20745
21187
|
return ["-c", "protocol.ext.allow=never", "clone", "-q", "--no-hardlinks", "--", repo, ws3];
|
|
20746
21188
|
}
|
|
20747
21189
|
function freshWorkspace(ledger, task, pair, arm, sourceBase) {
|
|
20748
|
-
const ws3 =
|
|
20749
|
-
|
|
20750
|
-
|
|
21190
|
+
const ws3 = join27(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
|
|
21191
|
+
rmSync13(ws3, { recursive: true, force: true });
|
|
21192
|
+
mkdirSync11(join27(ledger, "workspaces"), { recursive: true });
|
|
20751
21193
|
try {
|
|
20752
21194
|
execFileSync14("git", cloneArgs(task.repo, ws3), { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
20753
21195
|
} catch (e) {
|
|
@@ -20996,7 +21438,7 @@ function runResearch(opts) {
|
|
|
20996
21438
|
}
|
|
20997
21439
|
try {
|
|
20998
21440
|
const pairs = opts.pairs ?? 1;
|
|
20999
|
-
|
|
21441
|
+
mkdirSync11(join27(ledger, "pairs"), { recursive: true });
|
|
21000
21442
|
for (const task of tasks) {
|
|
21001
21443
|
const existingRecords = /* @__PURE__ */ new Map();
|
|
21002
21444
|
let sourceBase = null;
|
|
@@ -21122,27 +21564,95 @@ var init_run2 = __esm({
|
|
|
21122
21564
|
}
|
|
21123
21565
|
});
|
|
21124
21566
|
|
|
21125
|
-
// src/research/
|
|
21126
|
-
import {
|
|
21127
|
-
import { join as
|
|
21128
|
-
function
|
|
21129
|
-
|
|
21567
|
+
// src/research/ledger-files.ts
|
|
21568
|
+
import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync2, lstatSync as lstatSync11, openSync as openSync5, readFileSync as readFileSync24, readdirSync as readdirSync7, realpathSync as realpathSync11 } from "node:fs";
|
|
21569
|
+
import { join as join28, resolve as resolve17 } from "node:path";
|
|
21570
|
+
function lstatOrNull(path) {
|
|
21571
|
+
try {
|
|
21572
|
+
return lstatSync11(path);
|
|
21573
|
+
} catch {
|
|
21574
|
+
return null;
|
|
21575
|
+
}
|
|
21576
|
+
}
|
|
21577
|
+
function kindOf(st) {
|
|
21578
|
+
if (st.isSymbolicLink()) return "a symlink";
|
|
21579
|
+
if (st.isDirectory()) return "a directory";
|
|
21580
|
+
if (st.isFIFO()) return "a FIFO";
|
|
21581
|
+
if (st.isSocket()) return "a socket";
|
|
21582
|
+
if (st.isCharacterDevice() || st.isBlockDevice()) return "a device";
|
|
21583
|
+
return "not a regular file";
|
|
21584
|
+
}
|
|
21585
|
+
function readRegularFile(path, realPairsDir, name) {
|
|
21586
|
+
const listed = lstatOrNull(path);
|
|
21587
|
+
if (listed === null) throw new ResearchError(`ledger record ${path} vanished while the ledger was being read`);
|
|
21588
|
+
if (!listed.isFile()) {
|
|
21589
|
+
throw new ResearchError(`ledger record ${path} is ${kindOf(listed)}; only a regular file placed directly in pairs/ is read`);
|
|
21590
|
+
}
|
|
21591
|
+
if (listed.nlink !== 1) {
|
|
21592
|
+
throw new ResearchError(`ledger record ${path} has ${listed.nlink} links; a hard link can name a file outside pairs/, so only a singly linked regular file is read`);
|
|
21593
|
+
}
|
|
21594
|
+
const real = realpathSync11.native(path);
|
|
21595
|
+
if (real !== join28(realPairsDir, name)) {
|
|
21596
|
+
throw new ResearchError(`ledger record ${path} resolves outside the pairs directory (${real}); refusing to read it`);
|
|
21597
|
+
}
|
|
21598
|
+
const flags = constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0);
|
|
21599
|
+
let fd;
|
|
21600
|
+
try {
|
|
21601
|
+
fd = openSync5(path, flags);
|
|
21602
|
+
} catch (e) {
|
|
21603
|
+
throw new ResearchError(`cannot open ledger record ${path}: ${errorMessage(e)}`);
|
|
21604
|
+
}
|
|
21605
|
+
try {
|
|
21606
|
+
const opened = fstatSync2(fd);
|
|
21607
|
+
const sameInode = process.platform === "win32" || opened.dev === listed.dev && opened.ino === listed.ino;
|
|
21608
|
+
if (!opened.isFile() || !sameInode || opened.nlink !== 1) {
|
|
21609
|
+
throw new ResearchError(`ledger record ${path} changed between listing and reading; refusing to read it`);
|
|
21610
|
+
}
|
|
21611
|
+
return readFileSync24(fd);
|
|
21612
|
+
} finally {
|
|
21613
|
+
closeSync5(fd);
|
|
21614
|
+
}
|
|
21615
|
+
}
|
|
21616
|
+
function readPairFiles(ledger) {
|
|
21617
|
+
const root = resolve17(ledger);
|
|
21618
|
+
const pairsDir = join28(root, "pairs");
|
|
21619
|
+
const dirStat = lstatOrNull(pairsDir);
|
|
21620
|
+
if (dirStat !== null && dirStat.isSymbolicLink()) {
|
|
21621
|
+
throw new ResearchError(`ledger pairs directory ${pairsDir} is a symlink; only a real directory is read`);
|
|
21622
|
+
}
|
|
21623
|
+
if (dirStat === null || !dirStat.isDirectory()) throw new ResearchError(`ledger ${root} has no pairs directory`);
|
|
21624
|
+
const realPairsDir = realpathSync11.native(pairsDir);
|
|
21130
21625
|
let names;
|
|
21131
21626
|
try {
|
|
21132
21627
|
names = readdirSync7(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
|
|
21133
21628
|
} catch (e) {
|
|
21134
21629
|
throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
|
|
21135
21630
|
}
|
|
21136
|
-
|
|
21137
|
-
|
|
21138
|
-
|
|
21631
|
+
return names.map((name) => {
|
|
21632
|
+
const path = join28(pairsDir, name);
|
|
21633
|
+
return { name, path, bytes: readRegularFile(path, realPairsDir, name) };
|
|
21634
|
+
});
|
|
21635
|
+
}
|
|
21636
|
+
var init_ledger_files = __esm({
|
|
21637
|
+
"src/research/ledger-files.ts"() {
|
|
21638
|
+
"use strict";
|
|
21639
|
+
init_narrow();
|
|
21640
|
+
init_adapter();
|
|
21641
|
+
}
|
|
21642
|
+
});
|
|
21643
|
+
|
|
21644
|
+
// src/research/summarize.ts
|
|
21645
|
+
function readLedger2(dir) {
|
|
21646
|
+
const files = readPairFiles(dir);
|
|
21647
|
+
if (files.length === 0) throw new ResearchError(`ledger ${dir} holds no pair records`);
|
|
21648
|
+
return files.map((f) => {
|
|
21139
21649
|
let raw;
|
|
21140
21650
|
try {
|
|
21141
|
-
raw = JSON.parse(
|
|
21651
|
+
raw = JSON.parse(f.bytes.toString("utf8"));
|
|
21142
21652
|
} catch (e) {
|
|
21143
|
-
throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
|
|
21653
|
+
throw new ResearchError(`ledger record ${f.path} is not valid JSON: ${errorMessage(e)}`);
|
|
21144
21654
|
}
|
|
21145
|
-
return pairRecordFrom(raw, path);
|
|
21655
|
+
return pairRecordFrom(raw, f.path);
|
|
21146
21656
|
});
|
|
21147
21657
|
}
|
|
21148
21658
|
function weakened(t) {
|
|
@@ -21263,15 +21773,16 @@ var init_summarize = __esm({
|
|
|
21263
21773
|
"use strict";
|
|
21264
21774
|
init_machine_output();
|
|
21265
21775
|
init_narrow();
|
|
21776
|
+
init_ledger_files();
|
|
21266
21777
|
init_adapter();
|
|
21267
21778
|
init_record();
|
|
21268
21779
|
}
|
|
21269
21780
|
});
|
|
21270
21781
|
|
|
21271
21782
|
// src/research/init.ts
|
|
21272
|
-
import { createHash as
|
|
21273
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
21274
|
-
import { basename as basename5, dirname as
|
|
21783
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
21784
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync25, writeFileSync as writeFileSync14 } from "node:fs";
|
|
21785
|
+
import { basename as basename5, dirname as dirname13, resolve as resolve18 } from "node:path";
|
|
21275
21786
|
function idFrom(repo) {
|
|
21276
21787
|
const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
|
|
21277
21788
|
return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
|
|
@@ -21299,8 +21810,8 @@ function createResearchManifest(opts) {
|
|
|
21299
21810
|
}]
|
|
21300
21811
|
};
|
|
21301
21812
|
const text = JSON.stringify(doc, null, 2) + "\n";
|
|
21302
|
-
|
|
21303
|
-
|
|
21813
|
+
mkdirSync12(dirname13(out3), { recursive: true });
|
|
21814
|
+
writeFileSync14(out3, text, { flag: "wx", mode: 384 });
|
|
21304
21815
|
const parsed = readManifest(out3);
|
|
21305
21816
|
if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
|
|
21306
21817
|
return out3;
|
|
@@ -21322,7 +21833,7 @@ function runResearchInit(opts) {
|
|
|
21322
21833
|
}
|
|
21323
21834
|
}
|
|
21324
21835
|
function requireHash(path) {
|
|
21325
|
-
return
|
|
21836
|
+
return createHash16("sha256").update(readFileSync25(path)).digest("hex");
|
|
21326
21837
|
}
|
|
21327
21838
|
var init_init2 = __esm({
|
|
21328
21839
|
"src/research/init.ts"() {
|
|
@@ -21387,65 +21898,107 @@ var init_report2 = __esm({
|
|
|
21387
21898
|
});
|
|
21388
21899
|
|
|
21389
21900
|
// src/research/bundle.ts
|
|
21390
|
-
import { createHash as
|
|
21901
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
21391
21902
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
21392
|
-
import {
|
|
21393
|
-
import { dirname as
|
|
21903
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
|
|
21904
|
+
import { dirname as dirname14, resolve as resolve19 } from "node:path";
|
|
21394
21905
|
function octal(value, width) {
|
|
21395
21906
|
return value.toString(8).padStart(width - 1, "0") + "\0";
|
|
21396
21907
|
}
|
|
21397
|
-
function
|
|
21908
|
+
function tarHeader(name, size) {
|
|
21398
21909
|
const header = Buffer.alloc(BLOCK, 0);
|
|
21399
|
-
|
|
21400
|
-
header.write(safe, 0, "utf8");
|
|
21910
|
+
header.write(name, 0, "utf8");
|
|
21401
21911
|
header.write(octal(420, 8), 100, "ascii");
|
|
21402
21912
|
header.write(octal(0, 8), 108, "ascii");
|
|
21403
21913
|
header.write(octal(0, 8), 116, "ascii");
|
|
21404
|
-
header.write(octal(
|
|
21914
|
+
header.write(octal(size, 12), 124, "ascii");
|
|
21405
21915
|
header.write(octal(0, 12), 136, "ascii");
|
|
21406
21916
|
header.fill(32, 148, 156);
|
|
21407
|
-
header[
|
|
21917
|
+
header[TYPE_OFFSET] = 48;
|
|
21408
21918
|
header.write("ustar", 257, "ascii");
|
|
21409
21919
|
header.write("00", 263, "ascii");
|
|
21410
21920
|
const checksum = header.reduce((sum, b) => sum + b, 0);
|
|
21411
21921
|
header.write(octal(checksum, 8), 148, "ascii");
|
|
21922
|
+
return header;
|
|
21923
|
+
}
|
|
21924
|
+
function tarEntry(name, bytes) {
|
|
21925
|
+
if (name.startsWith("/") || Buffer.byteLength(name, "utf8") > TAR_NAME_MAX) {
|
|
21926
|
+
throw new ResearchError(`archive entry name ${name} cannot be stored byte for byte in a ustar header`);
|
|
21927
|
+
}
|
|
21412
21928
|
const padding = Buffer.alloc((BLOCK - bytes.length % BLOCK) % BLOCK, 0);
|
|
21413
|
-
return Buffer.concat([
|
|
21929
|
+
return Buffer.concat([tarHeader(name, bytes.length), bytes, padding]);
|
|
21414
21930
|
}
|
|
21415
21931
|
function makeTar(entries) {
|
|
21416
21932
|
return Buffer.concat([...entries.map((e) => tarEntry(e.name, e.bytes)), Buffer.alloc(BLOCK * 2, 0)]);
|
|
21417
21933
|
}
|
|
21934
|
+
function field(header, start, length) {
|
|
21935
|
+
const raw = header.subarray(start, start + length);
|
|
21936
|
+
const end = raw.indexOf(0);
|
|
21937
|
+
return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8");
|
|
21938
|
+
}
|
|
21418
21939
|
function parseTar(bytes) {
|
|
21419
21940
|
const files = /* @__PURE__ */ new Map();
|
|
21420
|
-
|
|
21941
|
+
let offset = 0;
|
|
21942
|
+
for (; ; ) {
|
|
21943
|
+
if (offset + BLOCK > bytes.length) throw new ResearchError("research bundle is missing its end-of-archive blocks");
|
|
21421
21944
|
const header = bytes.subarray(offset, offset + BLOCK);
|
|
21422
|
-
if (header.every((b) => b === 0))
|
|
21423
|
-
|
|
21424
|
-
|
|
21425
|
-
|
|
21945
|
+
if (header.every((b) => b === 0)) {
|
|
21946
|
+
const second = bytes.subarray(offset + BLOCK, offset + 2 * BLOCK);
|
|
21947
|
+
if (second.length !== BLOCK || !second.every((b) => b === 0) || offset + 2 * BLOCK !== bytes.length) {
|
|
21948
|
+
throw new ResearchError("research bundle carries bytes after its end-of-archive blocks");
|
|
21949
|
+
}
|
|
21950
|
+
return files;
|
|
21951
|
+
}
|
|
21952
|
+
const name = field(header, 0, NAME_FIELD);
|
|
21953
|
+
const prefix = field(header, PREFIX_OFFSET, PREFIX_FIELD);
|
|
21954
|
+
if (prefix) {
|
|
21955
|
+
throw new ResearchError(`research bundle entry ${prefix}/${name} uses a ustar prefix; this bundle format stores every name in the name field`);
|
|
21956
|
+
}
|
|
21957
|
+
const type = header[TYPE_OFFSET];
|
|
21958
|
+
if (type !== 48) {
|
|
21959
|
+
throw new ResearchError(`research bundle entry ${name} is not a regular file entry (type flag ${JSON.stringify(String.fromCharCode(type))})`);
|
|
21960
|
+
}
|
|
21961
|
+
const size = Number.parseInt(field(header, 124, 12).trim() || "0", 8);
|
|
21426
21962
|
if (!name || name.startsWith("/") || name.split("/").includes("..") || !Number.isSafeInteger(size) || size < 0) {
|
|
21427
21963
|
throw new ResearchError("research bundle contains an unsafe or malformed archive entry");
|
|
21428
21964
|
}
|
|
21965
|
+
if (Buffer.byteLength(name, "utf8") > TAR_NAME_MAX || !header.equals(tarHeader(name, size))) {
|
|
21966
|
+
throw new ResearchError(`research bundle entry ${name} has a header this bundle format does not write`);
|
|
21967
|
+
}
|
|
21429
21968
|
const start = offset + BLOCK;
|
|
21430
21969
|
const end = start + size;
|
|
21431
21970
|
if (end > bytes.length) throw new ResearchError("research bundle is truncated");
|
|
21971
|
+
const padded = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
21972
|
+
if (padded > bytes.length || !bytes.subarray(end, padded).every((b) => b === 0)) {
|
|
21973
|
+
throw new ResearchError(`research bundle entry ${name} carries bytes in its padding`);
|
|
21974
|
+
}
|
|
21975
|
+
if (files.has(name)) throw new ResearchError(`research bundle contains a duplicate archive entry ${name}`);
|
|
21432
21976
|
files.set(name, Buffer.from(bytes.subarray(start, end)));
|
|
21433
|
-
offset =
|
|
21977
|
+
offset = padded;
|
|
21434
21978
|
}
|
|
21435
|
-
return files;
|
|
21436
21979
|
}
|
|
21437
|
-
function
|
|
21980
|
+
function packagedRecords(ledger) {
|
|
21981
|
+
const files = readPairFiles(ledger);
|
|
21982
|
+
if (files.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
|
|
21983
|
+
const records = [];
|
|
21438
21984
|
const entries = [];
|
|
21439
|
-
const
|
|
21440
|
-
|
|
21441
|
-
|
|
21442
|
-
|
|
21443
|
-
|
|
21444
|
-
|
|
21445
|
-
|
|
21446
|
-
|
|
21985
|
+
for (const f of files) {
|
|
21986
|
+
let raw;
|
|
21987
|
+
try {
|
|
21988
|
+
raw = JSON.parse(f.bytes.toString("utf8"));
|
|
21989
|
+
} catch (e) {
|
|
21990
|
+
throw new ResearchError(`ledger record ${f.path} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
21991
|
+
}
|
|
21992
|
+
const record = portablePairRecordFrom(raw, f.path);
|
|
21993
|
+
const archiveName = `ledger/pairs/${f.name}`;
|
|
21994
|
+
const nameBytes = Buffer.byteLength(archiveName, "utf8");
|
|
21995
|
+
if (nameBytes > TAR_NAME_MAX) {
|
|
21996
|
+
throw new ResearchError(`pair record name ${f.name} is too long for the archive (${nameBytes} bytes as ${archiveName}; the limit is ${TAR_NAME_MAX})`);
|
|
21997
|
+
}
|
|
21998
|
+
records.push(record);
|
|
21999
|
+
entries.push({ name: archiveName, bytes: jsonBytes(record) });
|
|
21447
22000
|
}
|
|
21448
|
-
return entries;
|
|
22001
|
+
return { records, entries };
|
|
21449
22002
|
}
|
|
21450
22003
|
function jsonBytes(value) {
|
|
21451
22004
|
return Buffer.from(JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
@@ -21457,6 +22010,11 @@ function validateResearchBundle(path) {
|
|
|
21457
22010
|
} catch (e) {
|
|
21458
22011
|
throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
21459
22012
|
}
|
|
22013
|
+
for (const name of files.keys()) {
|
|
22014
|
+
if (!TOP_LEVEL_ENTRIES.has(name) && !PAIR_ENTRY_RE.test(name)) {
|
|
22015
|
+
throw new ResearchError(`research bundle carries an entry that is not evidence: ${name}`);
|
|
22016
|
+
}
|
|
22017
|
+
}
|
|
21460
22018
|
const provenance = files.get("provenance.json");
|
|
21461
22019
|
const summaryBytes = files.get("summary.json");
|
|
21462
22020
|
const report2 = files.get("report.txt");
|
|
@@ -21476,21 +22034,23 @@ function validateResearchBundle(path) {
|
|
|
21476
22034
|
if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
|
|
21477
22035
|
throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
|
|
21478
22036
|
}
|
|
21479
|
-
if (manifest &&
|
|
22037
|
+
if (manifest && createHash17("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
|
|
21480
22038
|
throw new ResearchError("research bundle manifest does not match its recorded sha256");
|
|
21481
22039
|
}
|
|
21482
22040
|
const parsedSummary = summary;
|
|
21483
22041
|
if (parsedSummary.document !== "summary" || parsedSummary.command !== "research") throw new ResearchError("research bundle summary is not a research summary");
|
|
21484
22042
|
const records = [];
|
|
21485
22043
|
for (const [name, bytes] of files) {
|
|
21486
|
-
if (!
|
|
22044
|
+
if (!PAIR_ENTRY_RE.test(name)) continue;
|
|
21487
22045
|
let raw;
|
|
21488
22046
|
try {
|
|
21489
22047
|
raw = JSON.parse(bytes.toString("utf8"));
|
|
21490
22048
|
} catch {
|
|
21491
22049
|
throw new ResearchError(`invalid JSON in ${name}`);
|
|
21492
22050
|
}
|
|
21493
|
-
|
|
22051
|
+
const record = portablePairRecordFrom(raw, name);
|
|
22052
|
+
if (!bytes.equals(jsonBytes(record))) throw new ResearchError(`${name} is not the canonical serialization of its pair record`);
|
|
22053
|
+
records.push(record);
|
|
21494
22054
|
}
|
|
21495
22055
|
if (records.length === 0) throw new ResearchError("research bundle contains no pair records");
|
|
21496
22056
|
const derived = summarizeRecords(records);
|
|
@@ -21504,11 +22064,11 @@ function createResearchBundle(opts) {
|
|
|
21504
22064
|
if (!opts.ledger) throw new ResearchError("research bundle requires --ledger");
|
|
21505
22065
|
if (!opts.out) throw new ResearchError("research bundle requires --out");
|
|
21506
22066
|
const ledger = resolve19(opts.ledger);
|
|
21507
|
-
const
|
|
21508
|
-
const
|
|
22067
|
+
const { records, entries } = packagedRecords(ledger);
|
|
22068
|
+
const summary = summarizeRecords(records);
|
|
21509
22069
|
if (opts.manifest) {
|
|
21510
22070
|
const manifest = readFileSync26(resolve19(opts.manifest));
|
|
21511
|
-
const sha3 =
|
|
22071
|
+
const sha3 = createHash17("sha256").update(manifest).digest("hex");
|
|
21512
22072
|
if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
|
|
21513
22073
|
entries.push({ name: "manifest.json", bytes: manifest });
|
|
21514
22074
|
}
|
|
@@ -21530,8 +22090,8 @@ function createResearchBundle(opts) {
|
|
|
21530
22090
|
entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
|
|
21531
22091
|
entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
|
|
21532
22092
|
const out3 = resolve19(opts.out);
|
|
21533
|
-
|
|
21534
|
-
|
|
22093
|
+
mkdirSync13(dirname14(out3), { recursive: true });
|
|
22094
|
+
writeFileSync15(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
|
|
21535
22095
|
return out3;
|
|
21536
22096
|
}
|
|
21537
22097
|
function runResearchBundle(opts) {
|
|
@@ -21555,17 +22115,25 @@ function runResearchBundle(opts) {
|
|
|
21555
22115
|
throw e;
|
|
21556
22116
|
}
|
|
21557
22117
|
}
|
|
21558
|
-
var BLOCK, BUNDLE_VERSION;
|
|
22118
|
+
var BLOCK, BUNDLE_VERSION, TOP_LEVEL_ENTRIES, PAIR_ENTRY_RE, TAR_NAME_MAX, NAME_FIELD, TYPE_OFFSET, PREFIX_OFFSET, PREFIX_FIELD;
|
|
21559
22119
|
var init_bundle = __esm({
|
|
21560
22120
|
"src/research/bundle.ts"() {
|
|
21561
22121
|
"use strict";
|
|
21562
22122
|
init_machine_output();
|
|
21563
22123
|
init_adapter();
|
|
22124
|
+
init_ledger_files();
|
|
21564
22125
|
init_record();
|
|
21565
22126
|
init_report2();
|
|
21566
22127
|
init_summarize();
|
|
21567
22128
|
BLOCK = 512;
|
|
21568
22129
|
BUNDLE_VERSION = 1;
|
|
22130
|
+
TOP_LEVEL_ENTRIES = /* @__PURE__ */ new Set(["summary.json", "report.txt", "provenance.json", "manifest.json"]);
|
|
22131
|
+
PAIR_ENTRY_RE = /^ledger\/pairs\/[^/]+\.json$/;
|
|
22132
|
+
TAR_NAME_MAX = 99;
|
|
22133
|
+
NAME_FIELD = 100;
|
|
22134
|
+
TYPE_OFFSET = 156;
|
|
22135
|
+
PREFIX_OFFSET = 345;
|
|
22136
|
+
PREFIX_FIELD = 155;
|
|
21569
22137
|
}
|
|
21570
22138
|
});
|
|
21571
22139
|
|
|
@@ -22535,9 +23103,9 @@ var init_adapter4 = __esm({
|
|
|
22535
23103
|
|
|
22536
23104
|
// src/adapters/copilot-sdk/changes.ts
|
|
22537
23105
|
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
22538
|
-
import { mkdtempSync as mkdtempSync7, writeFileSync as
|
|
23106
|
+
import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync16, readFileSync as readFileSync27, rmSync as rmSync14, existsSync as existsSync20, realpathSync as realpathSync12 } from "node:fs";
|
|
22539
23107
|
import { tmpdir as tmpdir8 } from "node:os";
|
|
22540
|
-
import { basename as basename6, dirname as
|
|
23108
|
+
import { basename as basename6, dirname as dirname15, isAbsolute as isAbsolute17, relative as relative12, resolve as resolve22, join as join30 } from "node:path";
|
|
22541
23109
|
function asStr4(v) {
|
|
22542
23110
|
return typeof v === "string" ? v : "";
|
|
22543
23111
|
}
|
|
@@ -22548,25 +23116,25 @@ function relForDisplay5(path, cwd) {
|
|
|
22548
23116
|
function canonicalContainedTarget(abs, root) {
|
|
22549
23117
|
let realRoot;
|
|
22550
23118
|
try {
|
|
22551
|
-
realRoot =
|
|
23119
|
+
realRoot = realpathSync12(root);
|
|
22552
23120
|
} catch (e) {
|
|
22553
23121
|
throw new Error(`trusted repository root ${root} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
22554
23122
|
}
|
|
22555
23123
|
let dir = abs;
|
|
22556
23124
|
const tail = [];
|
|
22557
|
-
while (!
|
|
22558
|
-
const parent =
|
|
23125
|
+
while (!existsSync20(dir)) {
|
|
23126
|
+
const parent = dirname15(dir);
|
|
22559
23127
|
if (parent === dir) break;
|
|
22560
23128
|
tail.unshift(basename6(dir));
|
|
22561
23129
|
dir = parent;
|
|
22562
23130
|
}
|
|
22563
23131
|
let realDir;
|
|
22564
23132
|
try {
|
|
22565
|
-
realDir =
|
|
23133
|
+
realDir = realpathSync12(dir);
|
|
22566
23134
|
} catch (e) {
|
|
22567
23135
|
throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
22568
23136
|
}
|
|
22569
|
-
const real = tail.length ?
|
|
23137
|
+
const real = tail.length ? join30(realDir, ...tail) : realDir;
|
|
22570
23138
|
const rel = relative12(realRoot, real);
|
|
22571
23139
|
if (rel === "" || rel.startsWith("..") || isAbsolute17(rel)) {
|
|
22572
23140
|
throw new Error(`write target ${abs} resolves outside the trusted repository root ${realRoot} \u2014 refusing to judge a write beyond the repository`);
|
|
@@ -22609,12 +23177,12 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
|
|
|
22609
23177
|
const bytes = Buffer.byteLength(rawDiff);
|
|
22610
23178
|
if (bytes > DIFF_MAX_BYTES()) throw new Error(`write diff for ${display2} is ${bytes} bytes (over the ${DIFF_MAX_BYTES()}-byte reconstruction budget)`);
|
|
22611
23179
|
const { op, patch } = canonicalPatch(rawDiff, display2, isCreate);
|
|
22612
|
-
const dir = mkdtempSync7(
|
|
23180
|
+
const dir = mkdtempSync7(join30(tmpdir8(), "hf-sdk-apply-"));
|
|
22613
23181
|
try {
|
|
22614
|
-
const targetPath =
|
|
22615
|
-
if (op !== "create")
|
|
22616
|
-
const patchPath =
|
|
22617
|
-
|
|
23182
|
+
const targetPath = join30(dir, TARGET);
|
|
23183
|
+
if (op !== "create") writeFileSync16(targetPath, before ?? "");
|
|
23184
|
+
const patchPath = join30(dir, "change.patch");
|
|
23185
|
+
writeFileSync16(patchPath, patch);
|
|
22618
23186
|
const git8 = (extra) => execFileSync15("git", ["apply", ...extra, patchPath], {
|
|
22619
23187
|
cwd: dir,
|
|
22620
23188
|
encoding: "utf8",
|
|
@@ -22633,7 +23201,7 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
|
|
|
22633
23201
|
} catch (e) {
|
|
22634
23202
|
throw new Error(`could not reconstruct the write to ${display2} from its diff via git apply: ${e instanceof Error ? e.message : String(e)}`);
|
|
22635
23203
|
} finally {
|
|
22636
|
-
|
|
23204
|
+
rmSync14(dir, { recursive: true, force: true });
|
|
22637
23205
|
}
|
|
22638
23206
|
}
|
|
22639
23207
|
function canonicalPatch(raw, display2, isCreate) {
|
|
@@ -23082,7 +23650,7 @@ var init_evidence = __esm({
|
|
|
23082
23650
|
});
|
|
23083
23651
|
|
|
23084
23652
|
// src/runtime-qualification.ts
|
|
23085
|
-
import { createHash as
|
|
23653
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
23086
23654
|
function includesKind(set, kind) {
|
|
23087
23655
|
return set.includes(kind);
|
|
23088
23656
|
}
|
|
@@ -23253,7 +23821,7 @@ function aggregateInLoop(assessments) {
|
|
|
23253
23821
|
return "PARTIAL";
|
|
23254
23822
|
}
|
|
23255
23823
|
function sha16(s) {
|
|
23256
|
-
return
|
|
23824
|
+
return createHash18("sha256").update(s).digest("hex").slice(0, 16);
|
|
23257
23825
|
}
|
|
23258
23826
|
function capabilityHash(caps) {
|
|
23259
23827
|
const stable = JSON.stringify({
|
|
@@ -23373,15 +23941,15 @@ var init_runtime_qualification = __esm({
|
|
|
23373
23941
|
|
|
23374
23942
|
// src/cli/runtime.ts
|
|
23375
23943
|
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
23376
|
-
import { existsSync as
|
|
23377
|
-
import { join as
|
|
23944
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync14, readFileSync as readFileSync28, writeFileSync as writeFileSync17 } from "node:fs";
|
|
23945
|
+
import { join as join31, dirname as dirname16 } from "node:path";
|
|
23378
23946
|
function storePath(cwd) {
|
|
23379
23947
|
const dir = gitDir(cwd);
|
|
23380
|
-
return dir ?
|
|
23948
|
+
return dir ? join31(dir, "tamperward", "runtime-qualification.json") : null;
|
|
23381
23949
|
}
|
|
23382
23950
|
function readStore(cwd) {
|
|
23383
23951
|
const path = storePath(cwd);
|
|
23384
|
-
if (!path || !
|
|
23952
|
+
if (!path || !existsSync21(path)) return null;
|
|
23385
23953
|
try {
|
|
23386
23954
|
const parsed = JSON.parse(readFileSync28(path, "utf8"));
|
|
23387
23955
|
if (parsed && typeof parsed === "object" && parsed.records && typeof parsed.records === "object") return parsed;
|
|
@@ -23504,8 +24072,8 @@ function writeRecord(cwd, id, report2) {
|
|
|
23504
24072
|
store.records[id] = report2;
|
|
23505
24073
|
store.updated_at = report2.timestamp;
|
|
23506
24074
|
try {
|
|
23507
|
-
|
|
23508
|
-
|
|
24075
|
+
mkdirSync14(dirname16(path), { recursive: true });
|
|
24076
|
+
writeFileSync17(path, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
23509
24077
|
return true;
|
|
23510
24078
|
} catch {
|
|
23511
24079
|
return false;
|
|
@@ -23540,10 +24108,10 @@ function resolveHookConfigHash(canonicalId, cwd) {
|
|
|
23540
24108
|
return sha16(JSON.stringify(interventionWiring(cwd)));
|
|
23541
24109
|
}
|
|
23542
24110
|
const files = {
|
|
23543
|
-
"github-copilot-cli":
|
|
24111
|
+
"github-copilot-cli": join31(cwd, ".github", "hooks", "tamperward.json")
|
|
23544
24112
|
};
|
|
23545
24113
|
const file = files[canonicalId];
|
|
23546
|
-
if (!file || !
|
|
24114
|
+
if (!file || !existsSync21(file)) return null;
|
|
23547
24115
|
try {
|
|
23548
24116
|
const raw = readFileSync28(file, "utf8");
|
|
23549
24117
|
let hooks = raw;
|
|
@@ -23810,6 +24378,352 @@ var init_runtime = __esm({
|
|
|
23810
24378
|
}
|
|
23811
24379
|
});
|
|
23812
24380
|
|
|
24381
|
+
// src/cli/receipt.ts
|
|
24382
|
+
import { appendFileSync as appendFileSync6, readFileSync as readFileSync29, writeFileSync as writeFileSync18 } from "node:fs";
|
|
24383
|
+
function parseReceipt(args) {
|
|
24384
|
+
const [sub, ...rest] = args;
|
|
24385
|
+
const exportOpts = {};
|
|
24386
|
+
const reconcileOpts = {};
|
|
24387
|
+
for (let i = 0; i < rest.length; i++) {
|
|
24388
|
+
const a = rest[i];
|
|
24389
|
+
const next = () => rest[++i];
|
|
24390
|
+
switch (a) {
|
|
24391
|
+
case "--json":
|
|
24392
|
+
reconcileOpts.json = true;
|
|
24393
|
+
break;
|
|
24394
|
+
case "--require-ancestor":
|
|
24395
|
+
reconcileOpts.requireAncestor = true;
|
|
24396
|
+
break;
|
|
24397
|
+
case "--out":
|
|
24398
|
+
exportOpts.out = next();
|
|
24399
|
+
break;
|
|
24400
|
+
case "--cwd": {
|
|
24401
|
+
const v = next();
|
|
24402
|
+
exportOpts.cwd = v;
|
|
24403
|
+
reconcileOpts.cwd = v;
|
|
24404
|
+
break;
|
|
24405
|
+
}
|
|
24406
|
+
case "--base":
|
|
24407
|
+
reconcileOpts.base = next();
|
|
24408
|
+
break;
|
|
24409
|
+
case "--cmd":
|
|
24410
|
+
reconcileOpts.cmd = next();
|
|
24411
|
+
break;
|
|
24412
|
+
case "--budget": {
|
|
24413
|
+
const v = next();
|
|
24414
|
+
if (v !== void 0) reconcileOpts.budget = Number(v);
|
|
24415
|
+
break;
|
|
24416
|
+
}
|
|
24417
|
+
case "--receipt":
|
|
24418
|
+
reconcileOpts.receipt = next();
|
|
24419
|
+
break;
|
|
24420
|
+
case "--ci-result":
|
|
24421
|
+
reconcileOpts.ciResult = next();
|
|
24422
|
+
break;
|
|
24423
|
+
}
|
|
24424
|
+
}
|
|
24425
|
+
return {
|
|
24426
|
+
sub: isOneOf(RECEIPT_SUBCOMMANDS, sub) ? sub : void 0,
|
|
24427
|
+
exportOpts,
|
|
24428
|
+
reconcileOpts
|
|
24429
|
+
};
|
|
24430
|
+
}
|
|
24431
|
+
function runReceipt(args) {
|
|
24432
|
+
const { sub, exportOpts, reconcileOpts } = parseReceipt(args);
|
|
24433
|
+
if (sub === "export") return runReceiptExport(exportOpts);
|
|
24434
|
+
if (sub === "reconcile") return runReceiptReconcile(reconcileOpts);
|
|
24435
|
+
process.stderr.write(`tamperward: receipt requires a subcommand (${RECEIPT_SUBCOMMANDS.join(" | ")})
|
|
24436
|
+
`);
|
|
24437
|
+
return 2;
|
|
24438
|
+
}
|
|
24439
|
+
function reconcileInputs(opts) {
|
|
24440
|
+
return {
|
|
24441
|
+
base_ref: opts.base ?? "HEAD",
|
|
24442
|
+
explicit_base: opts.base !== void 0,
|
|
24443
|
+
command_source: opts.cmd !== void 0 ? "flag" : "policy",
|
|
24444
|
+
command: opts.cmd ?? "",
|
|
24445
|
+
budget_source: opts.budget !== void 0 ? "flag" : "policy",
|
|
24446
|
+
budget: opts.budget ?? 300
|
|
24447
|
+
};
|
|
24448
|
+
}
|
|
24449
|
+
function verdictExit(verdict2, signedOff) {
|
|
24450
|
+
switch (verdict2) {
|
|
24451
|
+
case "VERIFIED":
|
|
24452
|
+
return 0;
|
|
24453
|
+
case "MASKED_FAILURE":
|
|
24454
|
+
return signedOff ? 0 : 1;
|
|
24455
|
+
case "SUITE_RED":
|
|
24456
|
+
return 1;
|
|
24457
|
+
default:
|
|
24458
|
+
return 2;
|
|
24459
|
+
}
|
|
24460
|
+
}
|
|
24461
|
+
function needRepo(cwd, what) {
|
|
24462
|
+
if (repoContext(cwd)) return null;
|
|
24463
|
+
const why = outsideRepository(cwd) ?? "cwd is not inside a repository the gate can read";
|
|
24464
|
+
process.stderr.write(`tamperward: receipt ${what} needs a git repository (${why})
|
|
24465
|
+
`);
|
|
24466
|
+
return 2;
|
|
24467
|
+
}
|
|
24468
|
+
function runReceiptExport(opts = {}) {
|
|
24469
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
24470
|
+
const guard = needRepo(cwd, "export");
|
|
24471
|
+
if (guard !== null) return guard;
|
|
24472
|
+
const evaluation = evaluateVerificationState(cwd);
|
|
24473
|
+
const record = readVerificationRecord(cwd);
|
|
24474
|
+
if (evaluation.state !== "CURRENT" || !record) {
|
|
24475
|
+
const detail = evaluation.state === "CURRENT" ? "no verification record is available to export" : `no CURRENT verification to export (state: ${evaluation.state}) \u2014 run \`tamperward verify\` first`;
|
|
24476
|
+
process.stderr.write(`tamperward: ${detail}
|
|
24477
|
+
`);
|
|
24478
|
+
return 2;
|
|
24479
|
+
}
|
|
24480
|
+
const receipt = receiptFromRecord(record);
|
|
24481
|
+
const serialized = JSON.stringify(receipt) + "\n";
|
|
24482
|
+
if (opts.out) {
|
|
24483
|
+
try {
|
|
24484
|
+
writeFileSync18(opts.out, serialized);
|
|
24485
|
+
} catch (e) {
|
|
24486
|
+
process.stderr.write(`tamperward: could not write receipt to ${opts.out} (${e instanceof Error ? e.message : String(e)})
|
|
24487
|
+
`);
|
|
24488
|
+
return 2;
|
|
24489
|
+
}
|
|
24490
|
+
process.stdout.write(`receipt: exported VERIFIED receipt for tree ${receipt.binding.tree.slice(0, 10)} to ${opts.out}
|
|
24491
|
+
`);
|
|
24492
|
+
} else {
|
|
24493
|
+
process.stdout.write(serialized);
|
|
24494
|
+
}
|
|
24495
|
+
return 0;
|
|
24496
|
+
}
|
|
24497
|
+
function ciAdjudicate(cwd, opts) {
|
|
24498
|
+
const inputs = reconcileInputs(opts);
|
|
24499
|
+
let binding = null;
|
|
24500
|
+
let bindingError;
|
|
24501
|
+
try {
|
|
24502
|
+
binding = computeBinding(cwd, inputs);
|
|
24503
|
+
} catch (e) {
|
|
24504
|
+
bindingError = e instanceof Error ? e.message : String(e);
|
|
24505
|
+
}
|
|
24506
|
+
if (opts.ciResult !== void 0) {
|
|
24507
|
+
let doc;
|
|
24508
|
+
try {
|
|
24509
|
+
doc = JSON.parse(readFileSync29(opts.ciResult, "utf8"));
|
|
24510
|
+
} catch (e) {
|
|
24511
|
+
return {
|
|
24512
|
+
ci: { verdict: "CANNOT_VERIFY", binding, binding_error: `unreadable CI verify result: ${e instanceof Error ? e.message : String(e)}` },
|
|
24513
|
+
exit: 2
|
|
24514
|
+
};
|
|
24515
|
+
}
|
|
24516
|
+
const check2 = readCiVerify(doc, binding, cwd, opts);
|
|
24517
|
+
if (check2.reason !== void 0) {
|
|
24518
|
+
return { ci: { verdict: "CANNOT_VERIFY", binding, binding_error: check2.reason }, exit: 2 };
|
|
24519
|
+
}
|
|
24520
|
+
const signedOff = check2.verdict === "MASKED_FAILURE" && oobToken("verify", oobFromEnv(), oobHeadFromEnv()) !== null;
|
|
24521
|
+
return {
|
|
24522
|
+
ci: {
|
|
24523
|
+
verdict: check2.verdict,
|
|
24524
|
+
binding,
|
|
24525
|
+
...check2.adjudicated_tree !== void 0 ? { adjudicated_tree: check2.adjudicated_tree } : {},
|
|
24526
|
+
...bindingError ? { binding_error: bindingError } : {}
|
|
24527
|
+
},
|
|
24528
|
+
exit: verdictExit(check2.verdict, signedOff)
|
|
24529
|
+
};
|
|
24530
|
+
}
|
|
24531
|
+
const impl = opts.runVerifyImpl ?? runVerify;
|
|
24532
|
+
let summary;
|
|
24533
|
+
const exit = impl({
|
|
24534
|
+
cwd,
|
|
24535
|
+
base: opts.base,
|
|
24536
|
+
cmd: opts.cmd,
|
|
24537
|
+
budget: opts.budget,
|
|
24538
|
+
requireAncestor: opts.requireAncestor,
|
|
24539
|
+
silent: true,
|
|
24540
|
+
// reconcile prints its own summary; suppress verify's stdout
|
|
24541
|
+
onVerdict: (s) => {
|
|
24542
|
+
summary = s;
|
|
24543
|
+
}
|
|
24544
|
+
});
|
|
24545
|
+
const verdict2 = summary?.verdict ?? "CANNOT_VERIFY";
|
|
24546
|
+
return {
|
|
24547
|
+
ci: {
|
|
24548
|
+
verdict: verdict2,
|
|
24549
|
+
binding,
|
|
24550
|
+
...binding ? { adjudicated_tree: binding.tree } : {},
|
|
24551
|
+
...bindingError ? { binding_error: bindingError } : {}
|
|
24552
|
+
},
|
|
24553
|
+
exit
|
|
24554
|
+
};
|
|
24555
|
+
}
|
|
24556
|
+
function isCommitId(v) {
|
|
24557
|
+
return typeof v === "string" && /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(v);
|
|
24558
|
+
}
|
|
24559
|
+
function readCiVerify(doc, binding, cwd, opts) {
|
|
24560
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
24561
|
+
return { verdict: "CANNOT_VERIFY", reason: "CI verify result is not a JSON object" };
|
|
24562
|
+
}
|
|
24563
|
+
const d = doc;
|
|
24564
|
+
if (d.schema_version !== 1) {
|
|
24565
|
+
return { verdict: "CANNOT_VERIFY", reason: `CI verify result is not a verify --json document (schema_version ${JSON.stringify(d.schema_version)})` };
|
|
24566
|
+
}
|
|
24567
|
+
if (!isCommitId(d.base)) {
|
|
24568
|
+
return { verdict: "CANNOT_VERIFY", reason: "CI verify result has no resolved base commit \u2014 not a verify --json document (a receipt is not a verify result)" };
|
|
24569
|
+
}
|
|
24570
|
+
if (!isRecordObject(d.visible) || !isRecordObject(d.pristine)) {
|
|
24571
|
+
return { verdict: "CANNOT_VERIFY", reason: "CI verify result is missing the visible/pristine stage objects \u2014 not a verify --json document" };
|
|
24572
|
+
}
|
|
24573
|
+
if (!isOneOf(VERIFY_VERDICTS, d.verdict)) {
|
|
24574
|
+
return { verdict: "CANNOT_VERIFY", reason: `CI verify result has an unknown verdict ${JSON.stringify(d.verdict)}` };
|
|
24575
|
+
}
|
|
24576
|
+
if (binding) {
|
|
24577
|
+
const concrete = resolveConcreteCommit(cwd, opts.base ?? "HEAD");
|
|
24578
|
+
const accepted = /* @__PURE__ */ new Set([binding.base, ...concrete ? [concrete] : []]);
|
|
24579
|
+
if (!accepted.has(d.base)) {
|
|
24580
|
+
return {
|
|
24581
|
+
verdict: "CANNOT_VERIFY",
|
|
24582
|
+
reason: `CI verify result binds base ${d.base.slice(0, 10)} but CI computed ${binding.base.slice(0, 10)} \u2014 the document describes a different base`
|
|
24583
|
+
};
|
|
24584
|
+
}
|
|
24585
|
+
}
|
|
24586
|
+
const adjudicated_tree = typeof d.adjudicated_tree === "string" && d.adjudicated_tree.length > 0 ? d.adjudicated_tree : void 0;
|
|
24587
|
+
return { verdict: d.verdict, adjudicated_tree };
|
|
24588
|
+
}
|
|
24589
|
+
function isRecordObject(v) {
|
|
24590
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
24591
|
+
}
|
|
24592
|
+
function reconcileDocument(r) {
|
|
24593
|
+
return machineOutput({
|
|
24594
|
+
command: "reconcile",
|
|
24595
|
+
result: r.result,
|
|
24596
|
+
ci: r.ci,
|
|
24597
|
+
local: r.local,
|
|
24598
|
+
reconciliation: {
|
|
24599
|
+
agreement: r.agreement,
|
|
24600
|
+
applicable: r.applicable,
|
|
24601
|
+
...r.mismatched_input ? { mismatched_input: r.mismatched_input } : {},
|
|
24602
|
+
divergence: r.divergence,
|
|
24603
|
+
environment_divergence: r.environment_divergence
|
|
24604
|
+
}
|
|
24605
|
+
});
|
|
24606
|
+
}
|
|
24607
|
+
function renderReconcile(r, claimedReceipt, ciBinding, adjudicatedTree, colour) {
|
|
24608
|
+
const tick = (s) => paint(s, severityColour("ok"), colour);
|
|
24609
|
+
const cross = (s) => paint(s, BOLD + severityColour("bad"), colour);
|
|
24610
|
+
const stageMark = (value, pass) => value === pass ? tick("\u2713") : cross("\u2717");
|
|
24611
|
+
const lines = [];
|
|
24612
|
+
lines.push("LOCAL");
|
|
24613
|
+
if (claimedReceipt) {
|
|
24614
|
+
lines.push(` ${stageMark(claimedReceipt.stages.candidate, "PASS")} candidate ${claimedReceipt.binding.tree.slice(0, 10)} locally verified`);
|
|
24615
|
+
lines.push(` ${stageMark(claimedReceipt.stages.pristine, "PASS")} pristine ${claimedReceipt.stages.pristine}`);
|
|
24616
|
+
lines.push(` ${stageMark(claimedReceipt.stages.integrity, "CLEAN")} integrity ${claimedReceipt.stages.integrity}`);
|
|
24617
|
+
} else if (r.local.disposition === "ABSENT") {
|
|
24618
|
+
lines.push(" (no receipt provided)");
|
|
24619
|
+
} else {
|
|
24620
|
+
lines.push(` ${cross("\u2717")} receipt ${r.local.disposition}${r.local.detail ? ` \u2014 ${r.local.detail}` : ""}`);
|
|
24621
|
+
}
|
|
24622
|
+
lines.push("");
|
|
24623
|
+
lines.push("CI");
|
|
24624
|
+
if (ciBinding) {
|
|
24625
|
+
const mark = r.ci.verdict === "VERIFIED" ? tick("\u2713") : cross("\u2717");
|
|
24626
|
+
const shownTree = adjudicatedTree ?? ciBinding.tree;
|
|
24627
|
+
lines.push(` ${mark} independently ${r.ci.verdict} ${shownTree.slice(0, 10)}`);
|
|
24628
|
+
} else {
|
|
24629
|
+
lines.push(` ${cross("\u2717")} ${r.ci.verdict} (identity not computable)`);
|
|
24630
|
+
}
|
|
24631
|
+
lines.push("");
|
|
24632
|
+
lines.push("RESULT");
|
|
24633
|
+
lines.push(` ${paint(r.result, (r.result === "VERIFIED" ? "" : BOLD) + severityColour(r.result === "VERIFIED" ? "ok" : "bad"), colour)}`);
|
|
24634
|
+
const agreementLine = {
|
|
24635
|
+
AGREE: "Local and CI evidence agree",
|
|
24636
|
+
DIVERGENCE: "EVIDENCE DIVERGENCE \u2014 local claim not confirmed by CI",
|
|
24637
|
+
NON_APPLICABLE: "Local receipt does not apply to the CI-adjudicated state \u2014 ignored",
|
|
24638
|
+
NO_CLAIM: "No local receipt to reconcile \u2014 CI verdict stands"
|
|
24639
|
+
};
|
|
24640
|
+
lines.push(` ${paint(agreementLine[r.agreement], severityColour(AGREEMENT_SEVERITY[r.agreement]), colour)}`);
|
|
24641
|
+
for (const d of r.divergence) lines.push(` ${d}`);
|
|
24642
|
+
return lines.join("\n") + "\n";
|
|
24643
|
+
}
|
|
24644
|
+
function loadClaimedReceipt(path) {
|
|
24645
|
+
if (path === void 0) {
|
|
24646
|
+
return { disposition: "ABSENT", detail: "no local verification receipt was provided (pass one with --receipt)" };
|
|
24647
|
+
}
|
|
24648
|
+
let raw;
|
|
24649
|
+
try {
|
|
24650
|
+
raw = readFileSync29(path, "utf8");
|
|
24651
|
+
} catch (e) {
|
|
24652
|
+
return { disposition: "MALFORMED", detail: `receipt file could not be read (${e instanceof Error ? e.message : String(e)})` };
|
|
24653
|
+
}
|
|
24654
|
+
let parsed;
|
|
24655
|
+
try {
|
|
24656
|
+
parsed = JSON.parse(raw);
|
|
24657
|
+
} catch (e) {
|
|
24658
|
+
return { disposition: "MALFORMED", detail: `receipt file could not be parsed as JSON (${e instanceof Error ? e.message : String(e)})` };
|
|
24659
|
+
}
|
|
24660
|
+
return classifyReceipt(parsed);
|
|
24661
|
+
}
|
|
24662
|
+
function runReceiptReconcile(opts = {}) {
|
|
24663
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
24664
|
+
const guard = needRepo(cwd, "reconcile");
|
|
24665
|
+
if (guard !== null) return guard;
|
|
24666
|
+
const { ci, exit } = ciAdjudicate(cwd, opts);
|
|
24667
|
+
const claimed = loadClaimedReceipt(opts.receipt);
|
|
24668
|
+
const r = reconcile(ci, claimed);
|
|
24669
|
+
const claimedReceipt = claimed.disposition === "PRESENT" ? claimed.receipt : null;
|
|
24670
|
+
if (opts.json) {
|
|
24671
|
+
process.stdout.write(JSON.stringify(reconcileDocument(r)) + "\n");
|
|
24672
|
+
} else {
|
|
24673
|
+
process.stdout.write(renderReconcile(r, claimedReceipt, ci.binding, ci.adjudicated_tree, colourEnabled(process.env, process.stdout)));
|
|
24674
|
+
}
|
|
24675
|
+
writeJobSummary(r, claimedReceipt, ci.binding, ci.adjudicated_tree);
|
|
24676
|
+
return exit;
|
|
24677
|
+
}
|
|
24678
|
+
function writeJobSummary(r, receipt, ciBinding, adjudicatedTree) {
|
|
24679
|
+
const path = process.env.GITHUB_STEP_SUMMARY;
|
|
24680
|
+
if (!path) return;
|
|
24681
|
+
const md = ["## TamperWard receipt reconciliation", ""];
|
|
24682
|
+
const mark = (value, pass) => value === pass ? "\u2713" : "\u2717";
|
|
24683
|
+
md.push("### LOCAL");
|
|
24684
|
+
if (receipt) {
|
|
24685
|
+
md.push(`- ${mark(receipt.stages.candidate, "PASS")} candidate \`${receipt.binding.tree.slice(0, 10)}\` locally verified`);
|
|
24686
|
+
md.push(`- ${mark(receipt.stages.pristine, "PASS")} pristine ${receipt.stages.pristine}`);
|
|
24687
|
+
md.push(`- ${mark(receipt.stages.integrity, "CLEAN")} integrity ${receipt.stages.integrity}`);
|
|
24688
|
+
} else if (r.local.disposition === "ABSENT") {
|
|
24689
|
+
md.push("- _no receipt provided_");
|
|
24690
|
+
} else {
|
|
24691
|
+
md.push(`- receipt **${r.local.disposition}**${r.local.detail ? ` \u2014 ${r.local.detail}` : ""}`);
|
|
24692
|
+
}
|
|
24693
|
+
md.push("", "### CI");
|
|
24694
|
+
const shownTree = adjudicatedTree ?? ciBinding?.tree;
|
|
24695
|
+
md.push(`- independently **${r.ci.verdict}**${shownTree ? ` \`${shownTree.slice(0, 10)}\`` : " (identity not computable)"}`);
|
|
24696
|
+
md.push("", "### RESULT");
|
|
24697
|
+
md.push(`**${r.result}** \u2014 ${r.agreement}`);
|
|
24698
|
+
for (const d of r.divergence) md.push(`- ${d}`);
|
|
24699
|
+
md.push("");
|
|
24700
|
+
try {
|
|
24701
|
+
appendFileSync6(path, md.join("\n") + "\n");
|
|
24702
|
+
} catch {
|
|
24703
|
+
}
|
|
24704
|
+
}
|
|
24705
|
+
var RECEIPT_SUBCOMMANDS, AGREEMENT_SEVERITY;
|
|
24706
|
+
var init_receipt = __esm({
|
|
24707
|
+
"src/cli/receipt.ts"() {
|
|
24708
|
+
"use strict";
|
|
24709
|
+
init_machine_output();
|
|
24710
|
+
init_repo_context();
|
|
24711
|
+
init_verification_state();
|
|
24712
|
+
init_verification_receipt();
|
|
24713
|
+
init_verify();
|
|
24714
|
+
init_signoff();
|
|
24715
|
+
init_text();
|
|
24716
|
+
init_status();
|
|
24717
|
+
RECEIPT_SUBCOMMANDS = ["export", "reconcile"];
|
|
24718
|
+
AGREEMENT_SEVERITY = {
|
|
24719
|
+
AGREE: "ok",
|
|
24720
|
+
DIVERGENCE: "bad",
|
|
24721
|
+
NON_APPLICABLE: "warn",
|
|
24722
|
+
NO_CLAIM: "info"
|
|
24723
|
+
};
|
|
24724
|
+
}
|
|
24725
|
+
});
|
|
24726
|
+
|
|
23813
24727
|
// src/cli/signoff-label.ts
|
|
23814
24728
|
function runSignoffLabel(opts) {
|
|
23815
24729
|
if (!opts.rule) {
|
|
@@ -24267,6 +25181,29 @@ function validateCliArgs(cmd, args) {
|
|
|
24267
25181
|
}
|
|
24268
25182
|
return `unknown research subcommand "${sub}" (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
24269
25183
|
}
|
|
25184
|
+
if (cmd === "receipt") {
|
|
25185
|
+
const [sub, ...rest] = args;
|
|
25186
|
+
if (sub === void 0) return `receipt requires a subcommand (${RECEIPT_SUBCOMMANDS.join(" | ")})`;
|
|
25187
|
+
if (sub === "export") {
|
|
25188
|
+
return validateFlatArgs(rest, {
|
|
25189
|
+
values: { "--out": "string", "--cwd": "string" }
|
|
25190
|
+
}).error;
|
|
25191
|
+
}
|
|
25192
|
+
if (sub === "reconcile") {
|
|
25193
|
+
return validateFlatArgs(rest, {
|
|
25194
|
+
flags: ["--json", "--require-ancestor"],
|
|
25195
|
+
values: {
|
|
25196
|
+
"--base": "string",
|
|
25197
|
+
"--cmd": "string",
|
|
25198
|
+
"--budget": "positive",
|
|
25199
|
+
"--cwd": "string",
|
|
25200
|
+
"--receipt": "string",
|
|
25201
|
+
"--ci-result": "string"
|
|
25202
|
+
}
|
|
25203
|
+
}).error;
|
|
25204
|
+
}
|
|
25205
|
+
return `unknown receipt subcommand "${sub}" (${RECEIPT_SUBCOMMANDS.join(" | ")})`;
|
|
25206
|
+
}
|
|
24270
25207
|
return void 0;
|
|
24271
25208
|
}
|
|
24272
25209
|
function printHelp() {
|
|
@@ -24402,6 +25339,23 @@ Formats:
|
|
|
24402
25339
|
emits the versioned status document for
|
|
24403
25340
|
editors, CI and dashboards. Local CURRENT
|
|
24404
25341
|
is posture, never repository/CI authority.
|
|
25342
|
+
tamperward receipt export [--out F] emit the bounded, transportable verification
|
|
25343
|
+
[--cwd D] receipt for the CURRENT verified state (to F,
|
|
25344
|
+
else stdout). Refuses when the state is not
|
|
25345
|
+
CURRENT. Raw evidence otherwise stays under
|
|
25346
|
+
.git/tamperward/, never in the tracked tree.
|
|
25347
|
+
tamperward receipt reconcile [--base R] CI reruns the canonical verification FIRST,
|
|
25348
|
+
[--cmd C] [--budget S] then reconciles a claimed receipt against its
|
|
25349
|
+
[--receipt F] [--ci-result F] own adjudication. --receipt is the claimed
|
|
25350
|
+
[--require-ancestor] [--json] local receipt (absent \u2192 NO_CLAIM, never a
|
|
25351
|
+
[--cwd D] failure by itself); --ci-result consumes a
|
|
25352
|
+
preceding "verify --json" so the suite runs
|
|
25353
|
+
once. Prints LOCAL / CI / RESULT and (--json)
|
|
25354
|
+
a machine-readable reconciliation document,
|
|
25355
|
+
plus a GitHub job summary. The receipt is
|
|
25356
|
+
evidence, never authority: a stale, mismatched,
|
|
25357
|
+
tampered, missing or unknown-schema receipt can
|
|
25358
|
+
NEVER promote a CI result. Exit is CI's verdict.
|
|
24405
25359
|
tamperward runtime verify [--runtime ID] qualify the in-loop runtime: an operation-
|
|
24406
25360
|
[--mode headless|interactive] specific capability matrix (pre-deny:shell,
|
|
24407
25361
|
[--model M] [--json] [--cwd D] native-edit, mcp, end-of-turn, transport:*,
|
|
@@ -24492,6 +25446,8 @@ function main(argv) {
|
|
|
24492
25446
|
const { sub, opts } = parseRuntime(rest);
|
|
24493
25447
|
return runRuntime(sub, opts);
|
|
24494
25448
|
}
|
|
25449
|
+
case "receipt":
|
|
25450
|
+
return runReceipt(rest);
|
|
24495
25451
|
case "run":
|
|
24496
25452
|
return runEnvelope({
|
|
24497
25453
|
...parseRun(rest),
|
|
@@ -24543,6 +25499,7 @@ var init_main = __esm({
|
|
|
24543
25499
|
init_onboard();
|
|
24544
25500
|
init_research();
|
|
24545
25501
|
init_runtime();
|
|
25502
|
+
init_receipt();
|
|
24546
25503
|
init_audit();
|
|
24547
25504
|
init_signoff_label();
|
|
24548
25505
|
}
|
|
@@ -24551,7 +25508,7 @@ var init_main = __esm({
|
|
|
24551
25508
|
// src/cli/index.ts
|
|
24552
25509
|
init_hook_client();
|
|
24553
25510
|
init_exit();
|
|
24554
|
-
import { readFileSync as
|
|
25511
|
+
import { readFileSync as readFileSync30 } from "node:fs";
|
|
24555
25512
|
function loadMain() {
|
|
24556
25513
|
return Promise.resolve().then(() => (init_main(), main_exports));
|
|
24557
25514
|
}
|
|
@@ -24564,7 +25521,7 @@ async function launch(argv) {
|
|
|
24564
25521
|
if (kind && hookServiceEnabled()) {
|
|
24565
25522
|
let raw = null;
|
|
24566
25523
|
try {
|
|
24567
|
-
raw =
|
|
25524
|
+
raw = readFileSync30(0, "utf8");
|
|
24568
25525
|
} catch {
|
|
24569
25526
|
raw = null;
|
|
24570
25527
|
}
|