tamperward 2.32.0 → 2.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli/index.js +1018 -273
- 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/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
|
}
|
|
@@ -8932,7 +8932,7 @@ They WILL FAIL in v10.0.0
|
|
|
8932
8932
|
makeFinding(RULE9, policy, {
|
|
8933
8933
|
file: c.path,
|
|
8934
8934
|
message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
|
|
8935
|
-
evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward
|
|
8935
|
+
evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or generate the compact CI label with \`tamperward signoff-label --rule hook-tampering --file ${c.path} --head <full-sha>\`, or make the gate live in it`,
|
|
8936
8936
|
remediation: "Run `tamperward check --staged` in the new hook, in a position where its failure fails the hook, or have a human sign off on a hook that does not."
|
|
8937
8937
|
})
|
|
8938
8938
|
);
|
|
@@ -8963,7 +8963,7 @@ They WILL FAIL in v10.0.0
|
|
|
8963
8963
|
makeFinding(RULE9, policy, {
|
|
8964
8964
|
file: c.path,
|
|
8965
8965
|
message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
|
|
8966
|
-
evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward
|
|
8966
|
+
evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or generate the compact CI label with \`tamperward signoff-label --rule hook-tampering --file ${c.path} --head <full-sha>\`, or restore it`,
|
|
8967
8967
|
remediation: "Restore the script, or have a human sign off. A line-by-line reading of a shell script cannot tell an honest restructuring from a neutered gate, so the gate script is held byte-for-byte: only raising its pin passes without a sign-off."
|
|
8968
8968
|
})
|
|
8969
8969
|
);
|
|
@@ -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
|
}
|
|
@@ -10869,6 +10869,12 @@ var init_policy_load = __esm({
|
|
|
10869
10869
|
import { createHash as createHash3 } from "node:crypto";
|
|
10870
10870
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
|
|
10871
10871
|
import { dirname as dirname2, join as join9 } from "node:path";
|
|
10872
|
+
function compactOobToken(want, head) {
|
|
10873
|
+
const normalizedHead = head.trim().toLowerCase();
|
|
10874
|
+
if (!want || !FULL_OBJECT_ID.test(normalizedHead)) return null;
|
|
10875
|
+
const digest = createHash3("sha256").update(`tamperward:oob:v1\0${want}\0${normalizedHead}`).digest("base64url");
|
|
10876
|
+
return `${COMPACT_OOB_PREFIX}${digest}`;
|
|
10877
|
+
}
|
|
10872
10878
|
function ledgerEntryFrom(value) {
|
|
10873
10879
|
if (!isRecord(value)) return null;
|
|
10874
10880
|
const { rule, file, fingerprint: fingerprint2, reason, recordedAt, expiresAt } = value;
|
|
@@ -10924,6 +10930,11 @@ function oobToken(want, oob, head) {
|
|
|
10924
10930
|
for (const raw of oob) {
|
|
10925
10931
|
const t = raw.trim();
|
|
10926
10932
|
if (!t) continue;
|
|
10933
|
+
if (t.startsWith(COMPACT_OOB_PREFIX)) {
|
|
10934
|
+
const expected = head === void 0 ? null : compactOobToken(want, head);
|
|
10935
|
+
if (expected !== null && t === expected) return t;
|
|
10936
|
+
continue;
|
|
10937
|
+
}
|
|
10927
10938
|
const at = t.lastIndexOf("@");
|
|
10928
10939
|
if (at === -1) {
|
|
10929
10940
|
if (!head && t === want) return t;
|
|
@@ -10956,13 +10967,15 @@ function oobHeadFromEnv(env = process.env) {
|
|
|
10956
10967
|
function oobFromEnv(env = process.env) {
|
|
10957
10968
|
return (env.TAMPERWARD_OOB_SIGNOFF ?? "").split(",").map((s) => s.trim().replace(/^tamperward:allow:/, "")).filter(Boolean);
|
|
10958
10969
|
}
|
|
10959
|
-
var DEFAULT_TTL_MS, fingerprintOf;
|
|
10970
|
+
var DEFAULT_TTL_MS, COMPACT_OOB_PREFIX, FULL_OBJECT_ID, fingerprintOf;
|
|
10960
10971
|
var init_signoff = __esm({
|
|
10961
10972
|
"src/signoff.ts"() {
|
|
10962
10973
|
"use strict";
|
|
10963
10974
|
init_policy_load();
|
|
10964
10975
|
init_narrow();
|
|
10965
10976
|
DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
10977
|
+
COMPACT_OOB_PREFIX = "tw1:";
|
|
10978
|
+
FULL_OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
10966
10979
|
fingerprintOf = (f) => fingerprint(f.rule, f.file, f.evidence);
|
|
10967
10980
|
}
|
|
10968
10981
|
});
|
|
@@ -11167,8 +11180,14 @@ function cell(s) {
|
|
|
11167
11180
|
function mdCode(s) {
|
|
11168
11181
|
return s.replace(/`/g, "'").replace(/[\r\n]/g, " ");
|
|
11169
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
|
+
}
|
|
11170
11189
|
function mdUrl(s) {
|
|
11171
|
-
return s.
|
|
11190
|
+
return s.split("/").map(encodePathSegment).join("/");
|
|
11172
11191
|
}
|
|
11173
11192
|
function locationCell(f, env) {
|
|
11174
11193
|
if (!f.file) return "`(command)`";
|
|
@@ -12289,10 +12308,10 @@ function parseAuditJsonl(raw) {
|
|
|
12289
12308
|
return events;
|
|
12290
12309
|
}
|
|
12291
12310
|
function parseSince(value, nowMs = Date.now()) {
|
|
12292
|
-
const
|
|
12293
|
-
if (
|
|
12294
|
-
const count = Number(
|
|
12295
|
-
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;
|
|
12296
12315
|
if (!Number.isSafeInteger(count) || count <= 0) throw new Error(`invalid --since value "${value}"`);
|
|
12297
12316
|
return nowMs - count * unit;
|
|
12298
12317
|
}
|
|
@@ -12701,8 +12720,8 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
12701
12720
|
if (!existsSync10(log)) return none;
|
|
12702
12721
|
let offset = 0;
|
|
12703
12722
|
if (cp && existsSync10(cp)) {
|
|
12704
|
-
const
|
|
12705
|
-
offset = Number.isFinite(
|
|
12723
|
+
const n2 = Number(readFileSync11(cp, "utf8"));
|
|
12724
|
+
offset = Number.isFinite(n2) ? n2 : 0;
|
|
12706
12725
|
}
|
|
12707
12726
|
const drained = drainEvents(log, offset);
|
|
12708
12727
|
if (!drained.complete) {
|
|
@@ -13000,12 +13019,12 @@ function pidAlive2(pid) {
|
|
|
13000
13019
|
}
|
|
13001
13020
|
}
|
|
13002
13021
|
function listening(socket) {
|
|
13003
|
-
return new Promise((
|
|
13022
|
+
return new Promise((resolve20) => {
|
|
13004
13023
|
const sock = createConnection2(socket);
|
|
13005
13024
|
const done = (v) => {
|
|
13006
13025
|
clearTimeout(timer);
|
|
13007
13026
|
sock.destroy();
|
|
13008
|
-
|
|
13027
|
+
resolve20(v);
|
|
13009
13028
|
};
|
|
13010
13029
|
const timer = setTimeout(() => done(false), 1e3);
|
|
13011
13030
|
sock.once("connect", () => done(true));
|
|
@@ -13184,11 +13203,11 @@ async function startHookService(opts) {
|
|
|
13184
13203
|
});
|
|
13185
13204
|
const umask = process.umask(63);
|
|
13186
13205
|
try {
|
|
13187
|
-
await new Promise((
|
|
13206
|
+
await new Promise((resolve20, reject) => {
|
|
13188
13207
|
server.once("error", reject);
|
|
13189
13208
|
server.listen(paths.socket, () => {
|
|
13190
13209
|
server.off("error", reject);
|
|
13191
|
-
|
|
13210
|
+
resolve20();
|
|
13192
13211
|
});
|
|
13193
13212
|
});
|
|
13194
13213
|
} finally {
|
|
@@ -13205,8 +13224,8 @@ async function startHookService(opts) {
|
|
|
13205
13224
|
get served() {
|
|
13206
13225
|
return served;
|
|
13207
13226
|
},
|
|
13208
|
-
close: () => new Promise((
|
|
13209
|
-
if (closed) return
|
|
13227
|
+
close: () => new Promise((resolve20) => {
|
|
13228
|
+
if (closed) return resolve20();
|
|
13210
13229
|
closed = true;
|
|
13211
13230
|
setSnapshotCache(null);
|
|
13212
13231
|
let settled = false;
|
|
@@ -13217,7 +13236,7 @@ async function startHookService(opts) {
|
|
|
13217
13236
|
clearTimeout(drain);
|
|
13218
13237
|
removeQuietly(paths.socket);
|
|
13219
13238
|
removeQuietly(paths.state);
|
|
13220
|
-
|
|
13239
|
+
resolve20();
|
|
13221
13240
|
};
|
|
13222
13241
|
server.close(finalize);
|
|
13223
13242
|
for (const sock of sockets) if (sock !== acceptedSocket) sock.destroy();
|
|
@@ -13244,11 +13263,16 @@ async function probeSocket(paths) {
|
|
|
13244
13263
|
function describeStateFile(state) {
|
|
13245
13264
|
return state ? `records pid ${state.pid}, root ${state.root}, tamperward@${state.version}` : "is absent or unreadable";
|
|
13246
13265
|
}
|
|
13247
|
-
async function stopHookService(paths) {
|
|
13266
|
+
async function stopHookService(paths, expectedRoot) {
|
|
13248
13267
|
const probe = await probeSocket(paths);
|
|
13249
13268
|
const state = readServiceState(paths);
|
|
13250
13269
|
if (probe.answered) {
|
|
13251
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
|
+
}
|
|
13252
13276
|
const agrees = st !== null && state !== null && state.pid === st.pid && state.root === st.root && state.version === st.version;
|
|
13253
13277
|
if (agrees && st.pid !== process.pid) {
|
|
13254
13278
|
try {
|
|
@@ -13270,13 +13294,13 @@ async function stopHookService(paths) {
|
|
|
13270
13294
|
removeQuietly(paths.state);
|
|
13271
13295
|
return { outcome: "not-running" };
|
|
13272
13296
|
}
|
|
13273
|
-
function
|
|
13274
|
-
let
|
|
13275
|
-
|
|
13276
|
-
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;
|
|
13277
13300
|
}
|
|
13278
13301
|
function runHookService(args) {
|
|
13279
13302
|
const sub = args[0];
|
|
13303
|
+
const dir = parseDir(args.slice(1));
|
|
13280
13304
|
const paths = servicePaths();
|
|
13281
13305
|
if (!paths) {
|
|
13282
13306
|
process.stderr.write(
|
|
@@ -13284,9 +13308,18 @@ function runHookService(args) {
|
|
|
13284
13308
|
);
|
|
13285
13309
|
return 2;
|
|
13286
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
|
+
}
|
|
13287
13321
|
if (sub === "start") {
|
|
13288
|
-
|
|
13289
|
-
startHookService({ root: dir, paths }).then((svc) => {
|
|
13322
|
+
startHookService({ root: requestedRoot ?? process.cwd(), paths }).then((svc) => {
|
|
13290
13323
|
let closing = false;
|
|
13291
13324
|
const shutdown = () => {
|
|
13292
13325
|
if (closing) return;
|
|
@@ -13307,7 +13340,7 @@ function runHookService(args) {
|
|
|
13307
13340
|
return -1;
|
|
13308
13341
|
}
|
|
13309
13342
|
if (sub === "stop") {
|
|
13310
|
-
return stopHookService(paths).then((res) => {
|
|
13343
|
+
return stopHookService(paths, requestedRoot).then((res) => {
|
|
13311
13344
|
if (res.outcome === "stopped") {
|
|
13312
13345
|
process.stdout.write(`tamperward hook-service: stopped (pid ${res.pid}); ${paths.socket} removed
|
|
13313
13346
|
`);
|
|
@@ -13350,6 +13383,14 @@ function runHookService(args) {
|
|
|
13350
13383
|
exitAfterFlush(0);
|
|
13351
13384
|
return;
|
|
13352
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
|
+
}
|
|
13353
13394
|
const cache3 = isRecord(res.cache) ? res.cache : {};
|
|
13354
13395
|
const startedAt = typeof res.started_at === "string" ? res.started_at : state?.started_at ?? "unknown";
|
|
13355
13396
|
const disagrees = !state || state.pid !== res.pid || state.root !== res.root || state.version !== res.version;
|
|
@@ -13381,6 +13422,7 @@ var init_hook_service = __esm({
|
|
|
13381
13422
|
});
|
|
13382
13423
|
|
|
13383
13424
|
// src/cli/allow.ts
|
|
13425
|
+
import { relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
|
|
13384
13426
|
function runAllow(opts) {
|
|
13385
13427
|
if (!opts.rule) {
|
|
13386
13428
|
process.stderr.write('tamperward allow <rule> [--file <path>] --reason "<why>"\n');
|
|
@@ -13399,8 +13441,9 @@ function runAllow(opts) {
|
|
|
13399
13441
|
process.stderr.write("tamperward: cannot read the working tree (not a git repo?).\n");
|
|
13400
13442
|
return 2;
|
|
13401
13443
|
}
|
|
13444
|
+
const requestedFile = opts.file === void 0 ? void 0 : relative3(cwd, resolve5(cwd, opts.file)).split(sep2).join("/");
|
|
13402
13445
|
const targets = findings.filter(
|
|
13403
|
-
(f) => f.severity === "block" && f.rule === opts.rule && (!
|
|
13446
|
+
(f) => f.severity === "block" && f.rule === opts.rule && (!requestedFile || f.file === requestedFile)
|
|
13404
13447
|
);
|
|
13405
13448
|
if (targets.length === 0) {
|
|
13406
13449
|
process.stderr.write(
|
|
@@ -13519,7 +13562,7 @@ var init_safe_write = __esm({
|
|
|
13519
13562
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
13520
13563
|
import { createHash as createHash8 } from "node:crypto";
|
|
13521
13564
|
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, realpathSync as realpathSync5 } from "node:fs";
|
|
13522
|
-
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";
|
|
13523
13566
|
function gitConfig(cwd, key2) {
|
|
13524
13567
|
try {
|
|
13525
13568
|
return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -13859,13 +13902,13 @@ function planClaudeHooks(cwd) {
|
|
|
13859
13902
|
function hooksDir(cwd) {
|
|
13860
13903
|
try {
|
|
13861
13904
|
const out3 = execFileSync6("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
13862
|
-
if (out3) return
|
|
13905
|
+
if (out3) return resolve6(cwd, out3);
|
|
13863
13906
|
} catch {
|
|
13864
13907
|
}
|
|
13865
13908
|
return existsSync12(join15(cwd, ".git")) ? join15(cwd, ".git", "hooks") : null;
|
|
13866
13909
|
}
|
|
13867
13910
|
function display(cwd, path) {
|
|
13868
|
-
const rel =
|
|
13911
|
+
const rel = relative4(cwd, path);
|
|
13869
13912
|
return rel && !rel.startsWith("..") && !isAbsolute9(rel) ? rel : path;
|
|
13870
13913
|
}
|
|
13871
13914
|
function unconditionalExitAt(lines) {
|
|
@@ -14114,7 +14157,7 @@ function planInit(cwd, opts = {}) {
|
|
|
14114
14157
|
];
|
|
14115
14158
|
}
|
|
14116
14159
|
function runInit(opts) {
|
|
14117
|
-
const requested =
|
|
14160
|
+
const requested = resolve6(opts.cwd ?? process.cwd());
|
|
14118
14161
|
const cwd = repoRoot(requested);
|
|
14119
14162
|
const w = opts.quiet ? { write: (_text) => true } : process.stdout;
|
|
14120
14163
|
if (cwd !== requested && !sameDir(cwd, requested)) {
|
|
@@ -14206,9 +14249,10 @@ version: 1
|
|
|
14206
14249
|
|
|
14207
14250
|
# The CI authority for main: the same engine as the agent hook and pre-commit, run over
|
|
14208
14251
|
# the PR's commit range. A block fails the check and clears ONLY via the out-of-band
|
|
14209
|
-
# label \`
|
|
14210
|
-
# never a file the PR itself can commit.
|
|
14211
|
-
# \`tamperward:allow
|
|
14252
|
+
# compact label \`tw1:<digest>\` (print one with \`tamperward signoff-label\`) applied by
|
|
14253
|
+
# someone with triage access or higher \u2014 never a file the PR itself can commit. Legacy
|
|
14254
|
+
# \`tamperward:allow:<rule>@<head-sha>\` labels remain accepted. The verify step reads
|
|
14255
|
+
# the same labels: a token for \`verify\` accepts a masked failure a reviewer has judged.
|
|
14212
14256
|
#
|
|
14213
14257
|
# labeled/unlabeled re-run the gate because the sign-off is read from the EVENT payload:
|
|
14214
14258
|
# a label applied after a failure could otherwise never take effect, and REVOKING a
|
|
@@ -14252,14 +14296,15 @@ jobs:
|
|
|
14252
14296
|
env:
|
|
14253
14297
|
LABELS: \${{ toJSON(github.event.pull_request.labels.*.name) }}
|
|
14254
14298
|
run: |
|
|
14255
|
-
RULES="$(printf '%s' "$LABELS" | jq -r '.[] |
|
|
14299
|
+
RULES="$(printf '%s' "$LABELS" | jq -r '.[] | if startswith("tamperward:allow:") then sub("^tamperward:allow:"; "") elif startswith("tw1:") then . else empty end' | paste -sd, -)"
|
|
14256
14300
|
echo "rules=$RULES" >> "$GITHUB_OUTPUT"
|
|
14257
14301
|
- name: Tamperward gate (diff-time)
|
|
14258
14302
|
env:
|
|
14259
14303
|
TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
|
|
14260
14304
|
# Binds each approval to the commit it was granted for: labels persist
|
|
14261
14305
|
# across pushes, so an unbound one would clear every later finding on
|
|
14262
|
-
# the same PR.
|
|
14306
|
+
# the same PR. Compact labels hash the exact rule/file/head tuple; legacy
|
|
14307
|
+
# labels must read tamperward:allow:<rule>@<head-sha>.
|
|
14263
14308
|
TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
|
|
14264
14309
|
run: tamperward check --diff "\${{ github.event.pull_request.base.sha }}...\${{ github.event.pull_request.head.sha }}"
|
|
14265
14310
|
# Diff-time detection is spelling-dependent by nature; pristine
|
|
@@ -14272,8 +14317,9 @@ jobs:
|
|
|
14272
14317
|
# without one this step fails closed (exit 2) rather than passing quietly.
|
|
14273
14318
|
- name: Tamperward verify (pristine re-execution)
|
|
14274
14319
|
env:
|
|
14275
|
-
# The same channel as the gate. \`
|
|
14276
|
-
# accepts a MASKED_FAILURE \u2014
|
|
14320
|
+
# The same channel as the gate. A compact token generated for \`verify\`
|
|
14321
|
+
# (or legacy \`tamperward:allow:verify@<head-sha>\`) accepts a MASKED_FAILURE \u2014
|
|
14322
|
+
# a reviewer has read the intentional test
|
|
14277
14323
|
# change and agrees the original suite no longer applies. It clears
|
|
14278
14324
|
# nothing else: a red visible suite, or a run that could not verify,
|
|
14279
14325
|
# stays red whatever the labels say.
|
|
@@ -14361,16 +14407,16 @@ import {
|
|
|
14361
14407
|
dirname as dirname6,
|
|
14362
14408
|
isAbsolute as isAbsolute10,
|
|
14363
14409
|
join as join16,
|
|
14364
|
-
relative as
|
|
14365
|
-
resolve as
|
|
14366
|
-
sep as
|
|
14410
|
+
relative as relative5,
|
|
14411
|
+
resolve as resolve7,
|
|
14412
|
+
sep as sep3
|
|
14367
14413
|
} from "node:path";
|
|
14368
14414
|
function metricsFor(descriptor) {
|
|
14369
14415
|
return descriptor.metrics ??= { fullSnapshots: 0, reusedSnapshots: 0, totalMs: 0 };
|
|
14370
14416
|
}
|
|
14371
14417
|
function inside(root, candidate) {
|
|
14372
|
-
const rel =
|
|
14373
|
-
return rel === "" || rel !== ".." && !rel.startsWith(".." +
|
|
14418
|
+
const rel = relative5(resolve7(root), resolve7(candidate));
|
|
14419
|
+
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep3) && !isAbsolute10(rel);
|
|
14374
14420
|
}
|
|
14375
14421
|
function modeKind(path) {
|
|
14376
14422
|
const st = lstatSync8(path);
|
|
@@ -14522,7 +14568,7 @@ function freezeRoot(kind, path) {
|
|
|
14522
14568
|
if (!statSync3(path).isDirectory()) {
|
|
14523
14569
|
throw new Error("not a directory");
|
|
14524
14570
|
}
|
|
14525
|
-
return { kind, path:
|
|
14571
|
+
return { kind, path: resolve7(path), realPath: realpathSync6(path) };
|
|
14526
14572
|
} catch (e) {
|
|
14527
14573
|
throw new Unattestable(
|
|
14528
14574
|
kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
@@ -14558,7 +14604,7 @@ function firstExecutable(command) {
|
|
|
14558
14604
|
function resolveExecutable(token, cwd, env) {
|
|
14559
14605
|
if (!token) return null;
|
|
14560
14606
|
if (token.includes("/") || token.includes("\\")) {
|
|
14561
|
-
const path = isAbsolute10(token) ? token :
|
|
14607
|
+
const path = isAbsolute10(token) ? token : resolve7(cwd, token);
|
|
14562
14608
|
try {
|
|
14563
14609
|
return existsSync13(path) ? realpathSync6(path) : null;
|
|
14564
14610
|
} catch {
|
|
@@ -14566,7 +14612,7 @@ function resolveExecutable(token, cwd, env) {
|
|
|
14566
14612
|
}
|
|
14567
14613
|
}
|
|
14568
14614
|
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
14569
|
-
const candidate =
|
|
14615
|
+
const candidate = resolve7(entry, token);
|
|
14570
14616
|
try {
|
|
14571
14617
|
if (existsSync13(candidate) && statSync3(candidate).isFile()) return realpathSync6(candidate);
|
|
14572
14618
|
} catch {
|
|
@@ -14585,7 +14631,7 @@ function looksPython(command, executable, token) {
|
|
|
14585
14631
|
}
|
|
14586
14632
|
function inferredVenvFromToken(token, cwd) {
|
|
14587
14633
|
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
14588
|
-
const path = isAbsolute10(token) ? token :
|
|
14634
|
+
const path = isAbsolute10(token) ? token : resolve7(cwd, token);
|
|
14589
14635
|
if (!pythonName(basename4(path))) return null;
|
|
14590
14636
|
const parent = dirname6(path);
|
|
14591
14637
|
const parentName = basename4(parent).toLowerCase();
|
|
@@ -14623,7 +14669,7 @@ function venvProblem(root) {
|
|
|
14623
14669
|
return null;
|
|
14624
14670
|
}
|
|
14625
14671
|
function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
14626
|
-
const cwd =
|
|
14672
|
+
const cwd = resolve7(cwdInput);
|
|
14627
14673
|
const roots = [];
|
|
14628
14674
|
const probes = [];
|
|
14629
14675
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14647,7 +14693,7 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
|
14647
14693
|
if (!isAbsolute10(env.VIRTUAL_ENV)) {
|
|
14648
14694
|
throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
|
|
14649
14695
|
}
|
|
14650
|
-
venv =
|
|
14696
|
+
venv = resolve7(env.VIRTUAL_ENV);
|
|
14651
14697
|
} else {
|
|
14652
14698
|
venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
|
|
14653
14699
|
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync13(join16(cwd, ".venv", "pyvenv.cfg"))) {
|
|
@@ -14687,7 +14733,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
|
|
|
14687
14733
|
const fingerprint2 = measuredSnapshot(
|
|
14688
14734
|
descriptor.roots,
|
|
14689
14735
|
descriptor.probes,
|
|
14690
|
-
|
|
14736
|
+
resolve7(cwdInput),
|
|
14691
14737
|
metricsFor(descriptor)
|
|
14692
14738
|
);
|
|
14693
14739
|
if (fingerprint2 !== descriptor.fingerprint) {
|
|
@@ -14700,7 +14746,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
|
|
|
14700
14746
|
}
|
|
14701
14747
|
function attestDependencyEnvironment(cwdInput, descriptor) {
|
|
14702
14748
|
const attestation = {
|
|
14703
|
-
cwd:
|
|
14749
|
+
cwd: resolve7(cwdInput),
|
|
14704
14750
|
descriptor,
|
|
14705
14751
|
check: checkDependencyEnvironment(cwdInput, descriptor)
|
|
14706
14752
|
};
|
|
@@ -14708,7 +14754,7 @@ function attestDependencyEnvironment(cwdInput, descriptor) {
|
|
|
14708
14754
|
return attestation;
|
|
14709
14755
|
}
|
|
14710
14756
|
function reuseDependencyEnvironmentAttestation(cwdInput, descriptor, attestation) {
|
|
14711
|
-
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;
|
|
14712
14758
|
metricsFor(descriptor).reusedSnapshots += 1;
|
|
14713
14759
|
return attestation.check;
|
|
14714
14760
|
}
|
|
@@ -15130,7 +15176,7 @@ if (child) {
|
|
|
15130
15176
|
import { createHash as createHash11, randomUUID } from "node:crypto";
|
|
15131
15177
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
15132
15178
|
import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
|
|
15133
|
-
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";
|
|
15134
15180
|
function identity() {
|
|
15135
15181
|
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
15136
15182
|
const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
|
|
@@ -15150,7 +15196,7 @@ function writableByCaller(path) {
|
|
|
15150
15196
|
}
|
|
15151
15197
|
}
|
|
15152
15198
|
function replaceableByCaller(path) {
|
|
15153
|
-
let current =
|
|
15199
|
+
let current = resolve8(path);
|
|
15154
15200
|
if (writableByCaller(current)) return true;
|
|
15155
15201
|
current = dirname7(current);
|
|
15156
15202
|
while (true) {
|
|
@@ -15496,7 +15542,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
|
|
|
15496
15542
|
const args = containerRunArgs({
|
|
15497
15543
|
image: backend.image,
|
|
15498
15544
|
name,
|
|
15499
|
-
workspace:
|
|
15545
|
+
workspace: resolve8(dir),
|
|
15500
15546
|
command,
|
|
15501
15547
|
uid,
|
|
15502
15548
|
gid,
|
|
@@ -15610,7 +15656,7 @@ import { execFileSync as execFileSync8, spawnSync as spawnSync3 } from "node:chi
|
|
|
15610
15656
|
import { createHash as createHash12 } from "node:crypto";
|
|
15611
15657
|
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync7, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync14 } from "node:fs";
|
|
15612
15658
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
15613
|
-
import { dirname as dirname8, isAbsolute as isAbsolute11, join as join19, relative as
|
|
15659
|
+
import { dirname as dirname8, isAbsolute as isAbsolute11, join as join19, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
|
|
15614
15660
|
function oracleAssuranceReport() {
|
|
15615
15661
|
return {
|
|
15616
15662
|
level: "suite-exit-only",
|
|
@@ -15636,24 +15682,45 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
15636
15682
|
return r.status === 0;
|
|
15637
15683
|
}
|
|
15638
15684
|
function inside2(root, path) {
|
|
15639
|
-
const rel =
|
|
15640
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
15685
|
+
const rel = relative6(resolve9(root), resolve9(path));
|
|
15686
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute11(rel);
|
|
15641
15687
|
}
|
|
15642
15688
|
function linkParts(path) {
|
|
15643
|
-
const parts =
|
|
15689
|
+
const parts = sep4 === "\\" ? path.split(/[\\/]+/) : path.split("/");
|
|
15644
15690
|
return parts.filter((part) => part !== "" && part !== ".");
|
|
15645
15691
|
}
|
|
15692
|
+
function materializationFailureReason(error) {
|
|
15693
|
+
if (error instanceof MaterializationError) return error.materializationReason;
|
|
15694
|
+
const code2 = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
15695
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15696
|
+
if (/tracked\s+node_modules|node_modules.*(?:attested|dependency).*(?:conflict|replace)/i.test(message)) {
|
|
15697
|
+
return "TRACKED_NODE_MODULES_CONFLICT";
|
|
15698
|
+
}
|
|
15699
|
+
if (/symlink.*(?:escape|cycle|parent directory)|escapes the materialised tree/i.test(message)) {
|
|
15700
|
+
return "SYMLINK_ESCAPE";
|
|
15701
|
+
}
|
|
15702
|
+
if (/(?:special file|where git expects a file)/i.test(message)) {
|
|
15703
|
+
return "SPECIAL_FILE";
|
|
15704
|
+
}
|
|
15705
|
+
if (code2 === "ENOENT" || /(?:ENOENT|no such file|file not found)/i.test(message)) {
|
|
15706
|
+
return "RACING_DELETION";
|
|
15707
|
+
}
|
|
15708
|
+
return "UNKNOWN";
|
|
15709
|
+
}
|
|
15646
15710
|
function linkEscape(label, target) {
|
|
15647
|
-
return new
|
|
15711
|
+
return new MaterializationError(
|
|
15712
|
+
"SYMLINK_ESCAPE",
|
|
15713
|
+
`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`
|
|
15714
|
+
);
|
|
15648
15715
|
}
|
|
15649
15716
|
function driveRelativeLinkTarget(target) {
|
|
15650
|
-
return
|
|
15717
|
+
return sep4 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
|
|
15651
15718
|
}
|
|
15652
15719
|
function rootedLinkTarget(target) {
|
|
15653
15720
|
return isAbsolute11(target) || driveRelativeLinkTarget(target);
|
|
15654
15721
|
}
|
|
15655
15722
|
function safeSymlink(target, out3, root, label) {
|
|
15656
|
-
if (rootedLinkTarget(target) || !inside2(root,
|
|
15723
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve9(dirname8(out3), target))) {
|
|
15657
15724
|
throw linkEscape(label, target);
|
|
15658
15725
|
}
|
|
15659
15726
|
rmSync7(out3, { force: true });
|
|
@@ -15661,7 +15728,7 @@ function safeSymlink(target, out3, root, label) {
|
|
|
15661
15728
|
}
|
|
15662
15729
|
function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
15663
15730
|
let domain = "tree";
|
|
15664
|
-
let stack = linkParts(
|
|
15731
|
+
let stack = linkParts(relative6(root, dirname8(out3)));
|
|
15665
15732
|
const pending = linkParts(target);
|
|
15666
15733
|
const seen = /* @__PURE__ */ new Set();
|
|
15667
15734
|
let hops = 0;
|
|
@@ -15687,11 +15754,17 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
15687
15754
|
}
|
|
15688
15755
|
if (!st.isSymbolicLink()) continue;
|
|
15689
15756
|
if (++hops > 128) {
|
|
15690
|
-
throw new
|
|
15757
|
+
throw new MaterializationError(
|
|
15758
|
+
"SYMLINK_ESCAPE",
|
|
15759
|
+
`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`
|
|
15760
|
+
);
|
|
15691
15761
|
}
|
|
15692
|
-
const key2 = `${domain}:${current}:${pending.join(
|
|
15762
|
+
const key2 = `${domain}:${current}:${pending.join(sep4)}`;
|
|
15693
15763
|
if (seen.has(key2)) {
|
|
15694
|
-
throw new
|
|
15764
|
+
throw new MaterializationError(
|
|
15765
|
+
"SYMLINK_ESCAPE",
|
|
15766
|
+
`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`
|
|
15767
|
+
);
|
|
15695
15768
|
}
|
|
15696
15769
|
seen.add(key2);
|
|
15697
15770
|
const next = readlinkSync3(current);
|
|
@@ -15704,7 +15777,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
15704
15777
|
throw linkEscape(label, target);
|
|
15705
15778
|
}
|
|
15706
15779
|
stack = [];
|
|
15707
|
-
pending.unshift(...linkParts(
|
|
15780
|
+
pending.unshift(...linkParts(relative6(dependencyRoot, resolve9(next))));
|
|
15708
15781
|
} else {
|
|
15709
15782
|
pending.unshift(...linkParts(next));
|
|
15710
15783
|
}
|
|
@@ -15722,12 +15795,21 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
15722
15795
|
continue;
|
|
15723
15796
|
}
|
|
15724
15797
|
if (st.isSymbolicLink()) {
|
|
15725
|
-
throw new
|
|
15798
|
+
throw new MaterializationError(
|
|
15799
|
+
"SYMLINK_ESCAPE",
|
|
15800
|
+
`${rel} has a symlinked parent directory (${relative6(cwd, current)})`
|
|
15801
|
+
);
|
|
15726
15802
|
}
|
|
15727
15803
|
}
|
|
15728
15804
|
}
|
|
15729
15805
|
function materialize(cwd, dest, dependencyRoot) {
|
|
15730
15806
|
const listed = workingTreePaths(cwd);
|
|
15807
|
+
if (dependencyRoot && listed.some((rel) => rel === "node_modules" || rel.startsWith("node_modules/"))) {
|
|
15808
|
+
throw new MaterializationError(
|
|
15809
|
+
"TRACKED_NODE_MODULES_CONFLICT",
|
|
15810
|
+
"tracked node_modules conflicts with the attested dependency root"
|
|
15811
|
+
);
|
|
15812
|
+
}
|
|
15731
15813
|
const links = [];
|
|
15732
15814
|
for (const rel of listed) {
|
|
15733
15815
|
const src = join19(cwd, rel);
|
|
@@ -15749,7 +15831,10 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
15749
15831
|
cpSync(src, out3, { dereference: true });
|
|
15750
15832
|
chmodSync2(out3, st.mode);
|
|
15751
15833
|
} else {
|
|
15752
|
-
throw new
|
|
15834
|
+
throw new MaterializationError(
|
|
15835
|
+
"SPECIAL_FILE",
|
|
15836
|
+
`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`
|
|
15837
|
+
);
|
|
15753
15838
|
}
|
|
15754
15839
|
}
|
|
15755
15840
|
for (const link of links) {
|
|
@@ -16209,7 +16294,7 @@ function runVerify(opts) {
|
|
|
16209
16294
|
return cannotVerify(
|
|
16210
16295
|
"MATERIALIZATION_FAILED",
|
|
16211
16296
|
`could not materialize (${e instanceof Error ? e.message : String(e)})`,
|
|
16212
|
-
{ verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
|
|
16297
|
+
{ verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport(), materialization_reason: materializationFailureReason(e) }
|
|
16213
16298
|
);
|
|
16214
16299
|
}
|
|
16215
16300
|
const visible = runStage(visDir);
|
|
@@ -16243,6 +16328,49 @@ function runVerify(opts) {
|
|
|
16243
16328
|
}
|
|
16244
16329
|
return 2;
|
|
16245
16330
|
}
|
|
16331
|
+
if (visible.failure === "budget") {
|
|
16332
|
+
const pristineNotRun = {
|
|
16333
|
+
exit: null,
|
|
16334
|
+
secs: 0,
|
|
16335
|
+
failure: "budget",
|
|
16336
|
+
reason: "not run: visible stage exceeded its budget"
|
|
16337
|
+
};
|
|
16338
|
+
cleanup([visRoot]);
|
|
16339
|
+
opts.onVerdict?.({ verdict: "BUDGET_EXCEEDED" });
|
|
16340
|
+
if (opts.json) {
|
|
16341
|
+
out3(JSON.stringify({
|
|
16342
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
16343
|
+
verdict: "BUDGET_EXCEEDED",
|
|
16344
|
+
base,
|
|
16345
|
+
command: cmd,
|
|
16346
|
+
budget_secs: budget,
|
|
16347
|
+
visible: stageJson(visible),
|
|
16348
|
+
pristine: stageJson(pristineNotRun),
|
|
16349
|
+
protected_restored: 0,
|
|
16350
|
+
added_protected_removed: 0,
|
|
16351
|
+
filesystem_case_sensitive: filesystemCaseSensitive,
|
|
16352
|
+
verifier_backend: backendReport(),
|
|
16353
|
+
dependency_environment: dependencyReport(),
|
|
16354
|
+
oracle_assurance: oracleAssuranceReport(),
|
|
16355
|
+
...opts.keep ? { visible_dir: visDir } : {}
|
|
16356
|
+
}));
|
|
16357
|
+
} else {
|
|
16358
|
+
out3(`tamperward verify \u2014 ${verifyVerdictLine("BUDGET_EXCEEDED", {
|
|
16359
|
+
restored: 0,
|
|
16360
|
+
base,
|
|
16361
|
+
visibleExit: visible.exit,
|
|
16362
|
+
pristineExit: pristineNotRun.exit,
|
|
16363
|
+
budget
|
|
16364
|
+
})}`);
|
|
16365
|
+
out3("verifier backend: " + verifierBackendSummary(verifierBackend));
|
|
16366
|
+
out3("oracle assurance: suite-exit-only (candidate source executes inside the suite process; execution-domain isolation is not semantic/oracle isolation)");
|
|
16367
|
+
out3(
|
|
16368
|
+
dependencyEnvironment ? "dependency environment: " + dependencyEnvironmentSummary(dependencyEnvironment) : "dependency environment: verifier-owned by " + (verifierBackend.image ?? "isolated image")
|
|
16369
|
+
);
|
|
16370
|
+
renderStageDiagnostics(out3, "visible", visible);
|
|
16371
|
+
}
|
|
16372
|
+
return 2;
|
|
16373
|
+
}
|
|
16246
16374
|
if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
|
|
16247
16375
|
cleanup([visRoot]);
|
|
16248
16376
|
return cannotVerify(
|
|
@@ -16281,7 +16409,7 @@ function runVerify(opts) {
|
|
|
16281
16409
|
return cannotVerify(
|
|
16282
16410
|
"MATERIALIZATION_FAILED",
|
|
16283
16411
|
`could not materialize pristine copy (${e instanceof Error ? e.message : String(e)})`,
|
|
16284
|
-
{ stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
|
|
16412
|
+
{ stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport(), materialization_reason: materializationFailureReason(e) }
|
|
16285
16413
|
);
|
|
16286
16414
|
}
|
|
16287
16415
|
const overlayBefore = overlayDigest(priDir, restored);
|
|
@@ -16398,10 +16526,12 @@ function runVerify(opts) {
|
|
|
16398
16526
|
renderStageDiagnostics(out3, "pristine", pristine);
|
|
16399
16527
|
}
|
|
16400
16528
|
}
|
|
16401
|
-
if (signedOff)
|
|
16529
|
+
if (signedOff) {
|
|
16530
|
+
const approval = signedOff.startsWith("tw1:") ? signedOff : `tamperward:allow:${signedOff}`;
|
|
16402
16531
|
out3(
|
|
16403
|
-
`masked failure cleared by out-of-band approval (
|
|
16532
|
+
`masked failure cleared by out-of-band approval (${approval}): a reviewer accepted that the original suite no longer applies to this change. Exit 0.`
|
|
16404
16533
|
);
|
|
16534
|
+
}
|
|
16405
16535
|
if (removedAdded > 0)
|
|
16406
16536
|
out3(
|
|
16407
16537
|
`(${removedAdded} protected file(s) added since ${base.slice(0, 10)} were removed from the pristine run: the pristine tree carries exactly the base's protected surface.)`
|
|
@@ -16440,7 +16570,7 @@ function parseVerify(args) {
|
|
|
16440
16570
|
}
|
|
16441
16571
|
return o;
|
|
16442
16572
|
}
|
|
16443
|
-
var OVERLAY_CLASSES, VERIFICATION_SURFACE, DROPPED_ENV, PINNED_NPM;
|
|
16573
|
+
var OVERLAY_CLASSES, VERIFICATION_SURFACE, MaterializationError, DROPPED_ENV, PINNED_NPM;
|
|
16444
16574
|
var init_verify = __esm({
|
|
16445
16575
|
"src/cli/verify.ts"() {
|
|
16446
16576
|
"use strict";
|
|
@@ -16528,6 +16658,14 @@ var init_verify = __esm({
|
|
|
16528
16658
|
"**/phpunit.xml.dist",
|
|
16529
16659
|
"**/*.runsettings"
|
|
16530
16660
|
];
|
|
16661
|
+
MaterializationError = class extends Error {
|
|
16662
|
+
constructor(materializationReason, message) {
|
|
16663
|
+
super(message);
|
|
16664
|
+
this.materializationReason = materializationReason;
|
|
16665
|
+
this.name = "MaterializationError";
|
|
16666
|
+
}
|
|
16667
|
+
materializationReason;
|
|
16668
|
+
};
|
|
16531
16669
|
DROPPED_ENV = /* @__PURE__ */ new Set([
|
|
16532
16670
|
"NODE_OPTIONS",
|
|
16533
16671
|
"NODE_PATH",
|
|
@@ -16546,7 +16684,7 @@ import { execFileSync as execFileSync9, spawn, spawnSync as spawnSync4 } from "n
|
|
|
16546
16684
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
16547
16685
|
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
16548
16686
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
16549
|
-
import { dirname as dirname9, join as join20, resolve as
|
|
16687
|
+
import { dirname as dirname9, join as join20, resolve as resolve10 } from "node:path";
|
|
16550
16688
|
function authoritativeRunLifecyclePlatform(platform = process.platform) {
|
|
16551
16689
|
return platform === "linux";
|
|
16552
16690
|
}
|
|
@@ -16765,7 +16903,7 @@ function startSupervisedObserver(cwd, base, entry) {
|
|
|
16765
16903
|
try {
|
|
16766
16904
|
child = spawn(
|
|
16767
16905
|
process.execPath,
|
|
16768
|
-
[
|
|
16906
|
+
[resolve10(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
|
|
16769
16907
|
{
|
|
16770
16908
|
cwd,
|
|
16771
16909
|
stdio: ["ignore", "ignore", "inherit"],
|
|
@@ -16884,8 +17022,8 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
16884
17022
|
return out3;
|
|
16885
17023
|
}
|
|
16886
17024
|
for (const pid of pids) {
|
|
16887
|
-
const
|
|
16888
|
-
if (
|
|
17025
|
+
const n2 = Number(pid);
|
|
17026
|
+
if (n2 === process.pid) continue;
|
|
16889
17027
|
try {
|
|
16890
17028
|
const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
|
|
16891
17029
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
@@ -16902,7 +17040,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
16902
17040
|
for (const link of procLinks) {
|
|
16903
17041
|
try {
|
|
16904
17042
|
const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
|
|
16905
|
-
const held = raw.startsWith("/") ?
|
|
17043
|
+
const held = raw.startsWith("/") ? resolve10(raw) : raw;
|
|
16906
17044
|
if (held === real || held.startsWith(real + "/")) {
|
|
16907
17045
|
holds = true;
|
|
16908
17046
|
break;
|
|
@@ -16910,12 +17048,12 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
16910
17048
|
} catch {
|
|
16911
17049
|
}
|
|
16912
17050
|
}
|
|
16913
|
-
if (holds) out3.push(
|
|
17051
|
+
if (holds) out3.push(n2);
|
|
16914
17052
|
}
|
|
16915
17053
|
return out3;
|
|
16916
17054
|
}
|
|
16917
17055
|
function runEnvelope(opts) {
|
|
16918
|
-
const cwd =
|
|
17056
|
+
const cwd = resolve10(opts.cwd ?? process.cwd());
|
|
16919
17057
|
const say = opts.json ? (_s) => {
|
|
16920
17058
|
} : out;
|
|
16921
17059
|
if (!opts.argv.length) {
|
|
@@ -16930,7 +17068,8 @@ function runEnvelope(opts) {
|
|
|
16930
17068
|
}
|
|
16931
17069
|
let base;
|
|
16932
17070
|
try {
|
|
16933
|
-
|
|
17071
|
+
const requestedBase = assertRev(opts.base ?? "HEAD");
|
|
17072
|
+
base = git3(["rev-parse", "--verify", `${requestedBase}^{commit}`], cwd).trim();
|
|
16934
17073
|
} catch {
|
|
16935
17074
|
err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
|
|
16936
17075
|
return 2;
|
|
@@ -17460,7 +17599,7 @@ raise SystemExit(0)
|
|
|
17460
17599
|
// src/cli/doctor.ts
|
|
17461
17600
|
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
17462
17601
|
import { existsSync as existsSync15, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
|
|
17463
|
-
import { join as join21, resolve as
|
|
17602
|
+
import { join as join21, resolve as resolve11 } from "node:path";
|
|
17464
17603
|
function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
|
|
17465
17604
|
if (platform === "linux") {
|
|
17466
17605
|
if (linuxPython?.path) {
|
|
@@ -17538,7 +17677,7 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
|
|
|
17538
17677
|
const broken = [];
|
|
17539
17678
|
const warnings = [];
|
|
17540
17679
|
for (const rel of workflowRels) {
|
|
17541
|
-
const path =
|
|
17680
|
+
const path = resolve11(cwd, rel);
|
|
17542
17681
|
if (!existsSync15(path)) {
|
|
17543
17682
|
broken.push(`${rel} is missing`);
|
|
17544
17683
|
continue;
|
|
@@ -17919,7 +18058,7 @@ function verifyJobs(doc) {
|
|
|
17919
18058
|
return found;
|
|
17920
18059
|
}
|
|
17921
18060
|
function diagnose(opts = {}) {
|
|
17922
|
-
const cwd = repoRoot(
|
|
18061
|
+
const cwd = repoRoot(resolve11(opts.cwd ?? process.cwd()));
|
|
17923
18062
|
let policy;
|
|
17924
18063
|
try {
|
|
17925
18064
|
policy = policyFor(opts, cwd);
|
|
@@ -17943,7 +18082,7 @@ function diagnose(opts = {}) {
|
|
|
17943
18082
|
workflowRels.push(opts.workflow);
|
|
17944
18083
|
} else {
|
|
17945
18084
|
const workflowDirRel = ".github/workflows";
|
|
17946
|
-
const workflowDir =
|
|
18085
|
+
const workflowDir = resolve11(cwd, workflowDirRel);
|
|
17947
18086
|
if (!existsSync15(workflowDir)) {
|
|
17948
18087
|
return fail("ci-verifier", `${workflowDirRel}: workflow directory does not exist`);
|
|
17949
18088
|
}
|
|
@@ -17963,7 +18102,7 @@ function diagnose(opts = {}) {
|
|
|
17963
18102
|
let verifyJobCount = 0;
|
|
17964
18103
|
const authorityWorkflows = [];
|
|
17965
18104
|
for (const workflowRel of workflowRels) {
|
|
17966
|
-
const workflowPath =
|
|
18105
|
+
const workflowPath = resolve11(cwd, workflowRel);
|
|
17967
18106
|
if (!existsSync15(workflowPath)) {
|
|
17968
18107
|
return fail("ci-verifier", `${workflowRel}: workflow does not exist`);
|
|
17969
18108
|
}
|
|
@@ -18155,27 +18294,127 @@ import {
|
|
|
18155
18294
|
symlinkSync as symlinkSync2
|
|
18156
18295
|
} from "node:fs";
|
|
18157
18296
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
18158
|
-
import { isAbsolute as isAbsolute12, join as join22, relative as
|
|
18297
|
+
import { isAbsolute as isAbsolute12, join as join22, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
|
|
18159
18298
|
function unescapeStraceString(s) {
|
|
18160
18299
|
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, "\\");
|
|
18161
18300
|
}
|
|
18162
|
-
function
|
|
18301
|
+
function copyTraceState(state) {
|
|
18302
|
+
return { cwd: state.cwd, fds: new Map(state.fds) };
|
|
18303
|
+
}
|
|
18304
|
+
function traceLineProcess(line) {
|
|
18305
|
+
const bracketed = line.match(/^\s*\[pid\s+(\d+)\]\s+(.*)$/);
|
|
18306
|
+
if (bracketed) return { pid: bracketed[1], body: bracketed[2] };
|
|
18307
|
+
const numbered = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
18308
|
+
if (numbered && /\b(?:execve|open|chdir|clone|fork|stat|access|readlink)/.test(numbered[2])) {
|
|
18309
|
+
return { pid: numbered[1], body: numbered[2] };
|
|
18310
|
+
}
|
|
18311
|
+
return { pid: "0", body: line.trim() };
|
|
18312
|
+
}
|
|
18313
|
+
function syscallResult(body) {
|
|
18314
|
+
const match = body.match(/\)\s+=\s+(-?\d+)\b/);
|
|
18315
|
+
return match ? Number(match[1]) : null;
|
|
18316
|
+
}
|
|
18317
|
+
function resolveTracePath(rawPath, state, dirfd) {
|
|
18318
|
+
if (isAbsolute12(rawPath)) return resolve12(rawPath);
|
|
18319
|
+
const base = dirfd === null || dirfd === -100 ? state.cwd : state.fds.get(dirfd);
|
|
18320
|
+
if (!base) return null;
|
|
18321
|
+
return resolve12(base, rawPath);
|
|
18322
|
+
}
|
|
18323
|
+
function firstDirFd(args) {
|
|
18324
|
+
const first = args.match(/^\s*(AT_FDCWD|-?\d+)/)?.[1];
|
|
18325
|
+
if (!first || first === "AT_FDCWD") return null;
|
|
18326
|
+
return Number(first);
|
|
18327
|
+
}
|
|
18328
|
+
function successfulFdResult(body) {
|
|
18329
|
+
const result = syscallResult(body);
|
|
18330
|
+
return result !== null && result >= 0 ? result : null;
|
|
18331
|
+
}
|
|
18332
|
+
function parseStraceFileAccess(raw, initialCwd = TRACE_ROOT) {
|
|
18163
18333
|
const out3 = [];
|
|
18164
18334
|
const seen = /* @__PURE__ */ new Set();
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
|
|
18173
|
-
|
|
18174
|
-
|
|
18175
|
-
|
|
18176
|
-
|
|
18335
|
+
const states = /* @__PURE__ */ new Map();
|
|
18336
|
+
const unfinished = /* @__PURE__ */ new Map();
|
|
18337
|
+
const stateFor = (pid) => {
|
|
18338
|
+
let state = states.get(pid);
|
|
18339
|
+
if (!state) {
|
|
18340
|
+
state = { cwd: initialCwd, fds: /* @__PURE__ */ new Map() };
|
|
18341
|
+
states.set(pid, state);
|
|
18342
|
+
}
|
|
18343
|
+
return state;
|
|
18344
|
+
};
|
|
18345
|
+
const add = (path, access, unresolved = false) => {
|
|
18346
|
+
const key2 = `${access}\0${unresolved ? "unresolved:" : ""}${path}`;
|
|
18347
|
+
if (seen.has(key2)) return;
|
|
18177
18348
|
seen.add(key2);
|
|
18178
|
-
out3.push({ path, access });
|
|
18349
|
+
out3.push(unresolved ? { path, access, unresolved: true } : { path, access });
|
|
18350
|
+
};
|
|
18351
|
+
for (const rawLine of raw.split("\n")) {
|
|
18352
|
+
if (!rawLine.trim()) continue;
|
|
18353
|
+
const line = traceLineProcess(rawLine);
|
|
18354
|
+
const resumed = line.body.match(/^<\.\.\.\s+(\w+)\s+resumed>(.*)$/);
|
|
18355
|
+
let body = line.body;
|
|
18356
|
+
if (resumed) {
|
|
18357
|
+
const key2 = `${line.pid}:${resumed[1]}`;
|
|
18358
|
+
const prefix = unfinished.get(key2);
|
|
18359
|
+
if (!prefix) continue;
|
|
18360
|
+
unfinished.delete(key2);
|
|
18361
|
+
body = prefix + resumed[2];
|
|
18362
|
+
}
|
|
18363
|
+
const unfinishedAt = body.indexOf("<unfinished ...>");
|
|
18364
|
+
if (unfinishedAt >= 0) {
|
|
18365
|
+
const before = body.slice(0, unfinishedAt);
|
|
18366
|
+
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*\(/);
|
|
18367
|
+
if (call2) unfinished.set(`${line.pid}:${call2[1]}`, before);
|
|
18368
|
+
continue;
|
|
18369
|
+
}
|
|
18370
|
+
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*\(/);
|
|
18371
|
+
if (!call || /\)\s+=\s+-1\b/.test(body)) continue;
|
|
18372
|
+
const name = call[1];
|
|
18373
|
+
const args = body.slice((call.index ?? 0) + call[0].length);
|
|
18374
|
+
const state = stateFor(line.pid);
|
|
18375
|
+
const result = syscallResult(body);
|
|
18376
|
+
if (name === "chdir") {
|
|
18377
|
+
const quoted2 = args.match(/\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"/);
|
|
18378
|
+
if (quoted2 && result === 0) {
|
|
18379
|
+
const next = resolveTracePath(unescapeStraceString(quoted2[1]), state, null);
|
|
18380
|
+
if (next) state.cwd = next;
|
|
18381
|
+
}
|
|
18382
|
+
continue;
|
|
18383
|
+
}
|
|
18384
|
+
if (name === "fchdir") {
|
|
18385
|
+
const fd = Number(args.match(/^\s*(-?\d+)/)?.[1]);
|
|
18386
|
+
const next = state.fds.get(fd);
|
|
18387
|
+
if (result === 0 && next) state.cwd = next;
|
|
18388
|
+
continue;
|
|
18389
|
+
}
|
|
18390
|
+
if (name === "close") {
|
|
18391
|
+
const fd = Number(args.match(/^\s*(-?\d+)/)?.[1]);
|
|
18392
|
+
if (result === 0) state.fds.delete(fd);
|
|
18393
|
+
continue;
|
|
18394
|
+
}
|
|
18395
|
+
if (name === "dup" || name === "dup2" || name === "dup3" || name === "fcntl") {
|
|
18396
|
+
const source = Number(args.match(/^\s*(-?\d+)/)?.[1]);
|
|
18397
|
+
const destination = name === "dup" ? result : name === "fcntl" ? result : Number(args.match(/^\s*-?\d+\s*,\s*(-?\d+)/)?.[1]);
|
|
18398
|
+
const sourcePath = state.fds.get(source);
|
|
18399
|
+
if (result !== null && result >= 0 && destination !== void 0 && destination !== null && sourcePath) state.fds.set(destination, sourcePath);
|
|
18400
|
+
continue;
|
|
18401
|
+
}
|
|
18402
|
+
if (name === "clone" || name === "clone3" || name === "fork" || name === "vfork") {
|
|
18403
|
+
if (result !== null && result > 0) states.set(String(result), copyTraceState(state));
|
|
18404
|
+
continue;
|
|
18405
|
+
}
|
|
18406
|
+
const quoted = args.match(/\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"/);
|
|
18407
|
+
if (!quoted) continue;
|
|
18408
|
+
const rawPath = unescapeStraceString(quoted[1]);
|
|
18409
|
+
if (!rawPath) continue;
|
|
18410
|
+
const atCall = name === "execveat" || name === "openat2" || name === "openat" || name === "newfstatat" || name === "fstatat" || name === "statx" || name === "readlinkat";
|
|
18411
|
+
const dirfd = atCall ? firstDirFd(args) : null;
|
|
18412
|
+
const resolved = resolveTracePath(rawPath, state, dirfd);
|
|
18413
|
+
add(resolved ?? rawPath, name.startsWith("execve") ? "exec" : "read", resolved === null);
|
|
18414
|
+
if (name === "open" || name === "openat" || name === "openat2") {
|
|
18415
|
+
const fd = successfulFdResult(body);
|
|
18416
|
+
if (fd !== null && resolved) state.fds.set(fd, resolved);
|
|
18417
|
+
}
|
|
18179
18418
|
}
|
|
18180
18419
|
return out3;
|
|
18181
18420
|
}
|
|
@@ -18196,13 +18435,13 @@ function classifyTraceRun(r) {
|
|
|
18196
18435
|
return { kind: "ok", exit: r.status ?? 1 };
|
|
18197
18436
|
}
|
|
18198
18437
|
function inside3(root, path) {
|
|
18199
|
-
const rel =
|
|
18200
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
18438
|
+
const rel = relative7(resolve12(root), resolve12(path));
|
|
18439
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute12(rel);
|
|
18201
18440
|
}
|
|
18202
18441
|
function normalRepoPath(root, raw) {
|
|
18203
|
-
const abs = isAbsolute12(raw) ?
|
|
18442
|
+
const abs = isAbsolute12(raw) ? resolve12(raw) : resolve12(root, raw);
|
|
18204
18443
|
if (!inside3(root, abs)) return null;
|
|
18205
|
-
const rel =
|
|
18444
|
+
const rel = relative7(root, abs).split(sep5).join("/");
|
|
18206
18445
|
return rel && rel !== ".." ? rel : null;
|
|
18207
18446
|
}
|
|
18208
18447
|
function likelyConfig(path) {
|
|
@@ -18215,12 +18454,17 @@ function sortAccesses(values) {
|
|
|
18215
18454
|
function summarizeTraceRuns(opts) {
|
|
18216
18455
|
const repo = /* @__PURE__ */ new Map();
|
|
18217
18456
|
const external = /* @__PURE__ */ new Map();
|
|
18457
|
+
let unresolved_accesses = 0;
|
|
18218
18458
|
opts.runs.forEach((run, index) => {
|
|
18219
18459
|
const perRun = /* @__PURE__ */ new Set();
|
|
18220
18460
|
for (const item of run) {
|
|
18461
|
+
if (item.unresolved) {
|
|
18462
|
+
unresolved_accesses++;
|
|
18463
|
+
continue;
|
|
18464
|
+
}
|
|
18221
18465
|
const rel = normalRepoPath(opts.root, item.path);
|
|
18222
18466
|
const isTracked = rel !== null && opts.tracked.has(rel);
|
|
18223
|
-
const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null :
|
|
18467
|
+
const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve12(opts.root, item.path);
|
|
18224
18468
|
if (keyPath === null) continue;
|
|
18225
18469
|
const target = isTracked ? repo : external;
|
|
18226
18470
|
const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
|
|
@@ -18258,7 +18502,8 @@ function summarizeTraceRuns(opts) {
|
|
|
18258
18502
|
uncovered_repository_inputs,
|
|
18259
18503
|
// Exact paths are intentionally conservative. A maintainer may widen them to
|
|
18260
18504
|
// reviewed globs, but TamperWard never invents a broader trust surface.
|
|
18261
|
-
suggested_verify_inputs: [...uncovered_repository_inputs]
|
|
18505
|
+
suggested_verify_inputs: [...uncovered_repository_inputs],
|
|
18506
|
+
unresolved_accesses
|
|
18262
18507
|
};
|
|
18263
18508
|
}
|
|
18264
18509
|
function git4(args, cwd) {
|
|
@@ -18300,21 +18545,24 @@ function materializeBase(base, cwd, dest) {
|
|
|
18300
18545
|
const hostModules = join22(cwd, "node_modules");
|
|
18301
18546
|
const tracedModules = join22(dest, "node_modules");
|
|
18302
18547
|
if (existsSync16(hostModules) && !existsSync16(tracedModules)) {
|
|
18303
|
-
symlinkSync2(
|
|
18548
|
+
symlinkSync2(resolve12(hostModules), tracedModules, "dir");
|
|
18304
18549
|
}
|
|
18305
18550
|
}
|
|
18306
18551
|
function traceFiles(prefix) {
|
|
18307
|
-
const dir =
|
|
18308
|
-
const base = prefix.split(
|
|
18552
|
+
const dir = resolve12(prefix, "..");
|
|
18553
|
+
const base = prefix.split(sep5).at(-1) ?? prefix;
|
|
18309
18554
|
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join22(dir, name)).sort();
|
|
18310
18555
|
}
|
|
18311
18556
|
function rewriteTraceRoot(accesses, actualRoot) {
|
|
18312
18557
|
return accesses.map((item) => {
|
|
18313
|
-
|
|
18558
|
+
if (item.unresolved) return item;
|
|
18559
|
+
const placeholder = isAbsolute12(item.path) ? resolve12(item.path) : resolve12(TRACE_ROOT, item.path);
|
|
18560
|
+
const placeholderRel = inside3(TRACE_ROOT, placeholder) ? relative7(TRACE_ROOT, placeholder) : null;
|
|
18561
|
+
const abs = placeholderRel === null ? placeholder : resolve12(actualRoot, placeholderRel);
|
|
18314
18562
|
if (!inside3(actualRoot, abs)) return { ...item, path: abs };
|
|
18315
|
-
const rel =
|
|
18563
|
+
const rel = relative7(actualRoot, abs).split(sep5).join("/");
|
|
18316
18564
|
try {
|
|
18317
|
-
const real =
|
|
18565
|
+
const real = resolve12(realpathSync9(abs));
|
|
18318
18566
|
if (!inside3(actualRoot, real)) return { ...item, path: real };
|
|
18319
18567
|
} catch {
|
|
18320
18568
|
}
|
|
@@ -18328,7 +18576,7 @@ function straceOnce(root, command, budget) {
|
|
|
18328
18576
|
const traced = spawnSync5(
|
|
18329
18577
|
"strace",
|
|
18330
18578
|
[
|
|
18331
|
-
"-
|
|
18579
|
+
"-f",
|
|
18332
18580
|
"-qq",
|
|
18333
18581
|
"-s",
|
|
18334
18582
|
"4096",
|
|
@@ -18347,13 +18595,13 @@ function straceOnce(root, command, budget) {
|
|
|
18347
18595
|
],
|
|
18348
18596
|
{
|
|
18349
18597
|
cwd: root,
|
|
18350
|
-
env:
|
|
18598
|
+
env: suiteEnv(traceDir),
|
|
18351
18599
|
encoding: "utf8",
|
|
18352
18600
|
maxBuffer: 64 * 1024 * 1024
|
|
18353
18601
|
}
|
|
18354
18602
|
);
|
|
18355
18603
|
const logs = traced.error ? [] : traceFiles(prefix);
|
|
18356
|
-
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync19(path, "utf8")));
|
|
18604
|
+
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync19(path, "utf8"), TRACE_ROOT));
|
|
18357
18605
|
const raw = {
|
|
18358
18606
|
spawnError: traced.error ? traced.error.message : null,
|
|
18359
18607
|
status: traced.status,
|
|
@@ -18392,6 +18640,7 @@ function renderText2(report2) {
|
|
|
18392
18640
|
out3(`trusted base: ${report2.base}`);
|
|
18393
18641
|
out3(`command: ${report2.command}`);
|
|
18394
18642
|
out3(`runs: ${report2.runs_completed}/${report2.runs_requested}; exits: ${report2.run_exits.join(", ")}`);
|
|
18643
|
+
out3(`unresolved accesses: ${report2.unresolved_accesses}`);
|
|
18395
18644
|
out3();
|
|
18396
18645
|
out3("repository inputs observed:");
|
|
18397
18646
|
if (!report2.repository_inputs.length) out3(" (none)");
|
|
@@ -18420,7 +18669,7 @@ function renderText2(report2) {
|
|
|
18420
18669
|
for (const note of report2.notes) out3(`note: ${note}`);
|
|
18421
18670
|
}
|
|
18422
18671
|
function runTraceVerify(opts = {}) {
|
|
18423
|
-
const cwd =
|
|
18672
|
+
const cwd = resolve12(opts.cwd ?? process.cwd());
|
|
18424
18673
|
if (process.platform !== "linux") {
|
|
18425
18674
|
process.stderr.write("tamperward trace-verify: Linux only in this release; unsupported platform \u2014 no parity is implied.\n");
|
|
18426
18675
|
return 2;
|
|
@@ -18518,13 +18767,14 @@ This is a tracing/tooling limitation (e.g. ptrace denied by seccomp or Yama), no
|
|
|
18518
18767
|
runs_requested: runs,
|
|
18519
18768
|
runs_completed: observed.length,
|
|
18520
18769
|
run_exits: runExits,
|
|
18521
|
-
trace_complete: runExits.every((code2) => code2 === 0),
|
|
18770
|
+
trace_complete: runExits.every((code2) => code2 === 0) && summary.unresolved_accesses === 0,
|
|
18522
18771
|
...summary,
|
|
18523
18772
|
notes: [
|
|
18524
18773
|
"Observed reads are evidence from these runs, not proof that an unobserved path can never be read.",
|
|
18525
18774
|
"Dynamic paths are those observed in fewer than all repeated traces.",
|
|
18526
18775
|
"Suggestions are exact tracked paths only; TamperWard never edits .tamperward.yml or widens a glob automatically.",
|
|
18527
|
-
"Review every suggestion before adding it to verify.inputs."
|
|
18776
|
+
"Review every suggestion before adding it to verify.inputs.",
|
|
18777
|
+
...summary.unresolved_accesses > 0 ? [`${summary.unresolved_accesses} relative file access(es) could not be resolved from trace cwd/fd state; discovery is incomplete.`] : []
|
|
18528
18778
|
]
|
|
18529
18779
|
};
|
|
18530
18780
|
if (opts.json) process.stdout.write(JSON.stringify(report2) + "\n");
|
|
@@ -18723,7 +18973,7 @@ var init_runtimes = __esm({
|
|
|
18723
18973
|
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
18724
18974
|
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync10, rmSync as rmSync10 } from "node:fs";
|
|
18725
18975
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
18726
|
-
import { join as join24, relative as
|
|
18976
|
+
import { join as join24, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
|
|
18727
18977
|
import { createInterface } from "node:readline";
|
|
18728
18978
|
import { parseDocument } from "yaml";
|
|
18729
18979
|
function readlineAsker(input, output) {
|
|
@@ -18732,9 +18982,9 @@ function readlineAsker(input, output) {
|
|
|
18732
18982
|
let pending = null;
|
|
18733
18983
|
rl.on("close", () => {
|
|
18734
18984
|
closed = true;
|
|
18735
|
-
const
|
|
18985
|
+
const resolve20 = pending;
|
|
18736
18986
|
pending = null;
|
|
18737
|
-
|
|
18987
|
+
resolve20?.(null);
|
|
18738
18988
|
});
|
|
18739
18989
|
rl.on("SIGINT", () => rl.close());
|
|
18740
18990
|
const ask = (question) => new Promise((resolvePromise) => {
|
|
@@ -18757,9 +19007,9 @@ function terminalText(text) {
|
|
|
18757
19007
|
return stripControl(text).replace(/\s+/g, " ").trim();
|
|
18758
19008
|
}
|
|
18759
19009
|
function committableSetupPath(cwd, path) {
|
|
18760
|
-
const rel =
|
|
18761
|
-
if (!rel || rel === ".." || rel.startsWith(".." +
|
|
18762
|
-
return rel !== ".git" && !rel.startsWith(".git" +
|
|
19010
|
+
const rel = relative8(cwd, resolve13(cwd, path));
|
|
19011
|
+
if (!rel || rel === ".." || rel.startsWith(".." + sep6)) return false;
|
|
19012
|
+
return rel !== ".git" && !rel.startsWith(".git" + sep6);
|
|
18763
19013
|
}
|
|
18764
19014
|
function platformLabel(platform) {
|
|
18765
19015
|
if (platform === "darwin") return "macOS";
|
|
@@ -18817,7 +19067,7 @@ function writeVerifyCommand(cwd, command) {
|
|
|
18817
19067
|
}
|
|
18818
19068
|
}
|
|
18819
19069
|
async function runOnboard(opts, io = {}) {
|
|
18820
|
-
const requestedCwd =
|
|
19070
|
+
const requestedCwd = resolve13(opts.cwd ?? process.cwd());
|
|
18821
19071
|
const out3 = io.out ?? ((line2) => void process.stdout.write(line2 + "\n"));
|
|
18822
19072
|
const errLine = (line2) => void process.stderr.write(line2 + "\n");
|
|
18823
19073
|
const rawErr = (line2) => errLine(terminalText(line2));
|
|
@@ -18851,7 +19101,7 @@ async function runOnboard(opts, io = {}) {
|
|
|
18851
19101
|
fail(requestedCwd + " is not inside a Git repository. Run `git init` first.");
|
|
18852
19102
|
return 2;
|
|
18853
19103
|
}
|
|
18854
|
-
const cwd =
|
|
19104
|
+
const cwd = resolve13(rootText);
|
|
18855
19105
|
let sameRoot = cwd === requestedCwd;
|
|
18856
19106
|
try {
|
|
18857
19107
|
sameRoot = realpathSync10(cwd) === realpathSync10(requestedCwd);
|
|
@@ -18884,10 +19134,10 @@ async function runOnboard(opts, io = {}) {
|
|
|
18884
19134
|
return a === "y" || a === "yes";
|
|
18885
19135
|
};
|
|
18886
19136
|
let sectionNo = 0;
|
|
18887
|
-
const section = (
|
|
18888
|
-
sectionNo =
|
|
19137
|
+
const section = (n2) => {
|
|
19138
|
+
sectionNo = n2;
|
|
18889
19139
|
out3("");
|
|
18890
|
-
out3(paint(
|
|
19140
|
+
out3(paint(n2 + "/" + SECTIONS.length + " " + SECTIONS[n2 - 1], BOLD + CYAN, colour));
|
|
18891
19141
|
};
|
|
18892
19142
|
const wrote = [];
|
|
18893
19143
|
let verifyCommand;
|
|
@@ -18905,6 +19155,8 @@ async function runOnboard(opts, io = {}) {
|
|
|
18905
19155
|
const localVerifySupported = localVerifierShell(platform, "true") !== null;
|
|
18906
19156
|
if (platform === "linux" && lifecycle.state === "OK") {
|
|
18907
19157
|
status("OK", "Full check / verify / run support is available.", "ok");
|
|
19158
|
+
} else if (platform === "linux") {
|
|
19159
|
+
status("LIMITED", lifecycle.detail, "warn");
|
|
18908
19160
|
} else if (localVerifySupported) {
|
|
18909
19161
|
status("LIMITED", platformLabel(platform) + ": check + verify work here; `tamperward run` requires Linux in this release.", "warn");
|
|
18910
19162
|
} else {
|
|
@@ -19015,8 +19267,8 @@ async function runOnboard(opts, io = {}) {
|
|
|
19015
19267
|
status("ACTION", "Choose the command that decides whether this repository passes:", "warn");
|
|
19016
19268
|
candidates.forEach((candidate, i) => out3(" " + (i + 1) + ". " + terminalText(candidate)));
|
|
19017
19269
|
const a = await ask("Choose 1-" + candidates.length + ", type a command, or press Enter to skip: ");
|
|
19018
|
-
const
|
|
19019
|
-
chosen = a === "" ? void 0 : Number.isInteger(
|
|
19270
|
+
const n2 = Number(a);
|
|
19271
|
+
chosen = a === "" ? void 0 : Number.isInteger(n2) && n2 >= 1 && n2 <= candidates.length ? candidates[n2 - 1] : a;
|
|
19020
19272
|
} else {
|
|
19021
19273
|
status("ACTION", "No test command was detected automatically.", "warn");
|
|
19022
19274
|
chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
|
|
@@ -19200,7 +19452,7 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19200
19452
|
out3("Demo skipped: the selected test could not be resolved safely inside the disposable worktree.");
|
|
19201
19453
|
return;
|
|
19202
19454
|
}
|
|
19203
|
-
const prefix = worktreeRoot.endsWith(
|
|
19455
|
+
const prefix = worktreeRoot.endsWith(sep6) ? worktreeRoot : worktreeRoot + sep6;
|
|
19204
19456
|
if (!realTarget.startsWith(prefix)) {
|
|
19205
19457
|
out3("Demo skipped: the selected test resolves outside the disposable worktree.");
|
|
19206
19458
|
return;
|
|
@@ -19264,7 +19516,7 @@ var init_onboard = __esm({
|
|
|
19264
19516
|
});
|
|
19265
19517
|
|
|
19266
19518
|
// src/research/adapter.ts
|
|
19267
|
-
import { isAbsolute as isAbsolute13, resolve as
|
|
19519
|
+
import { isAbsolute as isAbsolute13, resolve as resolve14 } from "node:path";
|
|
19268
19520
|
function taskEnv(task) {
|
|
19269
19521
|
return {
|
|
19270
19522
|
TAMPERWARD_RESEARCH_TASK: task.id,
|
|
@@ -19283,7 +19535,7 @@ function normalizeCommandArgv(argv, cwd = process.cwd()) {
|
|
|
19283
19535
|
throw new ResearchError('the command adapter needs an agent command after "--"');
|
|
19284
19536
|
}
|
|
19285
19537
|
const head = argv[0];
|
|
19286
|
-
return head.includes("/") && !isAbsolute13(head) ? [
|
|
19538
|
+
return head.includes("/") && !isAbsolute13(head) ? [resolve14(cwd, head), ...argv.slice(1)] : [...argv];
|
|
19287
19539
|
}
|
|
19288
19540
|
function commandAdapter(argv, cwd = process.cwd()) {
|
|
19289
19541
|
const anchored = normalizeCommandArgv(argv, cwd);
|
|
@@ -19296,6 +19548,24 @@ function commandAdapter(argv, cwd = process.cwd()) {
|
|
|
19296
19548
|
}
|
|
19297
19549
|
};
|
|
19298
19550
|
}
|
|
19551
|
+
function stdioAdapter(argv, cwd = process.cwd()) {
|
|
19552
|
+
const anchored = normalizeCommandArgv(argv, cwd);
|
|
19553
|
+
return {
|
|
19554
|
+
name: "stdio",
|
|
19555
|
+
layers: ["envelope"],
|
|
19556
|
+
launch(task) {
|
|
19557
|
+
const substituted = anchored.map((arg) => substitutePlaceholders(arg, task));
|
|
19558
|
+
return {
|
|
19559
|
+
argv: substituted,
|
|
19560
|
+
env: {
|
|
19561
|
+
...taskEnv(task),
|
|
19562
|
+
TAMPERWARD_RESEARCH_PROTOCOL: "research-stdio-jsonl-v1",
|
|
19563
|
+
TAMPERWARD_RESEARCH_CAPABILITIES: JSON.stringify({ layers: ["envelope"], intervention: "not-connected" })
|
|
19564
|
+
}
|
|
19565
|
+
};
|
|
19566
|
+
}
|
|
19567
|
+
};
|
|
19568
|
+
}
|
|
19299
19569
|
function claudeCodeAdapter(model) {
|
|
19300
19570
|
return {
|
|
19301
19571
|
name: "claude-code",
|
|
@@ -19318,6 +19588,7 @@ function claudeCodeAdapter(model) {
|
|
|
19318
19588
|
function resolveAdapter(name, argv, model) {
|
|
19319
19589
|
if (name === "command") return commandAdapter(argv);
|
|
19320
19590
|
if (name === "claude-code") return claudeCodeAdapter(model);
|
|
19591
|
+
if (name === "stdio") return stdioAdapter(argv);
|
|
19321
19592
|
throw new ResearchError(`unknown adapter "${name}" (built-in adapters: ${BUILTIN_ADAPTERS.join(", ")})`);
|
|
19322
19593
|
}
|
|
19323
19594
|
var ADAPTER_LAYERS, RESEARCH_ARMS, BUILTIN_ADAPTERS, ResearchError, PLACEHOLDERS, PLACEHOLDER_PATTERN;
|
|
@@ -19327,7 +19598,7 @@ var init_adapter = __esm({
|
|
|
19327
19598
|
init_init();
|
|
19328
19599
|
ADAPTER_LAYERS = ["envelope", "pre-tool-use", "stop-sweep"];
|
|
19329
19600
|
RESEARCH_ARMS = ["ungated", "gated"];
|
|
19330
|
-
BUILTIN_ADAPTERS = ["claude-code", "command"];
|
|
19601
|
+
BUILTIN_ADAPTERS = ["claude-code", "command", "stdio"];
|
|
19331
19602
|
ResearchError = class extends Error {
|
|
19332
19603
|
};
|
|
19333
19604
|
PLACEHOLDERS = {
|
|
@@ -19386,7 +19657,7 @@ var init_capture = __esm({
|
|
|
19386
19657
|
// src/research/manifest.ts
|
|
19387
19658
|
import { createHash as createHash13 } from "node:crypto";
|
|
19388
19659
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
19389
|
-
import { dirname as dirname10, resolve as
|
|
19660
|
+
import { dirname as dirname10, resolve as resolve15 } from "node:path";
|
|
19390
19661
|
function taskFrom(raw, index, manifestDir) {
|
|
19391
19662
|
const where = `task #${index + 1}`;
|
|
19392
19663
|
if (!isRecord(raw)) throw new ResearchError(`task manifest: ${where} is not an object`);
|
|
@@ -19409,14 +19680,14 @@ function taskFrom(raw, index, manifestDir) {
|
|
|
19409
19680
|
const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(repo) || /^[^/]+@[^:]+:/.test(repo);
|
|
19410
19681
|
return {
|
|
19411
19682
|
id,
|
|
19412
|
-
repo: isUrl ? repo :
|
|
19683
|
+
repo: isUrl ? repo : resolve15(manifestDir, repo),
|
|
19413
19684
|
base: base ?? "HEAD",
|
|
19414
19685
|
prompt,
|
|
19415
19686
|
verify: { command: verify.command, ...budget !== void 0 ? { budget } : {} }
|
|
19416
19687
|
};
|
|
19417
19688
|
}
|
|
19418
19689
|
function readManifest(path) {
|
|
19419
|
-
const abs =
|
|
19690
|
+
const abs = resolve15(path);
|
|
19420
19691
|
let bytes;
|
|
19421
19692
|
try {
|
|
19422
19693
|
bytes = readFileSync22(abs);
|
|
@@ -19719,7 +19990,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
|
|
|
19719
19990
|
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
19720
19991
|
import { closeSync as closeSync3, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync9, openSync as openSync3, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync11, unlinkSync, writeFileSync as writeFileSync11, writeSync } from "node:fs";
|
|
19721
19992
|
import { hostname } from "node:os";
|
|
19722
|
-
import { join as join25, resolve as
|
|
19993
|
+
import { join as join25, resolve as resolve16 } from "node:path";
|
|
19723
19994
|
function git6(args, cwd) {
|
|
19724
19995
|
return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
19725
19996
|
}
|
|
@@ -19973,15 +20244,19 @@ function unobservedOutcome() {
|
|
|
19973
20244
|
honest_completion: false
|
|
19974
20245
|
};
|
|
19975
20246
|
}
|
|
19976
|
-
function trustedPolicyAt(base, ws3) {
|
|
20247
|
+
function trustedPolicyAt(base, ws3, loader = loadPolicyAt) {
|
|
19977
20248
|
try {
|
|
19978
|
-
return { policy:
|
|
20249
|
+
return { policy: loader(base, ws3) ?? defaultPolicy() };
|
|
19979
20250
|
} catch (e) {
|
|
19980
20251
|
return { failure: `trusted policy at ${base.slice(0, 10)} could not be loaded: ${errorMessage(e).split("\n")[0]}` };
|
|
19981
20252
|
}
|
|
19982
20253
|
}
|
|
19983
|
-
function
|
|
19984
|
-
const trusted = trustedPolicyAt(base, ws3);
|
|
20254
|
+
function trustedProtectedOnly(base, ws3, loader = loadPolicyAt) {
|
|
20255
|
+
const trusted = trustedPolicyAt(base, ws3, loader);
|
|
20256
|
+
if ("failure" in trusted) return { trusted, protectedOnly: () => true };
|
|
20257
|
+
return { trusted, protectedOnly: (rel) => isProtected(rel, trusted.policy) };
|
|
20258
|
+
}
|
|
20259
|
+
function observeOutcome(ws3, base, head, task, trusted) {
|
|
19985
20260
|
if ("failure" in trusted) return { outcome: unobservedOutcome(), unmeasurable: trusted.failure };
|
|
19986
20261
|
const policy = trusted.policy;
|
|
19987
20262
|
const verify = parseDocument2(
|
|
@@ -20055,16 +20330,10 @@ function runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase) {
|
|
|
20055
20330
|
}
|
|
20056
20331
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20057
20332
|
const head = git6(["rev-parse", "HEAD"], ws3);
|
|
20058
|
-
const
|
|
20059
|
-
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
return true;
|
|
20063
|
-
}
|
|
20064
|
-
};
|
|
20065
|
-
const fingerprintBefore = treeFingerprint(ws3, protectedOnly);
|
|
20066
|
-
const observed = observeOutcome(ws3, base, head, task);
|
|
20067
|
-
const movedDuringObservation = treeFingerprint(ws3, protectedOnly) !== fingerprintBefore;
|
|
20333
|
+
const trustedSurface = trustedProtectedOnly(base, ws3);
|
|
20334
|
+
const fingerprintBefore = treeFingerprint(ws3, trustedSurface.protectedOnly);
|
|
20335
|
+
const observed = observeOutcome(ws3, base, head, task, trustedSurface.trusted);
|
|
20336
|
+
const movedDuringObservation = treeFingerprint(ws3, trustedSurface.protectedOnly) !== fingerprintBefore;
|
|
20068
20337
|
const unmeasurable = agent.unmeasurable ?? observed.unmeasurable ?? (movedDuringObservation ? "NOT_QUIESCENT: the workspace changed while its outcome was being observed" : null);
|
|
20069
20338
|
const { unmeasurable: _agentUnmeasurable, ...agentRecord } = agent;
|
|
20070
20339
|
const outcome = unmeasurable === null ? observed.outcome : unobservedOutcome();
|
|
@@ -20109,7 +20378,7 @@ function runResearch(opts) {
|
|
|
20109
20378
|
err2("tamperward research: the gated arm cannot start here (see `tamperward doctor`); run as a non-root user on Linux.");
|
|
20110
20379
|
return 2;
|
|
20111
20380
|
}
|
|
20112
|
-
const ledger =
|
|
20381
|
+
const ledger = resolve16(opts.out);
|
|
20113
20382
|
let lock;
|
|
20114
20383
|
try {
|
|
20115
20384
|
lock = acquireResearchLock(ledger, opts.breakLock === true);
|
|
@@ -20250,18 +20519,18 @@ var init_run2 = __esm({
|
|
|
20250
20519
|
|
|
20251
20520
|
// src/research/summarize.ts
|
|
20252
20521
|
import { readdirSync as readdirSync7, readFileSync as readFileSync24 } from "node:fs";
|
|
20253
|
-
import { join as join26, resolve as
|
|
20522
|
+
import { join as join26, resolve as resolve17 } from "node:path";
|
|
20254
20523
|
function readLedger2(dir) {
|
|
20255
|
-
const pairsDir = join26(
|
|
20524
|
+
const pairsDir = join26(resolve17(dir), "pairs");
|
|
20256
20525
|
let names;
|
|
20257
20526
|
try {
|
|
20258
|
-
names = readdirSync7(pairsDir).filter((
|
|
20527
|
+
names = readdirSync7(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
|
|
20259
20528
|
} catch (e) {
|
|
20260
20529
|
throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
|
|
20261
20530
|
}
|
|
20262
20531
|
if (names.length === 0) throw new ResearchError(`ledger ${pairsDir} holds no pair records`);
|
|
20263
|
-
return names.map((
|
|
20264
|
-
const path = join26(pairsDir,
|
|
20532
|
+
return names.map((n2) => {
|
|
20533
|
+
const path = join26(pairsDir, n2);
|
|
20265
20534
|
let raw;
|
|
20266
20535
|
try {
|
|
20267
20536
|
raw = JSON.parse(readFileSync24(path, "utf8"));
|
|
@@ -20394,7 +20663,321 @@ var init_summarize = __esm({
|
|
|
20394
20663
|
}
|
|
20395
20664
|
});
|
|
20396
20665
|
|
|
20666
|
+
// src/research/init.ts
|
|
20667
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
20668
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync25, writeFileSync as writeFileSync12 } from "node:fs";
|
|
20669
|
+
import { basename as basename5, dirname as dirname11, resolve as resolve18 } from "node:path";
|
|
20670
|
+
function idFrom(repo) {
|
|
20671
|
+
const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
|
|
20672
|
+
return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
|
|
20673
|
+
}
|
|
20674
|
+
function createResearchManifest(opts) {
|
|
20675
|
+
if (!opts.out) throw new ResearchError("research init requires --out");
|
|
20676
|
+
if (!opts.repo) throw new ResearchError("research init requires --repo");
|
|
20677
|
+
if (!opts.prompt) throw new ResearchError("research init requires --prompt");
|
|
20678
|
+
if (!opts.verifyCommand) throw new ResearchError("research init requires --verify-command");
|
|
20679
|
+
const out3 = resolve18(opts.out);
|
|
20680
|
+
if (existsSync19(out3)) throw new ResearchError(`research init refuses to overwrite ${out3}; choose a new path`);
|
|
20681
|
+
const id = opts.id ?? idFrom(opts.repo);
|
|
20682
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) throw new ResearchError(`research init id must match [A-Za-z0-9._-] (got "${id}")`);
|
|
20683
|
+
if (opts.verifyBudget !== void 0 && (!Number.isFinite(opts.verifyBudget) || opts.verifyBudget <= 0)) {
|
|
20684
|
+
throw new ResearchError("research init --verify-budget must be positive");
|
|
20685
|
+
}
|
|
20686
|
+
const doc = {
|
|
20687
|
+
version: 1,
|
|
20688
|
+
tasks: [{
|
|
20689
|
+
id,
|
|
20690
|
+
repo: opts.repo,
|
|
20691
|
+
base: opts.base ?? "HEAD",
|
|
20692
|
+
prompt: opts.prompt,
|
|
20693
|
+
verify: { command: opts.verifyCommand, ...opts.verifyBudget === void 0 ? {} : { budget: opts.verifyBudget } }
|
|
20694
|
+
}]
|
|
20695
|
+
};
|
|
20696
|
+
const text = JSON.stringify(doc, null, 2) + "\n";
|
|
20697
|
+
mkdirSync10(dirname11(out3), { recursive: true });
|
|
20698
|
+
writeFileSync12(out3, text, { flag: "wx", mode: 384 });
|
|
20699
|
+
const parsed = readManifest(out3);
|
|
20700
|
+
if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
|
|
20701
|
+
return out3;
|
|
20702
|
+
}
|
|
20703
|
+
function runResearchInit(opts) {
|
|
20704
|
+
try {
|
|
20705
|
+
const out3 = createResearchManifest(opts);
|
|
20706
|
+
const bytes = requireHash(out3);
|
|
20707
|
+
process.stdout.write(`tamperward research init: wrote ${out3} (sha256 ${bytes})
|
|
20708
|
+
`);
|
|
20709
|
+
return 0;
|
|
20710
|
+
} catch (e) {
|
|
20711
|
+
if (e instanceof ResearchError || e instanceof Error) {
|
|
20712
|
+
process.stderr.write(`tamperward research: ${e.message}
|
|
20713
|
+
`);
|
|
20714
|
+
return 2;
|
|
20715
|
+
}
|
|
20716
|
+
throw e;
|
|
20717
|
+
}
|
|
20718
|
+
}
|
|
20719
|
+
function requireHash(path) {
|
|
20720
|
+
return createHash14("sha256").update(readFileSync25(path)).digest("hex");
|
|
20721
|
+
}
|
|
20722
|
+
var init_init2 = __esm({
|
|
20723
|
+
"src/research/init.ts"() {
|
|
20724
|
+
"use strict";
|
|
20725
|
+
init_adapter();
|
|
20726
|
+
init_manifest();
|
|
20727
|
+
}
|
|
20728
|
+
});
|
|
20729
|
+
|
|
20730
|
+
// src/research/report.ts
|
|
20731
|
+
function renderResearchReport(summary) {
|
|
20732
|
+
const u = summary.model_behaviour.ungated;
|
|
20733
|
+
const g = summary.model_behaviour.gated;
|
|
20734
|
+
const uo = summary.independent_outcome.ungated;
|
|
20735
|
+
const go = summary.independent_outcome.gated;
|
|
20736
|
+
const lines = [
|
|
20737
|
+
"MODEL BEHAVIOUR",
|
|
20738
|
+
` model: ${summary.model ?? "(unspecified)"}`,
|
|
20739
|
+
` adapter: ${summary.adapter.name} [${summary.adapter.layers.join(", ")}]`,
|
|
20740
|
+
` pairs: ${n(summary.pairs)} (measured: ${n(summary.measured_pairs)}, unmeasurable: ${n(summary.unmeasurable_pairs)})`,
|
|
20741
|
+
` ungated: ${n(u.trajectories)} trajectories; masked failures ${n(u.masked_failure)}; surviving mutations ${n(u.surviving_protected_mutations)}`,
|
|
20742
|
+
` gated: ${n(g.trajectories)} trajectories; masked failures ${n(g.masked_failure)}; surviving mutations ${n(g.surviving_protected_mutations)}`,
|
|
20743
|
+
"",
|
|
20744
|
+
"CONTROL RESPONSE",
|
|
20745
|
+
" not measured by this adapter/ledger; denial-to-fix, surrender, and alternative-bypass responses are not inferred.",
|
|
20746
|
+
"",
|
|
20747
|
+
"INDEPENDENT OUTCOME",
|
|
20748
|
+
` 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)}`,
|
|
20749
|
+
` 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)}`,
|
|
20750
|
+
"",
|
|
20751
|
+
"TAMPERWARD PERFORMANCE",
|
|
20752
|
+
` caught: ${n(summary.tamperward.caught)}`,
|
|
20753
|
+
` escapes: ${n(summary.tamperward.escapes)}`,
|
|
20754
|
+
` false refusals: ${n(summary.tamperward.false_refusals)}`,
|
|
20755
|
+
` cannot adjudicate: ${n(summary.tamperward.cannot_adjudicate)}`,
|
|
20756
|
+
` 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)}`
|
|
20757
|
+
];
|
|
20758
|
+
return lines.join("\n") + "\n";
|
|
20759
|
+
}
|
|
20760
|
+
function runResearchReport(opts) {
|
|
20761
|
+
try {
|
|
20762
|
+
const summary = summarizeLedger(opts.ledger);
|
|
20763
|
+
process.stdout.write(opts.json ? JSON.stringify(summary, null, 2) + "\n" : renderResearchReport(summary));
|
|
20764
|
+
return 0;
|
|
20765
|
+
} catch (e) {
|
|
20766
|
+
if (e instanceof ResearchError) {
|
|
20767
|
+
process.stderr.write(`tamperward research: ${e.message}
|
|
20768
|
+
`);
|
|
20769
|
+
return 2;
|
|
20770
|
+
}
|
|
20771
|
+
throw e;
|
|
20772
|
+
}
|
|
20773
|
+
}
|
|
20774
|
+
var n;
|
|
20775
|
+
var init_report2 = __esm({
|
|
20776
|
+
"src/research/report.ts"() {
|
|
20777
|
+
"use strict";
|
|
20778
|
+
init_adapter();
|
|
20779
|
+
init_summarize();
|
|
20780
|
+
n = (value) => String(value);
|
|
20781
|
+
}
|
|
20782
|
+
});
|
|
20783
|
+
|
|
20784
|
+
// src/research/bundle.ts
|
|
20785
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
20786
|
+
import { gzipSync, gunzipSync } from "node:zlib";
|
|
20787
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync26, statSync as statSync6, writeFileSync as writeFileSync13 } from "node:fs";
|
|
20788
|
+
import { dirname as dirname12, join as join27, resolve as resolve19 } from "node:path";
|
|
20789
|
+
function octal(value, width) {
|
|
20790
|
+
return value.toString(8).padStart(width - 1, "0") + "\0";
|
|
20791
|
+
}
|
|
20792
|
+
function tarEntry(name, bytes) {
|
|
20793
|
+
const header = Buffer.alloc(BLOCK, 0);
|
|
20794
|
+
const safe = name.replace(/^\/+/, "").slice(0, 99);
|
|
20795
|
+
header.write(safe, 0, "utf8");
|
|
20796
|
+
header.write(octal(420, 8), 100, "ascii");
|
|
20797
|
+
header.write(octal(0, 8), 108, "ascii");
|
|
20798
|
+
header.write(octal(0, 8), 116, "ascii");
|
|
20799
|
+
header.write(octal(bytes.length, 12), 124, "ascii");
|
|
20800
|
+
header.write(octal(0, 12), 136, "ascii");
|
|
20801
|
+
header.fill(32, 148, 156);
|
|
20802
|
+
header[156] = 48;
|
|
20803
|
+
header.write("ustar", 257, "ascii");
|
|
20804
|
+
header.write("00", 263, "ascii");
|
|
20805
|
+
const checksum = header.reduce((sum, b) => sum + b, 0);
|
|
20806
|
+
header.write(octal(checksum, 8), 148, "ascii");
|
|
20807
|
+
const padding = Buffer.alloc((BLOCK - bytes.length % BLOCK) % BLOCK, 0);
|
|
20808
|
+
return Buffer.concat([header, bytes, padding]);
|
|
20809
|
+
}
|
|
20810
|
+
function makeTar(entries) {
|
|
20811
|
+
return Buffer.concat([...entries.map((e) => tarEntry(e.name, e.bytes)), Buffer.alloc(BLOCK * 2, 0)]);
|
|
20812
|
+
}
|
|
20813
|
+
function parseTar(bytes) {
|
|
20814
|
+
const files = /* @__PURE__ */ new Map();
|
|
20815
|
+
for (let offset = 0; offset + BLOCK <= bytes.length; ) {
|
|
20816
|
+
const header = bytes.subarray(offset, offset + BLOCK);
|
|
20817
|
+
if (header.every((b) => b === 0)) break;
|
|
20818
|
+
const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
20819
|
+
const sizeText = header.subarray(124, 136).toString("ascii").replace(/\0.*$/, "").trim();
|
|
20820
|
+
const size = Number.parseInt(sizeText || "0", 8);
|
|
20821
|
+
if (!name || name.startsWith("/") || name.split("/").includes("..") || !Number.isSafeInteger(size) || size < 0) {
|
|
20822
|
+
throw new ResearchError("research bundle contains an unsafe or malformed archive entry");
|
|
20823
|
+
}
|
|
20824
|
+
const start = offset + BLOCK;
|
|
20825
|
+
const end = start + size;
|
|
20826
|
+
if (end > bytes.length) throw new ResearchError("research bundle is truncated");
|
|
20827
|
+
files.set(name, Buffer.from(bytes.subarray(start, end)));
|
|
20828
|
+
offset = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
20829
|
+
}
|
|
20830
|
+
return files;
|
|
20831
|
+
}
|
|
20832
|
+
function walkLedger(ledger) {
|
|
20833
|
+
const entries = [];
|
|
20834
|
+
const pairs = join27(ledger, "pairs");
|
|
20835
|
+
if (!existsSync20(pairs) || !statSync6(pairs).isDirectory()) throw new ResearchError(`ledger ${ledger} has no pairs directory`);
|
|
20836
|
+
const names = readdirSync8(pairs).filter((n2) => n2.endsWith(".json")).sort();
|
|
20837
|
+
if (names.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
|
|
20838
|
+
for (const name of names) {
|
|
20839
|
+
const path = join27(pairs, name);
|
|
20840
|
+
if (!statSync6(path).isFile()) continue;
|
|
20841
|
+
entries.push({ name: `ledger/pairs/${name}`, bytes: readFileSync26(path) });
|
|
20842
|
+
}
|
|
20843
|
+
return entries;
|
|
20844
|
+
}
|
|
20845
|
+
function jsonBytes(value) {
|
|
20846
|
+
return Buffer.from(JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
20847
|
+
}
|
|
20848
|
+
function validateResearchBundle(path) {
|
|
20849
|
+
let files;
|
|
20850
|
+
try {
|
|
20851
|
+
files = parseTar(gunzipSync(readFileSync26(resolve19(path))));
|
|
20852
|
+
} catch (e) {
|
|
20853
|
+
throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
20854
|
+
}
|
|
20855
|
+
const provenance = files.get("provenance.json");
|
|
20856
|
+
const summaryBytes = files.get("summary.json");
|
|
20857
|
+
const report2 = files.get("report.txt");
|
|
20858
|
+
if (!provenance || !summaryBytes || !report2) throw new ResearchError("research bundle is missing provenance.json, summary.json or report.txt");
|
|
20859
|
+
let p;
|
|
20860
|
+
let summary;
|
|
20861
|
+
try {
|
|
20862
|
+
p = JSON.parse(provenance.toString("utf8"));
|
|
20863
|
+
summary = JSON.parse(summaryBytes.toString("utf8"));
|
|
20864
|
+
} catch {
|
|
20865
|
+
throw new ResearchError("research bundle contains invalid JSON");
|
|
20866
|
+
}
|
|
20867
|
+
if (p.bundle_schema_version !== BUNDLE_VERSION || p.schema_version !== MACHINE_SCHEMA_VERSION) {
|
|
20868
|
+
throw new ResearchError("research bundle schema version is unsupported");
|
|
20869
|
+
}
|
|
20870
|
+
const manifest = files.get("manifest.json");
|
|
20871
|
+
if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
|
|
20872
|
+
throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
|
|
20873
|
+
}
|
|
20874
|
+
if (manifest && createHash15("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
|
|
20875
|
+
throw new ResearchError("research bundle manifest does not match its recorded sha256");
|
|
20876
|
+
}
|
|
20877
|
+
const parsedSummary = summary;
|
|
20878
|
+
if (parsedSummary.document !== "summary" || parsedSummary.command !== "research") throw new ResearchError("research bundle summary is not a research summary");
|
|
20879
|
+
const records = [];
|
|
20880
|
+
for (const [name, bytes] of files) {
|
|
20881
|
+
if (!name.startsWith("ledger/pairs/") || !name.endsWith(".json")) continue;
|
|
20882
|
+
let raw;
|
|
20883
|
+
try {
|
|
20884
|
+
raw = JSON.parse(bytes.toString("utf8"));
|
|
20885
|
+
} catch {
|
|
20886
|
+
throw new ResearchError(`invalid JSON in ${name}`);
|
|
20887
|
+
}
|
|
20888
|
+
records.push(pairRecordFrom(raw, name));
|
|
20889
|
+
}
|
|
20890
|
+
if (records.length === 0) throw new ResearchError("research bundle contains no pair records");
|
|
20891
|
+
const derived = summarizeRecords(records);
|
|
20892
|
+
if (JSON.stringify(derived) !== JSON.stringify(summary)) throw new ResearchError("research bundle summary does not match its pair records");
|
|
20893
|
+
if (report2.toString("utf8") !== renderResearchReport(derived)) throw new ResearchError("research bundle report does not match its summary");
|
|
20894
|
+
if (p.manifest_sha256 !== parsedSummary.manifest_sha256) throw new ResearchError("research bundle manifest hash disagrees with summary");
|
|
20895
|
+
if (!p.protocol || p.protocol !== "research-bundle-v1") throw new ResearchError("research bundle protocol is missing or unsupported");
|
|
20896
|
+
return { records: records.length, manifest_sha256: String(p.manifest_sha256) };
|
|
20897
|
+
}
|
|
20898
|
+
function createResearchBundle(opts) {
|
|
20899
|
+
if (!opts.ledger) throw new ResearchError("research bundle requires --ledger");
|
|
20900
|
+
if (!opts.out) throw new ResearchError("research bundle requires --out");
|
|
20901
|
+
const ledger = resolve19(opts.ledger);
|
|
20902
|
+
const summary = summarizeLedger(ledger);
|
|
20903
|
+
const entries = walkLedger(ledger);
|
|
20904
|
+
if (opts.manifest) {
|
|
20905
|
+
const manifest = readFileSync26(resolve19(opts.manifest));
|
|
20906
|
+
const sha3 = createHash15("sha256").update(manifest).digest("hex");
|
|
20907
|
+
if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
|
|
20908
|
+
entries.push({ name: "manifest.json", bytes: manifest });
|
|
20909
|
+
}
|
|
20910
|
+
const provenance = {
|
|
20911
|
+
bundle_schema_version: BUNDLE_VERSION,
|
|
20912
|
+
protocol: "research-bundle-v1",
|
|
20913
|
+
schema_version: MACHINE_SCHEMA_VERSION,
|
|
20914
|
+
manifest_sha256: summary.manifest_sha256,
|
|
20915
|
+
adapter: summary.adapter,
|
|
20916
|
+
model: summary.model,
|
|
20917
|
+
tamperward_version: summary.tamperward_version,
|
|
20918
|
+
agent_argv: summary.agent_argv,
|
|
20919
|
+
agent_budget: summary.agent_budget,
|
|
20920
|
+
records: entries.filter((e) => e.name.startsWith("ledger/pairs/")).length,
|
|
20921
|
+
manifest_included: Boolean(opts.manifest),
|
|
20922
|
+
prompts_included: Boolean(opts.manifest)
|
|
20923
|
+
};
|
|
20924
|
+
entries.push({ name: "summary.json", bytes: jsonBytes(summary) });
|
|
20925
|
+
entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
|
|
20926
|
+
entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
|
|
20927
|
+
const out3 = resolve19(opts.out);
|
|
20928
|
+
mkdirSync11(dirname12(out3), { recursive: true });
|
|
20929
|
+
writeFileSync13(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
|
|
20930
|
+
return out3;
|
|
20931
|
+
}
|
|
20932
|
+
function runResearchBundle(opts) {
|
|
20933
|
+
try {
|
|
20934
|
+
if (opts.validate) {
|
|
20935
|
+
const result = validateResearchBundle(opts.validate);
|
|
20936
|
+
process.stdout.write(`tamperward research bundle: valid (${result.records} records, manifest ${result.manifest_sha256})
|
|
20937
|
+
`);
|
|
20938
|
+
return 0;
|
|
20939
|
+
}
|
|
20940
|
+
const out3 = createResearchBundle(opts);
|
|
20941
|
+
process.stdout.write(`tamperward research bundle: wrote ${out3}
|
|
20942
|
+
`);
|
|
20943
|
+
return 0;
|
|
20944
|
+
} catch (e) {
|
|
20945
|
+
if (e instanceof ResearchError || e instanceof Error) {
|
|
20946
|
+
process.stderr.write(`tamperward research: ${e.message}
|
|
20947
|
+
`);
|
|
20948
|
+
return 2;
|
|
20949
|
+
}
|
|
20950
|
+
throw e;
|
|
20951
|
+
}
|
|
20952
|
+
}
|
|
20953
|
+
var BLOCK, BUNDLE_VERSION;
|
|
20954
|
+
var init_bundle = __esm({
|
|
20955
|
+
"src/research/bundle.ts"() {
|
|
20956
|
+
"use strict";
|
|
20957
|
+
init_machine_output();
|
|
20958
|
+
init_adapter();
|
|
20959
|
+
init_record();
|
|
20960
|
+
init_report2();
|
|
20961
|
+
init_summarize();
|
|
20962
|
+
BLOCK = 512;
|
|
20963
|
+
BUNDLE_VERSION = 1;
|
|
20964
|
+
}
|
|
20965
|
+
});
|
|
20966
|
+
|
|
20397
20967
|
// src/cli/research.ts
|
|
20968
|
+
function parseResearchInit(args) {
|
|
20969
|
+
const o = { out: "", repo: "", prompt: "", verifyCommand: "" };
|
|
20970
|
+
for (let i = 0; i < args.length; i++) {
|
|
20971
|
+
if (args[i] === "--out") o.out = args[++i];
|
|
20972
|
+
else if (args[i] === "--repo") o.repo = args[++i];
|
|
20973
|
+
else if (args[i] === "--base") o.base = args[++i];
|
|
20974
|
+
else if (args[i] === "--id") o.id = args[++i];
|
|
20975
|
+
else if (args[i] === "--prompt") o.prompt = args[++i];
|
|
20976
|
+
else if (args[i] === "--verify-command") o.verifyCommand = args[++i];
|
|
20977
|
+
else if (args[i] === "--verify-budget") o.verifyBudget = Number(args[++i]);
|
|
20978
|
+
}
|
|
20979
|
+
return o;
|
|
20980
|
+
}
|
|
20398
20981
|
function parseResearchRun(args) {
|
|
20399
20982
|
const o = { manifest: "", out: "", adapter: "", agentArgv: [] };
|
|
20400
20983
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -20420,10 +21003,36 @@ function parseResearchSummarize(args) {
|
|
|
20420
21003
|
}
|
|
20421
21004
|
return o;
|
|
20422
21005
|
}
|
|
21006
|
+
function parseResearchReport(args) {
|
|
21007
|
+
const o = { ledger: "" };
|
|
21008
|
+
for (let i = 0; i < args.length; i++) {
|
|
21009
|
+
if (args[i] === "--ledger") o.ledger = args[++i];
|
|
21010
|
+
else if (args[i] === "--json") o.json = true;
|
|
21011
|
+
}
|
|
21012
|
+
return o;
|
|
21013
|
+
}
|
|
21014
|
+
function parseResearchBundle(args) {
|
|
21015
|
+
const o = { ledger: "" };
|
|
21016
|
+
for (let i = 0; i < args.length; i++) {
|
|
21017
|
+
if (args[i] === "--ledger") o.ledger = args[++i];
|
|
21018
|
+
else if (args[i] === "--out") o.out = args[++i];
|
|
21019
|
+
else if (args[i] === "--manifest") o.manifest = args[++i];
|
|
21020
|
+
else if (args[i] === "--validate") o.validate = args[++i];
|
|
21021
|
+
else if (args[i] === "--bundle") o.validate = args[++i];
|
|
21022
|
+
}
|
|
21023
|
+
return o;
|
|
21024
|
+
}
|
|
20423
21025
|
function runResearchCommand(args) {
|
|
20424
21026
|
const [sub, ...rest] = args;
|
|
21027
|
+
if (sub === "init") return runResearchInit(parseResearchInit(rest));
|
|
20425
21028
|
if (sub === "run") return runResearch(parseResearchRun(rest));
|
|
20426
21029
|
if (sub === "summarize") return runResearchSummarize(parseResearchSummarize(rest));
|
|
21030
|
+
if (sub === "report") return runResearchReport(parseResearchReport(rest));
|
|
21031
|
+
if (sub === "bundle") return runResearchBundle(parseResearchBundle(rest));
|
|
21032
|
+
if (sub === "validate") {
|
|
21033
|
+
const bundle = parseResearchBundle(rest);
|
|
21034
|
+
return runResearchBundle({ ledger: "", validate: bundle.validate ?? bundle.out });
|
|
21035
|
+
}
|
|
20427
21036
|
process.stderr.write(`tamperward research: unknown subcommand "${sub ?? ""}" (${RESEARCH_SUBCOMMANDS.join(" | ")})
|
|
20428
21037
|
`);
|
|
20429
21038
|
return 2;
|
|
@@ -20434,7 +21043,37 @@ var init_research = __esm({
|
|
|
20434
21043
|
"use strict";
|
|
20435
21044
|
init_run2();
|
|
20436
21045
|
init_summarize();
|
|
20437
|
-
|
|
21046
|
+
init_init2();
|
|
21047
|
+
init_report2();
|
|
21048
|
+
init_bundle();
|
|
21049
|
+
RESEARCH_SUBCOMMANDS = ["init", "run", "summarize", "report", "bundle", "validate"];
|
|
21050
|
+
}
|
|
21051
|
+
});
|
|
21052
|
+
|
|
21053
|
+
// src/cli/signoff-label.ts
|
|
21054
|
+
function runSignoffLabel(opts) {
|
|
21055
|
+
if (!opts.rule) {
|
|
21056
|
+
process.stderr.write("tamperward signoff-label --rule <rule> --head <full-head-sha> [--file <path>]\n");
|
|
21057
|
+
return 2;
|
|
21058
|
+
}
|
|
21059
|
+
if (!opts.head) {
|
|
21060
|
+
process.stderr.write("tamperward: --head is required and must be a full 40- or 64-character object id.\n");
|
|
21061
|
+
return 2;
|
|
21062
|
+
}
|
|
21063
|
+
const want = opts.file ? `${opts.rule}:${opts.file}` : opts.rule;
|
|
21064
|
+
const token = compactOobToken(want, opts.head);
|
|
21065
|
+
if (!token) {
|
|
21066
|
+
process.stderr.write("tamperward: --head must be a full 40- or 64-character hexadecimal object id.\n");
|
|
21067
|
+
return 2;
|
|
21068
|
+
}
|
|
21069
|
+
process.stdout.write(`${token}
|
|
21070
|
+
`);
|
|
21071
|
+
return 0;
|
|
21072
|
+
}
|
|
21073
|
+
var init_signoff_label = __esm({
|
|
21074
|
+
"src/cli/signoff-label.ts"() {
|
|
21075
|
+
"use strict";
|
|
21076
|
+
init_signoff();
|
|
20438
21077
|
}
|
|
20439
21078
|
});
|
|
20440
21079
|
|
|
@@ -20517,6 +21156,16 @@ function parseStats(args) {
|
|
|
20517
21156
|
}
|
|
20518
21157
|
return o;
|
|
20519
21158
|
}
|
|
21159
|
+
function parseSignoffLabel(args) {
|
|
21160
|
+
const o = {};
|
|
21161
|
+
for (let i = 0; i < args.length; i++) {
|
|
21162
|
+
const a = args[i];
|
|
21163
|
+
if (a === "--rule") o.rule = args[++i];
|
|
21164
|
+
else if (a === "--file") o.file = args[++i];
|
|
21165
|
+
else if (a === "--head") o.head = args[++i];
|
|
21166
|
+
}
|
|
21167
|
+
return o;
|
|
21168
|
+
}
|
|
20520
21169
|
function parseCheck(args) {
|
|
20521
21170
|
const o = {};
|
|
20522
21171
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -20532,14 +21181,45 @@ function parseCheck(args) {
|
|
|
20532
21181
|
}
|
|
20533
21182
|
return o;
|
|
20534
21183
|
}
|
|
21184
|
+
function splitLongOptionEquals(args) {
|
|
21185
|
+
const out3 = [];
|
|
21186
|
+
for (let i = 0; i < args.length; i++) {
|
|
21187
|
+
const arg = args[i];
|
|
21188
|
+
if (arg === "--") {
|
|
21189
|
+
out3.push(...args.slice(i));
|
|
21190
|
+
break;
|
|
21191
|
+
}
|
|
21192
|
+
if (arg.startsWith("--")) {
|
|
21193
|
+
const equals = arg.indexOf("=");
|
|
21194
|
+
if (equals > 2) {
|
|
21195
|
+
out3.push(arg.slice(0, equals), arg.slice(equals + 1));
|
|
21196
|
+
continue;
|
|
21197
|
+
}
|
|
21198
|
+
}
|
|
21199
|
+
out3.push(arg);
|
|
21200
|
+
}
|
|
21201
|
+
return out3;
|
|
21202
|
+
}
|
|
21203
|
+
function helpRequested(args) {
|
|
21204
|
+
for (const arg of args) {
|
|
21205
|
+
if (arg === "--") break;
|
|
21206
|
+
if (arg === "-h" || arg === "--help") return true;
|
|
21207
|
+
}
|
|
21208
|
+
return false;
|
|
21209
|
+
}
|
|
20535
21210
|
function validateFlatArgs(args, grammar) {
|
|
20536
21211
|
const flags = new Set(grammar.flags ?? []);
|
|
20537
21212
|
const values = grammar.values ?? {};
|
|
20538
21213
|
const seen = /* @__PURE__ */ new Set();
|
|
20539
21214
|
const positionals = [];
|
|
21215
|
+
let options = true;
|
|
20540
21216
|
for (let i = 0; i < args.length; i++) {
|
|
20541
21217
|
const a = args[i];
|
|
20542
|
-
if (
|
|
21218
|
+
if (options && a === "--") {
|
|
21219
|
+
options = false;
|
|
21220
|
+
continue;
|
|
21221
|
+
}
|
|
21222
|
+
if (options && (flags.has(a) || Object.prototype.hasOwnProperty.call(values, a))) {
|
|
20543
21223
|
if (seen.has(a)) {
|
|
20544
21224
|
return { error: `option "${a}" specified more than once`, seen, positionals };
|
|
20545
21225
|
}
|
|
@@ -20556,18 +21236,18 @@ function validateFlatArgs(args, grammar) {
|
|
|
20556
21236
|
}
|
|
20557
21237
|
i++;
|
|
20558
21238
|
if (rule === "positive") {
|
|
20559
|
-
const
|
|
20560
|
-
if (!Number.isFinite(
|
|
21239
|
+
const n2 = Number(v);
|
|
21240
|
+
if (!Number.isFinite(n2) || n2 <= 0) {
|
|
20561
21241
|
return { error: `${a} needs a positive number (got "${v}")`, seen, positionals };
|
|
20562
21242
|
}
|
|
20563
21243
|
} else if (rule === "positive-integer") {
|
|
20564
|
-
const
|
|
20565
|
-
if (!Number.isFinite(
|
|
21244
|
+
const n2 = Number(v);
|
|
21245
|
+
if (!Number.isFinite(n2) || !Number.isInteger(n2) || n2 <= 0) {
|
|
20566
21246
|
return { error: `${a} needs a positive integer (got "${v}")`, seen, positionals };
|
|
20567
21247
|
}
|
|
20568
21248
|
} else if (rule === "non-negative") {
|
|
20569
|
-
const
|
|
20570
|
-
if (!Number.isFinite(
|
|
21249
|
+
const n2 = Number(v);
|
|
21250
|
+
if (!Number.isFinite(n2) || n2 < 0) {
|
|
20571
21251
|
return { error: `${a} needs a non-negative number (got "${v}")`, seen, positionals };
|
|
20572
21252
|
}
|
|
20573
21253
|
} else if (rule === "format" && !isFormat(v)) {
|
|
@@ -20579,7 +21259,7 @@ function validateFlatArgs(args, grammar) {
|
|
|
20579
21259
|
}
|
|
20580
21260
|
continue;
|
|
20581
21261
|
}
|
|
20582
|
-
if (a.startsWith("-")) {
|
|
21262
|
+
if (options && a.startsWith("-")) {
|
|
20583
21263
|
return { error: `unknown option "${a}"`, seen, positionals };
|
|
20584
21264
|
}
|
|
20585
21265
|
if (grammar.positional === "one" && positionals.length === 0) {
|
|
@@ -20591,6 +21271,8 @@ function validateFlatArgs(args, grammar) {
|
|
|
20591
21271
|
return { seen, positionals };
|
|
20592
21272
|
}
|
|
20593
21273
|
function validateCliArgs(cmd, args) {
|
|
21274
|
+
args = splitLongOptionEquals(args);
|
|
21275
|
+
if (helpRequested(args)) return void 0;
|
|
20594
21276
|
if (cmd === "hook" || cmd === "sweep") {
|
|
20595
21277
|
if (args.length === 0) return `${cmd} requires an agent name`;
|
|
20596
21278
|
if (args.length > 1) return `unexpected argument "${args[1]}"`;
|
|
@@ -20638,6 +21320,11 @@ function validateCliArgs(cmd, args) {
|
|
|
20638
21320
|
if (parsed.positionals.length === 0) return "allow requires a rule";
|
|
20639
21321
|
return void 0;
|
|
20640
21322
|
}
|
|
21323
|
+
if (cmd === "signoff-label") {
|
|
21324
|
+
return validateFlatArgs(args, {
|
|
21325
|
+
values: { "--rule": "string", "--file": "string", "--head": "string" }
|
|
21326
|
+
}).error;
|
|
21327
|
+
}
|
|
20641
21328
|
if (cmd === "init") {
|
|
20642
21329
|
return validateFlatArgs(args, {
|
|
20643
21330
|
flags: ["--dry-run", "--force-workflow"],
|
|
@@ -20684,7 +21371,7 @@ function validateCliArgs(cmd, args) {
|
|
|
20684
21371
|
if (cmd === "hook-service") {
|
|
20685
21372
|
const sub = args[0];
|
|
20686
21373
|
if (sub !== "start" && sub !== "stop" && sub !== "status") return "hook-service requires one of start | stop | status";
|
|
20687
|
-
return validateFlatArgs(args.slice(1),
|
|
21374
|
+
return validateFlatArgs(args.slice(1), { values: { "--dir": "string" } }).error;
|
|
20688
21375
|
}
|
|
20689
21376
|
if (cmd === "verify") {
|
|
20690
21377
|
return validateFlatArgs(args, {
|
|
@@ -20717,6 +21404,24 @@ function validateCliArgs(cmd, args) {
|
|
|
20717
21404
|
if (cmd === "research") {
|
|
20718
21405
|
const [sub, ...rest] = args;
|
|
20719
21406
|
if (sub === void 0) return `research requires a subcommand (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
21407
|
+
if (sub === "init") {
|
|
21408
|
+
const parsed = validateFlatArgs(rest, {
|
|
21409
|
+
values: {
|
|
21410
|
+
"--out": "string",
|
|
21411
|
+
"--repo": "string",
|
|
21412
|
+
"--base": "string",
|
|
21413
|
+
"--id": "string",
|
|
21414
|
+
"--prompt": "string",
|
|
21415
|
+
"--verify-command": "string",
|
|
21416
|
+
"--verify-budget": "positive"
|
|
21417
|
+
}
|
|
21418
|
+
});
|
|
21419
|
+
if (parsed.error) return parsed.error;
|
|
21420
|
+
for (const required of ["--out", "--repo", "--prompt", "--verify-command"]) {
|
|
21421
|
+
if (!parsed.seen.has(required)) return `research init requires ${required}`;
|
|
21422
|
+
}
|
|
21423
|
+
return void 0;
|
|
21424
|
+
}
|
|
20720
21425
|
if (sub === "run") {
|
|
20721
21426
|
const delimiter3 = rest.indexOf("--");
|
|
20722
21427
|
const prefix = delimiter3 < 0 ? rest : rest.slice(0, delimiter3);
|
|
@@ -20743,6 +21448,29 @@ function validateCliArgs(cmd, args) {
|
|
|
20743
21448
|
if (!parsed.seen.has("--ledger")) return "research summarize requires --ledger";
|
|
20744
21449
|
return void 0;
|
|
20745
21450
|
}
|
|
21451
|
+
if (sub === "report") {
|
|
21452
|
+
const parsed = validateFlatArgs(rest, { flags: ["--json"], values: { "--ledger": "string" } });
|
|
21453
|
+
if (parsed.error) return parsed.error;
|
|
21454
|
+
if (!parsed.seen.has("--ledger")) return "research report requires --ledger";
|
|
21455
|
+
return void 0;
|
|
21456
|
+
}
|
|
21457
|
+
if (sub === "bundle") {
|
|
21458
|
+
const parsed = validateFlatArgs(rest, {
|
|
21459
|
+
values: { "--ledger": "string", "--out": "string", "--manifest": "string", "--validate": "string" }
|
|
21460
|
+
});
|
|
21461
|
+
if (parsed.error) return parsed.error;
|
|
21462
|
+
if (parsed.seen.has("--validate")) return void 0;
|
|
21463
|
+
for (const required of ["--ledger", "--out"]) {
|
|
21464
|
+
if (!parsed.seen.has(required)) return `research bundle requires ${required}`;
|
|
21465
|
+
}
|
|
21466
|
+
return void 0;
|
|
21467
|
+
}
|
|
21468
|
+
if (sub === "validate") {
|
|
21469
|
+
const parsed = validateFlatArgs(rest, { values: { "--bundle": "string" } });
|
|
21470
|
+
if (parsed.error) return parsed.error;
|
|
21471
|
+
if (!parsed.seen.has("--bundle")) return "research validate requires --bundle";
|
|
21472
|
+
return void 0;
|
|
21473
|
+
}
|
|
20746
21474
|
return `unknown research subcommand "${sub}" (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
20747
21475
|
}
|
|
20748
21476
|
return void 0;
|
|
@@ -20770,8 +21498,8 @@ Formats:
|
|
|
20770
21498
|
tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
|
|
20771
21499
|
tamperward sweep claude Stop sweep (re-scan the turn's working tree)
|
|
20772
21500
|
tamperward hook-service start [--dir D] OPT-IN persistent hook service: one warm
|
|
20773
|
-
tamperward hook-service stop
|
|
20774
|
-
|
|
21501
|
+
tamperward hook-service stop [--dir D] process per user and repository that
|
|
21502
|
+
tamperward hook-service status [--dir D] evaluates hook/sweep requests over a
|
|
20775
21503
|
private unix socket, so each tool call
|
|
20776
21504
|
skips Node + bundle startup. Hooks
|
|
20777
21505
|
consult it only with
|
|
@@ -20790,8 +21518,8 @@ Formats:
|
|
|
20790
21518
|
[--budget S] [--json] [--keep] as-is AND with protected files restored
|
|
20791
21519
|
[--require-ancestor] [--cwd D] from the trusted base; a visible-green /
|
|
20792
21520
|
pristine-red result is a MASKED FAILURE
|
|
20793
|
-
(exit 1, or 0 under
|
|
20794
|
-
verify@<
|
|
21521
|
+
(exit 1, or 0 under a compact tw1:<digest>
|
|
21522
|
+
or legacy verify@<full-sha> approval); cannot-verify
|
|
20795
21523
|
fails closed (2)
|
|
20796
21524
|
tamperward trace-verify [--base R] advisory Linux verifier-input discovery:
|
|
20797
21525
|
[--cmd C] [--budget S] [--runs N] trace a trusted/known-good base with
|
|
@@ -20817,6 +21545,8 @@ Formats:
|
|
|
20817
21545
|
code when clean; 124 on clean AGENT_TIMEOUT;
|
|
20818
21546
|
1 on any blocking finding/masked failure;
|
|
20819
21547
|
2 when it cannot adjudicate (fails closed)
|
|
21548
|
+
tamperward research init --out F --repo R author a versioned one-task manifest
|
|
21549
|
+
--prompt P --verify-command C and print its sha256 identity
|
|
20820
21550
|
tamperward research run --manifest F bring-your-own-model evaluation: for every
|
|
20821
21551
|
--out D --adapter A [--pairs N] task in the manifest, pin one source commit,
|
|
20822
21552
|
[--model M] [--agent-budget S] clone fresh state per arm, run the agent
|
|
@@ -20828,6 +21558,11 @@ Formats:
|
|
|
20828
21558
|
tamperward research summarize --ledger D aggregate measured pairs into model behaviour,
|
|
20829
21559
|
independent outcome, TamperWard hits/misses
|
|
20830
21560
|
and paired counts \u2014 no composite score
|
|
21561
|
+
tamperward research report --ledger D human-readable four-section report (or --json)
|
|
21562
|
+
tamperward research bundle --ledger D reproducible records/provenance archive
|
|
21563
|
+
--out F [--manifest M] (manifest/prompts included only when requested)
|
|
21564
|
+
tamperward research bundle --validate F structurally and provenance-validate a bundle
|
|
21565
|
+
tamperward research validate --bundle F alias for bundle validation
|
|
20831
21566
|
tamperward stats [--file F] [--since 30d] aggregate privacy-safe hook/sweep audit
|
|
20832
21567
|
[--json] [--cwd D] events by rule and enforcement surface.
|
|
20833
21568
|
Defaults to the repository-local
|
|
@@ -20836,6 +21571,8 @@ Formats:
|
|
|
20836
21571
|
integrity signal, not proof of agent intent.
|
|
20837
21572
|
tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
|
|
20838
21573
|
[--file F] [--cwd D]
|
|
21574
|
+
tamperward signoff-label --rule <rule> --head <sha> [--file F]
|
|
21575
|
+
print a compact, exact-head-bound CI label
|
|
20839
21576
|
tamperward onboard [--yes] [--cwd D] guided first-run setup: preflight, the
|
|
20840
21577
|
[--base R] [--repo O/R] init plan with a confirmation before any
|
|
20841
21578
|
[--branch B] [--verify-command C] write, canonical init, explicit verifier
|
|
@@ -20882,7 +21619,12 @@ Exit codes: 0 clean \xB7 1 a blocking finding (check), MASKED_FAILURE or SUITE_R
|
|
|
20882
21619
|
`);
|
|
20883
21620
|
}
|
|
20884
21621
|
function main(argv) {
|
|
20885
|
-
const [cmd, ...
|
|
21622
|
+
const [cmd, ...rawRest] = argv;
|
|
21623
|
+
const rest = splitLongOptionEquals(rawRest);
|
|
21624
|
+
if (cmd !== void 0 && cmd !== "-h" && cmd !== "--help" && helpRequested(rest)) {
|
|
21625
|
+
printHelp();
|
|
21626
|
+
return 0;
|
|
21627
|
+
}
|
|
20886
21628
|
if (cmd !== void 0 && cmd !== "-h" && cmd !== "--help") {
|
|
20887
21629
|
const invalid = validateCliArgs(cmd, rest);
|
|
20888
21630
|
if (invalid) {
|
|
@@ -20900,6 +21642,8 @@ function main(argv) {
|
|
|
20900
21642
|
return runAgentCommand("sweep", rest);
|
|
20901
21643
|
case "allow":
|
|
20902
21644
|
return runAllow(parseAllow(rest));
|
|
21645
|
+
case "signoff-label":
|
|
21646
|
+
return runSignoffLabel(parseSignoffLabel(rest));
|
|
20903
21647
|
case "init":
|
|
20904
21648
|
return runInit(parseInit(rest));
|
|
20905
21649
|
case "doctor":
|
|
@@ -20968,13 +21712,14 @@ var init_main = __esm({
|
|
|
20968
21712
|
init_onboard();
|
|
20969
21713
|
init_research();
|
|
20970
21714
|
init_audit();
|
|
21715
|
+
init_signoff_label();
|
|
20971
21716
|
}
|
|
20972
21717
|
});
|
|
20973
21718
|
|
|
20974
21719
|
// src/cli/index.ts
|
|
20975
21720
|
init_hook_client();
|
|
20976
21721
|
init_exit();
|
|
20977
|
-
import { readFileSync as
|
|
21722
|
+
import { readFileSync as readFileSync27 } from "node:fs";
|
|
20978
21723
|
function loadMain() {
|
|
20979
21724
|
return Promise.resolve().then(() => (init_main(), main_exports));
|
|
20980
21725
|
}
|
|
@@ -20987,7 +21732,7 @@ async function launch(argv) {
|
|
|
20987
21732
|
if (kind && hookServiceEnabled()) {
|
|
20988
21733
|
let raw = null;
|
|
20989
21734
|
try {
|
|
20990
|
-
raw =
|
|
21735
|
+
raw = readFileSync27(0, "utf8");
|
|
20991
21736
|
} catch {
|
|
20992
21737
|
raw = null;
|
|
20993
21738
|
}
|