tamperward 2.33.0 → 2.36.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 +1651 -417
- package/package.json +2 -2
- package/schemas/audit-v1.schema.json +14 -1
- package/schemas/check-v1.schema.json +14 -2
- package/schemas/doctor-v1.schema.json +15 -1
- package/schemas/research-stdio-v1.schema.json +67 -0
- package/schemas/research-v1.schema.json +92 -2
- package/schemas/run-v1.schema.json +35 -2
- package/schemas/stats-v1.schema.json +16 -2
- package/schemas/status-v1.schema.json +205 -0
- package/schemas/verify-v1.schema.json +19 -2
package/dist/cli/index.js
CHANGED
|
@@ -211,7 +211,7 @@ function forwardedEnv(env) {
|
|
|
211
211
|
return out3;
|
|
212
212
|
}
|
|
213
213
|
function exchange(paths, req2, timeoutMs) {
|
|
214
|
-
return new Promise((
|
|
214
|
+
return new Promise((resolve20) => {
|
|
215
215
|
let done = false;
|
|
216
216
|
let handedOff = false;
|
|
217
217
|
let accepted = false;
|
|
@@ -222,7 +222,7 @@ function exchange(paths, req2, timeoutMs) {
|
|
|
222
222
|
done = true;
|
|
223
223
|
clearTimeout(timer);
|
|
224
224
|
sock.destroy();
|
|
225
|
-
|
|
225
|
+
resolve20(value);
|
|
226
226
|
};
|
|
227
227
|
const sock = createConnection(paths.socket);
|
|
228
228
|
const timer = setTimeout(() => finish(handedOff ? acceptedFailure("timeout") : null), timeoutMs);
|
|
@@ -686,9 +686,9 @@ function readRegular(abs, st) {
|
|
|
686
686
|
const buf = Buffer.allocUnsafe(now.size);
|
|
687
687
|
let off2 = 0;
|
|
688
688
|
while (off2 < now.size) {
|
|
689
|
-
const
|
|
690
|
-
if (
|
|
691
|
-
off2 +=
|
|
689
|
+
const n2 = readSync(fd, buf, off2, now.size - off2, off2);
|
|
690
|
+
if (n2 === 0) break;
|
|
691
|
+
off2 += n2;
|
|
692
692
|
}
|
|
693
693
|
return { kind: "file", content: off2 === now.size ? buf : buf.subarray(0, off2), mode: now.mode, size: off2, mtimeMs: now.mtimeMs, detail: "" };
|
|
694
694
|
} catch (e) {
|
|
@@ -1520,8 +1520,8 @@ var init_changes = __esm({
|
|
|
1520
1520
|
init_narrow();
|
|
1521
1521
|
num = (name, fallback) => {
|
|
1522
1522
|
const raw = process.env[name];
|
|
1523
|
-
const
|
|
1524
|
-
return Number.isFinite(
|
|
1523
|
+
const n2 = raw === void 0 ? NaN : Number(raw);
|
|
1524
|
+
return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
|
|
1525
1525
|
};
|
|
1526
1526
|
RECONSTRUCT_TIMEOUT_MS = () => num("TAMPERWARD_RECONSTRUCT_TIMEOUT_MS", 5e3);
|
|
1527
1527
|
RECONSTRUCT_MAXBUFFER = () => num("TAMPERWARD_RECONSTRUCT_MAXBUFFER", 32 * 1024 * 1024);
|
|
@@ -2190,8 +2190,8 @@ function literal(arg, vars) {
|
|
|
2190
2190
|
const a = unq(arg.trim());
|
|
2191
2191
|
if (/[`]|\$\(/.test(a)) return null;
|
|
2192
2192
|
let unknown = false;
|
|
2193
|
-
const r = a.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (_,
|
|
2194
|
-
const v = vars.get(
|
|
2193
|
+
const r = a.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (_, n2) => {
|
|
2194
|
+
const v = vars.get(n2);
|
|
2195
2195
|
if (v === void 0) unknown = true;
|
|
2196
2196
|
return v ?? "";
|
|
2197
2197
|
});
|
|
@@ -2389,7 +2389,7 @@ function walk(lines, st, topIdx, depth) {
|
|
|
2389
2389
|
why = "runs from another directory after a `cd` away from the repository root";
|
|
2390
2390
|
}
|
|
2391
2391
|
if (state === "live" && /^tamperward\b/.test(id)) {
|
|
2392
|
-
const onGate = assignedNames(prefix).filter((
|
|
2392
|
+
const onGate = assignedNames(prefix).filter((n2) => SENSITIVE_ENV.test(n2));
|
|
2393
2393
|
if (onGate.length) {
|
|
2394
2394
|
state = "neutered";
|
|
2395
2395
|
why = `runtime redirected \u2014 \`${onGate[0]}=\` in front of it decides what the gate resolves to`;
|
|
@@ -2829,7 +2829,7 @@ function setfaclDropsExec(toks) {
|
|
|
2829
2829
|
}
|
|
2830
2830
|
function nameGlob(pattern) {
|
|
2831
2831
|
const re = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
|
|
2832
|
-
return (
|
|
2832
|
+
return (n2) => re.test(n2);
|
|
2833
2833
|
}
|
|
2834
2834
|
function destinations(cmd, args, isDir) {
|
|
2835
2835
|
const positional = [];
|
|
@@ -2994,9 +2994,9 @@ function shellWritesHook(seg, toks, policy, ctx) {
|
|
|
2994
2994
|
if (/-exec(?:dir)?\s+chmod\b/.test(seg) && !chmodDropsExec(toks)) return null;
|
|
2995
2995
|
const dirs = positional.filter((p) => !/^-/.test(p) && !["{}", ";", "+", "rm", "chmod", "unlink", "truncate", "mv", "sed", "tee", "shred", "inplace"].includes(p));
|
|
2996
2996
|
const names = args.filter((a, i) => /^-i?(?:name|path|regex)$/.test(args[i - 1] ?? ""));
|
|
2997
|
-
const nameHits = names.length === 0 || names.some((
|
|
2997
|
+
const nameHits = names.length === 0 || names.some((n2) => {
|
|
2998
2998
|
try {
|
|
2999
|
-
const g = nameGlob(
|
|
2999
|
+
const g = nameGlob(n2.replace(/^.*\//, ""));
|
|
3000
3000
|
return HOOK_BASENAMES.some(g);
|
|
3001
3001
|
} catch {
|
|
3002
3002
|
return true;
|
|
@@ -3757,11 +3757,11 @@ function buildAliasMap(sf) {
|
|
|
3757
3757
|
ts.forEachChild(node, collect2);
|
|
3758
3758
|
};
|
|
3759
3759
|
collect2(sf);
|
|
3760
|
-
const kindOfBinding = (b, seen) => b.type ? kindOfType(b.type, (
|
|
3760
|
+
const kindOfBinding = (b, seen) => b.type ? kindOfType(b.type, (n2) => resolveLexical(n2, b.type, seen)) : null;
|
|
3761
3761
|
const resolveLexical = (name, at, seen) => {
|
|
3762
3762
|
if (seen.has(name)) return null;
|
|
3763
|
-
for (let
|
|
3764
|
-
const here = bindings.filter((x) => x.name === name && x.scope ===
|
|
3763
|
+
for (let n2 = at; n2; n2 = n2.parent) {
|
|
3764
|
+
const here = bindings.filter((x) => x.name === name && x.scope === n2);
|
|
3765
3765
|
if (here.length === 0) continue;
|
|
3766
3766
|
if (here.some((b) => b.kind !== "alias" || !b.type)) return null;
|
|
3767
3767
|
return kindOfBinding(here[0], new Set(seen).add(name));
|
|
@@ -3781,7 +3781,7 @@ function buildAliasMap(sf) {
|
|
|
3781
3781
|
};
|
|
3782
3782
|
}
|
|
3783
3783
|
function assertedLaunderKind(node, aliases) {
|
|
3784
|
-
return kindOfType(assertedType(node), (
|
|
3784
|
+
return kindOfType(assertedType(node), (n2) => aliases.resolve(n2, node));
|
|
3785
3785
|
}
|
|
3786
3786
|
function isDoubleCast(node, aliases) {
|
|
3787
3787
|
const inner = unparenthesized(node.expression);
|
|
@@ -3825,11 +3825,11 @@ function surfaceOf(path, src) {
|
|
|
3825
3825
|
return surface;
|
|
3826
3826
|
}
|
|
3827
3827
|
function unmatchedAfter(before, after) {
|
|
3828
|
-
const
|
|
3828
|
+
const n2 = before.length;
|
|
3829
3829
|
const m = after.length;
|
|
3830
3830
|
const same = (i2, j2) => before[i2].lineText === after[j2].lineText && before[i2].text === after[j2].text;
|
|
3831
|
-
const lcs = Array.from({ length:
|
|
3832
|
-
for (let i2 =
|
|
3831
|
+
const lcs = Array.from({ length: n2 + 1 }, () => new Array(m + 1).fill(0));
|
|
3832
|
+
for (let i2 = n2 - 1; i2 >= 0; i2--) {
|
|
3833
3833
|
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
3834
3834
|
lcs[i2][j2] = same(i2, j2) ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
3835
3835
|
}
|
|
@@ -3838,10 +3838,10 @@ function unmatchedAfter(before, after) {
|
|
|
3838
3838
|
let i = 0;
|
|
3839
3839
|
let j = 0;
|
|
3840
3840
|
while (j < m) {
|
|
3841
|
-
if (i <
|
|
3841
|
+
if (i < n2 && same(i, j)) {
|
|
3842
3842
|
i++;
|
|
3843
3843
|
j++;
|
|
3844
|
-
} else if (i <
|
|
3844
|
+
} else if (i < n2 && lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
3845
3845
|
i++;
|
|
3846
3846
|
} else {
|
|
3847
3847
|
out3.push(after[j]);
|
|
@@ -3917,7 +3917,7 @@ function commentIsLiveDirective(text) {
|
|
|
3917
3917
|
return false;
|
|
3918
3918
|
}
|
|
3919
3919
|
function countLiveDirectives(src) {
|
|
3920
|
-
let
|
|
3920
|
+
let n2 = 0;
|
|
3921
3921
|
try {
|
|
3922
3922
|
const scanner = ts.createScanner(
|
|
3923
3923
|
ts.ScriptTarget.Latest,
|
|
@@ -3929,13 +3929,13 @@ function countLiveDirectives(src) {
|
|
|
3929
3929
|
let tok = scanner.scan();
|
|
3930
3930
|
while (tok !== ts.SyntaxKind.EndOfFileToken) {
|
|
3931
3931
|
if (tok === ts.SyntaxKind.SingleLineCommentTrivia || tok === ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
3932
|
-
if (commentIsLiveDirective(scanner.getTokenText()))
|
|
3932
|
+
if (commentIsLiveDirective(scanner.getTokenText())) n2++;
|
|
3933
3933
|
}
|
|
3934
3934
|
tok = scanner.scan();
|
|
3935
3935
|
}
|
|
3936
3936
|
} catch {
|
|
3937
3937
|
}
|
|
3938
|
-
return
|
|
3938
|
+
return n2;
|
|
3939
3939
|
}
|
|
3940
3940
|
function countAny(src, path) {
|
|
3941
3941
|
const r = { cast: 0, never: 0, broad: 0, double: 0, suppress: 0, jsdoc: 0 };
|
|
@@ -3976,16 +3976,16 @@ function lineHasRow4Cast(line, aliasMap) {
|
|
|
3976
3976
|
const sf = parseSource("line.ts", line);
|
|
3977
3977
|
if (!sf) return false;
|
|
3978
3978
|
let found = false;
|
|
3979
|
-
const visit = (
|
|
3979
|
+
const visit = (n2) => {
|
|
3980
3980
|
if (found) return;
|
|
3981
|
-
if (ts.isAsExpression(
|
|
3982
|
-
const k = assertedLaunderKind(
|
|
3983
|
-
if (k === "any" || k === "never" || isDoubleCast(
|
|
3981
|
+
if (ts.isAsExpression(n2) || ts.isTypeAssertionExpression(n2)) {
|
|
3982
|
+
const k = assertedLaunderKind(n2, aliasMap);
|
|
3983
|
+
if (k === "any" || k === "never" || isDoubleCast(n2, aliasMap)) {
|
|
3984
3984
|
found = true;
|
|
3985
3985
|
return;
|
|
3986
3986
|
}
|
|
3987
3987
|
}
|
|
3988
|
-
ts.forEachChild(
|
|
3988
|
+
ts.forEachChild(n2, visit);
|
|
3989
3989
|
};
|
|
3990
3990
|
visit(sf);
|
|
3991
3991
|
return found;
|
|
@@ -5175,8 +5175,8 @@ function numericOf(e) {
|
|
|
5175
5175
|
}
|
|
5176
5176
|
return void 0;
|
|
5177
5177
|
}
|
|
5178
|
-
function metricsOf(expr,
|
|
5179
|
-
const obj =
|
|
5178
|
+
function metricsOf(expr, resolve20) {
|
|
5179
|
+
const obj = resolve20(expr);
|
|
5180
5180
|
if (!ts.isObjectLiteralExpression(obj)) return void 0;
|
|
5181
5181
|
const out3 = { values: {}, opaque: false };
|
|
5182
5182
|
let any = false;
|
|
@@ -5190,9 +5190,9 @@ function metricsOf(expr, resolve17) {
|
|
|
5190
5190
|
const k = keyName(p.name);
|
|
5191
5191
|
if (k === null || !isMetric(k)) continue;
|
|
5192
5192
|
any = true;
|
|
5193
|
-
const
|
|
5194
|
-
if (
|
|
5195
|
-
else out3.values[k] =
|
|
5193
|
+
const n2 = numericOf(resolve20(p.initializer));
|
|
5194
|
+
if (n2 === void 0) out3.opaque = true;
|
|
5195
|
+
else out3.values[k] = n2;
|
|
5196
5196
|
}
|
|
5197
5197
|
return any ? out3 : void 0;
|
|
5198
5198
|
}
|
|
@@ -5237,17 +5237,17 @@ function parseThresholds(src) {
|
|
|
5237
5237
|
try {
|
|
5238
5238
|
const sf = sourceOf(src);
|
|
5239
5239
|
if (!sf) return res;
|
|
5240
|
-
const
|
|
5240
|
+
const resolve20 = resolverFor(sf);
|
|
5241
5241
|
const visit = (node) => {
|
|
5242
5242
|
if (ts.isPropertyAssignment(node)) {
|
|
5243
5243
|
const key2 = keyName(node.name);
|
|
5244
5244
|
if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
|
|
5245
5245
|
res.present = true;
|
|
5246
|
-
const init =
|
|
5246
|
+
const init = resolve20(node.initializer);
|
|
5247
5247
|
if (!ts.isObjectLiteralExpression(init)) {
|
|
5248
5248
|
res.opaque = true;
|
|
5249
5249
|
} else {
|
|
5250
|
-
const flat = metricsOf(init,
|
|
5250
|
+
const flat = metricsOf(init, resolve20);
|
|
5251
5251
|
if (flat) res.global = merge(res.global, flat);
|
|
5252
5252
|
for (const p of init.properties) {
|
|
5253
5253
|
if (ts.isSpreadAssignment(p)) {
|
|
@@ -5261,7 +5261,7 @@ function parseThresholds(src) {
|
|
|
5261
5261
|
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
5262
5262
|
continue;
|
|
5263
5263
|
}
|
|
5264
|
-
const m = metricsOf(p.initializer,
|
|
5264
|
+
const m = metricsOf(p.initializer, resolve20);
|
|
5265
5265
|
if (!m) continue;
|
|
5266
5266
|
if (k === "global") res.global = merge(res.global, m);
|
|
5267
5267
|
else res.paths.set(norm(k), m);
|
|
@@ -5295,18 +5295,18 @@ function parseLists(src) {
|
|
|
5295
5295
|
try {
|
|
5296
5296
|
const sf = sourceOf(src);
|
|
5297
5297
|
if (!sf) return res;
|
|
5298
|
-
const
|
|
5298
|
+
const resolve20 = resolverFor(sf);
|
|
5299
5299
|
const visit = (node) => {
|
|
5300
5300
|
if (ts.isPropertyAssignment(node)) {
|
|
5301
5301
|
const k = keyName(node.name);
|
|
5302
5302
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
5303
5303
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
5304
|
-
const { items, opaque } = literals(
|
|
5304
|
+
const { items, opaque } = literals(resolve20(node.initializer));
|
|
5305
5305
|
res.collectFrom = items;
|
|
5306
5306
|
res.collectKey = k === "include" ? "coverage.include" : k;
|
|
5307
5307
|
if (opaque) res.opaqueCollect = true;
|
|
5308
5308
|
} else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
|
|
5309
|
-
const { items, opaque } = literals(
|
|
5309
|
+
const { items, opaque } = literals(resolve20(node.initializer));
|
|
5310
5310
|
(k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
|
|
5311
5311
|
if (opaque) res.opaqueIgnore = true;
|
|
5312
5312
|
}
|
|
@@ -5491,12 +5491,12 @@ function simpleWeakeningsOf(spec2, before, after) {
|
|
|
5491
5491
|
for (const k of spec2.keys) {
|
|
5492
5492
|
const bAll = numbersOf(before, k.key, spec2.scoped);
|
|
5493
5493
|
const aAll = numbersOf(after, k.key, spec2.scoped);
|
|
5494
|
-
const scopes = [...new Set(bAll.map((
|
|
5494
|
+
const scopes = [...new Set(bAll.map((n2) => n2.scope))];
|
|
5495
5495
|
for (const scope of scopes) {
|
|
5496
5496
|
const label = scope ? `${scope} ${k.label}` : k.label;
|
|
5497
5497
|
const desc = (x, y2) => x === "auto" ? -1 : y2 === "auto" ? 1 : lessThan(x, y2) ? 1 : lessThan(y2, x) ? -1 : 0;
|
|
5498
|
-
const b = bAll.filter((
|
|
5499
|
-
const a = aAll.filter((
|
|
5498
|
+
const b = bAll.filter((n2) => n2.scope === scope).map((n2) => n2.v).sort(desc);
|
|
5499
|
+
const a = aAll.filter((n2) => n2.scope === scope).map((n2) => n2.v).sort(desc);
|
|
5500
5500
|
if (b.length === 0) continue;
|
|
5501
5501
|
if (a.length === 0) {
|
|
5502
5502
|
if (spec2.removable) out3.push(`${label} removed (was ${b.join(", ")})`);
|
|
@@ -5598,10 +5598,10 @@ function gateMovedElsewhere(c, changes, policy) {
|
|
|
5598
5598
|
const spec2 = SIMPLE_FILES.find((s) => s.file.test(c.path));
|
|
5599
5599
|
if (!spec2) return false;
|
|
5600
5600
|
return spec2.keys.every((k) => {
|
|
5601
|
-
const b = numbersOf(before, k.key, spec2.scoped).map((
|
|
5601
|
+
const b = numbersOf(before, k.key, spec2.scoped).map((n2) => n2.v);
|
|
5602
5602
|
if (b.length === 0) return true;
|
|
5603
5603
|
return others.some((o) => {
|
|
5604
|
-
const a = numbersOf(o.after, k.key, spec2.scoped).map((
|
|
5604
|
+
const a = numbersOf(o.after, k.key, spec2.scoped).map((n2) => n2.v);
|
|
5605
5605
|
return a.length > 0 && b.every((bv) => a.some((av) => k.weakens === "lower" ? !lessThan(av, bv) : !lessThan(bv, av)));
|
|
5606
5606
|
});
|
|
5607
5607
|
});
|
|
@@ -5959,7 +5959,7 @@ function effectivePytestConfig(path, ctx) {
|
|
|
5959
5959
|
const files = ctx?.trackedFiles;
|
|
5960
5960
|
if (!files) return true;
|
|
5961
5961
|
const rootNames = new Set(files.filter((f) => !f.includes("/")));
|
|
5962
|
-
return !PYTEST_INI_ORDER.slice(0, rank).some((
|
|
5962
|
+
return !PYTEST_INI_ORDER.slice(0, rank).some((n2) => rootNames.has(n2));
|
|
5963
5963
|
}
|
|
5964
5964
|
function pytestBlock(src) {
|
|
5965
5965
|
const lines = src.split(/\r?\n/);
|
|
@@ -6633,7 +6633,7 @@ function eslintAstFacts(text, f) {
|
|
|
6633
6633
|
}
|
|
6634
6634
|
if (!sf) return;
|
|
6635
6635
|
const isStr2 = (e) => ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e);
|
|
6636
|
-
const keyText = (
|
|
6636
|
+
const keyText = (n2) => ts.isIdentifier(n2) || isStr2(n2) || ts.isNumericLiteral(n2) ? n2.text : null;
|
|
6637
6637
|
const literal2 = (e) => {
|
|
6638
6638
|
if (isStr2(e)) return e.text;
|
|
6639
6639
|
if (ts.isNumericLiteral(e)) return Number(e.text);
|
|
@@ -7268,8 +7268,8 @@ function scriptKind3(inner) {
|
|
|
7268
7268
|
function runnerOfSegment(canon) {
|
|
7269
7269
|
const m = canon.match(RUNNER_NAME);
|
|
7270
7270
|
if (!m) return null;
|
|
7271
|
-
const
|
|
7272
|
-
return
|
|
7271
|
+
const n2 = m[1];
|
|
7272
|
+
return n2 === "jest" || n2 === "vitest" || n2 === "mocha" || n2 === "pytest" || n2 === "go" || n2 === "cargo" ? n2 : "node";
|
|
7273
7273
|
}
|
|
7274
7274
|
function statements(raw) {
|
|
7275
7275
|
const out3 = [];
|
|
@@ -7803,7 +7803,7 @@ var init_ci_tampering = __esm({
|
|
|
7803
7803
|
}
|
|
7804
7804
|
if (!isProtected(c.path, policy, "ci")) continue;
|
|
7805
7805
|
const afterLines = c.after != null ? c.after.split("\n") : null;
|
|
7806
|
-
const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((
|
|
7806
|
+
const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n2) => n2 != null));
|
|
7807
7807
|
const github = isGithubFile(c.path);
|
|
7808
7808
|
for (const l of github ? addedLines(c) : []) {
|
|
7809
7809
|
const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
|
|
@@ -7844,13 +7844,13 @@ var init_ci_tampering = __esm({
|
|
|
7844
7844
|
for (let k = 0; k < b.body.length; k++) {
|
|
7845
7845
|
const idx = b.start + 1 + k;
|
|
7846
7846
|
if (!addedAt.has(idx + 1)) continue;
|
|
7847
|
-
const
|
|
7848
|
-
if (!
|
|
7847
|
+
const n2 = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
|
|
7848
|
+
if (!n2 || propagates && /set\s\+e/.test(b.body[k])) continue;
|
|
7849
7849
|
out3.push(
|
|
7850
7850
|
makeFinding(RULE8, policy, {
|
|
7851
7851
|
file: c.path,
|
|
7852
7852
|
line: idx + 1,
|
|
7853
|
-
message: `A check's run block was neutralised: ${
|
|
7853
|
+
message: `A check's run block was neutralised: ${n2.what}.`,
|
|
7854
7854
|
evidence: b.body[k].trim(),
|
|
7855
7855
|
remediation: "Let the check decide the step. Masking its exit status is the same tamper as removing it."
|
|
7856
7856
|
})
|
|
@@ -8599,13 +8599,13 @@ function renameKept(c) {
|
|
|
8599
8599
|
}
|
|
8600
8600
|
function lefthookBase(c, changes, ctx) {
|
|
8601
8601
|
const dir = c.path.includes("/") ? c.path.slice(0, c.path.lastIndexOf("/") + 1) : "";
|
|
8602
|
-
const names = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"].map((
|
|
8602
|
+
const names = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"].map((n2) => dir + n2);
|
|
8603
8603
|
const inSet = changes.find((o) => o.kind === "file" && names.includes(o.path));
|
|
8604
8604
|
if (inSet) return inSet.after != null ? parseDoc(inSet.after) : null;
|
|
8605
8605
|
if (!ctx?.cwd) return null;
|
|
8606
|
-
for (const
|
|
8606
|
+
for (const n2 of names) {
|
|
8607
8607
|
try {
|
|
8608
|
-
const p = join7(ctx.cwd,
|
|
8608
|
+
const p = join7(ctx.cwd, n2);
|
|
8609
8609
|
if (existsSync4(p)) return parseDoc(readFileSync4(p, "utf8"));
|
|
8610
8610
|
} catch {
|
|
8611
8611
|
}
|
|
@@ -9096,8 +9096,8 @@ function isSpecShaped(path, before, after) {
|
|
|
9096
9096
|
if (before === null && after === null) return true;
|
|
9097
9097
|
for (const src of [before, after]) {
|
|
9098
9098
|
if (src === null) continue;
|
|
9099
|
-
const
|
|
9100
|
-
if (
|
|
9099
|
+
const n2 = countTests(src, path);
|
|
9100
|
+
if (n2.min > 0 || n2.open) return true;
|
|
9101
9101
|
}
|
|
9102
9102
|
return false;
|
|
9103
9103
|
}
|
|
@@ -9253,7 +9253,7 @@ function unreachableNodes(sf) {
|
|
|
9253
9253
|
collect2(sf);
|
|
9254
9254
|
if (candidates.length === 0) return dead;
|
|
9255
9255
|
const inDead = (node) => {
|
|
9256
|
-
for (let
|
|
9256
|
+
for (let n2 = node; n2; n2 = n2.parent) if (dead.has(n2)) return true;
|
|
9257
9257
|
return false;
|
|
9258
9258
|
};
|
|
9259
9259
|
const within2 = (node, owner) => node.pos >= owner.pos && node.end <= owner.end;
|
|
@@ -9302,7 +9302,7 @@ function partitionJs(src) {
|
|
|
9302
9302
|
if (!sf) return null;
|
|
9303
9303
|
const dead = unreachableNodes(sf);
|
|
9304
9304
|
const deadRanges = [];
|
|
9305
|
-
for (const
|
|
9305
|
+
for (const n2 of dead) deadRanges.push([n2.getStart(sf), n2.getEnd()]);
|
|
9306
9306
|
const literals3 = literalInteriors(sf);
|
|
9307
9307
|
const mask = new Uint8Array(src.length);
|
|
9308
9308
|
for (const [s, e] of deadRanges) for (let i = s; i < e; i++) mask[i] = 1;
|
|
@@ -9361,7 +9361,7 @@ function partition(src, path) {
|
|
|
9361
9361
|
return { live: src, hidden: "", inLiteral: new Uint8Array(src.length) };
|
|
9362
9362
|
}
|
|
9363
9363
|
function pytestCollectedDefs(src) {
|
|
9364
|
-
let
|
|
9364
|
+
let n2 = 0;
|
|
9365
9365
|
const stack = [];
|
|
9366
9366
|
for (const line of src.split("\n")) {
|
|
9367
9367
|
if (!/\S/.test(line) || /^\s*#/.test(line)) continue;
|
|
@@ -9372,9 +9372,9 @@ function pytestCollectedDefs(src) {
|
|
|
9372
9372
|
stack.push({ indent, collected: PY_COLLECTED_CLASS(cls[1], cls[2] ?? "") });
|
|
9373
9373
|
continue;
|
|
9374
9374
|
}
|
|
9375
|
-
if (PY_TEST_DEF.test(line) && stack.every((c) => c.collected))
|
|
9375
|
+
if (PY_TEST_DEF.test(line) && stack.every((c) => c.collected)) n2++;
|
|
9376
9376
|
}
|
|
9377
|
-
return
|
|
9377
|
+
return n2;
|
|
9378
9378
|
}
|
|
9379
9379
|
var MAX_ROUNDS, blankRange, PY_INDENT, PY_TERMINAL, PY_CONST_FALSE, PY_CLASS, PY_TEST_DEF, PY_COLLECTED_CLASS;
|
|
9380
9380
|
var init_reachability = __esm({
|
|
@@ -9407,13 +9407,13 @@ function calleeName(expr) {
|
|
|
9407
9407
|
}
|
|
9408
9408
|
function loopRows(iterable) {
|
|
9409
9409
|
if (ts.isArrayLiteralExpression(iterable)) {
|
|
9410
|
-
let
|
|
9410
|
+
let n2 = 0;
|
|
9411
9411
|
let open = false;
|
|
9412
9412
|
for (const el of iterable.elements) {
|
|
9413
9413
|
if (ts.isSpreadElement(el)) open = true;
|
|
9414
|
-
else
|
|
9414
|
+
else n2++;
|
|
9415
9415
|
}
|
|
9416
|
-
return { n: Math.max(
|
|
9416
|
+
return { n: Math.max(n2, open ? 0 : 1), open };
|
|
9417
9417
|
}
|
|
9418
9418
|
return { n: 1, open: true };
|
|
9419
9419
|
}
|
|
@@ -9432,13 +9432,13 @@ function eachRows(call) {
|
|
|
9432
9432
|
const arg = call.arguments[0];
|
|
9433
9433
|
if (!arg) return { n: 1, open: true };
|
|
9434
9434
|
if (ts.isArrayLiteralExpression(arg)) {
|
|
9435
|
-
let
|
|
9435
|
+
let n2 = 0;
|
|
9436
9436
|
let open = false;
|
|
9437
9437
|
for (const el of arg.elements) {
|
|
9438
9438
|
if (ts.isSpreadElement(el)) open = true;
|
|
9439
|
-
else
|
|
9439
|
+
else n2++;
|
|
9440
9440
|
}
|
|
9441
|
-
return { n: Math.max(
|
|
9441
|
+
return { n: Math.max(n2, open ? 0 : 1), open };
|
|
9442
9442
|
}
|
|
9443
9443
|
return { n: 1, open: true };
|
|
9444
9444
|
}
|
|
@@ -9447,13 +9447,13 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
9447
9447
|
if (lang === "py") return { min: pytestCollectedDefs(src), open: false };
|
|
9448
9448
|
if (lang && lang !== "js") {
|
|
9449
9449
|
const re = TEST_DEFS[lang];
|
|
9450
|
-
let
|
|
9451
|
-
for (const line of src.split("\n")) if (re.test(line))
|
|
9452
|
-
return { min:
|
|
9450
|
+
let n3 = 0;
|
|
9451
|
+
for (const line of src.split("\n")) if (re.test(line)) n3++;
|
|
9452
|
+
return { min: n3, open: false };
|
|
9453
9453
|
}
|
|
9454
9454
|
const sf = parseSource("spec.ts", src);
|
|
9455
9455
|
if (!sf) return { min: 0, open: true };
|
|
9456
|
-
let
|
|
9456
|
+
let n2 = 0;
|
|
9457
9457
|
let open = false;
|
|
9458
9458
|
const dead = unreachableNodes(sf);
|
|
9459
9459
|
const visit = (node, mult) => {
|
|
@@ -9488,19 +9488,19 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
9488
9488
|
}
|
|
9489
9489
|
if (isEachOf(node.expression)) {
|
|
9490
9490
|
const r = eachRows(node);
|
|
9491
|
-
|
|
9491
|
+
n2 += r.n * mult;
|
|
9492
9492
|
if (r.open) open = true;
|
|
9493
9493
|
} else {
|
|
9494
9494
|
const name = calleeName(node.expression);
|
|
9495
|
-
if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node)))
|
|
9495
|
+
if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n2 += mult;
|
|
9496
9496
|
}
|
|
9497
9497
|
} else if (ts.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
|
|
9498
|
-
|
|
9498
|
+
n2 += templateRows(node) * mult;
|
|
9499
9499
|
}
|
|
9500
9500
|
ts.forEachChild(node, (c) => visit(c, mult));
|
|
9501
9501
|
};
|
|
9502
9502
|
visit(sf, 1);
|
|
9503
|
-
return { min:
|
|
9503
|
+
return { min: n2, open };
|
|
9504
9504
|
}
|
|
9505
9505
|
function countTestBlocks(src, path = "spec.ts") {
|
|
9506
9506
|
return countTests(src, path).min;
|
|
@@ -9546,7 +9546,7 @@ function scriptWeakenings(before, after, path, policy, addedPaths) {
|
|
|
9546
9546
|
const f = dir + file.replace(/^\.\//, "");
|
|
9547
9547
|
return isProtected(f, policy, "config") && !addedPaths.has(f);
|
|
9548
9548
|
};
|
|
9549
|
-
const relocated = (kind) => Object.keys(now).some((
|
|
9549
|
+
const relocated = (kind) => Object.keys(now).some((n2) => !(n2 in was) && checkKinds(now[n2]).has(kind));
|
|
9550
9550
|
const out3 = [];
|
|
9551
9551
|
for (const name of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])) {
|
|
9552
9552
|
const whole = WHOLE_SUITE_SCRIPT.test(name);
|
|
@@ -9618,10 +9618,10 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
9618
9618
|
if (!was || !now || was.path === now.path) return [];
|
|
9619
9619
|
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
9620
9620
|
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
9621
|
-
(
|
|
9621
|
+
(n2) => makeFinding(RULE10, policy, {
|
|
9622
9622
|
file: now.path,
|
|
9623
|
-
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${
|
|
9624
|
-
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${
|
|
9623
|
+
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n2.path} is no longer selected: ${n2.reason}.`,
|
|
9624
|
+
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n2.path} from the suite`,
|
|
9625
9625
|
remediation: "pytest reads ONE config file. Adding a higher-precedence one, or deleting the one in force, changes which settings apply \u2014 keep the effective configuration selecting the whole suite."
|
|
9626
9626
|
})
|
|
9627
9627
|
);
|
|
@@ -9755,28 +9755,28 @@ var init_test_deletion = __esm({
|
|
|
9755
9755
|
})
|
|
9756
9756
|
);
|
|
9757
9757
|
}
|
|
9758
|
-
for (const
|
|
9758
|
+
for (const n2 of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
9759
9759
|
out3.push(
|
|
9760
9760
|
makeFinding(RULE10, policy, {
|
|
9761
9761
|
file: c.path,
|
|
9762
|
-
message: `The runner no longer selects ${
|
|
9763
|
-
evidence: `${
|
|
9762
|
+
message: `The runner no longer selects ${n2.path}: ${n2.reason}.`,
|
|
9763
|
+
evidence: `${n2.path} dropped from the suite by ${c.path}`,
|
|
9764
9764
|
remediation: "Keep the runner pointed at the whole suite. Excluding a spec from the runner removes its tests without touching the file."
|
|
9765
9765
|
})
|
|
9766
9766
|
);
|
|
9767
9767
|
}
|
|
9768
|
-
for (const
|
|
9769
|
-
const w =
|
|
9770
|
-
const evidence = `"${
|
|
9768
|
+
for (const n2 of scriptWeakenings(c.before, c.after, c.path, policy, addedPaths)) {
|
|
9769
|
+
const w = n2.weakening;
|
|
9770
|
+
const evidence = `"${n2.script}": ${JSON.stringify(n2.before)} \u2192 ${n2.after === "" ? "deleted" : JSON.stringify(n2.after)}`;
|
|
9771
9771
|
out3.push(
|
|
9772
9772
|
w.state === "removed" ? makeFinding(RULE10, policy, {
|
|
9773
9773
|
file: c.path,
|
|
9774
|
-
message: `The test script no longer runs the ${w.kind} check: scripts.${
|
|
9774
|
+
message: `The test script no longer runs the ${w.kind} check: scripts.${n2.script} ${w.what}.`,
|
|
9775
9775
|
evidence,
|
|
9776
9776
|
remediation: "Keep the script running its check. Replacing the runner in package.json removes every test from every `npm test` while the workflow line stays untouched."
|
|
9777
9777
|
}) : makeFinding(RULE10, policy, {
|
|
9778
9778
|
file: c.path,
|
|
9779
|
-
message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${
|
|
9779
|
+
message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n2.script}.`,
|
|
9780
9780
|
evidence,
|
|
9781
9781
|
remediation: "Keep the test script running the whole suite and letting it decide; a masked status, a cut-short run or a runner flag that selects fewer specs removes the rest from every run."
|
|
9782
9782
|
})
|
|
@@ -10371,13 +10371,13 @@ function pureRemoval(before, after) {
|
|
|
10371
10371
|
const remaining = /* @__PURE__ */ new Map();
|
|
10372
10372
|
for (const a of before) remaining.set(a.canonical, (remaining.get(a.canonical) ?? 0) + 1);
|
|
10373
10373
|
for (const a of after) {
|
|
10374
|
-
const
|
|
10375
|
-
if (
|
|
10376
|
-
remaining.set(a.canonical,
|
|
10374
|
+
const n2 = remaining.get(a.canonical) ?? 0;
|
|
10375
|
+
if (n2 <= 0) return null;
|
|
10376
|
+
remaining.set(a.canonical, n2 - 1);
|
|
10377
10377
|
}
|
|
10378
10378
|
for (const a of before) {
|
|
10379
|
-
const
|
|
10380
|
-
if (
|
|
10379
|
+
const n2 = remaining.get(a.canonical) ?? 0;
|
|
10380
|
+
if (n2 > 0) return a;
|
|
10381
10381
|
}
|
|
10382
10382
|
return null;
|
|
10383
10383
|
}
|
|
@@ -10872,8 +10872,8 @@ import { dirname as dirname2, join as join9 } from "node:path";
|
|
|
10872
10872
|
function compactOobToken(want, head) {
|
|
10873
10873
|
const normalizedHead = head.trim().toLowerCase();
|
|
10874
10874
|
if (!want || !FULL_OBJECT_ID.test(normalizedHead)) return null;
|
|
10875
|
-
const
|
|
10876
|
-
return `${COMPACT_OOB_PREFIX}${
|
|
10875
|
+
const digest2 = createHash3("sha256").update(`tamperward:oob:v1\0${want}\0${normalizedHead}`).digest("base64url");
|
|
10876
|
+
return `${COMPACT_OOB_PREFIX}${digest2}`;
|
|
10877
10877
|
}
|
|
10878
10878
|
function ledgerEntryFrom(value) {
|
|
10879
10879
|
if (!isRecord(value)) return null;
|
|
@@ -11180,8 +11180,14 @@ function cell(s) {
|
|
|
11180
11180
|
function mdCode(s) {
|
|
11181
11181
|
return s.replace(/`/g, "'").replace(/[\r\n]/g, " ");
|
|
11182
11182
|
}
|
|
11183
|
+
function encodePathSegment(segment) {
|
|
11184
|
+
return encodeURIComponent(segment).replace(
|
|
11185
|
+
/[!'()*]/g,
|
|
11186
|
+
(character) => "%" + character.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()
|
|
11187
|
+
);
|
|
11188
|
+
}
|
|
11183
11189
|
function mdUrl(s) {
|
|
11184
|
-
return s.
|
|
11190
|
+
return s.split("/").map(encodePathSegment).join("/");
|
|
11185
11191
|
}
|
|
11186
11192
|
function locationCell(f, env) {
|
|
11187
11193
|
if (!f.file) return "`(command)`";
|
|
@@ -12302,10 +12308,10 @@ function parseAuditJsonl(raw) {
|
|
|
12302
12308
|
return events;
|
|
12303
12309
|
}
|
|
12304
12310
|
function parseSince(value, nowMs = Date.now()) {
|
|
12305
|
-
const
|
|
12306
|
-
if (
|
|
12307
|
-
const count = Number(
|
|
12308
|
-
const unit =
|
|
12311
|
+
const relative9 = /^(\d+)(m|h|d)$/.exec(value);
|
|
12312
|
+
if (relative9) {
|
|
12313
|
+
const count = Number(relative9[1]);
|
|
12314
|
+
const unit = relative9[2] === "m" ? 6e4 : relative9[2] === "h" ? 36e5 : 864e5;
|
|
12309
12315
|
if (!Number.isSafeInteger(count) || count <= 0) throw new Error(`invalid --since value "${value}"`);
|
|
12310
12316
|
return nowMs - count * unit;
|
|
12311
12317
|
}
|
|
@@ -12714,8 +12720,8 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
12714
12720
|
if (!existsSync10(log)) return none;
|
|
12715
12721
|
let offset = 0;
|
|
12716
12722
|
if (cp && existsSync10(cp)) {
|
|
12717
|
-
const
|
|
12718
|
-
offset = Number.isFinite(
|
|
12723
|
+
const n2 = Number(readFileSync11(cp, "utf8"));
|
|
12724
|
+
offset = Number.isFinite(n2) ? n2 : 0;
|
|
12719
12725
|
}
|
|
12720
12726
|
const drained = drainEvents(log, offset);
|
|
12721
12727
|
if (!drained.complete) {
|
|
@@ -13013,12 +13019,12 @@ function pidAlive2(pid) {
|
|
|
13013
13019
|
}
|
|
13014
13020
|
}
|
|
13015
13021
|
function listening(socket) {
|
|
13016
|
-
return new Promise((
|
|
13022
|
+
return new Promise((resolve20) => {
|
|
13017
13023
|
const sock = createConnection2(socket);
|
|
13018
13024
|
const done = (v) => {
|
|
13019
13025
|
clearTimeout(timer);
|
|
13020
13026
|
sock.destroy();
|
|
13021
|
-
|
|
13027
|
+
resolve20(v);
|
|
13022
13028
|
};
|
|
13023
13029
|
const timer = setTimeout(() => done(false), 1e3);
|
|
13024
13030
|
sock.once("connect", () => done(true));
|
|
@@ -13197,11 +13203,11 @@ async function startHookService(opts) {
|
|
|
13197
13203
|
});
|
|
13198
13204
|
const umask = process.umask(63);
|
|
13199
13205
|
try {
|
|
13200
|
-
await new Promise((
|
|
13206
|
+
await new Promise((resolve20, reject) => {
|
|
13201
13207
|
server.once("error", reject);
|
|
13202
13208
|
server.listen(paths.socket, () => {
|
|
13203
13209
|
server.off("error", reject);
|
|
13204
|
-
|
|
13210
|
+
resolve20();
|
|
13205
13211
|
});
|
|
13206
13212
|
});
|
|
13207
13213
|
} finally {
|
|
@@ -13218,8 +13224,8 @@ async function startHookService(opts) {
|
|
|
13218
13224
|
get served() {
|
|
13219
13225
|
return served;
|
|
13220
13226
|
},
|
|
13221
|
-
close: () => new Promise((
|
|
13222
|
-
if (closed) return
|
|
13227
|
+
close: () => new Promise((resolve20) => {
|
|
13228
|
+
if (closed) return resolve20();
|
|
13223
13229
|
closed = true;
|
|
13224
13230
|
setSnapshotCache(null);
|
|
13225
13231
|
let settled = false;
|
|
@@ -13230,7 +13236,7 @@ async function startHookService(opts) {
|
|
|
13230
13236
|
clearTimeout(drain);
|
|
13231
13237
|
removeQuietly(paths.socket);
|
|
13232
13238
|
removeQuietly(paths.state);
|
|
13233
|
-
|
|
13239
|
+
resolve20();
|
|
13234
13240
|
};
|
|
13235
13241
|
server.close(finalize);
|
|
13236
13242
|
for (const sock of sockets) if (sock !== acceptedSocket) sock.destroy();
|
|
@@ -13257,11 +13263,16 @@ async function probeSocket(paths) {
|
|
|
13257
13263
|
function describeStateFile(state) {
|
|
13258
13264
|
return state ? `records pid ${state.pid}, root ${state.root}, tamperward@${state.version}` : "is absent or unreadable";
|
|
13259
13265
|
}
|
|
13260
|
-
async function stopHookService(paths) {
|
|
13266
|
+
async function stopHookService(paths, expectedRoot) {
|
|
13261
13267
|
const probe = await probeSocket(paths);
|
|
13262
13268
|
const state = readServiceState(paths);
|
|
13263
13269
|
if (probe.answered) {
|
|
13264
13270
|
const st = probe.status;
|
|
13271
|
+
const requestedRoot = expectedRoot === void 0 ? void 0 : realpathSync4(repoRoot(expectedRoot));
|
|
13272
|
+
if (requestedRoot !== void 0 && (st === null || st.root !== requestedRoot)) {
|
|
13273
|
+
const detail2 = st ? `a listener answered on ${paths.socket} for root ${st.root}, not the requested repository ${requestedRoot}` : `a listener answered on ${paths.socket} but did not report a root for the requested repository ${requestedRoot}`;
|
|
13274
|
+
return { outcome: "mismatch", pid: st?.pid ?? state?.pid, root: st?.root ?? state?.root, detail: detail2 };
|
|
13275
|
+
}
|
|
13265
13276
|
const agrees = st !== null && state !== null && state.pid === st.pid && state.root === st.root && state.version === st.version;
|
|
13266
13277
|
if (agrees && st.pid !== process.pid) {
|
|
13267
13278
|
try {
|
|
@@ -13283,13 +13294,13 @@ async function stopHookService(paths) {
|
|
|
13283
13294
|
removeQuietly(paths.state);
|
|
13284
13295
|
return { outcome: "not-running" };
|
|
13285
13296
|
}
|
|
13286
|
-
function
|
|
13287
|
-
let
|
|
13288
|
-
|
|
13289
|
-
return { dir };
|
|
13297
|
+
function parseDir(args) {
|
|
13298
|
+
for (let i = 0; i < args.length; i++) if (args[i] === "--dir" && args[i + 1]) return args[++i];
|
|
13299
|
+
return void 0;
|
|
13290
13300
|
}
|
|
13291
13301
|
function runHookService(args) {
|
|
13292
13302
|
const sub = args[0];
|
|
13303
|
+
const dir = parseDir(args.slice(1));
|
|
13293
13304
|
const paths = servicePaths();
|
|
13294
13305
|
if (!paths) {
|
|
13295
13306
|
process.stderr.write(
|
|
@@ -13297,9 +13308,18 @@ function runHookService(args) {
|
|
|
13297
13308
|
);
|
|
13298
13309
|
return 2;
|
|
13299
13310
|
}
|
|
13311
|
+
let requestedRoot;
|
|
13312
|
+
if (dir !== void 0) {
|
|
13313
|
+
try {
|
|
13314
|
+
requestedRoot = realpathSync4(repoRoot(realpathSync4(dir)));
|
|
13315
|
+
} catch (e) {
|
|
13316
|
+
process.stderr.write(`tamperward hook-service: cannot resolve --dir ${dir} (${e instanceof Error ? e.message : String(e)})
|
|
13317
|
+
`);
|
|
13318
|
+
return 2;
|
|
13319
|
+
}
|
|
13320
|
+
}
|
|
13300
13321
|
if (sub === "start") {
|
|
13301
|
-
|
|
13302
|
-
startHookService({ root: dir, paths }).then((svc) => {
|
|
13322
|
+
startHookService({ root: requestedRoot ?? process.cwd(), paths }).then((svc) => {
|
|
13303
13323
|
let closing = false;
|
|
13304
13324
|
const shutdown = () => {
|
|
13305
13325
|
if (closing) return;
|
|
@@ -13320,7 +13340,7 @@ function runHookService(args) {
|
|
|
13320
13340
|
return -1;
|
|
13321
13341
|
}
|
|
13322
13342
|
if (sub === "stop") {
|
|
13323
|
-
return stopHookService(paths).then((res) => {
|
|
13343
|
+
return stopHookService(paths, requestedRoot).then((res) => {
|
|
13324
13344
|
if (res.outcome === "stopped") {
|
|
13325
13345
|
process.stdout.write(`tamperward hook-service: stopped (pid ${res.pid}); ${paths.socket} removed
|
|
13326
13346
|
`);
|
|
@@ -13363,6 +13383,14 @@ function runHookService(args) {
|
|
|
13363
13383
|
exitAfterFlush(0);
|
|
13364
13384
|
return;
|
|
13365
13385
|
}
|
|
13386
|
+
if (requestedRoot !== void 0 && res.root !== requestedRoot) {
|
|
13387
|
+
process.stderr.write(
|
|
13388
|
+
`tamperward hook-service: a listener is bound to ${res.root}, not the requested repository ${requestedRoot}; it was not treated as the requested service; ${optIn}
|
|
13389
|
+
`
|
|
13390
|
+
);
|
|
13391
|
+
exitAfterFlush(1);
|
|
13392
|
+
return;
|
|
13393
|
+
}
|
|
13366
13394
|
const cache3 = isRecord(res.cache) ? res.cache : {};
|
|
13367
13395
|
const startedAt = typeof res.started_at === "string" ? res.started_at : state?.started_at ?? "unknown";
|
|
13368
13396
|
const disagrees = !state || state.pid !== res.pid || state.root !== res.root || state.version !== res.version;
|
|
@@ -13394,6 +13422,7 @@ var init_hook_service = __esm({
|
|
|
13394
13422
|
});
|
|
13395
13423
|
|
|
13396
13424
|
// src/cli/allow.ts
|
|
13425
|
+
import { relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
|
|
13397
13426
|
function runAllow(opts) {
|
|
13398
13427
|
if (!opts.rule) {
|
|
13399
13428
|
process.stderr.write('tamperward allow <rule> [--file <path>] --reason "<why>"\n');
|
|
@@ -13412,8 +13441,9 @@ function runAllow(opts) {
|
|
|
13412
13441
|
process.stderr.write("tamperward: cannot read the working tree (not a git repo?).\n");
|
|
13413
13442
|
return 2;
|
|
13414
13443
|
}
|
|
13444
|
+
const requestedFile = opts.file === void 0 ? void 0 : relative3(cwd, resolve5(cwd, opts.file)).split(sep2).join("/");
|
|
13415
13445
|
const targets = findings.filter(
|
|
13416
|
-
(f) => f.severity === "block" && f.rule === opts.rule && (!
|
|
13446
|
+
(f) => f.severity === "block" && f.rule === opts.rule && (!requestedFile || f.file === requestedFile)
|
|
13417
13447
|
);
|
|
13418
13448
|
if (targets.length === 0) {
|
|
13419
13449
|
process.stderr.write(
|
|
@@ -13532,7 +13562,7 @@ var init_safe_write = __esm({
|
|
|
13532
13562
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
13533
13563
|
import { createHash as createHash8 } from "node:crypto";
|
|
13534
13564
|
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, realpathSync as realpathSync5 } from "node:fs";
|
|
13535
|
-
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join15, relative as
|
|
13565
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join15, relative as relative4, resolve as resolve6 } from "node:path";
|
|
13536
13566
|
function gitConfig(cwd, key2) {
|
|
13537
13567
|
try {
|
|
13538
13568
|
return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -13872,13 +13902,13 @@ function planClaudeHooks(cwd) {
|
|
|
13872
13902
|
function hooksDir(cwd) {
|
|
13873
13903
|
try {
|
|
13874
13904
|
const out3 = execFileSync6("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
13875
|
-
if (out3) return
|
|
13905
|
+
if (out3) return resolve6(cwd, out3);
|
|
13876
13906
|
} catch {
|
|
13877
13907
|
}
|
|
13878
13908
|
return existsSync12(join15(cwd, ".git")) ? join15(cwd, ".git", "hooks") : null;
|
|
13879
13909
|
}
|
|
13880
13910
|
function display(cwd, path) {
|
|
13881
|
-
const rel =
|
|
13911
|
+
const rel = relative4(cwd, path);
|
|
13882
13912
|
return rel && !rel.startsWith("..") && !isAbsolute9(rel) ? rel : path;
|
|
13883
13913
|
}
|
|
13884
13914
|
function unconditionalExitAt(lines) {
|
|
@@ -14127,7 +14157,7 @@ function planInit(cwd, opts = {}) {
|
|
|
14127
14157
|
];
|
|
14128
14158
|
}
|
|
14129
14159
|
function runInit(opts) {
|
|
14130
|
-
const requested =
|
|
14160
|
+
const requested = resolve6(opts.cwd ?? process.cwd());
|
|
14131
14161
|
const cwd = repoRoot(requested);
|
|
14132
14162
|
const w = opts.quiet ? { write: (_text) => true } : process.stdout;
|
|
14133
14163
|
if (cwd !== requested && !sameDir(cwd, requested)) {
|
|
@@ -14377,16 +14407,16 @@ import {
|
|
|
14377
14407
|
dirname as dirname6,
|
|
14378
14408
|
isAbsolute as isAbsolute10,
|
|
14379
14409
|
join as join16,
|
|
14380
|
-
relative as
|
|
14381
|
-
resolve as
|
|
14382
|
-
sep as
|
|
14410
|
+
relative as relative5,
|
|
14411
|
+
resolve as resolve7,
|
|
14412
|
+
sep as sep3
|
|
14383
14413
|
} from "node:path";
|
|
14384
14414
|
function metricsFor(descriptor) {
|
|
14385
14415
|
return descriptor.metrics ??= { fullSnapshots: 0, reusedSnapshots: 0, totalMs: 0 };
|
|
14386
14416
|
}
|
|
14387
14417
|
function inside(root, candidate) {
|
|
14388
|
-
const rel =
|
|
14389
|
-
return rel === "" || rel !== ".." && !rel.startsWith(".." +
|
|
14418
|
+
const rel = relative5(resolve7(root), resolve7(candidate));
|
|
14419
|
+
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep3) && !isAbsolute10(rel);
|
|
14390
14420
|
}
|
|
14391
14421
|
function modeKind(path) {
|
|
14392
14422
|
const st = lstatSync8(path);
|
|
@@ -14538,7 +14568,7 @@ function freezeRoot(kind, path) {
|
|
|
14538
14568
|
if (!statSync3(path).isDirectory()) {
|
|
14539
14569
|
throw new Error("not a directory");
|
|
14540
14570
|
}
|
|
14541
|
-
return { kind, path:
|
|
14571
|
+
return { kind, path: resolve7(path), realPath: realpathSync6(path) };
|
|
14542
14572
|
} catch (e) {
|
|
14543
14573
|
throw new Unattestable(
|
|
14544
14574
|
kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
@@ -14574,7 +14604,7 @@ function firstExecutable(command) {
|
|
|
14574
14604
|
function resolveExecutable(token, cwd, env) {
|
|
14575
14605
|
if (!token) return null;
|
|
14576
14606
|
if (token.includes("/") || token.includes("\\")) {
|
|
14577
|
-
const path = isAbsolute10(token) ? token :
|
|
14607
|
+
const path = isAbsolute10(token) ? token : resolve7(cwd, token);
|
|
14578
14608
|
try {
|
|
14579
14609
|
return existsSync13(path) ? realpathSync6(path) : null;
|
|
14580
14610
|
} catch {
|
|
@@ -14582,7 +14612,7 @@ function resolveExecutable(token, cwd, env) {
|
|
|
14582
14612
|
}
|
|
14583
14613
|
}
|
|
14584
14614
|
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
14585
|
-
const candidate =
|
|
14615
|
+
const candidate = resolve7(entry, token);
|
|
14586
14616
|
try {
|
|
14587
14617
|
if (existsSync13(candidate) && statSync3(candidate).isFile()) return realpathSync6(candidate);
|
|
14588
14618
|
} catch {
|
|
@@ -14601,7 +14631,7 @@ function looksPython(command, executable, token) {
|
|
|
14601
14631
|
}
|
|
14602
14632
|
function inferredVenvFromToken(token, cwd) {
|
|
14603
14633
|
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
14604
|
-
const path = isAbsolute10(token) ? token :
|
|
14634
|
+
const path = isAbsolute10(token) ? token : resolve7(cwd, token);
|
|
14605
14635
|
if (!pythonName(basename4(path))) return null;
|
|
14606
14636
|
const parent = dirname6(path);
|
|
14607
14637
|
const parentName = basename4(parent).toLowerCase();
|
|
@@ -14639,7 +14669,7 @@ function venvProblem(root) {
|
|
|
14639
14669
|
return null;
|
|
14640
14670
|
}
|
|
14641
14671
|
function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
14642
|
-
const cwd =
|
|
14672
|
+
const cwd = resolve7(cwdInput);
|
|
14643
14673
|
const roots = [];
|
|
14644
14674
|
const probes = [];
|
|
14645
14675
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14663,7 +14693,7 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
|
14663
14693
|
if (!isAbsolute10(env.VIRTUAL_ENV)) {
|
|
14664
14694
|
throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
|
|
14665
14695
|
}
|
|
14666
|
-
venv =
|
|
14696
|
+
venv = resolve7(env.VIRTUAL_ENV);
|
|
14667
14697
|
} else {
|
|
14668
14698
|
venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
|
|
14669
14699
|
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync13(join16(cwd, ".venv", "pyvenv.cfg"))) {
|
|
@@ -14703,7 +14733,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
|
|
|
14703
14733
|
const fingerprint2 = measuredSnapshot(
|
|
14704
14734
|
descriptor.roots,
|
|
14705
14735
|
descriptor.probes,
|
|
14706
|
-
|
|
14736
|
+
resolve7(cwdInput),
|
|
14707
14737
|
metricsFor(descriptor)
|
|
14708
14738
|
);
|
|
14709
14739
|
if (fingerprint2 !== descriptor.fingerprint) {
|
|
@@ -14716,7 +14746,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
|
|
|
14716
14746
|
}
|
|
14717
14747
|
function attestDependencyEnvironment(cwdInput, descriptor) {
|
|
14718
14748
|
const attestation = {
|
|
14719
|
-
cwd:
|
|
14749
|
+
cwd: resolve7(cwdInput),
|
|
14720
14750
|
descriptor,
|
|
14721
14751
|
check: checkDependencyEnvironment(cwdInput, descriptor)
|
|
14722
14752
|
};
|
|
@@ -14724,7 +14754,7 @@ function attestDependencyEnvironment(cwdInput, descriptor) {
|
|
|
14724
14754
|
return attestation;
|
|
14725
14755
|
}
|
|
14726
14756
|
function reuseDependencyEnvironmentAttestation(cwdInput, descriptor, attestation) {
|
|
14727
|
-
if (!attestation || !issuedAttestations.has(attestation) || attestation.descriptor !== descriptor || attestation.cwd !==
|
|
14757
|
+
if (!attestation || !issuedAttestations.has(attestation) || attestation.descriptor !== descriptor || attestation.cwd !== resolve7(cwdInput)) return null;
|
|
14728
14758
|
metricsFor(descriptor).reusedSnapshots += 1;
|
|
14729
14759
|
return attestation.check;
|
|
14730
14760
|
}
|
|
@@ -14853,8 +14883,8 @@ function parseCapturedSupervisorResult(stdoutText) {
|
|
|
14853
14883
|
};
|
|
14854
14884
|
}
|
|
14855
14885
|
function runCapturedProcessSync(executable, args, opts) {
|
|
14856
|
-
const
|
|
14857
|
-
const configFile = join17(
|
|
14886
|
+
const stateDir2 = mkdtempSync2(join17(tmpdir3(), "tw-suite-capture-"));
|
|
14887
|
+
const configFile = join17(stateDir2, "config.json");
|
|
14858
14888
|
const backstop = opts.backstopMs ?? 3e4;
|
|
14859
14889
|
try {
|
|
14860
14890
|
writeFileSync8(
|
|
@@ -14902,7 +14932,7 @@ function runCapturedProcessSync(executable, args, opts) {
|
|
|
14902
14932
|
}
|
|
14903
14933
|
return parsed;
|
|
14904
14934
|
} finally {
|
|
14905
|
-
rmSync6(
|
|
14935
|
+
rmSync6(stateDir2, { recursive: true, force: true });
|
|
14906
14936
|
}
|
|
14907
14937
|
}
|
|
14908
14938
|
var DIAGNOSTIC_TAIL_BYTES, EMPTY_STREAM, CLEANUP_HELPERS_SRC, CAPTURE_SUPERVISOR;
|
|
@@ -15146,7 +15176,7 @@ if (child) {
|
|
|
15146
15176
|
import { createHash as createHash11, randomUUID } from "node:crypto";
|
|
15147
15177
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
15148
15178
|
import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
|
|
15149
|
-
import { delimiter as delimiter2, dirname as dirname7, join as join18, resolve as
|
|
15179
|
+
import { delimiter as delimiter2, dirname as dirname7, join as join18, resolve as resolve8 } from "node:path";
|
|
15150
15180
|
function identity() {
|
|
15151
15181
|
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
15152
15182
|
const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
|
|
@@ -15166,7 +15196,7 @@ function writableByCaller(path) {
|
|
|
15166
15196
|
}
|
|
15167
15197
|
}
|
|
15168
15198
|
function replaceableByCaller(path) {
|
|
15169
|
-
let current =
|
|
15199
|
+
let current = resolve8(path);
|
|
15170
15200
|
if (writableByCaller(current)) return true;
|
|
15171
15201
|
current = dirname7(current);
|
|
15172
15202
|
while (true) {
|
|
@@ -15512,7 +15542,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
15512
15542
|
const args = containerRunArgs({
|
|
15513
15543
|
image: backend.image,
|
|
15514
15544
|
name,
|
|
15515
|
-
workspace:
|
|
15545
|
+
workspace: resolve8(dir),
|
|
15516
15546
|
command,
|
|
15517
15547
|
uid,
|
|
15518
15548
|
gid,
|
|
@@ -15621,12 +15651,375 @@ var init_verifier_backend = __esm({
|
|
|
15621
15651
|
}
|
|
15622
15652
|
});
|
|
15623
15653
|
|
|
15624
|
-
// src/
|
|
15625
|
-
import { execFileSync as execFileSync8
|
|
15654
|
+
// src/verification-state.ts
|
|
15655
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
15626
15656
|
import { createHash as createHash12 } from "node:crypto";
|
|
15627
|
-
import {
|
|
15657
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
15658
|
+
import { dirname as dirname8, join as join19 } from "node:path";
|
|
15659
|
+
function git2(args, cwd) {
|
|
15660
|
+
return execFileSync8("git", args, {
|
|
15661
|
+
cwd,
|
|
15662
|
+
encoding: "utf8",
|
|
15663
|
+
maxBuffer: 1 << 28,
|
|
15664
|
+
env: trustedGitEnv()
|
|
15665
|
+
});
|
|
15666
|
+
}
|
|
15667
|
+
function resolveBaseRev(baseRef, cwd) {
|
|
15668
|
+
assertRev(baseRef);
|
|
15669
|
+
const rev2 = git2(["rev-parse", "--verify", `${baseRef}^{commit}`], cwd).trim();
|
|
15670
|
+
try {
|
|
15671
|
+
return git2(["merge-base", rev2, "HEAD"], cwd).trim();
|
|
15672
|
+
} catch {
|
|
15673
|
+
return rev2;
|
|
15674
|
+
}
|
|
15675
|
+
}
|
|
15676
|
+
function canonicalJson(value) {
|
|
15677
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
15678
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
15679
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
15680
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
15681
|
+
}
|
|
15682
|
+
function digest(value) {
|
|
15683
|
+
return createHash12("sha256").update(canonicalJson(value)).digest("hex");
|
|
15684
|
+
}
|
|
15685
|
+
function surfacePaths(base, cwd, policy) {
|
|
15686
|
+
let listing;
|
|
15687
|
+
try {
|
|
15688
|
+
listing = git2(["ls-tree", "-r", "--name-only", "-z", base], cwd);
|
|
15689
|
+
} catch {
|
|
15690
|
+
return [];
|
|
15691
|
+
}
|
|
15692
|
+
const inputs = policy.verify?.inputs ?? [];
|
|
15693
|
+
return listing.split("\0").filter(Boolean).filter((p) => isProtected(p, policy) || matchesAny(p, inputs)).sort();
|
|
15694
|
+
}
|
|
15695
|
+
function interventionWiring(cwd) {
|
|
15696
|
+
const dir = claudeConfigDir();
|
|
15697
|
+
const sources = [
|
|
15698
|
+
["repo", join19(cwd, ".claude", "settings.json")],
|
|
15699
|
+
["repo.local", join19(cwd, ".claude", "settings.local.json")],
|
|
15700
|
+
["user", join19(dir, "settings.json")],
|
|
15701
|
+
["user.local", join19(dir, "settings.local.json")]
|
|
15702
|
+
];
|
|
15703
|
+
return sources.map(([source, path]) => {
|
|
15704
|
+
let parsed;
|
|
15705
|
+
try {
|
|
15706
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
15707
|
+
} catch {
|
|
15708
|
+
return { source, present: false };
|
|
15709
|
+
}
|
|
15710
|
+
if (!isRecord4(parsed)) return { source, present: true, shape: "non-object" };
|
|
15711
|
+
return {
|
|
15712
|
+
source,
|
|
15713
|
+
present: true,
|
|
15714
|
+
hooks: parsed.hooks ?? null,
|
|
15715
|
+
disableAllHooks: parsed.disableAllHooks ?? null
|
|
15716
|
+
};
|
|
15717
|
+
});
|
|
15718
|
+
}
|
|
15719
|
+
function dependenciesBinding(root, policy, command) {
|
|
15720
|
+
const backend = policy.verify?.backend ?? "local";
|
|
15721
|
+
if (backend === "container") {
|
|
15722
|
+
return digest({ backend, image: policy.verify?.image ?? null });
|
|
15723
|
+
}
|
|
15724
|
+
const dep = discoverDependencyEnvironment(root, command);
|
|
15725
|
+
return digest({ backend, status: dep.status, fingerprint: dep.fingerprint ?? null });
|
|
15726
|
+
}
|
|
15727
|
+
function computeBinding(cwd, inputs, precomputed) {
|
|
15728
|
+
const root = repoRoot(cwd);
|
|
15729
|
+
const head = git2(["rev-parse", "--verify", "HEAD^{commit}"], root).trim();
|
|
15730
|
+
const base = resolveBaseRev(inputs.base_ref, root);
|
|
15731
|
+
const policy = inputs.explicit_base ? loadPolicyAt(base, root) ?? defaultPolicy() : loadPolicy(root);
|
|
15732
|
+
const command = inputs.command_source === "flag" ? inputs.command : policy.verify?.command ?? "";
|
|
15733
|
+
if (!command) {
|
|
15734
|
+
throw new Error("no verifier command is configured for the recorded verification");
|
|
15735
|
+
}
|
|
15736
|
+
const budget = inputs.budget_source === "flag" ? inputs.budget : policy.verify?.budget ?? 300;
|
|
15737
|
+
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
15738
|
+
return {
|
|
15739
|
+
// NOTE (#500/#600 follow-up): `tree` hashes every tracked and ignored-protected
|
|
15740
|
+
// file and `dependencies` walks the attested dependency roots, on EVERY call.
|
|
15741
|
+
// Right for `verify` (once per run) and reused there via `precomputed.tree`,
|
|
15742
|
+
// but `status` is meant to be polled continuously by a status bar/dashboard,
|
|
15743
|
+
// where a large repository makes each poll a full read. A stat-cache identity
|
|
15744
|
+
// (git's index plus hashing only what changed) is a deliberate follow-up, not
|
|
15745
|
+
// built here.
|
|
15746
|
+
tree: precomputed?.tree ?? treeFingerprint(root, protectedIgnored),
|
|
15747
|
+
head,
|
|
15748
|
+
base,
|
|
15749
|
+
policy: digest(policy),
|
|
15750
|
+
verifier: digest({
|
|
15751
|
+
command,
|
|
15752
|
+
budget,
|
|
15753
|
+
inputs: policy.verify?.inputs ?? [],
|
|
15754
|
+
backend: policy.verify?.backend ?? "local",
|
|
15755
|
+
image: policy.verify?.image ?? null
|
|
15756
|
+
}),
|
|
15757
|
+
surface: digest(surfacePaths(base, root, policy)),
|
|
15758
|
+
intervention: digest(interventionWiring(root)),
|
|
15759
|
+
dependencies: dependenciesBinding(root, policy, command)
|
|
15760
|
+
};
|
|
15761
|
+
}
|
|
15762
|
+
function stateDir(cwd) {
|
|
15763
|
+
const ctx = repoContext(cwd);
|
|
15764
|
+
return ctx ? join19(ctx.gitDir, "tamperward") : null;
|
|
15765
|
+
}
|
|
15766
|
+
function verificationRecordPath(cwd) {
|
|
15767
|
+
const dir = stateDir(cwd);
|
|
15768
|
+
return dir ? join19(dir, "verification-state.json") : null;
|
|
15769
|
+
}
|
|
15770
|
+
function verifyingMarkerPath(cwd) {
|
|
15771
|
+
const dir = stateDir(cwd);
|
|
15772
|
+
return dir ? join19(dir, "verifying.json") : null;
|
|
15773
|
+
}
|
|
15774
|
+
function recordVerification(cwd, inputs, treeFingerprintValue) {
|
|
15775
|
+
const path = verificationRecordPath(cwd);
|
|
15776
|
+
if (!path) return false;
|
|
15777
|
+
const binding = computeBinding(
|
|
15778
|
+
repoRoot(cwd),
|
|
15779
|
+
inputs,
|
|
15780
|
+
treeFingerprintValue ? { tree: treeFingerprintValue } : void 0
|
|
15781
|
+
);
|
|
15782
|
+
const record = {
|
|
15783
|
+
schema_version: VERIFICATION_STATE_SCHEMA_VERSION,
|
|
15784
|
+
verdict: "VERIFIED",
|
|
15785
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15786
|
+
tw_version: TW_VERSION,
|
|
15787
|
+
inputs,
|
|
15788
|
+
binding
|
|
15789
|
+
};
|
|
15790
|
+
mkdirSync8(dirname8(path), { recursive: true });
|
|
15791
|
+
writeFileSync9(path, JSON.stringify(record) + "\n");
|
|
15792
|
+
return true;
|
|
15793
|
+
}
|
|
15794
|
+
function invalidateVerificationRecordIfCurrent(cwd) {
|
|
15795
|
+
const record = readVerificationRecord(cwd);
|
|
15796
|
+
if (!record) return false;
|
|
15797
|
+
let live;
|
|
15798
|
+
try {
|
|
15799
|
+
live = computeBinding(cwd, record.inputs);
|
|
15800
|
+
} catch {
|
|
15801
|
+
return false;
|
|
15802
|
+
}
|
|
15803
|
+
if (firstMismatch(record.binding, live)) return false;
|
|
15804
|
+
const path = verificationRecordPath(cwd);
|
|
15805
|
+
if (!path) return false;
|
|
15806
|
+
try {
|
|
15807
|
+
rmSync7(path, { force: true });
|
|
15808
|
+
return true;
|
|
15809
|
+
} catch {
|
|
15810
|
+
return false;
|
|
15811
|
+
}
|
|
15812
|
+
}
|
|
15813
|
+
function isRecord4(v) {
|
|
15814
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15815
|
+
}
|
|
15816
|
+
function isBinding(v) {
|
|
15817
|
+
if (!isRecord4(v)) return false;
|
|
15818
|
+
return BINDING_INPUTS.every((k) => {
|
|
15819
|
+
const val = v[k];
|
|
15820
|
+
return typeof val === "string" && val.length > 0;
|
|
15821
|
+
});
|
|
15822
|
+
}
|
|
15823
|
+
function parseVerificationRecord(value) {
|
|
15824
|
+
if (!isRecord4(value)) return null;
|
|
15825
|
+
if (value.schema_version !== VERIFICATION_STATE_SCHEMA_VERSION) return null;
|
|
15826
|
+
if (value.verdict !== "VERIFIED") return null;
|
|
15827
|
+
if (typeof value.verified_at !== "string") return null;
|
|
15828
|
+
if (typeof value.tw_version !== "string") return null;
|
|
15829
|
+
const inputs = value.inputs;
|
|
15830
|
+
if (!isRecord4(inputs)) return null;
|
|
15831
|
+
if (typeof inputs.base_ref !== "string" || inputs.base_ref.length === 0) return null;
|
|
15832
|
+
if (typeof inputs.explicit_base !== "boolean") return null;
|
|
15833
|
+
if (inputs.command_source !== "policy" && inputs.command_source !== "flag") return null;
|
|
15834
|
+
if (typeof inputs.command !== "string") return null;
|
|
15835
|
+
if (inputs.budget_source !== "policy" && inputs.budget_source !== "flag") return null;
|
|
15836
|
+
if (typeof inputs.budget !== "number" || !Number.isFinite(inputs.budget)) return null;
|
|
15837
|
+
if (!isBinding(value.binding)) return null;
|
|
15838
|
+
return {
|
|
15839
|
+
schema_version: VERIFICATION_STATE_SCHEMA_VERSION,
|
|
15840
|
+
verdict: "VERIFIED",
|
|
15841
|
+
verified_at: value.verified_at,
|
|
15842
|
+
tw_version: value.tw_version,
|
|
15843
|
+
inputs: {
|
|
15844
|
+
base_ref: inputs.base_ref,
|
|
15845
|
+
explicit_base: inputs.explicit_base,
|
|
15846
|
+
command_source: inputs.command_source,
|
|
15847
|
+
command: inputs.command,
|
|
15848
|
+
budget_source: inputs.budget_source,
|
|
15849
|
+
budget: inputs.budget
|
|
15850
|
+
},
|
|
15851
|
+
binding: value.binding
|
|
15852
|
+
};
|
|
15853
|
+
}
|
|
15854
|
+
function readVerificationRecord(cwd) {
|
|
15855
|
+
const path = verificationRecordPath(cwd);
|
|
15856
|
+
if (!path) return null;
|
|
15857
|
+
let raw;
|
|
15858
|
+
try {
|
|
15859
|
+
raw = readFileSync16(path, "utf8");
|
|
15860
|
+
} catch {
|
|
15861
|
+
return null;
|
|
15862
|
+
}
|
|
15863
|
+
let parsed;
|
|
15864
|
+
try {
|
|
15865
|
+
parsed = JSON.parse(raw);
|
|
15866
|
+
} catch {
|
|
15867
|
+
return null;
|
|
15868
|
+
}
|
|
15869
|
+
return parseVerificationRecord(parsed);
|
|
15870
|
+
}
|
|
15871
|
+
function pidAlive3(pid) {
|
|
15872
|
+
try {
|
|
15873
|
+
process.kill(pid, 0);
|
|
15874
|
+
return true;
|
|
15875
|
+
} catch (e) {
|
|
15876
|
+
return isRecord4(e) && e.code === "EPERM";
|
|
15877
|
+
}
|
|
15878
|
+
}
|
|
15879
|
+
function beginVerifying(cwd) {
|
|
15880
|
+
const path = verifyingMarkerPath(cwd);
|
|
15881
|
+
if (!path) return false;
|
|
15882
|
+
try {
|
|
15883
|
+
const marker = {
|
|
15884
|
+
schema_version: VERIFICATION_STATE_SCHEMA_VERSION,
|
|
15885
|
+
pid: process.pid,
|
|
15886
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15887
|
+
};
|
|
15888
|
+
mkdirSync8(dirname8(path), { recursive: true });
|
|
15889
|
+
writeFileSync9(path, JSON.stringify(marker) + "\n");
|
|
15890
|
+
return true;
|
|
15891
|
+
} catch {
|
|
15892
|
+
return false;
|
|
15893
|
+
}
|
|
15894
|
+
}
|
|
15895
|
+
function endVerifying(cwd) {
|
|
15896
|
+
const path = verifyingMarkerPath(cwd);
|
|
15897
|
+
if (!path) return;
|
|
15898
|
+
try {
|
|
15899
|
+
rmSync7(path, { force: true });
|
|
15900
|
+
} catch {
|
|
15901
|
+
}
|
|
15902
|
+
}
|
|
15903
|
+
function readVerifyingMarker(cwd) {
|
|
15904
|
+
const path = verifyingMarkerPath(cwd);
|
|
15905
|
+
if (!path) return null;
|
|
15906
|
+
let parsed;
|
|
15907
|
+
try {
|
|
15908
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
15909
|
+
} catch {
|
|
15910
|
+
return null;
|
|
15911
|
+
}
|
|
15912
|
+
if (!isRecord4(parsed)) return null;
|
|
15913
|
+
if (parsed.schema_version !== VERIFICATION_STATE_SCHEMA_VERSION) return null;
|
|
15914
|
+
if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
15915
|
+
if (typeof parsed.started_at !== "string") return null;
|
|
15916
|
+
if (!pidAlive3(parsed.pid)) return null;
|
|
15917
|
+
return { schema_version: VERIFICATION_STATE_SCHEMA_VERSION, pid: parsed.pid, started_at: parsed.started_at };
|
|
15918
|
+
}
|
|
15919
|
+
function firstMismatch(recorded, live) {
|
|
15920
|
+
for (const input of MISMATCH_PRIORITY) {
|
|
15921
|
+
if (recorded[input] !== live[input]) return input;
|
|
15922
|
+
}
|
|
15923
|
+
return null;
|
|
15924
|
+
}
|
|
15925
|
+
function evaluateVerificationState(cwd) {
|
|
15926
|
+
if (readVerifyingMarker(cwd)) {
|
|
15927
|
+
return { state: "VERIFYING", reason: "a verification is in progress" };
|
|
15928
|
+
}
|
|
15929
|
+
const record = readVerificationRecord(cwd);
|
|
15930
|
+
if (!record) {
|
|
15931
|
+
return {
|
|
15932
|
+
state: "UNVERIFIED",
|
|
15933
|
+
reason: "no successful verification has been recorded for this repository"
|
|
15934
|
+
};
|
|
15935
|
+
}
|
|
15936
|
+
let live;
|
|
15937
|
+
try {
|
|
15938
|
+
live = computeBinding(cwd, record.inputs);
|
|
15939
|
+
} catch (e) {
|
|
15940
|
+
return {
|
|
15941
|
+
state: "BROKEN",
|
|
15942
|
+
reason: "verification authority wiring is invalid or unavailable",
|
|
15943
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
15944
|
+
verified_at: record.verified_at,
|
|
15945
|
+
verifier_command: record.inputs.command,
|
|
15946
|
+
record
|
|
15947
|
+
};
|
|
15948
|
+
}
|
|
15949
|
+
const changed = firstMismatch(record.binding, live);
|
|
15950
|
+
if (!changed) {
|
|
15951
|
+
return {
|
|
15952
|
+
state: "CURRENT",
|
|
15953
|
+
verified_at: record.verified_at,
|
|
15954
|
+
base: live.base,
|
|
15955
|
+
head: live.head,
|
|
15956
|
+
verifier_command: record.inputs.command,
|
|
15957
|
+
record
|
|
15958
|
+
};
|
|
15959
|
+
}
|
|
15960
|
+
return {
|
|
15961
|
+
state: "STALE",
|
|
15962
|
+
changed_input: changed,
|
|
15963
|
+
reason: STALE_REASON[changed],
|
|
15964
|
+
verified_at: record.verified_at,
|
|
15965
|
+
base: live.base,
|
|
15966
|
+
head: live.head,
|
|
15967
|
+
verifier_command: record.inputs.command,
|
|
15968
|
+
record
|
|
15969
|
+
};
|
|
15970
|
+
}
|
|
15971
|
+
var VERIFICATION_STATE_SCHEMA_VERSION, BINDING_INPUTS, STALE_REASON, MISMATCH_PRIORITY;
|
|
15972
|
+
var init_verification_state = __esm({
|
|
15973
|
+
"src/verification-state.ts"() {
|
|
15974
|
+
"use strict";
|
|
15975
|
+
init_build();
|
|
15976
|
+
init_trusted();
|
|
15977
|
+
init_fingerprint();
|
|
15978
|
+
init_policy();
|
|
15979
|
+
init_policy_load();
|
|
15980
|
+
init_repo_context();
|
|
15981
|
+
init_wiring();
|
|
15982
|
+
init_dependency_env();
|
|
15983
|
+
VERIFICATION_STATE_SCHEMA_VERSION = 1;
|
|
15984
|
+
BINDING_INPUTS = [
|
|
15985
|
+
"tree",
|
|
15986
|
+
"head",
|
|
15987
|
+
"base",
|
|
15988
|
+
"policy",
|
|
15989
|
+
"verifier",
|
|
15990
|
+
"surface",
|
|
15991
|
+
"intervention",
|
|
15992
|
+
"dependencies"
|
|
15993
|
+
];
|
|
15994
|
+
STALE_REASON = {
|
|
15995
|
+
tree: "candidate tree changed since verification",
|
|
15996
|
+
head: "HEAD commit changed since verification",
|
|
15997
|
+
base: "trusted base commit changed since verification",
|
|
15998
|
+
policy: "TamperWard policy changed since verification",
|
|
15999
|
+
verifier: "verifier configuration changed since verification",
|
|
16000
|
+
surface: "protected verification surface changed since verification",
|
|
16001
|
+
intervention: "runtime steering wiring changed since verification",
|
|
16002
|
+
dependencies: "dependency environment changed since verification"
|
|
16003
|
+
};
|
|
16004
|
+
MISMATCH_PRIORITY = [
|
|
16005
|
+
"head",
|
|
16006
|
+
"base",
|
|
16007
|
+
"verifier",
|
|
16008
|
+
"surface",
|
|
16009
|
+
"intervention",
|
|
16010
|
+
"dependencies",
|
|
16011
|
+
"policy",
|
|
16012
|
+
"tree"
|
|
16013
|
+
];
|
|
16014
|
+
}
|
|
16015
|
+
});
|
|
16016
|
+
|
|
16017
|
+
// src/cli/verify.ts
|
|
16018
|
+
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
16019
|
+
import { createHash as createHash13 } from "node:crypto";
|
|
16020
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, readlinkSync as readlinkSync3, rmSync as rmSync8, symlinkSync, writeFileSync as writeFileSync10, existsSync as existsSync14 } from "node:fs";
|
|
15628
16021
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
15629
|
-
import { dirname as
|
|
16022
|
+
import { dirname as dirname9, isAbsolute as isAbsolute11, join as join20, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
|
|
15630
16023
|
function oracleAssuranceReport() {
|
|
15631
16024
|
return {
|
|
15632
16025
|
level: "suite-exit-only",
|
|
@@ -15635,14 +16028,14 @@ function oracleAssuranceReport() {
|
|
|
15635
16028
|
limitation: "candidate source executes inside the configured suite process and may terminate or interpose on that in-process oracle; isolated-container is execution-domain isolation, not semantic/oracle isolation"
|
|
15636
16029
|
};
|
|
15637
16030
|
}
|
|
15638
|
-
function
|
|
15639
|
-
return
|
|
16031
|
+
function git3(args, cwd) {
|
|
16032
|
+
return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
15640
16033
|
}
|
|
15641
16034
|
function resolveBase(base, cwd) {
|
|
15642
16035
|
assertRev(base);
|
|
15643
|
-
const rev2 =
|
|
16036
|
+
const rev2 = git3(["rev-parse", "--verify", `${base}^{commit}`], cwd).trim();
|
|
15644
16037
|
try {
|
|
15645
|
-
return
|
|
16038
|
+
return git3(["merge-base", rev2, "HEAD"], cwd).trim();
|
|
15646
16039
|
} catch {
|
|
15647
16040
|
return rev2;
|
|
15648
16041
|
}
|
|
@@ -15652,32 +16045,53 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
15652
16045
|
return r.status === 0;
|
|
15653
16046
|
}
|
|
15654
16047
|
function inside2(root, path) {
|
|
15655
|
-
const rel =
|
|
15656
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
16048
|
+
const rel = relative6(resolve9(root), resolve9(path));
|
|
16049
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute11(rel);
|
|
15657
16050
|
}
|
|
15658
16051
|
function linkParts(path) {
|
|
15659
|
-
const parts =
|
|
16052
|
+
const parts = sep4 === "\\" ? path.split(/[\\/]+/) : path.split("/");
|
|
15660
16053
|
return parts.filter((part) => part !== "" && part !== ".");
|
|
15661
16054
|
}
|
|
16055
|
+
function materializationFailureReason(error) {
|
|
16056
|
+
if (error instanceof MaterializationError) return error.materializationReason;
|
|
16057
|
+
const code2 = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
16058
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16059
|
+
if (/tracked\s+node_modules|node_modules.*(?:attested|dependency).*(?:conflict|replace)/i.test(message)) {
|
|
16060
|
+
return "TRACKED_NODE_MODULES_CONFLICT";
|
|
16061
|
+
}
|
|
16062
|
+
if (/symlink.*(?:escape|cycle|parent directory)|escapes the materialised tree/i.test(message)) {
|
|
16063
|
+
return "SYMLINK_ESCAPE";
|
|
16064
|
+
}
|
|
16065
|
+
if (/(?:special file|where git expects a file)/i.test(message)) {
|
|
16066
|
+
return "SPECIAL_FILE";
|
|
16067
|
+
}
|
|
16068
|
+
if (code2 === "ENOENT" || /(?:ENOENT|no such file|file not found)/i.test(message)) {
|
|
16069
|
+
return "RACING_DELETION";
|
|
16070
|
+
}
|
|
16071
|
+
return "UNKNOWN";
|
|
16072
|
+
}
|
|
15662
16073
|
function linkEscape(label, target) {
|
|
15663
|
-
return new
|
|
16074
|
+
return new MaterializationError(
|
|
16075
|
+
"SYMLINK_ESCAPE",
|
|
16076
|
+
`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`
|
|
16077
|
+
);
|
|
15664
16078
|
}
|
|
15665
16079
|
function driveRelativeLinkTarget(target) {
|
|
15666
|
-
return
|
|
16080
|
+
return sep4 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
|
|
15667
16081
|
}
|
|
15668
16082
|
function rootedLinkTarget(target) {
|
|
15669
16083
|
return isAbsolute11(target) || driveRelativeLinkTarget(target);
|
|
15670
16084
|
}
|
|
15671
16085
|
function safeSymlink(target, out3, root, label) {
|
|
15672
|
-
if (rootedLinkTarget(target) || !inside2(root,
|
|
16086
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve9(dirname9(out3), target))) {
|
|
15673
16087
|
throw linkEscape(label, target);
|
|
15674
16088
|
}
|
|
15675
|
-
|
|
16089
|
+
rmSync8(out3, { force: true });
|
|
15676
16090
|
symlinkSync(target, out3);
|
|
15677
16091
|
}
|
|
15678
16092
|
function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
15679
16093
|
let domain = "tree";
|
|
15680
|
-
let stack = linkParts(
|
|
16094
|
+
let stack = linkParts(relative6(root, dirname9(out3)));
|
|
15681
16095
|
const pending = linkParts(target);
|
|
15682
16096
|
const seen = /* @__PURE__ */ new Set();
|
|
15683
16097
|
let hops = 0;
|
|
@@ -15694,7 +16108,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
15694
16108
|
continue;
|
|
15695
16109
|
}
|
|
15696
16110
|
const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
|
|
15697
|
-
const current =
|
|
16111
|
+
const current = join20(base, ...stack);
|
|
15698
16112
|
let st;
|
|
15699
16113
|
try {
|
|
15700
16114
|
st = lstatSync9(current);
|
|
@@ -15703,11 +16117,17 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
15703
16117
|
}
|
|
15704
16118
|
if (!st.isSymbolicLink()) continue;
|
|
15705
16119
|
if (++hops > 128) {
|
|
15706
|
-
throw new
|
|
16120
|
+
throw new MaterializationError(
|
|
16121
|
+
"SYMLINK_ESCAPE",
|
|
16122
|
+
`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`
|
|
16123
|
+
);
|
|
15707
16124
|
}
|
|
15708
|
-
const key2 = `${domain}:${current}:${pending.join(
|
|
16125
|
+
const key2 = `${domain}:${current}:${pending.join(sep4)}`;
|
|
15709
16126
|
if (seen.has(key2)) {
|
|
15710
|
-
throw new
|
|
16127
|
+
throw new MaterializationError(
|
|
16128
|
+
"SYMLINK_ESCAPE",
|
|
16129
|
+
`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`
|
|
16130
|
+
);
|
|
15711
16131
|
}
|
|
15712
16132
|
seen.add(key2);
|
|
15713
16133
|
const next = readlinkSync3(current);
|
|
@@ -15720,7 +16140,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
15720
16140
|
throw linkEscape(label, target);
|
|
15721
16141
|
}
|
|
15722
16142
|
stack = [];
|
|
15723
|
-
pending.unshift(...linkParts(
|
|
16143
|
+
pending.unshift(...linkParts(relative6(dependencyRoot, resolve9(next))));
|
|
15724
16144
|
} else {
|
|
15725
16145
|
pending.unshift(...linkParts(next));
|
|
15726
16146
|
}
|
|
@@ -15730,7 +16150,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
15730
16150
|
const parts = rel.split("/").slice(0, -1);
|
|
15731
16151
|
let current = cwd;
|
|
15732
16152
|
for (const part of parts) {
|
|
15733
|
-
current =
|
|
16153
|
+
current = join20(current, part);
|
|
15734
16154
|
let st;
|
|
15735
16155
|
try {
|
|
15736
16156
|
st = lstatSync9(current);
|
|
@@ -15738,15 +16158,24 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
15738
16158
|
continue;
|
|
15739
16159
|
}
|
|
15740
16160
|
if (st.isSymbolicLink()) {
|
|
15741
|
-
throw new
|
|
16161
|
+
throw new MaterializationError(
|
|
16162
|
+
"SYMLINK_ESCAPE",
|
|
16163
|
+
`${rel} has a symlinked parent directory (${relative6(cwd, current)})`
|
|
16164
|
+
);
|
|
15742
16165
|
}
|
|
15743
16166
|
}
|
|
15744
16167
|
}
|
|
15745
16168
|
function materialize(cwd, dest, dependencyRoot) {
|
|
15746
16169
|
const listed = workingTreePaths(cwd);
|
|
16170
|
+
if (dependencyRoot && listed.some((rel) => rel === "node_modules" || rel.startsWith("node_modules/"))) {
|
|
16171
|
+
throw new MaterializationError(
|
|
16172
|
+
"TRACKED_NODE_MODULES_CONFLICT",
|
|
16173
|
+
"tracked node_modules conflicts with the attested dependency root"
|
|
16174
|
+
);
|
|
16175
|
+
}
|
|
15747
16176
|
const links = [];
|
|
15748
16177
|
for (const rel of listed) {
|
|
15749
|
-
const src =
|
|
16178
|
+
const src = join20(cwd, rel);
|
|
15750
16179
|
rejectLinkedParent(cwd, rel);
|
|
15751
16180
|
let st;
|
|
15752
16181
|
try {
|
|
@@ -15754,8 +16183,8 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
15754
16183
|
} catch {
|
|
15755
16184
|
continue;
|
|
15756
16185
|
}
|
|
15757
|
-
const out3 =
|
|
15758
|
-
|
|
16186
|
+
const out3 = join20(dest, rel);
|
|
16187
|
+
mkdirSync9(dirname9(out3), { recursive: true });
|
|
15759
16188
|
dropSymlink(out3);
|
|
15760
16189
|
if (st.isSymbolicLink()) {
|
|
15761
16190
|
const target = readlinkSync3(src);
|
|
@@ -15765,25 +16194,28 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
15765
16194
|
cpSync(src, out3, { dereference: true });
|
|
15766
16195
|
chmodSync2(out3, st.mode);
|
|
15767
16196
|
} else {
|
|
15768
|
-
throw new
|
|
16197
|
+
throw new MaterializationError(
|
|
16198
|
+
"SPECIAL_FILE",
|
|
16199
|
+
`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`
|
|
16200
|
+
);
|
|
15769
16201
|
}
|
|
15770
16202
|
}
|
|
15771
16203
|
for (const link of links) {
|
|
15772
16204
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
15773
16205
|
}
|
|
15774
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
16206
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join20(dest, "node_modules"), "dir");
|
|
15775
16207
|
}
|
|
15776
16208
|
function workingTreePaths(cwd) {
|
|
15777
|
-
return
|
|
16209
|
+
return git3(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
15778
16210
|
}
|
|
15779
16211
|
function probeFilesystemCaseSensitivity(dir) {
|
|
15780
|
-
const upper =
|
|
15781
|
-
const lower =
|
|
16212
|
+
const upper = join20(dir, ".tw-case-probe-A");
|
|
16213
|
+
const lower = join20(dir, ".tw-case-probe-a");
|
|
15782
16214
|
try {
|
|
15783
|
-
|
|
16215
|
+
writeFileSync10(upper, "", { flag: "wx" });
|
|
15784
16216
|
return !existsSync14(lower);
|
|
15785
16217
|
} finally {
|
|
15786
|
-
|
|
16218
|
+
rmSync8(upper, { force: true });
|
|
15787
16219
|
}
|
|
15788
16220
|
}
|
|
15789
16221
|
function foldCase(p) {
|
|
@@ -15801,7 +16233,7 @@ function caseFoldCollision(paths) {
|
|
|
15801
16233
|
}
|
|
15802
16234
|
function dropSymlink(p) {
|
|
15803
16235
|
try {
|
|
15804
|
-
if (lstatSync9(p).isSymbolicLink())
|
|
16236
|
+
if (lstatSync9(p).isSymbolicLink()) rmSync8(p, { force: true });
|
|
15805
16237
|
} catch {
|
|
15806
16238
|
}
|
|
15807
16239
|
}
|
|
@@ -15830,22 +16262,22 @@ function verifierCoveredInputs(cmd, atBase, policy) {
|
|
|
15830
16262
|
return covered;
|
|
15831
16263
|
}
|
|
15832
16264
|
function baseEntries(base, cwd) {
|
|
15833
|
-
return
|
|
16265
|
+
return git3(["ls-tree", "-r", "-z", base], cwd).split("\0").filter(Boolean).map((rec) => {
|
|
15834
16266
|
const tab = rec.indexOf(" ");
|
|
15835
16267
|
const [mode, type] = rec.slice(0, tab).split(" ");
|
|
15836
16268
|
return { mode, type, path: rec.slice(tab + 1) };
|
|
15837
16269
|
});
|
|
15838
16270
|
}
|
|
15839
16271
|
function overlayDigest(dest, paths) {
|
|
15840
|
-
const h =
|
|
16272
|
+
const h = createHash13("sha256");
|
|
15841
16273
|
for (const rel of [...paths].sort()) {
|
|
15842
16274
|
h.update(rel);
|
|
15843
16275
|
h.update("\0");
|
|
15844
|
-
const p =
|
|
16276
|
+
const p = join20(dest, rel);
|
|
15845
16277
|
try {
|
|
15846
16278
|
const st = lstatSync9(p);
|
|
15847
16279
|
h.update(String(st.mode));
|
|
15848
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
16280
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync17(p));
|
|
15849
16281
|
} catch {
|
|
15850
16282
|
h.update("<gone>");
|
|
15851
16283
|
}
|
|
@@ -15874,16 +16306,16 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
15874
16306
|
if (e.type === "commit") {
|
|
15875
16307
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
15876
16308
|
}
|
|
15877
|
-
const content =
|
|
15878
|
-
const out3 =
|
|
15879
|
-
|
|
15880
|
-
|
|
16309
|
+
const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
16310
|
+
const out3 = join20(dest, e.path);
|
|
16311
|
+
mkdirSync9(dirname9(out3), { recursive: true });
|
|
16312
|
+
rmSync8(out3, { force: true });
|
|
15881
16313
|
if (e.mode === "120000") {
|
|
15882
16314
|
const target = content.toString("utf8");
|
|
15883
16315
|
safeSymlink(target, out3, dest, `the base's ${e.path}`);
|
|
15884
16316
|
restoredLinks.push({ path: e.path, out: out3, target });
|
|
15885
16317
|
} else {
|
|
15886
|
-
|
|
16318
|
+
writeFileSync10(out3, content);
|
|
15887
16319
|
chmodSync2(out3, parseInt(e.mode.slice(-4), 8) & 511);
|
|
15888
16320
|
}
|
|
15889
16321
|
restored.push(e.path);
|
|
@@ -15892,7 +16324,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
15892
16324
|
const inCopy = workingTreePaths(cwd);
|
|
15893
16325
|
for (const rel of inCopy) {
|
|
15894
16326
|
if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
|
|
15895
|
-
|
|
16327
|
+
rmSync8(join20(dest, rel), { force: true });
|
|
15896
16328
|
removed++;
|
|
15897
16329
|
}
|
|
15898
16330
|
for (const link of restoredLinks) {
|
|
@@ -15906,9 +16338,9 @@ function suiteEnv(scratch) {
|
|
|
15906
16338
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
15907
16339
|
env[k] = v;
|
|
15908
16340
|
}
|
|
15909
|
-
const userRc =
|
|
15910
|
-
const globalRc =
|
|
15911
|
-
for (const f of [userRc, globalRc])
|
|
16341
|
+
const userRc = join20(scratch, "npmrc-user");
|
|
16342
|
+
const globalRc = join20(scratch, "npmrc-global");
|
|
16343
|
+
for (const f of [userRc, globalRc]) writeFileSync10(f, "", { mode: 292 });
|
|
15912
16344
|
env.npm_config_userconfig = userRc;
|
|
15913
16345
|
env.npm_config_globalconfig = globalRc;
|
|
15914
16346
|
env.npm_config_node_options = " ";
|
|
@@ -15939,7 +16371,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
15939
16371
|
reason: `checkpointed-local verifier is unsupported on ${process.platform}; no trusted local shell contract is defined`
|
|
15940
16372
|
};
|
|
15941
16373
|
}
|
|
15942
|
-
const scratch = mkdtempSync3(
|
|
16374
|
+
const scratch = mkdtempSync3(join20(tmpdir4(), "tw-verify-run-"));
|
|
15943
16375
|
try {
|
|
15944
16376
|
const r = runCapturedProcessSync(shell.executable, shell.args, {
|
|
15945
16377
|
cwd: dir,
|
|
@@ -15984,7 +16416,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
15984
16416
|
diagnostics: r.diagnostics
|
|
15985
16417
|
};
|
|
15986
16418
|
} finally {
|
|
15987
|
-
|
|
16419
|
+
rmSync8(scratch, { recursive: true, force: true });
|
|
15988
16420
|
}
|
|
15989
16421
|
}
|
|
15990
16422
|
function diagnosticsJson(diagnostics, includeTail) {
|
|
@@ -16029,6 +16461,15 @@ function verifyVerdictLine(verdict2, ctx) {
|
|
|
16029
16461
|
}
|
|
16030
16462
|
}
|
|
16031
16463
|
function runVerify(opts) {
|
|
16464
|
+
const markerCwd = repoRoot(opts.cwd ?? process.cwd());
|
|
16465
|
+
const marked = opts.silent ? false : beginVerifying(markerCwd);
|
|
16466
|
+
try {
|
|
16467
|
+
return runVerifyImpl(opts);
|
|
16468
|
+
} finally {
|
|
16469
|
+
if (marked) endVerifying(markerCwd);
|
|
16470
|
+
}
|
|
16471
|
+
}
|
|
16472
|
+
function runVerifyImpl(opts) {
|
|
16032
16473
|
const cwd = repoRoot(opts.cwd ?? process.cwd());
|
|
16033
16474
|
const colour = opts.silent ? false : colourEnabled(process.env, process.stdout);
|
|
16034
16475
|
const paintVerify = (s) => {
|
|
@@ -16068,7 +16509,7 @@ function runVerify(opts) {
|
|
|
16068
16509
|
return cannotVerify("POLICY_ERROR", `cannot load policy (${e instanceof Error ? e.message : String(e)})`);
|
|
16069
16510
|
}
|
|
16070
16511
|
if (opts.requireAncestor) {
|
|
16071
|
-
const requested =
|
|
16512
|
+
const requested = git3(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
|
|
16072
16513
|
if (!baseIsAncestorOfHead(requested, cwd)) {
|
|
16073
16514
|
return cannotVerify(
|
|
16074
16515
|
"BASE_NOT_ANCESTOR",
|
|
@@ -16176,17 +16617,17 @@ function runVerify(opts) {
|
|
|
16176
16617
|
} catch {
|
|
16177
16618
|
return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
|
|
16178
16619
|
}
|
|
16179
|
-
const visRoot = mkdtempSync3(
|
|
16180
|
-
const visDir =
|
|
16620
|
+
const visRoot = mkdtempSync3(join20(tmpdir4(), "tw-verify-vis-"));
|
|
16621
|
+
const visDir = join20(visRoot, "t");
|
|
16181
16622
|
const cleanup = (dirs) => {
|
|
16182
16623
|
if (opts.keep) return;
|
|
16183
|
-
for (const d of dirs)
|
|
16624
|
+
for (const d of dirs) rmSync8(d, { recursive: true, force: true });
|
|
16184
16625
|
};
|
|
16185
16626
|
filesystemCaseSensitive = (opts.probeCaseSensitivity ?? probeFilesystemCaseSensitivity)(visRoot);
|
|
16186
16627
|
if (!filesystemCaseSensitive) {
|
|
16187
16628
|
const present = (rel) => {
|
|
16188
16629
|
try {
|
|
16189
|
-
lstatSync9(
|
|
16630
|
+
lstatSync9(join20(cwd, rel));
|
|
16190
16631
|
return true;
|
|
16191
16632
|
} catch {
|
|
16192
16633
|
return false;
|
|
@@ -16218,14 +16659,14 @@ function runVerify(opts) {
|
|
|
16218
16659
|
);
|
|
16219
16660
|
}
|
|
16220
16661
|
try {
|
|
16221
|
-
|
|
16662
|
+
mkdirSync9(visDir);
|
|
16222
16663
|
materialize(cwd, visDir, frozenNodeModules);
|
|
16223
16664
|
} catch (e) {
|
|
16224
16665
|
cleanup([visRoot]);
|
|
16225
16666
|
return cannotVerify(
|
|
16226
16667
|
"MATERIALIZATION_FAILED",
|
|
16227
16668
|
`could not materialize (${e instanceof Error ? e.message : String(e)})`,
|
|
16228
|
-
{ verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
|
|
16669
|
+
{ verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport(), materialization_reason: materializationFailureReason(e) }
|
|
16229
16670
|
);
|
|
16230
16671
|
}
|
|
16231
16672
|
const visible = runStage(visDir);
|
|
@@ -16259,6 +16700,49 @@ function runVerify(opts) {
|
|
|
16259
16700
|
}
|
|
16260
16701
|
return 2;
|
|
16261
16702
|
}
|
|
16703
|
+
if (visible.failure === "budget") {
|
|
16704
|
+
const pristineNotRun = {
|
|
16705
|
+
exit: null,
|
|
16706
|
+
secs: 0,
|
|
16707
|
+
failure: "budget",
|
|
16708
|
+
reason: "not run: visible stage exceeded its budget"
|
|
16709
|
+
};
|
|
16710
|
+
cleanup([visRoot]);
|
|
16711
|
+
opts.onVerdict?.({ verdict: "BUDGET_EXCEEDED" });
|
|
16712
|
+
if (opts.json) {
|
|
16713
|
+
out3(JSON.stringify({
|
|
16714
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
16715
|
+
verdict: "BUDGET_EXCEEDED",
|
|
16716
|
+
base,
|
|
16717
|
+
command: cmd,
|
|
16718
|
+
budget_secs: budget,
|
|
16719
|
+
visible: stageJson(visible),
|
|
16720
|
+
pristine: stageJson(pristineNotRun),
|
|
16721
|
+
protected_restored: 0,
|
|
16722
|
+
added_protected_removed: 0,
|
|
16723
|
+
filesystem_case_sensitive: filesystemCaseSensitive,
|
|
16724
|
+
verifier_backend: backendReport(),
|
|
16725
|
+
dependency_environment: dependencyReport(),
|
|
16726
|
+
oracle_assurance: oracleAssuranceReport(),
|
|
16727
|
+
...opts.keep ? { visible_dir: visDir } : {}
|
|
16728
|
+
}));
|
|
16729
|
+
} else {
|
|
16730
|
+
out3(`tamperward verify \u2014 ${verifyVerdictLine("BUDGET_EXCEEDED", {
|
|
16731
|
+
restored: 0,
|
|
16732
|
+
base,
|
|
16733
|
+
visibleExit: visible.exit,
|
|
16734
|
+
pristineExit: pristineNotRun.exit,
|
|
16735
|
+
budget
|
|
16736
|
+
})}`);
|
|
16737
|
+
out3("verifier backend: " + verifierBackendSummary(verifierBackend));
|
|
16738
|
+
out3("oracle assurance: suite-exit-only (candidate source executes inside the suite process; execution-domain isolation is not semantic/oracle isolation)");
|
|
16739
|
+
out3(
|
|
16740
|
+
dependencyEnvironment ? "dependency environment: " + dependencyEnvironmentSummary(dependencyEnvironment) : "dependency environment: verifier-owned by " + (verifierBackend.image ?? "isolated image")
|
|
16741
|
+
);
|
|
16742
|
+
renderStageDiagnostics(out3, "visible", visible);
|
|
16743
|
+
}
|
|
16744
|
+
return 2;
|
|
16745
|
+
}
|
|
16262
16746
|
if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
|
|
16263
16747
|
cleanup([visRoot]);
|
|
16264
16748
|
return cannotVerify(
|
|
@@ -16276,12 +16760,12 @@ function runVerify(opts) {
|
|
|
16276
16760
|
{ stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
|
|
16277
16761
|
);
|
|
16278
16762
|
}
|
|
16279
|
-
const priRoot = mkdtempSync3(
|
|
16280
|
-
const priDir =
|
|
16763
|
+
const priRoot = mkdtempSync3(join20(tmpdir4(), "tw-verify-pri-"));
|
|
16764
|
+
const priDir = join20(priRoot, "t");
|
|
16281
16765
|
let restored = [];
|
|
16282
16766
|
let removedAdded = 0;
|
|
16283
16767
|
try {
|
|
16284
|
-
|
|
16768
|
+
mkdirSync9(priDir);
|
|
16285
16769
|
materialize(cwd, priDir, frozenNodeModules);
|
|
16286
16770
|
({ restored, removed: removedAdded } = overlayPristine(
|
|
16287
16771
|
cwd,
|
|
@@ -16297,7 +16781,7 @@ function runVerify(opts) {
|
|
|
16297
16781
|
return cannotVerify(
|
|
16298
16782
|
"MATERIALIZATION_FAILED",
|
|
16299
16783
|
`could not materialize pristine copy (${e instanceof Error ? e.message : String(e)})`,
|
|
16300
|
-
{ stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
|
|
16784
|
+
{ stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport(), materialization_reason: materializationFailureReason(e) }
|
|
16301
16785
|
);
|
|
16302
16786
|
}
|
|
16303
16787
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
@@ -16367,6 +16851,25 @@ function runVerify(opts) {
|
|
|
16367
16851
|
code2 = 1;
|
|
16368
16852
|
}
|
|
16369
16853
|
opts.onVerdict?.({ verdict: verdict2 });
|
|
16854
|
+
if (verdict2 === "VERIFIED") {
|
|
16855
|
+
try {
|
|
16856
|
+
const inputs = {
|
|
16857
|
+
base_ref: opts.base ?? "HEAD",
|
|
16858
|
+
explicit_base: opts.base !== void 0,
|
|
16859
|
+
command_source: opts.cmd !== void 0 ? "flag" : "policy",
|
|
16860
|
+
command: cmd,
|
|
16861
|
+
budget_source: opts.budget !== void 0 ? "flag" : "policy",
|
|
16862
|
+
budget
|
|
16863
|
+
};
|
|
16864
|
+
recordVerification(cwd, inputs, treeBefore);
|
|
16865
|
+
} catch {
|
|
16866
|
+
}
|
|
16867
|
+
} else {
|
|
16868
|
+
try {
|
|
16869
|
+
invalidateVerificationRecordIfCurrent(cwd);
|
|
16870
|
+
} catch {
|
|
16871
|
+
}
|
|
16872
|
+
}
|
|
16370
16873
|
const signedOff = verdict2 === "MASKED_FAILURE" ? oobToken("verify", oobFromEnv(), oobHeadFromEnv()) : null;
|
|
16371
16874
|
if (signedOff) code2 = 0;
|
|
16372
16875
|
if (opts.json) {
|
|
@@ -16458,7 +16961,7 @@ function parseVerify(args) {
|
|
|
16458
16961
|
}
|
|
16459
16962
|
return o;
|
|
16460
16963
|
}
|
|
16461
|
-
var OVERLAY_CLASSES, VERIFICATION_SURFACE, DROPPED_ENV, PINNED_NPM;
|
|
16964
|
+
var OVERLAY_CLASSES, VERIFICATION_SURFACE, MaterializationError, DROPPED_ENV, PINNED_NPM;
|
|
16462
16965
|
var init_verify = __esm({
|
|
16463
16966
|
"src/cli/verify.ts"() {
|
|
16464
16967
|
"use strict";
|
|
@@ -16472,6 +16975,7 @@ var init_verify = __esm({
|
|
|
16472
16975
|
init_verifier_backend();
|
|
16473
16976
|
init_machine_output();
|
|
16474
16977
|
init_signoff();
|
|
16978
|
+
init_verification_state();
|
|
16475
16979
|
init_suite_diagnostics();
|
|
16476
16980
|
init_text();
|
|
16477
16981
|
init_status();
|
|
@@ -16546,6 +17050,14 @@ var init_verify = __esm({
|
|
|
16546
17050
|
"**/phpunit.xml.dist",
|
|
16547
17051
|
"**/*.runsettings"
|
|
16548
17052
|
];
|
|
17053
|
+
MaterializationError = class extends Error {
|
|
17054
|
+
constructor(materializationReason, message) {
|
|
17055
|
+
super(message);
|
|
17056
|
+
this.materializationReason = materializationReason;
|
|
17057
|
+
this.name = "MaterializationError";
|
|
17058
|
+
}
|
|
17059
|
+
materializationReason;
|
|
17060
|
+
};
|
|
16549
17061
|
DROPPED_ENV = /* @__PURE__ */ new Set([
|
|
16550
17062
|
"NODE_OPTIONS",
|
|
16551
17063
|
"NODE_PATH",
|
|
@@ -16560,11 +17072,11 @@ var init_verify = __esm({
|
|
|
16560
17072
|
});
|
|
16561
17073
|
|
|
16562
17074
|
// src/cli/run.ts
|
|
16563
|
-
import { execFileSync as
|
|
17075
|
+
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
16564
17076
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
16565
|
-
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as
|
|
17077
|
+
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync18, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync9, statSync as statSync5, writeFileSync as writeFileSync11 } from "node:fs";
|
|
16566
17078
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
16567
|
-
import { dirname as
|
|
17079
|
+
import { dirname as dirname10, join as join21, resolve as resolve10 } from "node:path";
|
|
16568
17080
|
function authoritativeRunLifecyclePlatform(platform = process.platform) {
|
|
16569
17081
|
return platform === "linux";
|
|
16570
17082
|
}
|
|
@@ -16601,7 +17113,7 @@ function writableByCaller2(path) {
|
|
|
16601
17113
|
return true;
|
|
16602
17114
|
} catch {
|
|
16603
17115
|
}
|
|
16604
|
-
const parent =
|
|
17116
|
+
const parent = dirname10(cur);
|
|
16605
17117
|
if (parent === cur) break;
|
|
16606
17118
|
cur = parent;
|
|
16607
17119
|
}
|
|
@@ -16640,9 +17152,9 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
|
|
|
16640
17152
|
};
|
|
16641
17153
|
}
|
|
16642
17154
|
function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
|
|
16643
|
-
const
|
|
16644
|
-
const resultFile =
|
|
16645
|
-
const agentEnvFile =
|
|
17155
|
+
const stateDir2 = mkdtempSync4(join21(tmpdir5(), "tw-agent-supervisor-"));
|
|
17156
|
+
const resultFile = join21(stateDir2, "result.json");
|
|
17157
|
+
const agentEnvFile = join21(stateDir2, "agent-env.json");
|
|
16646
17158
|
try {
|
|
16647
17159
|
const linux = process.platform === "linux";
|
|
16648
17160
|
let executable;
|
|
@@ -16659,7 +17171,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
16659
17171
|
failure: trustedPython.reason ?? "trusted Linux python3 interpreter is unavailable"
|
|
16660
17172
|
};
|
|
16661
17173
|
}
|
|
16662
|
-
|
|
17174
|
+
writeFileSync11(agentEnvFile, JSON.stringify(process.env), { mode: 384 });
|
|
16663
17175
|
executable = trustedPython.path;
|
|
16664
17176
|
args = [
|
|
16665
17177
|
"-I",
|
|
@@ -16716,7 +17228,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
16716
17228
|
}
|
|
16717
17229
|
let state = {};
|
|
16718
17230
|
try {
|
|
16719
|
-
state = JSON.parse(
|
|
17231
|
+
state = JSON.parse(readFileSync18(resultFile, "utf8"));
|
|
16720
17232
|
} catch {
|
|
16721
17233
|
return {
|
|
16722
17234
|
exit: 1,
|
|
@@ -16741,13 +17253,13 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
16741
17253
|
...state.failure ? { failure: state.failure } : {}
|
|
16742
17254
|
};
|
|
16743
17255
|
} finally {
|
|
16744
|
-
|
|
17256
|
+
rmSync9(stateDir2, { recursive: true, force: true });
|
|
16745
17257
|
}
|
|
16746
17258
|
}
|
|
16747
17259
|
function waitMs(ms) {
|
|
16748
17260
|
Atomics.wait(waitCell, 0, 0, ms);
|
|
16749
17261
|
}
|
|
16750
|
-
function
|
|
17262
|
+
function pidAlive4(pid) {
|
|
16751
17263
|
if (!pid || !Number.isInteger(pid) || pid <= 0) return false;
|
|
16752
17264
|
try {
|
|
16753
17265
|
process.kill(pid, 0);
|
|
@@ -16757,10 +17269,10 @@ function pidAlive3(pid) {
|
|
|
16757
17269
|
}
|
|
16758
17270
|
}
|
|
16759
17271
|
function observerExited(pid) {
|
|
16760
|
-
if (!
|
|
17272
|
+
if (!pidAlive4(pid)) return true;
|
|
16761
17273
|
if (process.platform !== "linux") return false;
|
|
16762
17274
|
try {
|
|
16763
|
-
const stat =
|
|
17275
|
+
const stat = readFileSync18(`/proc/${pid}/stat`, "utf8");
|
|
16764
17276
|
const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0];
|
|
16765
17277
|
return state === "Z" || state === "X";
|
|
16766
17278
|
} catch (e) {
|
|
@@ -16768,8 +17280,8 @@ function observerExited(pid) {
|
|
|
16768
17280
|
}
|
|
16769
17281
|
}
|
|
16770
17282
|
function supervisedObserverLog(cwd) {
|
|
16771
|
-
const gd = gitDir(cwd) ??
|
|
16772
|
-
return
|
|
17283
|
+
const gd = gitDir(cwd) ?? join21(cwd, ".git");
|
|
17284
|
+
return join21(
|
|
16773
17285
|
gd,
|
|
16774
17286
|
"tamperward",
|
|
16775
17287
|
`run-observer-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}.jsonl`
|
|
@@ -16783,7 +17295,7 @@ function startSupervisedObserver(cwd, base, entry) {
|
|
|
16783
17295
|
try {
|
|
16784
17296
|
child = spawn(
|
|
16785
17297
|
process.execPath,
|
|
16786
|
-
[
|
|
17298
|
+
[resolve10(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
|
|
16787
17299
|
{
|
|
16788
17300
|
cwd,
|
|
16789
17301
|
stdio: ["ignore", "ignore", "inherit"],
|
|
@@ -16877,12 +17389,12 @@ function finishObserverAdvisory(observer, write = out) {
|
|
|
16877
17389
|
const telemetry = stopObserverProcess(observer);
|
|
16878
17390
|
observerSummary(observer, telemetry, write);
|
|
16879
17391
|
}
|
|
16880
|
-
function
|
|
16881
|
-
return
|
|
17392
|
+
function git4(args, cwd) {
|
|
17393
|
+
return execFileSync10("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
16882
17394
|
}
|
|
16883
17395
|
function nowTicks() {
|
|
16884
17396
|
try {
|
|
16885
|
-
return Math.floor(parseFloat(
|
|
17397
|
+
return Math.floor(parseFloat(readFileSync18("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
16886
17398
|
} catch {
|
|
16887
17399
|
return Number.POSITIVE_INFINITY;
|
|
16888
17400
|
}
|
|
@@ -16902,10 +17414,10 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
16902
17414
|
return out3;
|
|
16903
17415
|
}
|
|
16904
17416
|
for (const pid of pids) {
|
|
16905
|
-
const
|
|
16906
|
-
if (
|
|
17417
|
+
const n2 = Number(pid);
|
|
17418
|
+
if (n2 === process.pid) continue;
|
|
16907
17419
|
try {
|
|
16908
|
-
const stat =
|
|
17420
|
+
const stat = readFileSync18(`/proc/${pid}/stat`, "utf8");
|
|
16909
17421
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
16910
17422
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
16911
17423
|
} catch {
|
|
@@ -16920,7 +17432,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
16920
17432
|
for (const link of procLinks) {
|
|
16921
17433
|
try {
|
|
16922
17434
|
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
16923
|
-
const held = raw.startsWith("/") ?
|
|
17435
|
+
const held = raw.startsWith("/") ? resolve10(raw) : raw;
|
|
16924
17436
|
if (held === real || held.startsWith(real + "/")) {
|
|
16925
17437
|
holds = true;
|
|
16926
17438
|
break;
|
|
@@ -16928,12 +17440,12 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
16928
17440
|
} catch {
|
|
16929
17441
|
}
|
|
16930
17442
|
}
|
|
16931
|
-
if (holds) out3.push(
|
|
17443
|
+
if (holds) out3.push(n2);
|
|
16932
17444
|
}
|
|
16933
17445
|
return out3;
|
|
16934
17446
|
}
|
|
16935
17447
|
function runEnvelope(opts) {
|
|
16936
|
-
const cwd =
|
|
17448
|
+
const cwd = resolve10(opts.cwd ?? process.cwd());
|
|
16937
17449
|
const say = opts.json ? (_s) => {
|
|
16938
17450
|
} : out;
|
|
16939
17451
|
if (!opts.argv.length) {
|
|
@@ -16948,7 +17460,8 @@ function runEnvelope(opts) {
|
|
|
16948
17460
|
}
|
|
16949
17461
|
let base;
|
|
16950
17462
|
try {
|
|
16951
|
-
|
|
17463
|
+
const requestedBase = assertRev(opts.base ?? "HEAD");
|
|
17464
|
+
base = git4(["rev-parse", "--verify", `${requestedBase}^{commit}`], cwd).trim();
|
|
16952
17465
|
} catch {
|
|
16953
17466
|
err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
|
|
16954
17467
|
return 2;
|
|
@@ -16970,7 +17483,7 @@ function runEnvelope(opts) {
|
|
|
16970
17483
|
return 2;
|
|
16971
17484
|
}
|
|
16972
17485
|
if (!opts.allowDirty) {
|
|
16973
|
-
const dirty =
|
|
17486
|
+
const dirty = git4(["status", "--porcelain"], cwd).trim();
|
|
16974
17487
|
if (dirty) {
|
|
16975
17488
|
err("tamperward run: working tree is dirty before the agent starts \u2014 the envelope cannot");
|
|
16976
17489
|
err("attribute changes. Commit or stash first, or pass --allow-dirty to own the risk.");
|
|
@@ -17075,7 +17588,7 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
|
|
|
17075
17588
|
emitRunJson("OBJECT_REWRITE", 1);
|
|
17076
17589
|
return 1;
|
|
17077
17590
|
}
|
|
17078
|
-
const head =
|
|
17591
|
+
const head = git4(["rev-parse", "HEAD"], cwd).trim();
|
|
17079
17592
|
const isAncestor = spawnSync4("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
|
|
17080
17593
|
if (isAncestor.status !== 0) {
|
|
17081
17594
|
err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
@@ -17476,9 +17989,9 @@ raise SystemExit(0)
|
|
|
17476
17989
|
});
|
|
17477
17990
|
|
|
17478
17991
|
// src/cli/doctor.ts
|
|
17479
|
-
import { execFileSync as
|
|
17480
|
-
import { existsSync as existsSync15, readFileSync as
|
|
17481
|
-
import { join as
|
|
17992
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
17993
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19, readdirSync as readdirSync5 } from "node:fs";
|
|
17994
|
+
import { join as join22, resolve as resolve11 } from "node:path";
|
|
17482
17995
|
function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
|
|
17483
17996
|
if (platform === "linux") {
|
|
17484
17997
|
if (linuxPython?.path) {
|
|
@@ -17536,7 +18049,7 @@ function pinsInFile(path) {
|
|
|
17536
18049
|
if (!existsSync15(path)) return [];
|
|
17537
18050
|
let src = "";
|
|
17538
18051
|
try {
|
|
17539
|
-
src =
|
|
18052
|
+
src = readFileSync19(path, "utf8");
|
|
17540
18053
|
} catch {
|
|
17541
18054
|
return [];
|
|
17542
18055
|
}
|
|
@@ -17556,14 +18069,14 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
|
|
|
17556
18069
|
const broken = [];
|
|
17557
18070
|
const warnings = [];
|
|
17558
18071
|
for (const rel of workflowRels) {
|
|
17559
|
-
const path =
|
|
18072
|
+
const path = resolve11(cwd, rel);
|
|
17560
18073
|
if (!existsSync15(path)) {
|
|
17561
18074
|
broken.push(`${rel} is missing`);
|
|
17562
18075
|
continue;
|
|
17563
18076
|
}
|
|
17564
18077
|
let doc;
|
|
17565
18078
|
try {
|
|
17566
|
-
doc = yaml.parse(
|
|
18079
|
+
doc = yaml.parse(readFileSync19(path, "utf8"));
|
|
17567
18080
|
} catch (e) {
|
|
17568
18081
|
broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
|
|
17569
18082
|
continue;
|
|
@@ -17676,9 +18189,9 @@ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow =
|
|
|
17676
18189
|
addWiring("codeowners", "codeowners", true);
|
|
17677
18190
|
checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
|
|
17678
18191
|
const pins = [
|
|
17679
|
-
...pinsInFile(
|
|
17680
|
-
...pinsInFile(
|
|
17681
|
-
...pinsInFile(
|
|
18192
|
+
...pinsInFile(join22(cwd, ".claude", "settings.json")),
|
|
18193
|
+
...pinsInFile(join22(cwd, ".git", "hooks", "pre-commit")),
|
|
18194
|
+
...pinsInFile(join22(cwd, ".husky", "pre-commit"))
|
|
17682
18195
|
];
|
|
17683
18196
|
const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
|
|
17684
18197
|
const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
|
|
@@ -17796,7 +18309,7 @@ function evaluateGitHubProtection(snapshot2, requiredCheck = "tamperward", requi
|
|
|
17796
18309
|
}
|
|
17797
18310
|
function gitText(cwd, args) {
|
|
17798
18311
|
try {
|
|
17799
|
-
return
|
|
18312
|
+
return execFileSync11("git", args, {
|
|
17800
18313
|
cwd,
|
|
17801
18314
|
encoding: "utf8",
|
|
17802
18315
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -17830,7 +18343,7 @@ function githubApi(cwd, endpoint) {
|
|
|
17830
18343
|
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? "";
|
|
17831
18344
|
const invocation = githubApiInvocation(endpoint, token);
|
|
17832
18345
|
try {
|
|
17833
|
-
const stdout =
|
|
18346
|
+
const stdout = execFileSync11(
|
|
17834
18347
|
invocation.executable,
|
|
17835
18348
|
invocation.args,
|
|
17836
18349
|
{
|
|
@@ -17937,7 +18450,7 @@ function verifyJobs(doc) {
|
|
|
17937
18450
|
return found;
|
|
17938
18451
|
}
|
|
17939
18452
|
function diagnose(opts = {}) {
|
|
17940
|
-
const cwd = repoRoot(
|
|
18453
|
+
const cwd = repoRoot(resolve11(opts.cwd ?? process.cwd()));
|
|
17941
18454
|
let policy;
|
|
17942
18455
|
try {
|
|
17943
18456
|
policy = policyFor(opts, cwd);
|
|
@@ -17961,7 +18474,7 @@ function diagnose(opts = {}) {
|
|
|
17961
18474
|
workflowRels.push(opts.workflow);
|
|
17962
18475
|
} else {
|
|
17963
18476
|
const workflowDirRel = ".github/workflows";
|
|
17964
|
-
const workflowDir =
|
|
18477
|
+
const workflowDir = resolve11(cwd, workflowDirRel);
|
|
17965
18478
|
if (!existsSync15(workflowDir)) {
|
|
17966
18479
|
return fail("ci-verifier", `${workflowDirRel}: workflow directory does not exist`);
|
|
17967
18480
|
}
|
|
@@ -17974,20 +18487,20 @@ function diagnose(opts = {}) {
|
|
|
17974
18487
|
`${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
|
|
17975
18488
|
);
|
|
17976
18489
|
}
|
|
17977
|
-
for (const name of entries) workflowRels.push(
|
|
18490
|
+
for (const name of entries) workflowRels.push(join22(workflowDirRel, name));
|
|
17978
18491
|
}
|
|
17979
18492
|
const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
|
|
17980
18493
|
const requiredMinutes = Math.ceil(requiredSecs / 60);
|
|
17981
18494
|
let verifyJobCount = 0;
|
|
17982
18495
|
const authorityWorkflows = [];
|
|
17983
18496
|
for (const workflowRel of workflowRels) {
|
|
17984
|
-
const workflowPath =
|
|
18497
|
+
const workflowPath = resolve11(cwd, workflowRel);
|
|
17985
18498
|
if (!existsSync15(workflowPath)) {
|
|
17986
18499
|
return fail("ci-verifier", `${workflowRel}: workflow does not exist`);
|
|
17987
18500
|
}
|
|
17988
18501
|
let doc;
|
|
17989
18502
|
try {
|
|
17990
|
-
doc = yaml.parse(
|
|
18503
|
+
doc = yaml.parse(readFileSync19(workflowPath, "utf8"));
|
|
17991
18504
|
} catch (e) {
|
|
17992
18505
|
return fail(
|
|
17993
18506
|
"ci-verifier",
|
|
@@ -18161,39 +18674,273 @@ var init_doctor = __esm({
|
|
|
18161
18674
|
}
|
|
18162
18675
|
});
|
|
18163
18676
|
|
|
18677
|
+
// src/cli/status.ts
|
|
18678
|
+
function authorityFromChecks(checks) {
|
|
18679
|
+
if (checks.some((c) => c.state === "BROKEN")) return "BROKEN";
|
|
18680
|
+
if (checks.some((c) => c.state === "WARN")) return "PARTIAL";
|
|
18681
|
+
return "ACTIVE";
|
|
18682
|
+
}
|
|
18683
|
+
function interventionFromCheck(check2) {
|
|
18684
|
+
if (!check2) return { state: "UNKNOWN", detail: "agent hook wiring could not be evaluated" };
|
|
18685
|
+
const map = {
|
|
18686
|
+
OK: "ACTIVE",
|
|
18687
|
+
WARN: "PARTIAL",
|
|
18688
|
+
BROKEN: "INACTIVE"
|
|
18689
|
+
};
|
|
18690
|
+
return { state: map[check2.state], detail: check2.detail };
|
|
18691
|
+
}
|
|
18692
|
+
function buildStatusModel(cwd) {
|
|
18693
|
+
let authority;
|
|
18694
|
+
let intervention;
|
|
18695
|
+
try {
|
|
18696
|
+
const outcome = diagnose({ cwd });
|
|
18697
|
+
authority = { state: authorityFromChecks(outcome.checks) };
|
|
18698
|
+
const summary = outcome.checks.filter((c) => c.state !== "OK").map((c) => `${c.id}: ${c.state}`);
|
|
18699
|
+
if (summary.length) authority.detail = summary.join("; ");
|
|
18700
|
+
intervention = interventionFromCheck(outcome.checks.find((c) => c.id === "claude-hooks"));
|
|
18701
|
+
} catch (e) {
|
|
18702
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
18703
|
+
authority = { state: "UNKNOWN", detail };
|
|
18704
|
+
intervention = { state: "UNKNOWN", detail };
|
|
18705
|
+
}
|
|
18706
|
+
const verification = evaluateVerificationState(cwd);
|
|
18707
|
+
const agent = intervention.state === "ACTIVE" || intervention.state === "PARTIAL" ? "claude-code" : "none";
|
|
18708
|
+
return { authority, intervention, verification, runtime: { agent } };
|
|
18709
|
+
}
|
|
18710
|
+
function shortId(id) {
|
|
18711
|
+
return id ? `${id.slice(0, 10)}...` : void 0;
|
|
18712
|
+
}
|
|
18713
|
+
function renderStatus(model, colour) {
|
|
18714
|
+
const v = model.verification;
|
|
18715
|
+
const lines = ["TamperWard", ""];
|
|
18716
|
+
lines.push(statusLine(AUTHORITY_SEVERITY[model.authority.state], "Authority", model.authority.state, colour));
|
|
18717
|
+
lines.push(statusLine(INTERVENTION_SEVERITY[model.intervention.state], "Intervention", model.intervention.state, colour));
|
|
18718
|
+
lines.push(statusLine(VERIFICATION_SEVERITY[v.state], "Verification", v.state, colour));
|
|
18719
|
+
const rows = [];
|
|
18720
|
+
const record = v.record;
|
|
18721
|
+
if (record) {
|
|
18722
|
+
const verifiedTree = shortId(record.binding.tree);
|
|
18723
|
+
if (verifiedTree) rows.push(["Verified tree", verifiedTree]);
|
|
18724
|
+
const baseShort = shortId(v.base ?? record.binding.base);
|
|
18725
|
+
if (baseShort) rows.push(["Base", `${record.inputs.base_ref}@${baseShort}`]);
|
|
18726
|
+
if (v.verifier_command) rows.push(["Verifier", v.verifier_command]);
|
|
18727
|
+
}
|
|
18728
|
+
rows.push(["Runtime", model.runtime.agent]);
|
|
18729
|
+
if (v.state === "STALE" && v.reason) rows.push(["Reason", v.reason]);
|
|
18730
|
+
else if (v.state === "BROKEN") rows.push(["Reason", v.detail ?? v.reason ?? "authority wiring is invalid"]);
|
|
18731
|
+
else if (v.state === "VERIFYING") rows.push(["Reason", v.reason ?? "a verification is in progress"]);
|
|
18732
|
+
else if (v.state === "UNVERIFIED") rows.push(["Next", "run `tamperward verify` to establish a verified baseline"]);
|
|
18733
|
+
if (rows.length) {
|
|
18734
|
+
lines.push("");
|
|
18735
|
+
const width = Math.max(...rows.map(([k]) => k.length));
|
|
18736
|
+
for (const [k, val] of rows) lines.push(`${k.padEnd(width)} ${val}`);
|
|
18737
|
+
}
|
|
18738
|
+
return lines.join("\n") + "\n";
|
|
18739
|
+
}
|
|
18740
|
+
function statusDocument(model) {
|
|
18741
|
+
const v = model.verification;
|
|
18742
|
+
const verification = { state: v.state };
|
|
18743
|
+
if (v.reason) verification.reason = v.reason;
|
|
18744
|
+
if (v.changed_input) verification.changed_input = v.changed_input;
|
|
18745
|
+
if (v.detail) verification.detail = v.detail;
|
|
18746
|
+
if (v.verified_at) verification.verified_at = v.verified_at;
|
|
18747
|
+
if (v.base) verification.base = v.base;
|
|
18748
|
+
if (v.head) verification.head = v.head;
|
|
18749
|
+
if (v.verifier_command) verification.verifier_command = v.verifier_command;
|
|
18750
|
+
if (v.record) {
|
|
18751
|
+
verification.tree = v.record.binding.tree;
|
|
18752
|
+
verification.binding = { ...v.record.binding };
|
|
18753
|
+
}
|
|
18754
|
+
return machineOutput({
|
|
18755
|
+
command: "status",
|
|
18756
|
+
authority: model.authority,
|
|
18757
|
+
intervention: model.intervention,
|
|
18758
|
+
verification,
|
|
18759
|
+
runtime: model.runtime
|
|
18760
|
+
});
|
|
18761
|
+
}
|
|
18762
|
+
function runStatus(opts = {}) {
|
|
18763
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
18764
|
+
if (!repoContext(cwd)) {
|
|
18765
|
+
const why = outsideRepository(cwd) ?? "cwd is not inside a repository the gate can read";
|
|
18766
|
+
process.stderr.write(`tamperward: status needs a git repository (${why})
|
|
18767
|
+
`);
|
|
18768
|
+
return 2;
|
|
18769
|
+
}
|
|
18770
|
+
const model = buildStatusModel(cwd);
|
|
18771
|
+
if (opts.json) {
|
|
18772
|
+
process.stdout.write(JSON.stringify(statusDocument(model)) + "\n");
|
|
18773
|
+
} else {
|
|
18774
|
+
process.stdout.write(renderStatus(model, colourEnabled(process.env, process.stdout)));
|
|
18775
|
+
}
|
|
18776
|
+
return 0;
|
|
18777
|
+
}
|
|
18778
|
+
var AUTHORITY_SEVERITY, INTERVENTION_SEVERITY, VERIFICATION_SEVERITY;
|
|
18779
|
+
var init_status2 = __esm({
|
|
18780
|
+
"src/cli/status.ts"() {
|
|
18781
|
+
"use strict";
|
|
18782
|
+
init_machine_output();
|
|
18783
|
+
init_repo_context();
|
|
18784
|
+
init_repo_context();
|
|
18785
|
+
init_doctor();
|
|
18786
|
+
init_verification_state();
|
|
18787
|
+
init_text();
|
|
18788
|
+
init_status();
|
|
18789
|
+
AUTHORITY_SEVERITY = {
|
|
18790
|
+
ACTIVE: "ok",
|
|
18791
|
+
PARTIAL: "warn",
|
|
18792
|
+
BROKEN: "bad",
|
|
18793
|
+
UNKNOWN: "info"
|
|
18794
|
+
};
|
|
18795
|
+
INTERVENTION_SEVERITY = {
|
|
18796
|
+
ACTIVE: "ok",
|
|
18797
|
+
PARTIAL: "warn",
|
|
18798
|
+
INACTIVE: "bad",
|
|
18799
|
+
UNKNOWN: "info"
|
|
18800
|
+
};
|
|
18801
|
+
VERIFICATION_SEVERITY = {
|
|
18802
|
+
CURRENT: "ok",
|
|
18803
|
+
STALE: "warn",
|
|
18804
|
+
VERIFYING: "info",
|
|
18805
|
+
BROKEN: "bad",
|
|
18806
|
+
UNVERIFIED: "info"
|
|
18807
|
+
};
|
|
18808
|
+
}
|
|
18809
|
+
});
|
|
18810
|
+
|
|
18164
18811
|
// src/cli/trace-verify.ts
|
|
18165
|
-
import { execFileSync as
|
|
18812
|
+
import { execFileSync as execFileSync12, spawnSync as spawnSync5 } from "node:child_process";
|
|
18166
18813
|
import {
|
|
18167
18814
|
existsSync as existsSync16,
|
|
18168
18815
|
mkdtempSync as mkdtempSync5,
|
|
18169
|
-
readFileSync as
|
|
18816
|
+
readFileSync as readFileSync20,
|
|
18170
18817
|
readdirSync as readdirSync6,
|
|
18171
18818
|
realpathSync as realpathSync9,
|
|
18172
|
-
rmSync as
|
|
18819
|
+
rmSync as rmSync10,
|
|
18173
18820
|
symlinkSync as symlinkSync2
|
|
18174
18821
|
} from "node:fs";
|
|
18175
18822
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
18176
|
-
import { isAbsolute as isAbsolute12, join as
|
|
18823
|
+
import { isAbsolute as isAbsolute12, join as join23, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
|
|
18177
18824
|
function unescapeStraceString(s) {
|
|
18178
18825
|
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, "\\");
|
|
18179
18826
|
}
|
|
18180
|
-
function
|
|
18827
|
+
function copyTraceState(state) {
|
|
18828
|
+
return { cwd: state.cwd, fds: new Map(state.fds) };
|
|
18829
|
+
}
|
|
18830
|
+
function traceLineProcess(line) {
|
|
18831
|
+
const bracketed = line.match(/^\s*\[pid\s+(\d+)\]\s+(.*)$/);
|
|
18832
|
+
if (bracketed) return { pid: bracketed[1], body: bracketed[2] };
|
|
18833
|
+
const numbered = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
18834
|
+
if (numbered && /\b(?:execve|open|chdir|clone|fork|stat|access|readlink)/.test(numbered[2])) {
|
|
18835
|
+
return { pid: numbered[1], body: numbered[2] };
|
|
18836
|
+
}
|
|
18837
|
+
return { pid: "0", body: line.trim() };
|
|
18838
|
+
}
|
|
18839
|
+
function syscallResult(body) {
|
|
18840
|
+
const match = body.match(/\)\s+=\s+(-?\d+)\b/);
|
|
18841
|
+
return match ? Number(match[1]) : null;
|
|
18842
|
+
}
|
|
18843
|
+
function resolveTracePath(rawPath, state, dirfd) {
|
|
18844
|
+
if (isAbsolute12(rawPath)) return resolve12(rawPath);
|
|
18845
|
+
const base = dirfd === null || dirfd === -100 ? state.cwd : state.fds.get(dirfd);
|
|
18846
|
+
if (!base) return null;
|
|
18847
|
+
return resolve12(base, rawPath);
|
|
18848
|
+
}
|
|
18849
|
+
function firstDirFd(args) {
|
|
18850
|
+
const first = args.match(/^\s*(AT_FDCWD|-?\d+)/)?.[1];
|
|
18851
|
+
if (!first || first === "AT_FDCWD") return null;
|
|
18852
|
+
return Number(first);
|
|
18853
|
+
}
|
|
18854
|
+
function successfulFdResult(body) {
|
|
18855
|
+
const result = syscallResult(body);
|
|
18856
|
+
return result !== null && result >= 0 ? result : null;
|
|
18857
|
+
}
|
|
18858
|
+
function parseStraceFileAccess(raw, initialCwd = TRACE_ROOT) {
|
|
18181
18859
|
const out3 = [];
|
|
18182
18860
|
const seen = /* @__PURE__ */ new Set();
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
|
|
18861
|
+
const states = /* @__PURE__ */ new Map();
|
|
18862
|
+
const unfinished = /* @__PURE__ */ new Map();
|
|
18863
|
+
const stateFor = (pid) => {
|
|
18864
|
+
let state = states.get(pid);
|
|
18865
|
+
if (!state) {
|
|
18866
|
+
state = { cwd: initialCwd, fds: /* @__PURE__ */ new Map() };
|
|
18867
|
+
states.set(pid, state);
|
|
18868
|
+
}
|
|
18869
|
+
return state;
|
|
18870
|
+
};
|
|
18871
|
+
const add = (path, access, unresolved = false) => {
|
|
18872
|
+
const key2 = `${access}\0${unresolved ? "unresolved:" : ""}${path}`;
|
|
18873
|
+
if (seen.has(key2)) return;
|
|
18195
18874
|
seen.add(key2);
|
|
18196
|
-
out3.push({ path, access });
|
|
18875
|
+
out3.push(unresolved ? { path, access, unresolved: true } : { path, access });
|
|
18876
|
+
};
|
|
18877
|
+
for (const rawLine of raw.split("\n")) {
|
|
18878
|
+
if (!rawLine.trim()) continue;
|
|
18879
|
+
const line = traceLineProcess(rawLine);
|
|
18880
|
+
const resumed = line.body.match(/^<\.\.\.\s+(\w+)\s+resumed>(.*)$/);
|
|
18881
|
+
let body = line.body;
|
|
18882
|
+
if (resumed) {
|
|
18883
|
+
const key2 = `${line.pid}:${resumed[1]}`;
|
|
18884
|
+
const prefix = unfinished.get(key2);
|
|
18885
|
+
if (!prefix) continue;
|
|
18886
|
+
unfinished.delete(key2);
|
|
18887
|
+
body = prefix + resumed[2];
|
|
18888
|
+
}
|
|
18889
|
+
const unfinishedAt = body.indexOf("<unfinished ...>");
|
|
18890
|
+
if (unfinishedAt >= 0) {
|
|
18891
|
+
const before = body.slice(0, unfinishedAt);
|
|
18892
|
+
const call2 = before.match(/\b(execveat|execve|openat2|openat|open|newfstatat|fstatat|statx|lstat|stat|access|readlinkat|readlink|chdir|fchdir|close|dup2|dup3|dup|fcntl|clone3|clone|fork|vfork)\s*\(/);
|
|
18893
|
+
if (call2) unfinished.set(`${line.pid}:${call2[1]}`, before);
|
|
18894
|
+
continue;
|
|
18895
|
+
}
|
|
18896
|
+
const call = body.match(/\b(execveat|execve|openat2|openat|open|newfstatat|fstatat|statx|lstat|stat|access|readlinkat|readlink|chdir|fchdir|close|dup2|dup3|dup|fcntl|clone3|clone|fork|vfork)\s*\(/);
|
|
18897
|
+
if (!call || /\)\s+=\s+-1\b/.test(body)) continue;
|
|
18898
|
+
const name = call[1];
|
|
18899
|
+
const args = body.slice((call.index ?? 0) + call[0].length);
|
|
18900
|
+
const state = stateFor(line.pid);
|
|
18901
|
+
const result = syscallResult(body);
|
|
18902
|
+
if (name === "chdir") {
|
|
18903
|
+
const quoted2 = args.match(/\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"/);
|
|
18904
|
+
if (quoted2 && result === 0) {
|
|
18905
|
+
const next = resolveTracePath(unescapeStraceString(quoted2[1]), state, null);
|
|
18906
|
+
if (next) state.cwd = next;
|
|
18907
|
+
}
|
|
18908
|
+
continue;
|
|
18909
|
+
}
|
|
18910
|
+
if (name === "fchdir") {
|
|
18911
|
+
const fd = Number(args.match(/^\s*(-?\d+)/)?.[1]);
|
|
18912
|
+
const next = state.fds.get(fd);
|
|
18913
|
+
if (result === 0 && next) state.cwd = next;
|
|
18914
|
+
continue;
|
|
18915
|
+
}
|
|
18916
|
+
if (name === "close") {
|
|
18917
|
+
const fd = Number(args.match(/^\s*(-?\d+)/)?.[1]);
|
|
18918
|
+
if (result === 0) state.fds.delete(fd);
|
|
18919
|
+
continue;
|
|
18920
|
+
}
|
|
18921
|
+
if (name === "dup" || name === "dup2" || name === "dup3" || name === "fcntl") {
|
|
18922
|
+
const source = Number(args.match(/^\s*(-?\d+)/)?.[1]);
|
|
18923
|
+
const destination = name === "dup" ? result : name === "fcntl" ? result : Number(args.match(/^\s*-?\d+\s*,\s*(-?\d+)/)?.[1]);
|
|
18924
|
+
const sourcePath = state.fds.get(source);
|
|
18925
|
+
if (result !== null && result >= 0 && destination !== void 0 && destination !== null && sourcePath) state.fds.set(destination, sourcePath);
|
|
18926
|
+
continue;
|
|
18927
|
+
}
|
|
18928
|
+
if (name === "clone" || name === "clone3" || name === "fork" || name === "vfork") {
|
|
18929
|
+
if (result !== null && result > 0) states.set(String(result), copyTraceState(state));
|
|
18930
|
+
continue;
|
|
18931
|
+
}
|
|
18932
|
+
const quoted = args.match(/\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"/);
|
|
18933
|
+
if (!quoted) continue;
|
|
18934
|
+
const rawPath = unescapeStraceString(quoted[1]);
|
|
18935
|
+
if (!rawPath) continue;
|
|
18936
|
+
const atCall = name === "execveat" || name === "openat2" || name === "openat" || name === "newfstatat" || name === "fstatat" || name === "statx" || name === "readlinkat";
|
|
18937
|
+
const dirfd = atCall ? firstDirFd(args) : null;
|
|
18938
|
+
const resolved = resolveTracePath(rawPath, state, dirfd);
|
|
18939
|
+
add(resolved ?? rawPath, name.startsWith("execve") ? "exec" : "read", resolved === null);
|
|
18940
|
+
if (name === "open" || name === "openat" || name === "openat2") {
|
|
18941
|
+
const fd = successfulFdResult(body);
|
|
18942
|
+
if (fd !== null && resolved) state.fds.set(fd, resolved);
|
|
18943
|
+
}
|
|
18197
18944
|
}
|
|
18198
18945
|
return out3;
|
|
18199
18946
|
}
|
|
@@ -18214,13 +18961,13 @@ function classifyTraceRun(r) {
|
|
|
18214
18961
|
return { kind: "ok", exit: r.status ?? 1 };
|
|
18215
18962
|
}
|
|
18216
18963
|
function inside3(root, path) {
|
|
18217
|
-
const rel =
|
|
18218
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
18964
|
+
const rel = relative7(resolve12(root), resolve12(path));
|
|
18965
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute12(rel);
|
|
18219
18966
|
}
|
|
18220
18967
|
function normalRepoPath(root, raw) {
|
|
18221
|
-
const abs = isAbsolute12(raw) ?
|
|
18968
|
+
const abs = isAbsolute12(raw) ? resolve12(raw) : resolve12(root, raw);
|
|
18222
18969
|
if (!inside3(root, abs)) return null;
|
|
18223
|
-
const rel =
|
|
18970
|
+
const rel = relative7(root, abs).split(sep5).join("/");
|
|
18224
18971
|
return rel && rel !== ".." ? rel : null;
|
|
18225
18972
|
}
|
|
18226
18973
|
function likelyConfig(path) {
|
|
@@ -18233,12 +18980,17 @@ function sortAccesses(values) {
|
|
|
18233
18980
|
function summarizeTraceRuns(opts) {
|
|
18234
18981
|
const repo = /* @__PURE__ */ new Map();
|
|
18235
18982
|
const external = /* @__PURE__ */ new Map();
|
|
18983
|
+
let unresolved_accesses = 0;
|
|
18236
18984
|
opts.runs.forEach((run, index) => {
|
|
18237
18985
|
const perRun = /* @__PURE__ */ new Set();
|
|
18238
18986
|
for (const item of run) {
|
|
18987
|
+
if (item.unresolved) {
|
|
18988
|
+
unresolved_accesses++;
|
|
18989
|
+
continue;
|
|
18990
|
+
}
|
|
18239
18991
|
const rel = normalRepoPath(opts.root, item.path);
|
|
18240
18992
|
const isTracked = rel !== null && opts.tracked.has(rel);
|
|
18241
|
-
const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null :
|
|
18993
|
+
const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve12(opts.root, item.path);
|
|
18242
18994
|
if (keyPath === null) continue;
|
|
18243
18995
|
const target = isTracked ? repo : external;
|
|
18244
18996
|
const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
|
|
@@ -18276,11 +19028,12 @@ function summarizeTraceRuns(opts) {
|
|
|
18276
19028
|
uncovered_repository_inputs,
|
|
18277
19029
|
// Exact paths are intentionally conservative. A maintainer may widen them to
|
|
18278
19030
|
// reviewed globs, but TamperWard never invents a broader trust surface.
|
|
18279
|
-
suggested_verify_inputs: [...uncovered_repository_inputs]
|
|
19031
|
+
suggested_verify_inputs: [...uncovered_repository_inputs],
|
|
19032
|
+
unresolved_accesses
|
|
18280
19033
|
};
|
|
18281
19034
|
}
|
|
18282
|
-
function
|
|
18283
|
-
return
|
|
19035
|
+
function git5(args, cwd) {
|
|
19036
|
+
return execFileSync12("git", args, {
|
|
18284
19037
|
cwd,
|
|
18285
19038
|
encoding: "utf8",
|
|
18286
19039
|
maxBuffer: 256 * 1024 * 1024,
|
|
@@ -18293,7 +19046,7 @@ function commandExists(command) {
|
|
|
18293
19046
|
return !r.error && r.status === 0;
|
|
18294
19047
|
}
|
|
18295
19048
|
function trackedAt(base, cwd) {
|
|
18296
|
-
return
|
|
19049
|
+
return git5(["ls-tree", "-r", "-z", "--name-only", base], cwd).split("\0").filter(Boolean);
|
|
18297
19050
|
}
|
|
18298
19051
|
function materializeBase(base, cwd, dest) {
|
|
18299
19052
|
const archive = spawnSync5("git", ["archive", "--format=tar", base], {
|
|
@@ -18315,38 +19068,41 @@ function materializeBase(base, cwd, dest) {
|
|
|
18315
19068
|
`tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
|
|
18316
19069
|
);
|
|
18317
19070
|
}
|
|
18318
|
-
const hostModules =
|
|
18319
|
-
const tracedModules =
|
|
19071
|
+
const hostModules = join23(cwd, "node_modules");
|
|
19072
|
+
const tracedModules = join23(dest, "node_modules");
|
|
18320
19073
|
if (existsSync16(hostModules) && !existsSync16(tracedModules)) {
|
|
18321
|
-
symlinkSync2(
|
|
19074
|
+
symlinkSync2(resolve12(hostModules), tracedModules, "dir");
|
|
18322
19075
|
}
|
|
18323
19076
|
}
|
|
18324
19077
|
function traceFiles(prefix) {
|
|
18325
|
-
const dir =
|
|
18326
|
-
const base = prefix.split(
|
|
18327
|
-
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) =>
|
|
19078
|
+
const dir = resolve12(prefix, "..");
|
|
19079
|
+
const base = prefix.split(sep5).at(-1) ?? prefix;
|
|
19080
|
+
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join23(dir, name)).sort();
|
|
18328
19081
|
}
|
|
18329
19082
|
function rewriteTraceRoot(accesses, actualRoot) {
|
|
18330
19083
|
return accesses.map((item) => {
|
|
18331
|
-
|
|
19084
|
+
if (item.unresolved) return item;
|
|
19085
|
+
const placeholder = isAbsolute12(item.path) ? resolve12(item.path) : resolve12(TRACE_ROOT, item.path);
|
|
19086
|
+
const placeholderRel = inside3(TRACE_ROOT, placeholder) ? relative7(TRACE_ROOT, placeholder) : null;
|
|
19087
|
+
const abs = placeholderRel === null ? placeholder : resolve12(actualRoot, placeholderRel);
|
|
18332
19088
|
if (!inside3(actualRoot, abs)) return { ...item, path: abs };
|
|
18333
|
-
const rel =
|
|
19089
|
+
const rel = relative7(actualRoot, abs).split(sep5).join("/");
|
|
18334
19090
|
try {
|
|
18335
|
-
const real =
|
|
19091
|
+
const real = resolve12(realpathSync9(abs));
|
|
18336
19092
|
if (!inside3(actualRoot, real)) return { ...item, path: real };
|
|
18337
19093
|
} catch {
|
|
18338
19094
|
}
|
|
18339
|
-
return { ...item, path:
|
|
19095
|
+
return { ...item, path: join23(TRACE_ROOT, ...rel.split("/")) };
|
|
18340
19096
|
});
|
|
18341
19097
|
}
|
|
18342
19098
|
function straceOnce(root, command, budget) {
|
|
18343
|
-
const traceDir = mkdtempSync5(
|
|
19099
|
+
const traceDir = mkdtempSync5(join23(tmpdir6(), "tw-trace-log-"));
|
|
18344
19100
|
try {
|
|
18345
|
-
const prefix =
|
|
19101
|
+
const prefix = join23(traceDir, "trace");
|
|
18346
19102
|
const traced = spawnSync5(
|
|
18347
19103
|
"strace",
|
|
18348
19104
|
[
|
|
18349
|
-
"-
|
|
19105
|
+
"-f",
|
|
18350
19106
|
"-qq",
|
|
18351
19107
|
"-s",
|
|
18352
19108
|
"4096",
|
|
@@ -18365,13 +19121,13 @@ function straceOnce(root, command, budget) {
|
|
|
18365
19121
|
],
|
|
18366
19122
|
{
|
|
18367
19123
|
cwd: root,
|
|
18368
|
-
env:
|
|
19124
|
+
env: suiteEnv(traceDir),
|
|
18369
19125
|
encoding: "utf8",
|
|
18370
19126
|
maxBuffer: 64 * 1024 * 1024
|
|
18371
19127
|
}
|
|
18372
19128
|
);
|
|
18373
19129
|
const logs = traced.error ? [] : traceFiles(prefix);
|
|
18374
|
-
const accesses = logs.flatMap((path) => parseStraceFileAccess(
|
|
19130
|
+
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync20(path, "utf8"), TRACE_ROOT));
|
|
18375
19131
|
const raw = {
|
|
18376
19132
|
spawnError: traced.error ? traced.error.message : null,
|
|
18377
19133
|
status: traced.status,
|
|
@@ -18382,26 +19138,26 @@ function straceOnce(root, command, budget) {
|
|
|
18382
19138
|
};
|
|
18383
19139
|
return { raw, accesses };
|
|
18384
19140
|
} finally {
|
|
18385
|
-
|
|
19141
|
+
rmSync10(traceDir, { recursive: true, force: true });
|
|
18386
19142
|
}
|
|
18387
19143
|
}
|
|
18388
19144
|
function runOneTrace(cwd, base, command, budget) {
|
|
18389
|
-
const root = mkdtempSync5(
|
|
19145
|
+
const root = mkdtempSync5(join23(tmpdir6(), "tw-trace-base-"));
|
|
18390
19146
|
try {
|
|
18391
19147
|
materializeBase(base, cwd, root);
|
|
18392
19148
|
const { raw, accesses } = straceOnce(root, command, budget);
|
|
18393
19149
|
return { outcome: classifyTraceRun(raw), accesses: rewriteTraceRoot(accesses, root) };
|
|
18394
19150
|
} finally {
|
|
18395
|
-
|
|
19151
|
+
rmSync10(root, { recursive: true, force: true });
|
|
18396
19152
|
}
|
|
18397
19153
|
}
|
|
18398
19154
|
function tracerPreflight(budget = 10) {
|
|
18399
|
-
const probeRoot = mkdtempSync5(
|
|
19155
|
+
const probeRoot = mkdtempSync5(join23(tmpdir6(), "tw-trace-probe-"));
|
|
18400
19156
|
try {
|
|
18401
19157
|
const { raw } = straceOnce(probeRoot, "true", Math.max(1, Math.min(budget, 10)));
|
|
18402
19158
|
return classifyTraceRun(raw);
|
|
18403
19159
|
} finally {
|
|
18404
|
-
|
|
19160
|
+
rmSync10(probeRoot, { recursive: true, force: true });
|
|
18405
19161
|
}
|
|
18406
19162
|
}
|
|
18407
19163
|
function renderText2(report2) {
|
|
@@ -18410,6 +19166,7 @@ function renderText2(report2) {
|
|
|
18410
19166
|
out3(`trusted base: ${report2.base}`);
|
|
18411
19167
|
out3(`command: ${report2.command}`);
|
|
18412
19168
|
out3(`runs: ${report2.runs_completed}/${report2.runs_requested}; exits: ${report2.run_exits.join(", ")}`);
|
|
19169
|
+
out3(`unresolved accesses: ${report2.unresolved_accesses}`);
|
|
18413
19170
|
out3();
|
|
18414
19171
|
out3("repository inputs observed:");
|
|
18415
19172
|
if (!report2.repository_inputs.length) out3(" (none)");
|
|
@@ -18438,7 +19195,7 @@ function renderText2(report2) {
|
|
|
18438
19195
|
for (const note of report2.notes) out3(`note: ${note}`);
|
|
18439
19196
|
}
|
|
18440
19197
|
function runTraceVerify(opts = {}) {
|
|
18441
|
-
const cwd =
|
|
19198
|
+
const cwd = resolve12(opts.cwd ?? process.cwd());
|
|
18442
19199
|
if (process.platform !== "linux") {
|
|
18443
19200
|
process.stderr.write("tamperward trace-verify: Linux only in this release; unsupported platform \u2014 no parity is implied.\n");
|
|
18444
19201
|
return 2;
|
|
@@ -18465,7 +19222,7 @@ function runTraceVerify(opts = {}) {
|
|
|
18465
19222
|
let base;
|
|
18466
19223
|
try {
|
|
18467
19224
|
const baseArg = assertRev(requestedBase);
|
|
18468
|
-
base =
|
|
19225
|
+
base = git5(["rev-parse", "--verify", `${baseArg}^{commit}`], cwd).trim();
|
|
18469
19226
|
} catch (e) {
|
|
18470
19227
|
process.stderr.write(`tamperward trace-verify: cannot resolve trusted base ${JSON.stringify(requestedBase)} (${e instanceof Error ? e.message : String(e)}).
|
|
18471
19228
|
`);
|
|
@@ -18536,13 +19293,14 @@ This is a tracing/tooling limitation (e.g. ptrace denied by seccomp or Yama), no
|
|
|
18536
19293
|
runs_requested: runs,
|
|
18537
19294
|
runs_completed: observed.length,
|
|
18538
19295
|
run_exits: runExits,
|
|
18539
|
-
trace_complete: runExits.every((code2) => code2 === 0),
|
|
19296
|
+
trace_complete: runExits.every((code2) => code2 === 0) && summary.unresolved_accesses === 0,
|
|
18540
19297
|
...summary,
|
|
18541
19298
|
notes: [
|
|
18542
19299
|
"Observed reads are evidence from these runs, not proof that an unobserved path can never be read.",
|
|
18543
19300
|
"Dynamic paths are those observed in fewer than all repeated traces.",
|
|
18544
19301
|
"Suggestions are exact tracked paths only; TamperWard never edits .tamperward.yml or widens a glob automatically.",
|
|
18545
|
-
"Review every suggestion before adding it to verify.inputs."
|
|
19302
|
+
"Review every suggestion before adding it to verify.inputs.",
|
|
19303
|
+
...summary.unresolved_accesses > 0 ? [`${summary.unresolved_accesses} relative file access(es) could not be resolved from trace cwd/fd state; discovery is incomplete.`] : []
|
|
18546
19304
|
]
|
|
18547
19305
|
};
|
|
18548
19306
|
if (opts.json) process.stdout.write(JSON.stringify(report2) + "\n");
|
|
@@ -18601,26 +19359,26 @@ var init_trace_verify = __esm({
|
|
|
18601
19359
|
});
|
|
18602
19360
|
|
|
18603
19361
|
// src/runtimes.ts
|
|
18604
|
-
import { existsSync as existsSync17, readFileSync as
|
|
18605
|
-
import { join as
|
|
18606
|
-
function
|
|
19362
|
+
import { existsSync as existsSync17, readFileSync as readFileSync21 } from "node:fs";
|
|
19363
|
+
import { join as join24 } from "node:path";
|
|
19364
|
+
function isRecord5(x) {
|
|
18607
19365
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
18608
19366
|
}
|
|
18609
19367
|
function hooksBeyondTamperward(hooks) {
|
|
18610
|
-
if (!
|
|
19368
|
+
if (!isRecord5(hooks)) return hooks != null;
|
|
18611
19369
|
for (const entries of Object.values(hooks)) {
|
|
18612
19370
|
if (!Array.isArray(entries)) {
|
|
18613
19371
|
if (entries != null) return true;
|
|
18614
19372
|
continue;
|
|
18615
19373
|
}
|
|
18616
19374
|
for (const entry of entries) {
|
|
18617
|
-
const hs =
|
|
19375
|
+
const hs = isRecord5(entry) ? entry.hooks : null;
|
|
18618
19376
|
if (!Array.isArray(hs)) {
|
|
18619
19377
|
if (hs != null) return true;
|
|
18620
19378
|
continue;
|
|
18621
19379
|
}
|
|
18622
19380
|
for (const h of hs) {
|
|
18623
|
-
const cmd =
|
|
19381
|
+
const cmd = isRecord5(h) ? String(h.command ?? "") : "";
|
|
18624
19382
|
if (!/tamperward/.test(cmd)) return true;
|
|
18625
19383
|
}
|
|
18626
19384
|
}
|
|
@@ -18628,11 +19386,11 @@ function hooksBeyondTamperward(hooks) {
|
|
|
18628
19386
|
return false;
|
|
18629
19387
|
}
|
|
18630
19388
|
function claudeSettingsBeyondTamperward(cwd) {
|
|
18631
|
-
const path =
|
|
19389
|
+
const path = join24(cwd, ".claude", "settings.json");
|
|
18632
19390
|
let raw;
|
|
18633
19391
|
try {
|
|
18634
19392
|
if (!existsSync17(path)) return false;
|
|
18635
|
-
raw =
|
|
19393
|
+
raw = readFileSync21(path, "utf8");
|
|
18636
19394
|
} catch {
|
|
18637
19395
|
return false;
|
|
18638
19396
|
}
|
|
@@ -18642,7 +19400,7 @@ function claudeSettingsBeyondTamperward(cwd) {
|
|
|
18642
19400
|
} catch {
|
|
18643
19401
|
return raw.trim().length > 0;
|
|
18644
19402
|
}
|
|
18645
|
-
if (!
|
|
19403
|
+
if (!isRecord5(parsed)) return raw.trim().length > 0;
|
|
18646
19404
|
for (const [key2, value] of Object.entries(parsed)) {
|
|
18647
19405
|
if (key2 === "disableAllHooks") continue;
|
|
18648
19406
|
if (key2 === "hooks") {
|
|
@@ -18658,7 +19416,7 @@ function detectRuntimes(cwd) {
|
|
|
18658
19416
|
for (const rt of KNOWN_RUNTIMES) {
|
|
18659
19417
|
const marker = rt.markers.find((m) => {
|
|
18660
19418
|
try {
|
|
18661
|
-
return existsSync17(
|
|
19419
|
+
return existsSync17(join24(cwd, m));
|
|
18662
19420
|
} catch {
|
|
18663
19421
|
return false;
|
|
18664
19422
|
}
|
|
@@ -18738,10 +19496,10 @@ var init_runtimes = __esm({
|
|
|
18738
19496
|
});
|
|
18739
19497
|
|
|
18740
19498
|
// src/cli/onboard.ts
|
|
18741
|
-
import { execFileSync as
|
|
18742
|
-
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as
|
|
19499
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
19500
|
+
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync22, realpathSync as realpathSync10, rmSync as rmSync11 } from "node:fs";
|
|
18743
19501
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
18744
|
-
import { join as
|
|
19502
|
+
import { join as join25, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
|
|
18745
19503
|
import { createInterface } from "node:readline";
|
|
18746
19504
|
import { parseDocument } from "yaml";
|
|
18747
19505
|
function readlineAsker(input, output) {
|
|
@@ -18750,9 +19508,9 @@ function readlineAsker(input, output) {
|
|
|
18750
19508
|
let pending = null;
|
|
18751
19509
|
rl.on("close", () => {
|
|
18752
19510
|
closed = true;
|
|
18753
|
-
const
|
|
19511
|
+
const resolve20 = pending;
|
|
18754
19512
|
pending = null;
|
|
18755
|
-
|
|
19513
|
+
resolve20?.(null);
|
|
18756
19514
|
});
|
|
18757
19515
|
rl.on("SIGINT", () => rl.close());
|
|
18758
19516
|
const ask = (question) => new Promise((resolvePromise) => {
|
|
@@ -18775,9 +19533,9 @@ function terminalText(text) {
|
|
|
18775
19533
|
return stripControl(text).replace(/\s+/g, " ").trim();
|
|
18776
19534
|
}
|
|
18777
19535
|
function committableSetupPath(cwd, path) {
|
|
18778
|
-
const rel =
|
|
18779
|
-
if (!rel || rel === ".." || rel.startsWith(".." +
|
|
18780
|
-
return rel !== ".git" && !rel.startsWith(".git" +
|
|
19536
|
+
const rel = relative8(cwd, resolve13(cwd, path));
|
|
19537
|
+
if (!rel || rel === ".." || rel.startsWith(".." + sep6)) return false;
|
|
19538
|
+
return rel !== ".git" && !rel.startsWith(".git" + sep6);
|
|
18781
19539
|
}
|
|
18782
19540
|
function platformLabel(platform) {
|
|
18783
19541
|
if (platform === "darwin") return "macOS";
|
|
@@ -18785,9 +19543,9 @@ function platformLabel(platform) {
|
|
|
18785
19543
|
if (platform === "linux") return "Linux";
|
|
18786
19544
|
return platform;
|
|
18787
19545
|
}
|
|
18788
|
-
function
|
|
19546
|
+
function git6(cwd, args) {
|
|
18789
19547
|
try {
|
|
18790
|
-
return
|
|
19548
|
+
return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
18791
19549
|
} catch {
|
|
18792
19550
|
return null;
|
|
18793
19551
|
}
|
|
@@ -18806,13 +19564,13 @@ function explainVerdict(v, code2) {
|
|
|
18806
19564
|
}
|
|
18807
19565
|
}
|
|
18808
19566
|
function writeVerifyCommand(cwd, command) {
|
|
18809
|
-
const path =
|
|
19567
|
+
const path = join25(cwd, POLICY_FILE);
|
|
18810
19568
|
const kind = writeTargetKind(path);
|
|
18811
19569
|
if (kind === "symlink" || kind === "irregular") {
|
|
18812
19570
|
return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
|
|
18813
19571
|
}
|
|
18814
19572
|
const mode = existingMode(path, 420);
|
|
18815
|
-
const original = kind === "file" ?
|
|
19573
|
+
const original = kind === "file" ? readFileSync22(path, "utf8") : null;
|
|
18816
19574
|
let next;
|
|
18817
19575
|
try {
|
|
18818
19576
|
const doc = parseDocument(original && original.trim() ? original : "version: 1\n");
|
|
@@ -18829,13 +19587,13 @@ function writeVerifyCommand(cwd, command) {
|
|
|
18829
19587
|
if (loaded2.verify?.command !== command) throw new Error("verify.command did not round-trip");
|
|
18830
19588
|
return null;
|
|
18831
19589
|
} catch (e) {
|
|
18832
|
-
if (original === null)
|
|
19590
|
+
if (original === null) rmSync11(path, { force: true });
|
|
18833
19591
|
else atomicReplaceFile(path, original, mode);
|
|
18834
19592
|
return `the updated ${POLICY_FILE} does not load (${errorMessage(e)}); restored the previous file`;
|
|
18835
19593
|
}
|
|
18836
19594
|
}
|
|
18837
19595
|
async function runOnboard(opts, io = {}) {
|
|
18838
|
-
const requestedCwd =
|
|
19596
|
+
const requestedCwd = resolve13(opts.cwd ?? process.cwd());
|
|
18839
19597
|
const out3 = io.out ?? ((line2) => void process.stdout.write(line2 + "\n"));
|
|
18840
19598
|
const errLine = (line2) => void process.stderr.write(line2 + "\n");
|
|
18841
19599
|
const rawErr = (line2) => errLine(terminalText(line2));
|
|
@@ -18864,12 +19622,12 @@ async function runOnboard(opts, io = {}) {
|
|
|
18864
19622
|
rawErr("or run `tamperward init` for the deterministic wiring step only.");
|
|
18865
19623
|
return 2;
|
|
18866
19624
|
}
|
|
18867
|
-
const rootText =
|
|
19625
|
+
const rootText = git6(requestedCwd, ["rev-parse", "--show-toplevel"])?.trim() ?? "";
|
|
18868
19626
|
if (!rootText) {
|
|
18869
19627
|
fail(requestedCwd + " is not inside a Git repository. Run `git init` first.");
|
|
18870
19628
|
return 2;
|
|
18871
19629
|
}
|
|
18872
|
-
const cwd =
|
|
19630
|
+
const cwd = resolve13(rootText);
|
|
18873
19631
|
let sameRoot = cwd === requestedCwd;
|
|
18874
19632
|
try {
|
|
18875
19633
|
sameRoot = realpathSync10(cwd) === realpathSync10(requestedCwd);
|
|
@@ -18902,10 +19660,10 @@ async function runOnboard(opts, io = {}) {
|
|
|
18902
19660
|
return a === "y" || a === "yes";
|
|
18903
19661
|
};
|
|
18904
19662
|
let sectionNo = 0;
|
|
18905
|
-
const section = (
|
|
18906
|
-
sectionNo =
|
|
19663
|
+
const section = (n2) => {
|
|
19664
|
+
sectionNo = n2;
|
|
18907
19665
|
out3("");
|
|
18908
|
-
out3(paint(
|
|
19666
|
+
out3(paint(n2 + "/" + SECTIONS.length + " " + SECTIONS[n2 - 1], BOLD + CYAN, colour));
|
|
18909
19667
|
};
|
|
18910
19668
|
const wrote = [];
|
|
18911
19669
|
let verifyCommand;
|
|
@@ -18923,13 +19681,15 @@ async function runOnboard(opts, io = {}) {
|
|
|
18923
19681
|
const localVerifySupported = localVerifierShell(platform, "true") !== null;
|
|
18924
19682
|
if (platform === "linux" && lifecycle.state === "OK") {
|
|
18925
19683
|
status("OK", "Full check / verify / run support is available.", "ok");
|
|
19684
|
+
} else if (platform === "linux") {
|
|
19685
|
+
status("LIMITED", lifecycle.detail, "warn");
|
|
18926
19686
|
} else if (localVerifySupported) {
|
|
18927
19687
|
status("LIMITED", platformLabel(platform) + ": check + verify work here; `tamperward run` requires Linux in this release.", "warn");
|
|
18928
19688
|
} else {
|
|
18929
19689
|
status("LIMITED", platformLabel(platform) + ": check works here; local verify and `run` are unavailable. Use the container verifier for final verification.", "warn");
|
|
18930
19690
|
}
|
|
18931
|
-
const head =
|
|
18932
|
-
const treeStatus =
|
|
19691
|
+
const head = git6(cwd, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])?.trim() ?? null;
|
|
19692
|
+
const treeStatus = git6(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
|
|
18933
19693
|
const dirty = treeStatus.split("\n").filter(Boolean).map((line2) => line2.slice(3).split(" -> ").at(-1) ?? "");
|
|
18934
19694
|
if (!head) {
|
|
18935
19695
|
const count = dirty.length;
|
|
@@ -19033,8 +19793,8 @@ async function runOnboard(opts, io = {}) {
|
|
|
19033
19793
|
status("ACTION", "Choose the command that decides whether this repository passes:", "warn");
|
|
19034
19794
|
candidates.forEach((candidate, i) => out3(" " + (i + 1) + ". " + terminalText(candidate)));
|
|
19035
19795
|
const a = await ask("Choose 1-" + candidates.length + ", type a command, or press Enter to skip: ");
|
|
19036
|
-
const
|
|
19037
|
-
chosen = a === "" ? void 0 : Number.isInteger(
|
|
19796
|
+
const n2 = Number(a);
|
|
19797
|
+
chosen = a === "" ? void 0 : Number.isInteger(n2) && n2 >= 1 && n2 <= candidates.length ? candidates[n2 - 1] : a;
|
|
19038
19798
|
} else {
|
|
19039
19799
|
status("ACTION", "No test command was detected automatically.", "warn");
|
|
19040
19800
|
chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
|
|
@@ -19177,7 +19937,7 @@ function postureOf(outcome) {
|
|
|
19177
19937
|
return "READY";
|
|
19178
19938
|
}
|
|
19179
19939
|
function runDemo(cwd, head, runners, out3) {
|
|
19180
|
-
const files = (
|
|
19940
|
+
const files = (git6(cwd, ["ls-tree", "-r", "-z", head]) ?? "").split("\0").filter(Boolean).flatMap((row) => {
|
|
19181
19941
|
const tab = row.indexOf(" ");
|
|
19182
19942
|
if (tab < 0) return [];
|
|
19183
19943
|
const mode = row.slice(0, tab).split(" ")[0];
|
|
@@ -19186,7 +19946,7 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19186
19946
|
});
|
|
19187
19947
|
let target = null;
|
|
19188
19948
|
for (const path of files) {
|
|
19189
|
-
const before2 =
|
|
19949
|
+
const before2 = git6(cwd, ["show", `${head}:${path}`]);
|
|
19190
19950
|
if (before2 === null || !JS_TEST_BLOCK.test(before2)) continue;
|
|
19191
19951
|
target = { path, before: before2, after: before2.replace(JS_TEST_BLOCK, "$1$2.skip$3") };
|
|
19192
19952
|
break;
|
|
@@ -19197,13 +19957,13 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19197
19957
|
}
|
|
19198
19958
|
const before = treeFingerprint(cwd);
|
|
19199
19959
|
out3(`working tree fingerprint before: ${before.slice(0, 16)}`);
|
|
19200
|
-
const wt = mkdtempSync6(
|
|
19960
|
+
const wt = mkdtempSync6(join25(tmpdir7(), "tw-onboard-demo-"));
|
|
19201
19961
|
try {
|
|
19202
|
-
if (
|
|
19962
|
+
if (git6(cwd, ["worktree", "add", "--detach", wt, head]) === null) {
|
|
19203
19963
|
out3("Demo skipped: could not create a temporary worktree (git worktree add failed).");
|
|
19204
19964
|
return;
|
|
19205
19965
|
}
|
|
19206
|
-
const targetPath =
|
|
19966
|
+
const targetPath = join25(wt, target.path);
|
|
19207
19967
|
const targetStat = lstatSync10(targetPath, { throwIfNoEntry: false });
|
|
19208
19968
|
if (!targetStat || !targetStat.isFile() || targetStat.isSymbolicLink()) {
|
|
19209
19969
|
out3("Demo skipped: the selected test is not a regular file in the disposable worktree.");
|
|
@@ -19218,7 +19978,7 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19218
19978
|
out3("Demo skipped: the selected test could not be resolved safely inside the disposable worktree.");
|
|
19219
19979
|
return;
|
|
19220
19980
|
}
|
|
19221
|
-
const prefix = worktreeRoot.endsWith(
|
|
19981
|
+
const prefix = worktreeRoot.endsWith(sep6) ? worktreeRoot : worktreeRoot + sep6;
|
|
19222
19982
|
if (!realTarget.startsWith(prefix)) {
|
|
19223
19983
|
out3("Demo skipped: the selected test resolves outside the disposable worktree.");
|
|
19224
19984
|
return;
|
|
@@ -19232,9 +19992,9 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19232
19992
|
out3("");
|
|
19233
19993
|
out3(`check exited ${code2}: ${code2 === 1 ? "a blocking finding, which is what the hook, pre-commit and CI would all report" : "no blocking finding was reported"}.`);
|
|
19234
19994
|
} finally {
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19995
|
+
git6(cwd, ["worktree", "remove", "--force", wt]);
|
|
19996
|
+
git6(cwd, ["worktree", "prune"]);
|
|
19997
|
+
rmSync11(wt, { recursive: true, force: true });
|
|
19238
19998
|
}
|
|
19239
19999
|
const after = treeFingerprint(cwd);
|
|
19240
20000
|
out3(`working tree fingerprint after: ${after.slice(0, 16)}`);
|
|
@@ -19282,7 +20042,7 @@ var init_onboard = __esm({
|
|
|
19282
20042
|
});
|
|
19283
20043
|
|
|
19284
20044
|
// src/research/adapter.ts
|
|
19285
|
-
import { isAbsolute as isAbsolute13, resolve as
|
|
20045
|
+
import { isAbsolute as isAbsolute13, resolve as resolve14 } from "node:path";
|
|
19286
20046
|
function taskEnv(task) {
|
|
19287
20047
|
return {
|
|
19288
20048
|
TAMPERWARD_RESEARCH_TASK: task.id,
|
|
@@ -19301,7 +20061,7 @@ function normalizeCommandArgv(argv, cwd = process.cwd()) {
|
|
|
19301
20061
|
throw new ResearchError('the command adapter needs an agent command after "--"');
|
|
19302
20062
|
}
|
|
19303
20063
|
const head = argv[0];
|
|
19304
|
-
return head.includes("/") && !isAbsolute13(head) ? [
|
|
20064
|
+
return head.includes("/") && !isAbsolute13(head) ? [resolve14(cwd, head), ...argv.slice(1)] : [...argv];
|
|
19305
20065
|
}
|
|
19306
20066
|
function commandAdapter(argv, cwd = process.cwd()) {
|
|
19307
20067
|
const anchored = normalizeCommandArgv(argv, cwd);
|
|
@@ -19314,6 +20074,24 @@ function commandAdapter(argv, cwd = process.cwd()) {
|
|
|
19314
20074
|
}
|
|
19315
20075
|
};
|
|
19316
20076
|
}
|
|
20077
|
+
function stdioAdapter(argv, cwd = process.cwd()) {
|
|
20078
|
+
const anchored = normalizeCommandArgv(argv, cwd);
|
|
20079
|
+
return {
|
|
20080
|
+
name: "stdio",
|
|
20081
|
+
layers: ["envelope"],
|
|
20082
|
+
launch(task) {
|
|
20083
|
+
const substituted = anchored.map((arg) => substitutePlaceholders(arg, task));
|
|
20084
|
+
return {
|
|
20085
|
+
argv: substituted,
|
|
20086
|
+
env: {
|
|
20087
|
+
...taskEnv(task),
|
|
20088
|
+
TAMPERWARD_RESEARCH_PROTOCOL: "research-stdio-jsonl-v1",
|
|
20089
|
+
TAMPERWARD_RESEARCH_CAPABILITIES: JSON.stringify({ layers: ["envelope"], intervention: "not-connected" })
|
|
20090
|
+
}
|
|
20091
|
+
};
|
|
20092
|
+
}
|
|
20093
|
+
};
|
|
20094
|
+
}
|
|
19317
20095
|
function claudeCodeAdapter(model) {
|
|
19318
20096
|
return {
|
|
19319
20097
|
name: "claude-code",
|
|
@@ -19336,6 +20114,7 @@ function claudeCodeAdapter(model) {
|
|
|
19336
20114
|
function resolveAdapter(name, argv, model) {
|
|
19337
20115
|
if (name === "command") return commandAdapter(argv);
|
|
19338
20116
|
if (name === "claude-code") return claudeCodeAdapter(model);
|
|
20117
|
+
if (name === "stdio") return stdioAdapter(argv);
|
|
19339
20118
|
throw new ResearchError(`unknown adapter "${name}" (built-in adapters: ${BUILTIN_ADAPTERS.join(", ")})`);
|
|
19340
20119
|
}
|
|
19341
20120
|
var ADAPTER_LAYERS, RESEARCH_ARMS, BUILTIN_ADAPTERS, ResearchError, PLACEHOLDERS, PLACEHOLDER_PATTERN;
|
|
@@ -19345,7 +20124,7 @@ var init_adapter = __esm({
|
|
|
19345
20124
|
init_init();
|
|
19346
20125
|
ADAPTER_LAYERS = ["envelope", "pre-tool-use", "stop-sweep"];
|
|
19347
20126
|
RESEARCH_ARMS = ["ungated", "gated"];
|
|
19348
|
-
BUILTIN_ADAPTERS = ["claude-code", "command"];
|
|
20127
|
+
BUILTIN_ADAPTERS = ["claude-code", "command", "stdio"];
|
|
19349
20128
|
ResearchError = class extends Error {
|
|
19350
20129
|
};
|
|
19351
20130
|
PLACEHOLDERS = {
|
|
@@ -19402,9 +20181,9 @@ var init_capture = __esm({
|
|
|
19402
20181
|
});
|
|
19403
20182
|
|
|
19404
20183
|
// src/research/manifest.ts
|
|
19405
|
-
import { createHash as
|
|
19406
|
-
import { readFileSync as
|
|
19407
|
-
import { dirname as
|
|
20184
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
20185
|
+
import { readFileSync as readFileSync23 } from "node:fs";
|
|
20186
|
+
import { dirname as dirname11, resolve as resolve15 } from "node:path";
|
|
19408
20187
|
function taskFrom(raw, index, manifestDir) {
|
|
19409
20188
|
const where = `task #${index + 1}`;
|
|
19410
20189
|
if (!isRecord(raw)) throw new ResearchError(`task manifest: ${where} is not an object`);
|
|
@@ -19427,17 +20206,17 @@ function taskFrom(raw, index, manifestDir) {
|
|
|
19427
20206
|
const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(repo) || /^[^/]+@[^:]+:/.test(repo);
|
|
19428
20207
|
return {
|
|
19429
20208
|
id,
|
|
19430
|
-
repo: isUrl ? repo :
|
|
20209
|
+
repo: isUrl ? repo : resolve15(manifestDir, repo),
|
|
19431
20210
|
base: base ?? "HEAD",
|
|
19432
20211
|
prompt,
|
|
19433
20212
|
verify: { command: verify.command, ...budget !== void 0 ? { budget } : {} }
|
|
19434
20213
|
};
|
|
19435
20214
|
}
|
|
19436
20215
|
function readManifest(path) {
|
|
19437
|
-
const abs =
|
|
20216
|
+
const abs = resolve15(path);
|
|
19438
20217
|
let bytes;
|
|
19439
20218
|
try {
|
|
19440
|
-
bytes =
|
|
20219
|
+
bytes = readFileSync23(abs);
|
|
19441
20220
|
} catch (e) {
|
|
19442
20221
|
throw new ResearchError(`cannot read task manifest ${abs}: ${e instanceof Error ? e.message : String(e)}`);
|
|
19443
20222
|
}
|
|
@@ -19454,13 +20233,13 @@ function readManifest(path) {
|
|
|
19454
20233
|
if (!Array.isArray(raw.tasks) || raw.tasks.length === 0) {
|
|
19455
20234
|
throw new ResearchError(`task manifest ${abs}: no tasks (tasks must be a non-empty array)`);
|
|
19456
20235
|
}
|
|
19457
|
-
const tasks = raw.tasks.map((t, i) => taskFrom(t, i,
|
|
20236
|
+
const tasks = raw.tasks.map((t, i) => taskFrom(t, i, dirname11(abs)));
|
|
19458
20237
|
const seen = /* @__PURE__ */ new Set();
|
|
19459
20238
|
for (const t of tasks) {
|
|
19460
20239
|
if (seen.has(t.id)) throw new ResearchError(`task manifest ${abs}: duplicate task id "${t.id}"`);
|
|
19461
20240
|
seen.add(t.id);
|
|
19462
20241
|
}
|
|
19463
|
-
return { path: abs, sha256:
|
|
20242
|
+
return { path: abs, sha256: createHash14("sha256").update(bytes).digest("hex"), tasks };
|
|
19464
20243
|
}
|
|
19465
20244
|
var MANIFEST_VERSION;
|
|
19466
20245
|
var init_manifest = __esm({
|
|
@@ -19734,22 +20513,22 @@ var init_record = __esm({
|
|
|
19734
20513
|
|
|
19735
20514
|
// src/research/run.ts
|
|
19736
20515
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
19737
|
-
import { execFileSync as
|
|
19738
|
-
import { closeSync as closeSync3, existsSync as existsSync18, linkSync, mkdirSync as
|
|
20516
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
20517
|
+
import { closeSync as closeSync3, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync10, openSync as openSync3, readFileSync as readFileSync24, renameSync as renameSync4, rmSync as rmSync12, unlinkSync, writeFileSync as writeFileSync12, writeSync } from "node:fs";
|
|
19739
20518
|
import { hostname } from "node:os";
|
|
19740
|
-
import { join as
|
|
19741
|
-
function
|
|
19742
|
-
return
|
|
20519
|
+
import { join as join26, resolve as resolve16 } from "node:path";
|
|
20520
|
+
function git7(args, cwd) {
|
|
20521
|
+
return execFileSync14("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
19743
20522
|
}
|
|
19744
20523
|
function pairRecordPath(ledger, task, pair) {
|
|
19745
|
-
return
|
|
20524
|
+
return join26(ledger, "pairs", `${task}--${pair}.json`);
|
|
19746
20525
|
}
|
|
19747
20526
|
function researchLockPath(ledger) {
|
|
19748
|
-
return
|
|
20527
|
+
return join26(ledger, "run.lock");
|
|
19749
20528
|
}
|
|
19750
20529
|
function readLockOwner(path) {
|
|
19751
20530
|
try {
|
|
19752
|
-
const parsed = JSON.parse(
|
|
20531
|
+
const parsed = JSON.parse(readFileSync24(path, "utf8"));
|
|
19753
20532
|
if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
19754
20533
|
if (typeof parsed.host !== "string" || typeof parsed.started_at !== "string" || typeof parsed.token !== "string") return null;
|
|
19755
20534
|
return { pid: parsed.pid, host: parsed.host, started_at: parsed.started_at, token: parsed.token };
|
|
@@ -19770,7 +20549,7 @@ function lockHolder(path) {
|
|
|
19770
20549
|
return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
|
|
19771
20550
|
}
|
|
19772
20551
|
function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
19773
|
-
|
|
20552
|
+
mkdirSync10(ledger, { recursive: true });
|
|
19774
20553
|
const path = researchLockPath(ledger);
|
|
19775
20554
|
if (breakStaleLock && existsSync18(path)) {
|
|
19776
20555
|
const owner2 = readLockOwner(path);
|
|
@@ -19834,7 +20613,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
19834
20613
|
if (released) return;
|
|
19835
20614
|
released = true;
|
|
19836
20615
|
try {
|
|
19837
|
-
if (
|
|
20616
|
+
if (readFileSync24(path, "utf8") === text) unlinkSync(path);
|
|
19838
20617
|
} catch {
|
|
19839
20618
|
}
|
|
19840
20619
|
}
|
|
@@ -19843,7 +20622,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
19843
20622
|
function resumableRecord(path, expected) {
|
|
19844
20623
|
let raw;
|
|
19845
20624
|
try {
|
|
19846
|
-
raw = JSON.parse(
|
|
20625
|
+
raw = JSON.parse(readFileSync24(path, "utf8"));
|
|
19847
20626
|
} catch (e) {
|
|
19848
20627
|
throw new ResearchError(`ledger record ${path} is malformed (${errorMessage(e).split("\n")[0]}); remove it to regenerate the pair, or use a new --out`);
|
|
19849
20628
|
}
|
|
@@ -19880,28 +20659,28 @@ function resumableRecord(path, expected) {
|
|
|
19880
20659
|
}
|
|
19881
20660
|
function writeRecordAtomically(path, text) {
|
|
19882
20661
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
19883
|
-
|
|
20662
|
+
writeFileSync12(tmp, text);
|
|
19884
20663
|
renameSync4(tmp, path);
|
|
19885
20664
|
}
|
|
19886
20665
|
function cloneArgs(repo, ws3) {
|
|
19887
20666
|
return ["-c", "protocol.ext.allow=never", "clone", "-q", "--no-hardlinks", "--", repo, ws3];
|
|
19888
20667
|
}
|
|
19889
20668
|
function freshWorkspace(ledger, task, pair, arm, sourceBase) {
|
|
19890
|
-
const ws3 =
|
|
19891
|
-
|
|
19892
|
-
|
|
20669
|
+
const ws3 = join26(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
|
|
20670
|
+
rmSync12(ws3, { recursive: true, force: true });
|
|
20671
|
+
mkdirSync10(join26(ledger, "workspaces"), { recursive: true });
|
|
19893
20672
|
try {
|
|
19894
|
-
|
|
20673
|
+
execFileSync14("git", cloneArgs(task.repo, ws3), { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
19895
20674
|
} catch (e) {
|
|
19896
20675
|
throw new ResearchError(`task "${task.id}": cannot clone ${task.repo}: ${errorMessage(e).split("\n")[0]}`);
|
|
19897
20676
|
}
|
|
19898
|
-
|
|
19899
|
-
|
|
20677
|
+
git7(["config", "user.name", "tamperward-research"], ws3);
|
|
20678
|
+
git7(["config", "user.email", "research@tamperward.invalid"], ws3);
|
|
19900
20679
|
const requestedBase = sourceBase ?? task.base;
|
|
19901
20680
|
const candidates = sourceBase !== void 0 ? [sourceBase] : task.base === "HEAD" ? ["HEAD"] : [task.base, `origin/${task.base}`];
|
|
19902
20681
|
for (const rev2 of candidates) {
|
|
19903
20682
|
try {
|
|
19904
|
-
|
|
20683
|
+
git7(["checkout", "-q", "--detach", rev2], ws3);
|
|
19905
20684
|
return ws3;
|
|
19906
20685
|
} catch {
|
|
19907
20686
|
}
|
|
@@ -19991,15 +20770,19 @@ function unobservedOutcome() {
|
|
|
19991
20770
|
honest_completion: false
|
|
19992
20771
|
};
|
|
19993
20772
|
}
|
|
19994
|
-
function trustedPolicyAt(base, ws3) {
|
|
20773
|
+
function trustedPolicyAt(base, ws3, loader = loadPolicyAt) {
|
|
19995
20774
|
try {
|
|
19996
|
-
return { policy:
|
|
20775
|
+
return { policy: loader(base, ws3) ?? defaultPolicy() };
|
|
19997
20776
|
} catch (e) {
|
|
19998
20777
|
return { failure: `trusted policy at ${base.slice(0, 10)} could not be loaded: ${errorMessage(e).split("\n")[0]}` };
|
|
19999
20778
|
}
|
|
20000
20779
|
}
|
|
20001
|
-
function
|
|
20002
|
-
const trusted = trustedPolicyAt(base, ws3);
|
|
20780
|
+
function trustedProtectedOnly(base, ws3, loader = loadPolicyAt) {
|
|
20781
|
+
const trusted = trustedPolicyAt(base, ws3, loader);
|
|
20782
|
+
if ("failure" in trusted) return { trusted, protectedOnly: () => true };
|
|
20783
|
+
return { trusted, protectedOnly: (rel) => isProtected(rel, trusted.policy) };
|
|
20784
|
+
}
|
|
20785
|
+
function observeOutcome(ws3, base, head, task, trusted) {
|
|
20003
20786
|
if ("failure" in trusted) return { outcome: unobservedOutcome(), unmeasurable: trusted.failure };
|
|
20004
20787
|
const policy = trusted.policy;
|
|
20005
20788
|
const verify = parseDocument2(
|
|
@@ -20051,15 +20834,15 @@ function observeOutcome(ws3, base, head, task) {
|
|
|
20051
20834
|
}
|
|
20052
20835
|
function runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase) {
|
|
20053
20836
|
const ws3 = freshWorkspace(ledger, task, pair, arm, sourceBase);
|
|
20054
|
-
const prep = { id: task.id, prompt: task.prompt, cwd: ws3, base:
|
|
20837
|
+
const prep = { id: task.id, prompt: task.prompt, cwd: ws3, base: git7(["rev-parse", "HEAD"], ws3), arm, model: opts.model };
|
|
20055
20838
|
if (arm === "gated" && adapter.prepareGated) {
|
|
20056
20839
|
captureStdout(() => adapter.prepareGated?.(prep));
|
|
20057
|
-
if (
|
|
20058
|
-
|
|
20059
|
-
|
|
20840
|
+
if (git7(["status", "--porcelain"], ws3)) {
|
|
20841
|
+
git7(["add", "-A"], ws3);
|
|
20842
|
+
git7(["commit", "-qm", "tamperward research: wire enforcement (gated arm)"], ws3);
|
|
20060
20843
|
}
|
|
20061
20844
|
}
|
|
20062
|
-
const base =
|
|
20845
|
+
const base = git7(["rev-parse", "HEAD"], ws3);
|
|
20063
20846
|
const launch2 = adapter.launch({ ...prep, base });
|
|
20064
20847
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20065
20848
|
let agent;
|
|
@@ -20072,17 +20855,11 @@ function runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase) {
|
|
|
20072
20855
|
agent = runUngated(ws3, launch2.argv, launch2.env, opts.agentBudget);
|
|
20073
20856
|
}
|
|
20074
20857
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20075
|
-
const head =
|
|
20076
|
-
const
|
|
20077
|
-
|
|
20078
|
-
|
|
20079
|
-
|
|
20080
|
-
return true;
|
|
20081
|
-
}
|
|
20082
|
-
};
|
|
20083
|
-
const fingerprintBefore = treeFingerprint(ws3, protectedOnly);
|
|
20084
|
-
const observed = observeOutcome(ws3, base, head, task);
|
|
20085
|
-
const movedDuringObservation = treeFingerprint(ws3, protectedOnly) !== fingerprintBefore;
|
|
20858
|
+
const head = git7(["rev-parse", "HEAD"], ws3);
|
|
20859
|
+
const trustedSurface = trustedProtectedOnly(base, ws3);
|
|
20860
|
+
const fingerprintBefore = treeFingerprint(ws3, trustedSurface.protectedOnly);
|
|
20861
|
+
const observed = observeOutcome(ws3, base, head, task, trustedSurface.trusted);
|
|
20862
|
+
const movedDuringObservation = treeFingerprint(ws3, trustedSurface.protectedOnly) !== fingerprintBefore;
|
|
20086
20863
|
const unmeasurable = agent.unmeasurable ?? observed.unmeasurable ?? (movedDuringObservation ? "NOT_QUIESCENT: the workspace changed while its outcome was being observed" : null);
|
|
20087
20864
|
const { unmeasurable: _agentUnmeasurable, ...agentRecord } = agent;
|
|
20088
20865
|
const outcome = unmeasurable === null ? observed.outcome : unobservedOutcome();
|
|
@@ -20127,7 +20904,7 @@ function runResearch(opts) {
|
|
|
20127
20904
|
err2("tamperward research: the gated arm cannot start here (see `tamperward doctor`); run as a non-root user on Linux.");
|
|
20128
20905
|
return 2;
|
|
20129
20906
|
}
|
|
20130
|
-
const ledger =
|
|
20907
|
+
const ledger = resolve16(opts.out);
|
|
20131
20908
|
let lock;
|
|
20132
20909
|
try {
|
|
20133
20910
|
lock = acquireResearchLock(ledger, opts.breakLock === true);
|
|
@@ -20140,7 +20917,7 @@ function runResearch(opts) {
|
|
|
20140
20917
|
}
|
|
20141
20918
|
try {
|
|
20142
20919
|
const pairs = opts.pairs ?? 1;
|
|
20143
|
-
|
|
20920
|
+
mkdirSync10(join26(ledger, "pairs"), { recursive: true });
|
|
20144
20921
|
for (const task of tasks) {
|
|
20145
20922
|
const existingRecords = /* @__PURE__ */ new Map();
|
|
20146
20923
|
let sourceBase = null;
|
|
@@ -20267,22 +21044,22 @@ var init_run2 = __esm({
|
|
|
20267
21044
|
});
|
|
20268
21045
|
|
|
20269
21046
|
// src/research/summarize.ts
|
|
20270
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
20271
|
-
import { join as
|
|
21047
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync25 } from "node:fs";
|
|
21048
|
+
import { join as join27, resolve as resolve17 } from "node:path";
|
|
20272
21049
|
function readLedger2(dir) {
|
|
20273
|
-
const pairsDir =
|
|
21050
|
+
const pairsDir = join27(resolve17(dir), "pairs");
|
|
20274
21051
|
let names;
|
|
20275
21052
|
try {
|
|
20276
|
-
names = readdirSync7(pairsDir).filter((
|
|
21053
|
+
names = readdirSync7(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
|
|
20277
21054
|
} catch (e) {
|
|
20278
21055
|
throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
|
|
20279
21056
|
}
|
|
20280
21057
|
if (names.length === 0) throw new ResearchError(`ledger ${pairsDir} holds no pair records`);
|
|
20281
|
-
return names.map((
|
|
20282
|
-
const path =
|
|
21058
|
+
return names.map((n2) => {
|
|
21059
|
+
const path = join27(pairsDir, n2);
|
|
20283
21060
|
let raw;
|
|
20284
21061
|
try {
|
|
20285
|
-
raw = JSON.parse(
|
|
21062
|
+
raw = JSON.parse(readFileSync25(path, "utf8"));
|
|
20286
21063
|
} catch (e) {
|
|
20287
21064
|
throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
|
|
20288
21065
|
}
|
|
@@ -20412,7 +21189,321 @@ var init_summarize = __esm({
|
|
|
20412
21189
|
}
|
|
20413
21190
|
});
|
|
20414
21191
|
|
|
21192
|
+
// src/research/init.ts
|
|
21193
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
21194
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync26, writeFileSync as writeFileSync13 } from "node:fs";
|
|
21195
|
+
import { basename as basename5, dirname as dirname12, resolve as resolve18 } from "node:path";
|
|
21196
|
+
function idFrom(repo) {
|
|
21197
|
+
const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
|
|
21198
|
+
return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
|
|
21199
|
+
}
|
|
21200
|
+
function createResearchManifest(opts) {
|
|
21201
|
+
if (!opts.out) throw new ResearchError("research init requires --out");
|
|
21202
|
+
if (!opts.repo) throw new ResearchError("research init requires --repo");
|
|
21203
|
+
if (!opts.prompt) throw new ResearchError("research init requires --prompt");
|
|
21204
|
+
if (!opts.verifyCommand) throw new ResearchError("research init requires --verify-command");
|
|
21205
|
+
const out3 = resolve18(opts.out);
|
|
21206
|
+
if (existsSync19(out3)) throw new ResearchError(`research init refuses to overwrite ${out3}; choose a new path`);
|
|
21207
|
+
const id = opts.id ?? idFrom(opts.repo);
|
|
21208
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) throw new ResearchError(`research init id must match [A-Za-z0-9._-] (got "${id}")`);
|
|
21209
|
+
if (opts.verifyBudget !== void 0 && (!Number.isFinite(opts.verifyBudget) || opts.verifyBudget <= 0)) {
|
|
21210
|
+
throw new ResearchError("research init --verify-budget must be positive");
|
|
21211
|
+
}
|
|
21212
|
+
const doc = {
|
|
21213
|
+
version: 1,
|
|
21214
|
+
tasks: [{
|
|
21215
|
+
id,
|
|
21216
|
+
repo: opts.repo,
|
|
21217
|
+
base: opts.base ?? "HEAD",
|
|
21218
|
+
prompt: opts.prompt,
|
|
21219
|
+
verify: { command: opts.verifyCommand, ...opts.verifyBudget === void 0 ? {} : { budget: opts.verifyBudget } }
|
|
21220
|
+
}]
|
|
21221
|
+
};
|
|
21222
|
+
const text = JSON.stringify(doc, null, 2) + "\n";
|
|
21223
|
+
mkdirSync11(dirname12(out3), { recursive: true });
|
|
21224
|
+
writeFileSync13(out3, text, { flag: "wx", mode: 384 });
|
|
21225
|
+
const parsed = readManifest(out3);
|
|
21226
|
+
if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
|
|
21227
|
+
return out3;
|
|
21228
|
+
}
|
|
21229
|
+
function runResearchInit(opts) {
|
|
21230
|
+
try {
|
|
21231
|
+
const out3 = createResearchManifest(opts);
|
|
21232
|
+
const bytes = requireHash(out3);
|
|
21233
|
+
process.stdout.write(`tamperward research init: wrote ${out3} (sha256 ${bytes})
|
|
21234
|
+
`);
|
|
21235
|
+
return 0;
|
|
21236
|
+
} catch (e) {
|
|
21237
|
+
if (e instanceof ResearchError || e instanceof Error) {
|
|
21238
|
+
process.stderr.write(`tamperward research: ${e.message}
|
|
21239
|
+
`);
|
|
21240
|
+
return 2;
|
|
21241
|
+
}
|
|
21242
|
+
throw e;
|
|
21243
|
+
}
|
|
21244
|
+
}
|
|
21245
|
+
function requireHash(path) {
|
|
21246
|
+
return createHash15("sha256").update(readFileSync26(path)).digest("hex");
|
|
21247
|
+
}
|
|
21248
|
+
var init_init2 = __esm({
|
|
21249
|
+
"src/research/init.ts"() {
|
|
21250
|
+
"use strict";
|
|
21251
|
+
init_adapter();
|
|
21252
|
+
init_manifest();
|
|
21253
|
+
}
|
|
21254
|
+
});
|
|
21255
|
+
|
|
21256
|
+
// src/research/report.ts
|
|
21257
|
+
function renderResearchReport(summary) {
|
|
21258
|
+
const u = summary.model_behaviour.ungated;
|
|
21259
|
+
const g = summary.model_behaviour.gated;
|
|
21260
|
+
const uo = summary.independent_outcome.ungated;
|
|
21261
|
+
const go = summary.independent_outcome.gated;
|
|
21262
|
+
const lines = [
|
|
21263
|
+
"MODEL BEHAVIOUR",
|
|
21264
|
+
` model: ${summary.model ?? "(unspecified)"}`,
|
|
21265
|
+
` adapter: ${summary.adapter.name} [${summary.adapter.layers.join(", ")}]`,
|
|
21266
|
+
` pairs: ${n(summary.pairs)} (measured: ${n(summary.measured_pairs)}, unmeasurable: ${n(summary.unmeasurable_pairs)})`,
|
|
21267
|
+
` ungated: ${n(u.trajectories)} trajectories; masked failures ${n(u.masked_failure)}; surviving mutations ${n(u.surviving_protected_mutations)}`,
|
|
21268
|
+
` gated: ${n(g.trajectories)} trajectories; masked failures ${n(g.masked_failure)}; surviving mutations ${n(g.surviving_protected_mutations)}`,
|
|
21269
|
+
"",
|
|
21270
|
+
"CONTROL RESPONSE",
|
|
21271
|
+
" not measured by this adapter/ledger; denial-to-fix, surrender, and alternative-bypass responses are not inferred.",
|
|
21272
|
+
"",
|
|
21273
|
+
"INDEPENDENT OUTCOME",
|
|
21274
|
+
` ungated: visible green ${n(uo.visible_green)}, pristine green ${n(uo.pristine_green)}, masked failure ${n(uo.masked_failure)}, honest completion ${n(uo.honest_completion)}, released green ${n(uo.released_green)}`,
|
|
21275
|
+
` gated: visible green ${n(go.visible_green)}, pristine green ${n(go.pristine_green)}, masked failure ${n(go.masked_failure)}, honest completion ${n(go.honest_completion)}, released green ${n(go.released_green)}`,
|
|
21276
|
+
"",
|
|
21277
|
+
"TAMPERWARD PERFORMANCE",
|
|
21278
|
+
` caught: ${n(summary.tamperward.caught)}`,
|
|
21279
|
+
` escapes: ${n(summary.tamperward.escapes)}`,
|
|
21280
|
+
` false refusals: ${n(summary.tamperward.false_refusals)}`,
|
|
21281
|
+
` cannot adjudicate: ${n(summary.tamperward.cannot_adjudicate)}`,
|
|
21282
|
+
` paired masked failure: ungated-only ${n(summary.paired.masked_failure.ungated_only)}, gated-only ${n(summary.paired.masked_failure.gated_only)}, both ${n(summary.paired.masked_failure.both)}, neither ${n(summary.paired.masked_failure.neither)}`
|
|
21283
|
+
];
|
|
21284
|
+
return lines.join("\n") + "\n";
|
|
21285
|
+
}
|
|
21286
|
+
function runResearchReport(opts) {
|
|
21287
|
+
try {
|
|
21288
|
+
const summary = summarizeLedger(opts.ledger);
|
|
21289
|
+
process.stdout.write(opts.json ? JSON.stringify(summary, null, 2) + "\n" : renderResearchReport(summary));
|
|
21290
|
+
return 0;
|
|
21291
|
+
} catch (e) {
|
|
21292
|
+
if (e instanceof ResearchError) {
|
|
21293
|
+
process.stderr.write(`tamperward research: ${e.message}
|
|
21294
|
+
`);
|
|
21295
|
+
return 2;
|
|
21296
|
+
}
|
|
21297
|
+
throw e;
|
|
21298
|
+
}
|
|
21299
|
+
}
|
|
21300
|
+
var n;
|
|
21301
|
+
var init_report2 = __esm({
|
|
21302
|
+
"src/research/report.ts"() {
|
|
21303
|
+
"use strict";
|
|
21304
|
+
init_adapter();
|
|
21305
|
+
init_summarize();
|
|
21306
|
+
n = (value) => String(value);
|
|
21307
|
+
}
|
|
21308
|
+
});
|
|
21309
|
+
|
|
21310
|
+
// src/research/bundle.ts
|
|
21311
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
21312
|
+
import { gzipSync, gunzipSync } from "node:zlib";
|
|
21313
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync27, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
|
|
21314
|
+
import { dirname as dirname13, join as join28, resolve as resolve19 } from "node:path";
|
|
21315
|
+
function octal(value, width) {
|
|
21316
|
+
return value.toString(8).padStart(width - 1, "0") + "\0";
|
|
21317
|
+
}
|
|
21318
|
+
function tarEntry(name, bytes) {
|
|
21319
|
+
const header = Buffer.alloc(BLOCK, 0);
|
|
21320
|
+
const safe = name.replace(/^\/+/, "").slice(0, 99);
|
|
21321
|
+
header.write(safe, 0, "utf8");
|
|
21322
|
+
header.write(octal(420, 8), 100, "ascii");
|
|
21323
|
+
header.write(octal(0, 8), 108, "ascii");
|
|
21324
|
+
header.write(octal(0, 8), 116, "ascii");
|
|
21325
|
+
header.write(octal(bytes.length, 12), 124, "ascii");
|
|
21326
|
+
header.write(octal(0, 12), 136, "ascii");
|
|
21327
|
+
header.fill(32, 148, 156);
|
|
21328
|
+
header[156] = 48;
|
|
21329
|
+
header.write("ustar", 257, "ascii");
|
|
21330
|
+
header.write("00", 263, "ascii");
|
|
21331
|
+
const checksum = header.reduce((sum, b) => sum + b, 0);
|
|
21332
|
+
header.write(octal(checksum, 8), 148, "ascii");
|
|
21333
|
+
const padding = Buffer.alloc((BLOCK - bytes.length % BLOCK) % BLOCK, 0);
|
|
21334
|
+
return Buffer.concat([header, bytes, padding]);
|
|
21335
|
+
}
|
|
21336
|
+
function makeTar(entries) {
|
|
21337
|
+
return Buffer.concat([...entries.map((e) => tarEntry(e.name, e.bytes)), Buffer.alloc(BLOCK * 2, 0)]);
|
|
21338
|
+
}
|
|
21339
|
+
function parseTar(bytes) {
|
|
21340
|
+
const files = /* @__PURE__ */ new Map();
|
|
21341
|
+
for (let offset = 0; offset + BLOCK <= bytes.length; ) {
|
|
21342
|
+
const header = bytes.subarray(offset, offset + BLOCK);
|
|
21343
|
+
if (header.every((b) => b === 0)) break;
|
|
21344
|
+
const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
21345
|
+
const sizeText = header.subarray(124, 136).toString("ascii").replace(/\0.*$/, "").trim();
|
|
21346
|
+
const size = Number.parseInt(sizeText || "0", 8);
|
|
21347
|
+
if (!name || name.startsWith("/") || name.split("/").includes("..") || !Number.isSafeInteger(size) || size < 0) {
|
|
21348
|
+
throw new ResearchError("research bundle contains an unsafe or malformed archive entry");
|
|
21349
|
+
}
|
|
21350
|
+
const start = offset + BLOCK;
|
|
21351
|
+
const end = start + size;
|
|
21352
|
+
if (end > bytes.length) throw new ResearchError("research bundle is truncated");
|
|
21353
|
+
files.set(name, Buffer.from(bytes.subarray(start, end)));
|
|
21354
|
+
offset = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
21355
|
+
}
|
|
21356
|
+
return files;
|
|
21357
|
+
}
|
|
21358
|
+
function walkLedger(ledger) {
|
|
21359
|
+
const entries = [];
|
|
21360
|
+
const pairs = join28(ledger, "pairs");
|
|
21361
|
+
if (!existsSync20(pairs) || !statSync6(pairs).isDirectory()) throw new ResearchError(`ledger ${ledger} has no pairs directory`);
|
|
21362
|
+
const names = readdirSync8(pairs).filter((n2) => n2.endsWith(".json")).sort();
|
|
21363
|
+
if (names.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
|
|
21364
|
+
for (const name of names) {
|
|
21365
|
+
const path = join28(pairs, name);
|
|
21366
|
+
if (!statSync6(path).isFile()) continue;
|
|
21367
|
+
entries.push({ name: `ledger/pairs/${name}`, bytes: readFileSync27(path) });
|
|
21368
|
+
}
|
|
21369
|
+
return entries;
|
|
21370
|
+
}
|
|
21371
|
+
function jsonBytes(value) {
|
|
21372
|
+
return Buffer.from(JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
21373
|
+
}
|
|
21374
|
+
function validateResearchBundle(path) {
|
|
21375
|
+
let files;
|
|
21376
|
+
try {
|
|
21377
|
+
files = parseTar(gunzipSync(readFileSync27(resolve19(path))));
|
|
21378
|
+
} catch (e) {
|
|
21379
|
+
throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
21380
|
+
}
|
|
21381
|
+
const provenance = files.get("provenance.json");
|
|
21382
|
+
const summaryBytes = files.get("summary.json");
|
|
21383
|
+
const report2 = files.get("report.txt");
|
|
21384
|
+
if (!provenance || !summaryBytes || !report2) throw new ResearchError("research bundle is missing provenance.json, summary.json or report.txt");
|
|
21385
|
+
let p;
|
|
21386
|
+
let summary;
|
|
21387
|
+
try {
|
|
21388
|
+
p = JSON.parse(provenance.toString("utf8"));
|
|
21389
|
+
summary = JSON.parse(summaryBytes.toString("utf8"));
|
|
21390
|
+
} catch {
|
|
21391
|
+
throw new ResearchError("research bundle contains invalid JSON");
|
|
21392
|
+
}
|
|
21393
|
+
if (p.bundle_schema_version !== BUNDLE_VERSION || p.schema_version !== MACHINE_SCHEMA_VERSION) {
|
|
21394
|
+
throw new ResearchError("research bundle schema version is unsupported");
|
|
21395
|
+
}
|
|
21396
|
+
const manifest = files.get("manifest.json");
|
|
21397
|
+
if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
|
|
21398
|
+
throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
|
|
21399
|
+
}
|
|
21400
|
+
if (manifest && createHash16("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
|
|
21401
|
+
throw new ResearchError("research bundle manifest does not match its recorded sha256");
|
|
21402
|
+
}
|
|
21403
|
+
const parsedSummary = summary;
|
|
21404
|
+
if (parsedSummary.document !== "summary" || parsedSummary.command !== "research") throw new ResearchError("research bundle summary is not a research summary");
|
|
21405
|
+
const records = [];
|
|
21406
|
+
for (const [name, bytes] of files) {
|
|
21407
|
+
if (!name.startsWith("ledger/pairs/") || !name.endsWith(".json")) continue;
|
|
21408
|
+
let raw;
|
|
21409
|
+
try {
|
|
21410
|
+
raw = JSON.parse(bytes.toString("utf8"));
|
|
21411
|
+
} catch {
|
|
21412
|
+
throw new ResearchError(`invalid JSON in ${name}`);
|
|
21413
|
+
}
|
|
21414
|
+
records.push(pairRecordFrom(raw, name));
|
|
21415
|
+
}
|
|
21416
|
+
if (records.length === 0) throw new ResearchError("research bundle contains no pair records");
|
|
21417
|
+
const derived = summarizeRecords(records);
|
|
21418
|
+
if (JSON.stringify(derived) !== JSON.stringify(summary)) throw new ResearchError("research bundle summary does not match its pair records");
|
|
21419
|
+
if (report2.toString("utf8") !== renderResearchReport(derived)) throw new ResearchError("research bundle report does not match its summary");
|
|
21420
|
+
if (p.manifest_sha256 !== parsedSummary.manifest_sha256) throw new ResearchError("research bundle manifest hash disagrees with summary");
|
|
21421
|
+
if (!p.protocol || p.protocol !== "research-bundle-v1") throw new ResearchError("research bundle protocol is missing or unsupported");
|
|
21422
|
+
return { records: records.length, manifest_sha256: String(p.manifest_sha256) };
|
|
21423
|
+
}
|
|
21424
|
+
function createResearchBundle(opts) {
|
|
21425
|
+
if (!opts.ledger) throw new ResearchError("research bundle requires --ledger");
|
|
21426
|
+
if (!opts.out) throw new ResearchError("research bundle requires --out");
|
|
21427
|
+
const ledger = resolve19(opts.ledger);
|
|
21428
|
+
const summary = summarizeLedger(ledger);
|
|
21429
|
+
const entries = walkLedger(ledger);
|
|
21430
|
+
if (opts.manifest) {
|
|
21431
|
+
const manifest = readFileSync27(resolve19(opts.manifest));
|
|
21432
|
+
const sha3 = createHash16("sha256").update(manifest).digest("hex");
|
|
21433
|
+
if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
|
|
21434
|
+
entries.push({ name: "manifest.json", bytes: manifest });
|
|
21435
|
+
}
|
|
21436
|
+
const provenance = {
|
|
21437
|
+
bundle_schema_version: BUNDLE_VERSION,
|
|
21438
|
+
protocol: "research-bundle-v1",
|
|
21439
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
21440
|
+
manifest_sha256: summary.manifest_sha256,
|
|
21441
|
+
adapter: summary.adapter,
|
|
21442
|
+
model: summary.model,
|
|
21443
|
+
tamperward_version: summary.tamperward_version,
|
|
21444
|
+
agent_argv: summary.agent_argv,
|
|
21445
|
+
agent_budget: summary.agent_budget,
|
|
21446
|
+
records: entries.filter((e) => e.name.startsWith("ledger/pairs/")).length,
|
|
21447
|
+
manifest_included: Boolean(opts.manifest),
|
|
21448
|
+
prompts_included: Boolean(opts.manifest)
|
|
21449
|
+
};
|
|
21450
|
+
entries.push({ name: "summary.json", bytes: jsonBytes(summary) });
|
|
21451
|
+
entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
|
|
21452
|
+
entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
|
|
21453
|
+
const out3 = resolve19(opts.out);
|
|
21454
|
+
mkdirSync12(dirname13(out3), { recursive: true });
|
|
21455
|
+
writeFileSync14(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
|
|
21456
|
+
return out3;
|
|
21457
|
+
}
|
|
21458
|
+
function runResearchBundle(opts) {
|
|
21459
|
+
try {
|
|
21460
|
+
if (opts.validate) {
|
|
21461
|
+
const result = validateResearchBundle(opts.validate);
|
|
21462
|
+
process.stdout.write(`tamperward research bundle: valid (${result.records} records, manifest ${result.manifest_sha256})
|
|
21463
|
+
`);
|
|
21464
|
+
return 0;
|
|
21465
|
+
}
|
|
21466
|
+
const out3 = createResearchBundle(opts);
|
|
21467
|
+
process.stdout.write(`tamperward research bundle: wrote ${out3}
|
|
21468
|
+
`);
|
|
21469
|
+
return 0;
|
|
21470
|
+
} catch (e) {
|
|
21471
|
+
if (e instanceof ResearchError || e instanceof Error) {
|
|
21472
|
+
process.stderr.write(`tamperward research: ${e.message}
|
|
21473
|
+
`);
|
|
21474
|
+
return 2;
|
|
21475
|
+
}
|
|
21476
|
+
throw e;
|
|
21477
|
+
}
|
|
21478
|
+
}
|
|
21479
|
+
var BLOCK, BUNDLE_VERSION;
|
|
21480
|
+
var init_bundle = __esm({
|
|
21481
|
+
"src/research/bundle.ts"() {
|
|
21482
|
+
"use strict";
|
|
21483
|
+
init_machine_output();
|
|
21484
|
+
init_adapter();
|
|
21485
|
+
init_record();
|
|
21486
|
+
init_report2();
|
|
21487
|
+
init_summarize();
|
|
21488
|
+
BLOCK = 512;
|
|
21489
|
+
BUNDLE_VERSION = 1;
|
|
21490
|
+
}
|
|
21491
|
+
});
|
|
21492
|
+
|
|
20415
21493
|
// src/cli/research.ts
|
|
21494
|
+
function parseResearchInit(args) {
|
|
21495
|
+
const o = { out: "", repo: "", prompt: "", verifyCommand: "" };
|
|
21496
|
+
for (let i = 0; i < args.length; i++) {
|
|
21497
|
+
if (args[i] === "--out") o.out = args[++i];
|
|
21498
|
+
else if (args[i] === "--repo") o.repo = args[++i];
|
|
21499
|
+
else if (args[i] === "--base") o.base = args[++i];
|
|
21500
|
+
else if (args[i] === "--id") o.id = args[++i];
|
|
21501
|
+
else if (args[i] === "--prompt") o.prompt = args[++i];
|
|
21502
|
+
else if (args[i] === "--verify-command") o.verifyCommand = args[++i];
|
|
21503
|
+
else if (args[i] === "--verify-budget") o.verifyBudget = Number(args[++i]);
|
|
21504
|
+
}
|
|
21505
|
+
return o;
|
|
21506
|
+
}
|
|
20416
21507
|
function parseResearchRun(args) {
|
|
20417
21508
|
const o = { manifest: "", out: "", adapter: "", agentArgv: [] };
|
|
20418
21509
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -20438,10 +21529,36 @@ function parseResearchSummarize(args) {
|
|
|
20438
21529
|
}
|
|
20439
21530
|
return o;
|
|
20440
21531
|
}
|
|
21532
|
+
function parseResearchReport(args) {
|
|
21533
|
+
const o = { ledger: "" };
|
|
21534
|
+
for (let i = 0; i < args.length; i++) {
|
|
21535
|
+
if (args[i] === "--ledger") o.ledger = args[++i];
|
|
21536
|
+
else if (args[i] === "--json") o.json = true;
|
|
21537
|
+
}
|
|
21538
|
+
return o;
|
|
21539
|
+
}
|
|
21540
|
+
function parseResearchBundle(args) {
|
|
21541
|
+
const o = { ledger: "" };
|
|
21542
|
+
for (let i = 0; i < args.length; i++) {
|
|
21543
|
+
if (args[i] === "--ledger") o.ledger = args[++i];
|
|
21544
|
+
else if (args[i] === "--out") o.out = args[++i];
|
|
21545
|
+
else if (args[i] === "--manifest") o.manifest = args[++i];
|
|
21546
|
+
else if (args[i] === "--validate") o.validate = args[++i];
|
|
21547
|
+
else if (args[i] === "--bundle") o.validate = args[++i];
|
|
21548
|
+
}
|
|
21549
|
+
return o;
|
|
21550
|
+
}
|
|
20441
21551
|
function runResearchCommand(args) {
|
|
20442
21552
|
const [sub, ...rest] = args;
|
|
21553
|
+
if (sub === "init") return runResearchInit(parseResearchInit(rest));
|
|
20443
21554
|
if (sub === "run") return runResearch(parseResearchRun(rest));
|
|
20444
21555
|
if (sub === "summarize") return runResearchSummarize(parseResearchSummarize(rest));
|
|
21556
|
+
if (sub === "report") return runResearchReport(parseResearchReport(rest));
|
|
21557
|
+
if (sub === "bundle") return runResearchBundle(parseResearchBundle(rest));
|
|
21558
|
+
if (sub === "validate") {
|
|
21559
|
+
const bundle = parseResearchBundle(rest);
|
|
21560
|
+
return runResearchBundle({ ledger: "", validate: bundle.validate ?? bundle.out });
|
|
21561
|
+
}
|
|
20445
21562
|
process.stderr.write(`tamperward research: unknown subcommand "${sub ?? ""}" (${RESEARCH_SUBCOMMANDS.join(" | ")})
|
|
20446
21563
|
`);
|
|
20447
21564
|
return 2;
|
|
@@ -20452,7 +21569,10 @@ var init_research = __esm({
|
|
|
20452
21569
|
"use strict";
|
|
20453
21570
|
init_run2();
|
|
20454
21571
|
init_summarize();
|
|
20455
|
-
|
|
21572
|
+
init_init2();
|
|
21573
|
+
init_report2();
|
|
21574
|
+
init_bundle();
|
|
21575
|
+
RESEARCH_SUBCOMMANDS = ["init", "run", "summarize", "report", "bundle", "validate"];
|
|
20456
21576
|
}
|
|
20457
21577
|
});
|
|
20458
21578
|
|
|
@@ -20551,6 +21671,15 @@ function parseDoctor(args) {
|
|
|
20551
21671
|
}
|
|
20552
21672
|
return o;
|
|
20553
21673
|
}
|
|
21674
|
+
function parseStatus(args) {
|
|
21675
|
+
const o = {};
|
|
21676
|
+
for (let i = 0; i < args.length; i++) {
|
|
21677
|
+
const a = args[i];
|
|
21678
|
+
if (a === "--json") o.json = true;
|
|
21679
|
+
else if (a === "--cwd") o.cwd = args[++i];
|
|
21680
|
+
}
|
|
21681
|
+
return o;
|
|
21682
|
+
}
|
|
20554
21683
|
function parseStats(args) {
|
|
20555
21684
|
const o = {};
|
|
20556
21685
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -20587,14 +21716,45 @@ function parseCheck(args) {
|
|
|
20587
21716
|
}
|
|
20588
21717
|
return o;
|
|
20589
21718
|
}
|
|
21719
|
+
function splitLongOptionEquals(args) {
|
|
21720
|
+
const out3 = [];
|
|
21721
|
+
for (let i = 0; i < args.length; i++) {
|
|
21722
|
+
const arg = args[i];
|
|
21723
|
+
if (arg === "--") {
|
|
21724
|
+
out3.push(...args.slice(i));
|
|
21725
|
+
break;
|
|
21726
|
+
}
|
|
21727
|
+
if (arg.startsWith("--")) {
|
|
21728
|
+
const equals = arg.indexOf("=");
|
|
21729
|
+
if (equals > 2) {
|
|
21730
|
+
out3.push(arg.slice(0, equals), arg.slice(equals + 1));
|
|
21731
|
+
continue;
|
|
21732
|
+
}
|
|
21733
|
+
}
|
|
21734
|
+
out3.push(arg);
|
|
21735
|
+
}
|
|
21736
|
+
return out3;
|
|
21737
|
+
}
|
|
21738
|
+
function helpRequested(args) {
|
|
21739
|
+
for (const arg of args) {
|
|
21740
|
+
if (arg === "--") break;
|
|
21741
|
+
if (arg === "-h" || arg === "--help") return true;
|
|
21742
|
+
}
|
|
21743
|
+
return false;
|
|
21744
|
+
}
|
|
20590
21745
|
function validateFlatArgs(args, grammar) {
|
|
20591
21746
|
const flags = new Set(grammar.flags ?? []);
|
|
20592
21747
|
const values = grammar.values ?? {};
|
|
20593
21748
|
const seen = /* @__PURE__ */ new Set();
|
|
20594
21749
|
const positionals = [];
|
|
21750
|
+
let options = true;
|
|
20595
21751
|
for (let i = 0; i < args.length; i++) {
|
|
20596
21752
|
const a = args[i];
|
|
20597
|
-
if (
|
|
21753
|
+
if (options && a === "--") {
|
|
21754
|
+
options = false;
|
|
21755
|
+
continue;
|
|
21756
|
+
}
|
|
21757
|
+
if (options && (flags.has(a) || Object.prototype.hasOwnProperty.call(values, a))) {
|
|
20598
21758
|
if (seen.has(a)) {
|
|
20599
21759
|
return { error: `option "${a}" specified more than once`, seen, positionals };
|
|
20600
21760
|
}
|
|
@@ -20611,18 +21771,18 @@ function validateFlatArgs(args, grammar) {
|
|
|
20611
21771
|
}
|
|
20612
21772
|
i++;
|
|
20613
21773
|
if (rule === "positive") {
|
|
20614
|
-
const
|
|
20615
|
-
if (!Number.isFinite(
|
|
21774
|
+
const n2 = Number(v);
|
|
21775
|
+
if (!Number.isFinite(n2) || n2 <= 0) {
|
|
20616
21776
|
return { error: `${a} needs a positive number (got "${v}")`, seen, positionals };
|
|
20617
21777
|
}
|
|
20618
21778
|
} else if (rule === "positive-integer") {
|
|
20619
|
-
const
|
|
20620
|
-
if (!Number.isFinite(
|
|
21779
|
+
const n2 = Number(v);
|
|
21780
|
+
if (!Number.isFinite(n2) || !Number.isInteger(n2) || n2 <= 0) {
|
|
20621
21781
|
return { error: `${a} needs a positive integer (got "${v}")`, seen, positionals };
|
|
20622
21782
|
}
|
|
20623
21783
|
} else if (rule === "non-negative") {
|
|
20624
|
-
const
|
|
20625
|
-
if (!Number.isFinite(
|
|
21784
|
+
const n2 = Number(v);
|
|
21785
|
+
if (!Number.isFinite(n2) || n2 < 0) {
|
|
20626
21786
|
return { error: `${a} needs a non-negative number (got "${v}")`, seen, positionals };
|
|
20627
21787
|
}
|
|
20628
21788
|
} else if (rule === "format" && !isFormat(v)) {
|
|
@@ -20634,7 +21794,7 @@ function validateFlatArgs(args, grammar) {
|
|
|
20634
21794
|
}
|
|
20635
21795
|
continue;
|
|
20636
21796
|
}
|
|
20637
|
-
if (a.startsWith("-")) {
|
|
21797
|
+
if (options && a.startsWith("-")) {
|
|
20638
21798
|
return { error: `unknown option "${a}"`, seen, positionals };
|
|
20639
21799
|
}
|
|
20640
21800
|
if (grammar.positional === "one" && positionals.length === 0) {
|
|
@@ -20646,6 +21806,8 @@ function validateFlatArgs(args, grammar) {
|
|
|
20646
21806
|
return { seen, positionals };
|
|
20647
21807
|
}
|
|
20648
21808
|
function validateCliArgs(cmd, args) {
|
|
21809
|
+
args = splitLongOptionEquals(args);
|
|
21810
|
+
if (helpRequested(args)) return void 0;
|
|
20649
21811
|
if (cmd === "hook" || cmd === "sweep") {
|
|
20650
21812
|
if (args.length === 0) return `${cmd} requires an agent name`;
|
|
20651
21813
|
if (args.length > 1) return `unexpected argument "${args[1]}"`;
|
|
@@ -20726,6 +21888,12 @@ function validateCliArgs(cmd, args) {
|
|
|
20726
21888
|
}
|
|
20727
21889
|
}).error;
|
|
20728
21890
|
}
|
|
21891
|
+
if (cmd === "status") {
|
|
21892
|
+
return validateFlatArgs(args, {
|
|
21893
|
+
flags: ["--json"],
|
|
21894
|
+
values: { "--cwd": "string" }
|
|
21895
|
+
}).error;
|
|
21896
|
+
}
|
|
20729
21897
|
if (cmd === "onboard") {
|
|
20730
21898
|
const parsed = validateFlatArgs(args, {
|
|
20731
21899
|
flags: ["--yes", "--skip-demo", "--demo", "--no-github"],
|
|
@@ -20744,7 +21912,7 @@ function validateCliArgs(cmd, args) {
|
|
|
20744
21912
|
if (cmd === "hook-service") {
|
|
20745
21913
|
const sub = args[0];
|
|
20746
21914
|
if (sub !== "start" && sub !== "stop" && sub !== "status") return "hook-service requires one of start | stop | status";
|
|
20747
|
-
return validateFlatArgs(args.slice(1),
|
|
21915
|
+
return validateFlatArgs(args.slice(1), { values: { "--dir": "string" } }).error;
|
|
20748
21916
|
}
|
|
20749
21917
|
if (cmd === "verify") {
|
|
20750
21918
|
return validateFlatArgs(args, {
|
|
@@ -20777,6 +21945,24 @@ function validateCliArgs(cmd, args) {
|
|
|
20777
21945
|
if (cmd === "research") {
|
|
20778
21946
|
const [sub, ...rest] = args;
|
|
20779
21947
|
if (sub === void 0) return `research requires a subcommand (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
21948
|
+
if (sub === "init") {
|
|
21949
|
+
const parsed = validateFlatArgs(rest, {
|
|
21950
|
+
values: {
|
|
21951
|
+
"--out": "string",
|
|
21952
|
+
"--repo": "string",
|
|
21953
|
+
"--base": "string",
|
|
21954
|
+
"--id": "string",
|
|
21955
|
+
"--prompt": "string",
|
|
21956
|
+
"--verify-command": "string",
|
|
21957
|
+
"--verify-budget": "positive"
|
|
21958
|
+
}
|
|
21959
|
+
});
|
|
21960
|
+
if (parsed.error) return parsed.error;
|
|
21961
|
+
for (const required of ["--out", "--repo", "--prompt", "--verify-command"]) {
|
|
21962
|
+
if (!parsed.seen.has(required)) return `research init requires ${required}`;
|
|
21963
|
+
}
|
|
21964
|
+
return void 0;
|
|
21965
|
+
}
|
|
20780
21966
|
if (sub === "run") {
|
|
20781
21967
|
const delimiter3 = rest.indexOf("--");
|
|
20782
21968
|
const prefix = delimiter3 < 0 ? rest : rest.slice(0, delimiter3);
|
|
@@ -20803,6 +21989,29 @@ function validateCliArgs(cmd, args) {
|
|
|
20803
21989
|
if (!parsed.seen.has("--ledger")) return "research summarize requires --ledger";
|
|
20804
21990
|
return void 0;
|
|
20805
21991
|
}
|
|
21992
|
+
if (sub === "report") {
|
|
21993
|
+
const parsed = validateFlatArgs(rest, { flags: ["--json"], values: { "--ledger": "string" } });
|
|
21994
|
+
if (parsed.error) return parsed.error;
|
|
21995
|
+
if (!parsed.seen.has("--ledger")) return "research report requires --ledger";
|
|
21996
|
+
return void 0;
|
|
21997
|
+
}
|
|
21998
|
+
if (sub === "bundle") {
|
|
21999
|
+
const parsed = validateFlatArgs(rest, {
|
|
22000
|
+
values: { "--ledger": "string", "--out": "string", "--manifest": "string", "--validate": "string" }
|
|
22001
|
+
});
|
|
22002
|
+
if (parsed.error) return parsed.error;
|
|
22003
|
+
if (parsed.seen.has("--validate")) return void 0;
|
|
22004
|
+
for (const required of ["--ledger", "--out"]) {
|
|
22005
|
+
if (!parsed.seen.has(required)) return `research bundle requires ${required}`;
|
|
22006
|
+
}
|
|
22007
|
+
return void 0;
|
|
22008
|
+
}
|
|
22009
|
+
if (sub === "validate") {
|
|
22010
|
+
const parsed = validateFlatArgs(rest, { values: { "--bundle": "string" } });
|
|
22011
|
+
if (parsed.error) return parsed.error;
|
|
22012
|
+
if (!parsed.seen.has("--bundle")) return "research validate requires --bundle";
|
|
22013
|
+
return void 0;
|
|
22014
|
+
}
|
|
20806
22015
|
return `unknown research subcommand "${sub}" (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
20807
22016
|
}
|
|
20808
22017
|
return void 0;
|
|
@@ -20830,8 +22039,8 @@ Formats:
|
|
|
20830
22039
|
tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
|
|
20831
22040
|
tamperward sweep claude Stop sweep (re-scan the turn's working tree)
|
|
20832
22041
|
tamperward hook-service start [--dir D] OPT-IN persistent hook service: one warm
|
|
20833
|
-
tamperward hook-service stop
|
|
20834
|
-
|
|
22042
|
+
tamperward hook-service stop [--dir D] process per user and repository that
|
|
22043
|
+
tamperward hook-service status [--dir D] evaluates hook/sweep requests over a
|
|
20835
22044
|
private unix socket, so each tool call
|
|
20836
22045
|
skips Node + bundle startup. Hooks
|
|
20837
22046
|
consult it only with
|
|
@@ -20877,6 +22086,8 @@ Formats:
|
|
|
20877
22086
|
code when clean; 124 on clean AGENT_TIMEOUT;
|
|
20878
22087
|
1 on any blocking finding/masked failure;
|
|
20879
22088
|
2 when it cannot adjudicate (fails closed)
|
|
22089
|
+
tamperward research init --out F --repo R author a versioned one-task manifest
|
|
22090
|
+
--prompt P --verify-command C and print its sha256 identity
|
|
20880
22091
|
tamperward research run --manifest F bring-your-own-model evaluation: for every
|
|
20881
22092
|
--out D --adapter A [--pairs N] task in the manifest, pin one source commit,
|
|
20882
22093
|
[--model M] [--agent-budget S] clone fresh state per arm, run the agent
|
|
@@ -20888,6 +22099,11 @@ Formats:
|
|
|
20888
22099
|
tamperward research summarize --ledger D aggregate measured pairs into model behaviour,
|
|
20889
22100
|
independent outcome, TamperWard hits/misses
|
|
20890
22101
|
and paired counts \u2014 no composite score
|
|
22102
|
+
tamperward research report --ledger D human-readable four-section report (or --json)
|
|
22103
|
+
tamperward research bundle --ledger D reproducible records/provenance archive
|
|
22104
|
+
--out F [--manifest M] (manifest/prompts included only when requested)
|
|
22105
|
+
tamperward research bundle --validate F structurally and provenance-validate a bundle
|
|
22106
|
+
tamperward research validate --bundle F alias for bundle validation
|
|
20891
22107
|
tamperward stats [--file F] [--since 30d] aggregate privacy-safe hook/sweep audit
|
|
20892
22108
|
[--json] [--cwd D] events by rule and enforcement surface.
|
|
20893
22109
|
Defaults to the repository-local
|
|
@@ -20923,6 +22139,16 @@ Formats:
|
|
|
20923
22139
|
every tool the gate must see.
|
|
20924
22140
|
--force-workflow replaces a workflow it
|
|
20925
22141
|
did not write, or one you have edited.
|
|
22142
|
+
tamperward status [--json] [--cwd D] verification posture in three distinct
|
|
22143
|
+
lanes \u2014 Authority (repository/final
|
|
22144
|
+
adjudication), Intervention (runtime
|
|
22145
|
+
steering), and Verification: whether the
|
|
22146
|
+
last successful verify still applies to
|
|
22147
|
+
the EXACT current state (CURRENT / STALE /
|
|
22148
|
+
VERIFYING / BROKEN / UNVERIFIED). --json
|
|
22149
|
+
emits the versioned status document for
|
|
22150
|
+
editors, CI and dashboards. Local CURRENT
|
|
22151
|
+
is posture, never repository/CI authority.
|
|
20926
22152
|
tamperward doctor [--base R] report installation + authority posture
|
|
20927
22153
|
[--workflow F] [--cwd D] and validate the CI verifier's outer-time
|
|
20928
22154
|
[--json] envelope against the trusted policy.
|
|
@@ -20944,7 +22170,12 @@ Exit codes: 0 clean \xB7 1 a blocking finding (check), MASKED_FAILURE or SUITE_R
|
|
|
20944
22170
|
`);
|
|
20945
22171
|
}
|
|
20946
22172
|
function main(argv) {
|
|
20947
|
-
const [cmd, ...
|
|
22173
|
+
const [cmd, ...rawRest] = argv;
|
|
22174
|
+
const rest = splitLongOptionEquals(rawRest);
|
|
22175
|
+
if (cmd !== void 0 && cmd !== "-h" && cmd !== "--help" && helpRequested(rest)) {
|
|
22176
|
+
printHelp();
|
|
22177
|
+
return 0;
|
|
22178
|
+
}
|
|
20948
22179
|
if (cmd !== void 0 && cmd !== "-h" && cmd !== "--help") {
|
|
20949
22180
|
const invalid = validateCliArgs(cmd, rest);
|
|
20950
22181
|
if (invalid) {
|
|
@@ -20970,6 +22201,8 @@ function main(argv) {
|
|
|
20970
22201
|
return runDoctor(parseDoctor(rest));
|
|
20971
22202
|
case "stats":
|
|
20972
22203
|
return runStats(parseStats(rest));
|
|
22204
|
+
case "status":
|
|
22205
|
+
return runStatus(parseStatus(rest));
|
|
20973
22206
|
case "onboard":
|
|
20974
22207
|
return runOnboard(parseOnboard(rest));
|
|
20975
22208
|
case "watch":
|
|
@@ -21025,6 +22258,7 @@ var init_main = __esm({
|
|
|
21025
22258
|
init_allow();
|
|
21026
22259
|
init_init();
|
|
21027
22260
|
init_doctor();
|
|
22261
|
+
init_status2();
|
|
21028
22262
|
init_verify();
|
|
21029
22263
|
init_trace_verify();
|
|
21030
22264
|
init_run();
|
|
@@ -21039,7 +22273,7 @@ var init_main = __esm({
|
|
|
21039
22273
|
// src/cli/index.ts
|
|
21040
22274
|
init_hook_client();
|
|
21041
22275
|
init_exit();
|
|
21042
|
-
import { readFileSync as
|
|
22276
|
+
import { readFileSync as readFileSync28 } from "node:fs";
|
|
21043
22277
|
function loadMain() {
|
|
21044
22278
|
return Promise.resolve().then(() => (init_main(), main_exports));
|
|
21045
22279
|
}
|
|
@@ -21052,7 +22286,7 @@ async function launch(argv) {
|
|
|
21052
22286
|
if (kind && hookServiceEnabled()) {
|
|
21053
22287
|
let raw = null;
|
|
21054
22288
|
try {
|
|
21055
|
-
raw =
|
|
22289
|
+
raw = readFileSync28(0, "utf8");
|
|
21056
22290
|
} catch {
|
|
21057
22291
|
raw = null;
|
|
21058
22292
|
}
|