tamperward 2.37.2 → 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 +844 -134
- 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
|
@@ -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({
|
|
@@ -20714,17 +21035,17 @@ var init_record = __esm({
|
|
|
20714
21035
|
// src/research/run.ts
|
|
20715
21036
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20716
21037
|
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
20717
|
-
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";
|
|
20718
21039
|
import { hostname } from "node:os";
|
|
20719
|
-
import { join as
|
|
21040
|
+
import { join as join27, resolve as resolve16 } from "node:path";
|
|
20720
21041
|
function git7(args, cwd) {
|
|
20721
21042
|
return execFileSync14("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
20722
21043
|
}
|
|
20723
21044
|
function pairRecordPath(ledger, task, pair) {
|
|
20724
|
-
return
|
|
21045
|
+
return join27(ledger, "pairs", `${task}--${pair}.json`);
|
|
20725
21046
|
}
|
|
20726
21047
|
function researchLockPath(ledger) {
|
|
20727
|
-
return
|
|
21048
|
+
return join27(ledger, "run.lock");
|
|
20728
21049
|
}
|
|
20729
21050
|
function readLockOwner(path) {
|
|
20730
21051
|
try {
|
|
@@ -20749,7 +21070,7 @@ function lockHolder(path) {
|
|
|
20749
21070
|
return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
|
|
20750
21071
|
}
|
|
20751
21072
|
function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
20752
|
-
|
|
21073
|
+
mkdirSync11(ledger, { recursive: true });
|
|
20753
21074
|
const path = researchLockPath(ledger);
|
|
20754
21075
|
if (breakStaleLock && existsSync18(path)) {
|
|
20755
21076
|
const owner2 = readLockOwner(path);
|
|
@@ -20859,16 +21180,16 @@ function resumableRecord(path, expected) {
|
|
|
20859
21180
|
}
|
|
20860
21181
|
function writeRecordAtomically(path, text) {
|
|
20861
21182
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
20862
|
-
|
|
21183
|
+
writeFileSync13(tmp, text);
|
|
20863
21184
|
renameSync4(tmp, path);
|
|
20864
21185
|
}
|
|
20865
21186
|
function cloneArgs(repo, ws3) {
|
|
20866
21187
|
return ["-c", "protocol.ext.allow=never", "clone", "-q", "--no-hardlinks", "--", repo, ws3];
|
|
20867
21188
|
}
|
|
20868
21189
|
function freshWorkspace(ledger, task, pair, arm, sourceBase) {
|
|
20869
|
-
const ws3 =
|
|
20870
|
-
|
|
20871
|
-
|
|
21190
|
+
const ws3 = join27(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
|
|
21191
|
+
rmSync13(ws3, { recursive: true, force: true });
|
|
21192
|
+
mkdirSync11(join27(ledger, "workspaces"), { recursive: true });
|
|
20872
21193
|
try {
|
|
20873
21194
|
execFileSync14("git", cloneArgs(task.repo, ws3), { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
20874
21195
|
} catch (e) {
|
|
@@ -21117,7 +21438,7 @@ function runResearch(opts) {
|
|
|
21117
21438
|
}
|
|
21118
21439
|
try {
|
|
21119
21440
|
const pairs = opts.pairs ?? 1;
|
|
21120
|
-
|
|
21441
|
+
mkdirSync11(join27(ledger, "pairs"), { recursive: true });
|
|
21121
21442
|
for (const task of tasks) {
|
|
21122
21443
|
const existingRecords = /* @__PURE__ */ new Map();
|
|
21123
21444
|
let sourceBase = null;
|
|
@@ -21245,7 +21566,7 @@ var init_run2 = __esm({
|
|
|
21245
21566
|
|
|
21246
21567
|
// src/research/ledger-files.ts
|
|
21247
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";
|
|
21248
|
-
import { join as
|
|
21569
|
+
import { join as join28, resolve as resolve17 } from "node:path";
|
|
21249
21570
|
function lstatOrNull(path) {
|
|
21250
21571
|
try {
|
|
21251
21572
|
return lstatSync11(path);
|
|
@@ -21271,7 +21592,7 @@ function readRegularFile(path, realPairsDir, name) {
|
|
|
21271
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`);
|
|
21272
21593
|
}
|
|
21273
21594
|
const real = realpathSync11.native(path);
|
|
21274
|
-
if (real !==
|
|
21595
|
+
if (real !== join28(realPairsDir, name)) {
|
|
21275
21596
|
throw new ResearchError(`ledger record ${path} resolves outside the pairs directory (${real}); refusing to read it`);
|
|
21276
21597
|
}
|
|
21277
21598
|
const flags = constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0);
|
|
@@ -21294,7 +21615,7 @@ function readRegularFile(path, realPairsDir, name) {
|
|
|
21294
21615
|
}
|
|
21295
21616
|
function readPairFiles(ledger) {
|
|
21296
21617
|
const root = resolve17(ledger);
|
|
21297
|
-
const pairsDir =
|
|
21618
|
+
const pairsDir = join28(root, "pairs");
|
|
21298
21619
|
const dirStat = lstatOrNull(pairsDir);
|
|
21299
21620
|
if (dirStat !== null && dirStat.isSymbolicLink()) {
|
|
21300
21621
|
throw new ResearchError(`ledger pairs directory ${pairsDir} is a symlink; only a real directory is read`);
|
|
@@ -21308,7 +21629,7 @@ function readPairFiles(ledger) {
|
|
|
21308
21629
|
throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
|
|
21309
21630
|
}
|
|
21310
21631
|
return names.map((name) => {
|
|
21311
|
-
const path =
|
|
21632
|
+
const path = join28(pairsDir, name);
|
|
21312
21633
|
return { name, path, bytes: readRegularFile(path, realPairsDir, name) };
|
|
21313
21634
|
});
|
|
21314
21635
|
}
|
|
@@ -21459,9 +21780,9 @@ var init_summarize = __esm({
|
|
|
21459
21780
|
});
|
|
21460
21781
|
|
|
21461
21782
|
// src/research/init.ts
|
|
21462
|
-
import { createHash as
|
|
21463
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
21464
|
-
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";
|
|
21465
21786
|
function idFrom(repo) {
|
|
21466
21787
|
const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
|
|
21467
21788
|
return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
|
|
@@ -21489,8 +21810,8 @@ function createResearchManifest(opts) {
|
|
|
21489
21810
|
}]
|
|
21490
21811
|
};
|
|
21491
21812
|
const text = JSON.stringify(doc, null, 2) + "\n";
|
|
21492
|
-
|
|
21493
|
-
|
|
21813
|
+
mkdirSync12(dirname13(out3), { recursive: true });
|
|
21814
|
+
writeFileSync14(out3, text, { flag: "wx", mode: 384 });
|
|
21494
21815
|
const parsed = readManifest(out3);
|
|
21495
21816
|
if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
|
|
21496
21817
|
return out3;
|
|
@@ -21512,7 +21833,7 @@ function runResearchInit(opts) {
|
|
|
21512
21833
|
}
|
|
21513
21834
|
}
|
|
21514
21835
|
function requireHash(path) {
|
|
21515
|
-
return
|
|
21836
|
+
return createHash16("sha256").update(readFileSync25(path)).digest("hex");
|
|
21516
21837
|
}
|
|
21517
21838
|
var init_init2 = __esm({
|
|
21518
21839
|
"src/research/init.ts"() {
|
|
@@ -21577,10 +21898,10 @@ var init_report2 = __esm({
|
|
|
21577
21898
|
});
|
|
21578
21899
|
|
|
21579
21900
|
// src/research/bundle.ts
|
|
21580
|
-
import { createHash as
|
|
21901
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
21581
21902
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
21582
|
-
import { mkdirSync as
|
|
21583
|
-
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";
|
|
21584
21905
|
function octal(value, width) {
|
|
21585
21906
|
return value.toString(8).padStart(width - 1, "0") + "\0";
|
|
21586
21907
|
}
|
|
@@ -21713,7 +22034,7 @@ function validateResearchBundle(path) {
|
|
|
21713
22034
|
if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
|
|
21714
22035
|
throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
|
|
21715
22036
|
}
|
|
21716
|
-
if (manifest &&
|
|
22037
|
+
if (manifest && createHash17("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
|
|
21717
22038
|
throw new ResearchError("research bundle manifest does not match its recorded sha256");
|
|
21718
22039
|
}
|
|
21719
22040
|
const parsedSummary = summary;
|
|
@@ -21747,7 +22068,7 @@ function createResearchBundle(opts) {
|
|
|
21747
22068
|
const summary = summarizeRecords(records);
|
|
21748
22069
|
if (opts.manifest) {
|
|
21749
22070
|
const manifest = readFileSync26(resolve19(opts.manifest));
|
|
21750
|
-
const sha3 =
|
|
22071
|
+
const sha3 = createHash17("sha256").update(manifest).digest("hex");
|
|
21751
22072
|
if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
|
|
21752
22073
|
entries.push({ name: "manifest.json", bytes: manifest });
|
|
21753
22074
|
}
|
|
@@ -21769,8 +22090,8 @@ function createResearchBundle(opts) {
|
|
|
21769
22090
|
entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
|
|
21770
22091
|
entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
|
|
21771
22092
|
const out3 = resolve19(opts.out);
|
|
21772
|
-
|
|
21773
|
-
|
|
22093
|
+
mkdirSync13(dirname14(out3), { recursive: true });
|
|
22094
|
+
writeFileSync15(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
|
|
21774
22095
|
return out3;
|
|
21775
22096
|
}
|
|
21776
22097
|
function runResearchBundle(opts) {
|
|
@@ -22782,9 +23103,9 @@ var init_adapter4 = __esm({
|
|
|
22782
23103
|
|
|
22783
23104
|
// src/adapters/copilot-sdk/changes.ts
|
|
22784
23105
|
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
22785
|
-
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";
|
|
22786
23107
|
import { tmpdir as tmpdir8 } from "node:os";
|
|
22787
|
-
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";
|
|
22788
23109
|
function asStr4(v) {
|
|
22789
23110
|
return typeof v === "string" ? v : "";
|
|
22790
23111
|
}
|
|
@@ -22802,7 +23123,7 @@ function canonicalContainedTarget(abs, root) {
|
|
|
22802
23123
|
let dir = abs;
|
|
22803
23124
|
const tail = [];
|
|
22804
23125
|
while (!existsSync20(dir)) {
|
|
22805
|
-
const parent =
|
|
23126
|
+
const parent = dirname15(dir);
|
|
22806
23127
|
if (parent === dir) break;
|
|
22807
23128
|
tail.unshift(basename6(dir));
|
|
22808
23129
|
dir = parent;
|
|
@@ -22813,7 +23134,7 @@ function canonicalContainedTarget(abs, root) {
|
|
|
22813
23134
|
} catch (e) {
|
|
22814
23135
|
throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
22815
23136
|
}
|
|
22816
|
-
const real = tail.length ?
|
|
23137
|
+
const real = tail.length ? join30(realDir, ...tail) : realDir;
|
|
22817
23138
|
const rel = relative12(realRoot, real);
|
|
22818
23139
|
if (rel === "" || rel.startsWith("..") || isAbsolute17(rel)) {
|
|
22819
23140
|
throw new Error(`write target ${abs} resolves outside the trusted repository root ${realRoot} \u2014 refusing to judge a write beyond the repository`);
|
|
@@ -22856,12 +23177,12 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
|
|
|
22856
23177
|
const bytes = Buffer.byteLength(rawDiff);
|
|
22857
23178
|
if (bytes > DIFF_MAX_BYTES()) throw new Error(`write diff for ${display2} is ${bytes} bytes (over the ${DIFF_MAX_BYTES()}-byte reconstruction budget)`);
|
|
22858
23179
|
const { op, patch } = canonicalPatch(rawDiff, display2, isCreate);
|
|
22859
|
-
const dir = mkdtempSync7(
|
|
23180
|
+
const dir = mkdtempSync7(join30(tmpdir8(), "hf-sdk-apply-"));
|
|
22860
23181
|
try {
|
|
22861
|
-
const targetPath =
|
|
22862
|
-
if (op !== "create")
|
|
22863
|
-
const patchPath =
|
|
22864
|
-
|
|
23182
|
+
const targetPath = join30(dir, TARGET);
|
|
23183
|
+
if (op !== "create") writeFileSync16(targetPath, before ?? "");
|
|
23184
|
+
const patchPath = join30(dir, "change.patch");
|
|
23185
|
+
writeFileSync16(patchPath, patch);
|
|
22865
23186
|
const git8 = (extra) => execFileSync15("git", ["apply", ...extra, patchPath], {
|
|
22866
23187
|
cwd: dir,
|
|
22867
23188
|
encoding: "utf8",
|
|
@@ -22880,7 +23201,7 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
|
|
|
22880
23201
|
} catch (e) {
|
|
22881
23202
|
throw new Error(`could not reconstruct the write to ${display2} from its diff via git apply: ${e instanceof Error ? e.message : String(e)}`);
|
|
22882
23203
|
} finally {
|
|
22883
|
-
|
|
23204
|
+
rmSync14(dir, { recursive: true, force: true });
|
|
22884
23205
|
}
|
|
22885
23206
|
}
|
|
22886
23207
|
function canonicalPatch(raw, display2, isCreate) {
|
|
@@ -23329,7 +23650,7 @@ var init_evidence = __esm({
|
|
|
23329
23650
|
});
|
|
23330
23651
|
|
|
23331
23652
|
// src/runtime-qualification.ts
|
|
23332
|
-
import { createHash as
|
|
23653
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
23333
23654
|
function includesKind(set, kind) {
|
|
23334
23655
|
return set.includes(kind);
|
|
23335
23656
|
}
|
|
@@ -23500,7 +23821,7 @@ function aggregateInLoop(assessments) {
|
|
|
23500
23821
|
return "PARTIAL";
|
|
23501
23822
|
}
|
|
23502
23823
|
function sha16(s) {
|
|
23503
|
-
return
|
|
23824
|
+
return createHash18("sha256").update(s).digest("hex").slice(0, 16);
|
|
23504
23825
|
}
|
|
23505
23826
|
function capabilityHash(caps) {
|
|
23506
23827
|
const stable = JSON.stringify({
|
|
@@ -23620,11 +23941,11 @@ var init_runtime_qualification = __esm({
|
|
|
23620
23941
|
|
|
23621
23942
|
// src/cli/runtime.ts
|
|
23622
23943
|
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
23623
|
-
import { existsSync as existsSync21, mkdirSync as
|
|
23624
|
-
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";
|
|
23625
23946
|
function storePath(cwd) {
|
|
23626
23947
|
const dir = gitDir(cwd);
|
|
23627
|
-
return dir ?
|
|
23948
|
+
return dir ? join31(dir, "tamperward", "runtime-qualification.json") : null;
|
|
23628
23949
|
}
|
|
23629
23950
|
function readStore(cwd) {
|
|
23630
23951
|
const path = storePath(cwd);
|
|
@@ -23751,8 +24072,8 @@ function writeRecord(cwd, id, report2) {
|
|
|
23751
24072
|
store.records[id] = report2;
|
|
23752
24073
|
store.updated_at = report2.timestamp;
|
|
23753
24074
|
try {
|
|
23754
|
-
|
|
23755
|
-
|
|
24075
|
+
mkdirSync14(dirname16(path), { recursive: true });
|
|
24076
|
+
writeFileSync17(path, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
23756
24077
|
return true;
|
|
23757
24078
|
} catch {
|
|
23758
24079
|
return false;
|
|
@@ -23787,7 +24108,7 @@ function resolveHookConfigHash(canonicalId, cwd) {
|
|
|
23787
24108
|
return sha16(JSON.stringify(interventionWiring(cwd)));
|
|
23788
24109
|
}
|
|
23789
24110
|
const files = {
|
|
23790
|
-
"github-copilot-cli":
|
|
24111
|
+
"github-copilot-cli": join31(cwd, ".github", "hooks", "tamperward.json")
|
|
23791
24112
|
};
|
|
23792
24113
|
const file = files[canonicalId];
|
|
23793
24114
|
if (!file || !existsSync21(file)) return null;
|
|
@@ -24057,6 +24378,352 @@ var init_runtime = __esm({
|
|
|
24057
24378
|
}
|
|
24058
24379
|
});
|
|
24059
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
|
+
|
|
24060
24727
|
// src/cli/signoff-label.ts
|
|
24061
24728
|
function runSignoffLabel(opts) {
|
|
24062
24729
|
if (!opts.rule) {
|
|
@@ -24514,6 +25181,29 @@ function validateCliArgs(cmd, args) {
|
|
|
24514
25181
|
}
|
|
24515
25182
|
return `unknown research subcommand "${sub}" (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
24516
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
|
+
}
|
|
24517
25207
|
return void 0;
|
|
24518
25208
|
}
|
|
24519
25209
|
function printHelp() {
|
|
@@ -24649,6 +25339,23 @@ Formats:
|
|
|
24649
25339
|
emits the versioned status document for
|
|
24650
25340
|
editors, CI and dashboards. Local CURRENT
|
|
24651
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.
|
|
24652
25359
|
tamperward runtime verify [--runtime ID] qualify the in-loop runtime: an operation-
|
|
24653
25360
|
[--mode headless|interactive] specific capability matrix (pre-deny:shell,
|
|
24654
25361
|
[--model M] [--json] [--cwd D] native-edit, mcp, end-of-turn, transport:*,
|
|
@@ -24739,6 +25446,8 @@ function main(argv) {
|
|
|
24739
25446
|
const { sub, opts } = parseRuntime(rest);
|
|
24740
25447
|
return runRuntime(sub, opts);
|
|
24741
25448
|
}
|
|
25449
|
+
case "receipt":
|
|
25450
|
+
return runReceipt(rest);
|
|
24742
25451
|
case "run":
|
|
24743
25452
|
return runEnvelope({
|
|
24744
25453
|
...parseRun(rest),
|
|
@@ -24790,6 +25499,7 @@ var init_main = __esm({
|
|
|
24790
25499
|
init_onboard();
|
|
24791
25500
|
init_research();
|
|
24792
25501
|
init_runtime();
|
|
25502
|
+
init_receipt();
|
|
24793
25503
|
init_audit();
|
|
24794
25504
|
init_signoff_label();
|
|
24795
25505
|
}
|
|
@@ -24798,7 +25508,7 @@ var init_main = __esm({
|
|
|
24798
25508
|
// src/cli/index.ts
|
|
24799
25509
|
init_hook_client();
|
|
24800
25510
|
init_exit();
|
|
24801
|
-
import { readFileSync as
|
|
25511
|
+
import { readFileSync as readFileSync30 } from "node:fs";
|
|
24802
25512
|
function loadMain() {
|
|
24803
25513
|
return Promise.resolve().then(() => (init_main(), main_exports));
|
|
24804
25514
|
}
|
|
@@ -24811,7 +25521,7 @@ async function launch(argv) {
|
|
|
24811
25521
|
if (kind && hookServiceEnabled()) {
|
|
24812
25522
|
let raw = null;
|
|
24813
25523
|
try {
|
|
24814
|
-
raw =
|
|
25524
|
+
raw = readFileSync30(0, "utf8");
|
|
24815
25525
|
} catch {
|
|
24816
25526
|
raw = null;
|
|
24817
25527
|
}
|