tamperward 2.34.0 → 2.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +3026 -255
- package/package.json +1 -1
- package/schemas/runtime-qualification-v1.schema.json +184 -0
- package/schemas/status-v1.schema.json +205 -0
package/dist/cli/index.js
CHANGED
|
@@ -79,6 +79,22 @@ function shippedVersion() {
|
|
|
79
79
|
}
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
|
+
function shippedCommit() {
|
|
83
|
+
try {
|
|
84
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
85
|
+
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
86
|
+
try {
|
|
87
|
+
const pkg = JSON.parse(readFileSync(join(here, rel), "utf8"));
|
|
88
|
+
if (isRecord(pkg) && pkg.name === "tamperward") {
|
|
89
|
+
return typeof pkg.gitHead === "string" && GIT_HEAD_RE.test(pkg.gitHead) ? pkg.gitHead : null;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
82
98
|
function requireShippedVersion() {
|
|
83
99
|
if (RESOLVED_TW_VERSION === null) {
|
|
84
100
|
throw new Error(
|
|
@@ -137,7 +153,7 @@ function resolvesToClaudeSettings(path, env = process.env) {
|
|
|
137
153
|
if (isClaudeSettings(path, env)) return true;
|
|
138
154
|
return isAbsolute(path) && isClaudeSettings(canonicalPath(path), env);
|
|
139
155
|
}
|
|
140
|
-
var PLAIN_SEMVER, RESOLVED_TW_VERSION, TW_VERSION, PRE_TOOLS, PRE_MATCHER, MARKER, NPX_AUTHORITY, AUTHORITY_FAIL_CLOSED, HOOK_CMD, SWEEP_CMD, PRECOMMIT_CMD, OURS, INIT_SCRIPT, CLAUDE_SETTINGS;
|
|
156
|
+
var PLAIN_SEMVER, RESOLVED_TW_VERSION, TW_VERSION, GIT_HEAD_RE, TW_COMMIT, PRE_TOOLS, PRE_MATCHER, MARKER, NPX_AUTHORITY, AUTHORITY_FAIL_CLOSED, HOOK_CMD, SWEEP_CMD, PRECOMMIT_CMD, OURS, INIT_SCRIPT, CLAUDE_SETTINGS;
|
|
141
157
|
var init_wiring = __esm({
|
|
142
158
|
"src/wiring.ts"() {
|
|
143
159
|
"use strict";
|
|
@@ -145,6 +161,8 @@ var init_wiring = __esm({
|
|
|
145
161
|
PLAIN_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
146
162
|
RESOLVED_TW_VERSION = shippedVersion();
|
|
147
163
|
TW_VERSION = RESOLVED_TW_VERSION ?? "0.0.0-unresolved";
|
|
164
|
+
GIT_HEAD_RE = /^[0-9a-f]{7,40}$/i;
|
|
165
|
+
TW_COMMIT = shippedCommit();
|
|
148
166
|
PRE_TOOLS = ["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
149
167
|
PRE_MATCHER = PRE_TOOLS.join("|");
|
|
150
168
|
MARKER = "# tamperward: block agent shortcuts before they land";
|
|
@@ -211,7 +229,7 @@ function forwardedEnv(env) {
|
|
|
211
229
|
return out3;
|
|
212
230
|
}
|
|
213
231
|
function exchange(paths, req2, timeoutMs) {
|
|
214
|
-
return new Promise((
|
|
232
|
+
return new Promise((resolve23) => {
|
|
215
233
|
let done = false;
|
|
216
234
|
let handedOff = false;
|
|
217
235
|
let accepted = false;
|
|
@@ -222,7 +240,7 @@ function exchange(paths, req2, timeoutMs) {
|
|
|
222
240
|
done = true;
|
|
223
241
|
clearTimeout(timer);
|
|
224
242
|
sock.destroy();
|
|
225
|
-
|
|
243
|
+
resolve23(value);
|
|
226
244
|
};
|
|
227
245
|
const sock = createConnection(paths.socket);
|
|
228
246
|
const timer = setTimeout(() => finish(handedOff ? acceptedFailure("timeout") : null), timeoutMs);
|
|
@@ -5175,8 +5193,8 @@ function numericOf(e) {
|
|
|
5175
5193
|
}
|
|
5176
5194
|
return void 0;
|
|
5177
5195
|
}
|
|
5178
|
-
function metricsOf(expr,
|
|
5179
|
-
const obj =
|
|
5196
|
+
function metricsOf(expr, resolve23) {
|
|
5197
|
+
const obj = resolve23(expr);
|
|
5180
5198
|
if (!ts.isObjectLiteralExpression(obj)) return void 0;
|
|
5181
5199
|
const out3 = { values: {}, opaque: false };
|
|
5182
5200
|
let any = false;
|
|
@@ -5190,7 +5208,7 @@ function metricsOf(expr, resolve20) {
|
|
|
5190
5208
|
const k = keyName(p.name);
|
|
5191
5209
|
if (k === null || !isMetric(k)) continue;
|
|
5192
5210
|
any = true;
|
|
5193
|
-
const n2 = numericOf(
|
|
5211
|
+
const n2 = numericOf(resolve23(p.initializer));
|
|
5194
5212
|
if (n2 === void 0) out3.opaque = true;
|
|
5195
5213
|
else out3.values[k] = n2;
|
|
5196
5214
|
}
|
|
@@ -5237,17 +5255,17 @@ function parseThresholds(src) {
|
|
|
5237
5255
|
try {
|
|
5238
5256
|
const sf = sourceOf(src);
|
|
5239
5257
|
if (!sf) return res;
|
|
5240
|
-
const
|
|
5258
|
+
const resolve23 = resolverFor(sf);
|
|
5241
5259
|
const visit = (node) => {
|
|
5242
5260
|
if (ts.isPropertyAssignment(node)) {
|
|
5243
5261
|
const key2 = keyName(node.name);
|
|
5244
5262
|
if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
|
|
5245
5263
|
res.present = true;
|
|
5246
|
-
const init =
|
|
5264
|
+
const init = resolve23(node.initializer);
|
|
5247
5265
|
if (!ts.isObjectLiteralExpression(init)) {
|
|
5248
5266
|
res.opaque = true;
|
|
5249
5267
|
} else {
|
|
5250
|
-
const flat = metricsOf(init,
|
|
5268
|
+
const flat = metricsOf(init, resolve23);
|
|
5251
5269
|
if (flat) res.global = merge(res.global, flat);
|
|
5252
5270
|
for (const p of init.properties) {
|
|
5253
5271
|
if (ts.isSpreadAssignment(p)) {
|
|
@@ -5261,7 +5279,7 @@ function parseThresholds(src) {
|
|
|
5261
5279
|
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
5262
5280
|
continue;
|
|
5263
5281
|
}
|
|
5264
|
-
const m = metricsOf(p.initializer,
|
|
5282
|
+
const m = metricsOf(p.initializer, resolve23);
|
|
5265
5283
|
if (!m) continue;
|
|
5266
5284
|
if (k === "global") res.global = merge(res.global, m);
|
|
5267
5285
|
else res.paths.set(norm(k), m);
|
|
@@ -5295,18 +5313,18 @@ function parseLists(src) {
|
|
|
5295
5313
|
try {
|
|
5296
5314
|
const sf = sourceOf(src);
|
|
5297
5315
|
if (!sf) return res;
|
|
5298
|
-
const
|
|
5316
|
+
const resolve23 = resolverFor(sf);
|
|
5299
5317
|
const visit = (node) => {
|
|
5300
5318
|
if (ts.isPropertyAssignment(node)) {
|
|
5301
5319
|
const k = keyName(node.name);
|
|
5302
5320
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
5303
5321
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
5304
|
-
const { items, opaque } = literals(
|
|
5322
|
+
const { items, opaque } = literals(resolve23(node.initializer));
|
|
5305
5323
|
res.collectFrom = items;
|
|
5306
5324
|
res.collectKey = k === "include" ? "coverage.include" : k;
|
|
5307
5325
|
if (opaque) res.opaqueCollect = true;
|
|
5308
5326
|
} else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
|
|
5309
|
-
const { items, opaque } = literals(
|
|
5327
|
+
const { items, opaque } = literals(resolve23(node.initializer));
|
|
5310
5328
|
(k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
|
|
5311
5329
|
if (opaque) res.opaqueIgnore = true;
|
|
5312
5330
|
}
|
|
@@ -6976,10 +6994,10 @@ function lex(src) {
|
|
|
6976
6994
|
i = j + 1;
|
|
6977
6995
|
continue;
|
|
6978
6996
|
}
|
|
6979
|
-
const
|
|
6980
|
-
if (
|
|
6981
|
-
out3.push({ t: "num", v:
|
|
6982
|
-
i +=
|
|
6997
|
+
const num3 = src.slice(i).match(/^-?\d+(?:\.\d+)?/);
|
|
6998
|
+
if (num3) {
|
|
6999
|
+
out3.push({ t: "num", v: num3[0] });
|
|
7000
|
+
i += num3[0].length;
|
|
6983
7001
|
continue;
|
|
6984
7002
|
}
|
|
6985
7003
|
const op = src.slice(i).match(/^(?:==|!=|&&|\|\||<=|>=|[!()<>,[\]])/);
|
|
@@ -7004,13 +7022,13 @@ function foldConst(src) {
|
|
|
7004
7022
|
let p = 0;
|
|
7005
7023
|
const peek = () => toks[p];
|
|
7006
7024
|
const eat = (v2) => toks[p]?.t === "op" && toks[p].v === v2 ? (p++, true) : false;
|
|
7007
|
-
const
|
|
7025
|
+
const num3 = (v2) => v2 === TRUTHY ? NaN : typeof v2 === "number" ? v2 : typeof v2 === "boolean" ? v2 ? 1 : 0 : v2 === null ? 0 : v2.trim() === "" ? 0 : Number(v2);
|
|
7008
7026
|
const eq = (a, b) => {
|
|
7009
7027
|
if (a === TRUTHY || b === TRUTHY) return void 0;
|
|
7010
7028
|
if (typeof a === "string" && typeof b === "string") return a.toLowerCase() === b.toLowerCase();
|
|
7011
7029
|
if (a === null && b === null) return true;
|
|
7012
|
-
const x =
|
|
7013
|
-
const y2 =
|
|
7030
|
+
const x = num3(a);
|
|
7031
|
+
const y2 = num3(b);
|
|
7014
7032
|
return !Number.isNaN(x) && x === y2;
|
|
7015
7033
|
};
|
|
7016
7034
|
const or = () => {
|
|
@@ -7048,8 +7066,8 @@ function foldConst(src) {
|
|
|
7048
7066
|
const e = eq(l, r);
|
|
7049
7067
|
l = e === void 0 ? void 0 : t.v === "==" ? e : !e;
|
|
7050
7068
|
} else {
|
|
7051
|
-
const x =
|
|
7052
|
-
const y2 =
|
|
7069
|
+
const x = num3(l);
|
|
7070
|
+
const y2 = num3(r);
|
|
7053
7071
|
if (Number.isNaN(x) || Number.isNaN(y2)) l = void 0;
|
|
7054
7072
|
else l = t.v === "<" ? x < y2 : t.v === ">" ? x > y2 : t.v === "<=" ? x <= y2 : x >= y2;
|
|
7055
7073
|
}
|
|
@@ -10872,8 +10890,8 @@ import { dirname as dirname2, join as join9 } from "node:path";
|
|
|
10872
10890
|
function compactOobToken(want, head) {
|
|
10873
10891
|
const normalizedHead = head.trim().toLowerCase();
|
|
10874
10892
|
if (!want || !FULL_OBJECT_ID.test(normalizedHead)) return null;
|
|
10875
|
-
const
|
|
10876
|
-
return `${COMPACT_OOB_PREFIX}${
|
|
10893
|
+
const digest2 = createHash3("sha256").update(`tamperward:oob:v1\0${want}\0${normalizedHead}`).digest("base64url");
|
|
10894
|
+
return `${COMPACT_OOB_PREFIX}${digest2}`;
|
|
10877
10895
|
}
|
|
10878
10896
|
function ledgerEntryFrom(value) {
|
|
10879
10897
|
if (!isRecord(value)) return null;
|
|
@@ -12308,10 +12326,10 @@ function parseAuditJsonl(raw) {
|
|
|
12308
12326
|
return events;
|
|
12309
12327
|
}
|
|
12310
12328
|
function parseSince(value, nowMs = Date.now()) {
|
|
12311
|
-
const
|
|
12312
|
-
if (
|
|
12313
|
-
const count = Number(
|
|
12314
|
-
const unit =
|
|
12329
|
+
const relative13 = /^(\d+)(m|h|d)$/.exec(value);
|
|
12330
|
+
if (relative13) {
|
|
12331
|
+
const count = Number(relative13[1]);
|
|
12332
|
+
const unit = relative13[2] === "m" ? 6e4 : relative13[2] === "h" ? 36e5 : 864e5;
|
|
12315
12333
|
if (!Number.isSafeInteger(count) || count <= 0) throw new Error(`invalid --since value "${value}"`);
|
|
12316
12334
|
return nowMs - count * unit;
|
|
12317
12335
|
}
|
|
@@ -13019,12 +13037,12 @@ function pidAlive2(pid) {
|
|
|
13019
13037
|
}
|
|
13020
13038
|
}
|
|
13021
13039
|
function listening(socket) {
|
|
13022
|
-
return new Promise((
|
|
13040
|
+
return new Promise((resolve23) => {
|
|
13023
13041
|
const sock = createConnection2(socket);
|
|
13024
13042
|
const done = (v) => {
|
|
13025
13043
|
clearTimeout(timer);
|
|
13026
13044
|
sock.destroy();
|
|
13027
|
-
|
|
13045
|
+
resolve23(v);
|
|
13028
13046
|
};
|
|
13029
13047
|
const timer = setTimeout(() => done(false), 1e3);
|
|
13030
13048
|
sock.once("connect", () => done(true));
|
|
@@ -13203,11 +13221,11 @@ async function startHookService(opts) {
|
|
|
13203
13221
|
});
|
|
13204
13222
|
const umask = process.umask(63);
|
|
13205
13223
|
try {
|
|
13206
|
-
await new Promise((
|
|
13224
|
+
await new Promise((resolve23, reject) => {
|
|
13207
13225
|
server.once("error", reject);
|
|
13208
13226
|
server.listen(paths.socket, () => {
|
|
13209
13227
|
server.off("error", reject);
|
|
13210
|
-
|
|
13228
|
+
resolve23();
|
|
13211
13229
|
});
|
|
13212
13230
|
});
|
|
13213
13231
|
} finally {
|
|
@@ -13224,8 +13242,8 @@ async function startHookService(opts) {
|
|
|
13224
13242
|
get served() {
|
|
13225
13243
|
return served;
|
|
13226
13244
|
},
|
|
13227
|
-
close: () => new Promise((
|
|
13228
|
-
if (closed) return
|
|
13245
|
+
close: () => new Promise((resolve23) => {
|
|
13246
|
+
if (closed) return resolve23();
|
|
13229
13247
|
closed = true;
|
|
13230
13248
|
setSnapshotCache(null);
|
|
13231
13249
|
let settled = false;
|
|
@@ -13236,7 +13254,7 @@ async function startHookService(opts) {
|
|
|
13236
13254
|
clearTimeout(drain);
|
|
13237
13255
|
removeQuietly(paths.socket);
|
|
13238
13256
|
removeQuietly(paths.state);
|
|
13239
|
-
|
|
13257
|
+
resolve23();
|
|
13240
13258
|
};
|
|
13241
13259
|
server.close(finalize);
|
|
13242
13260
|
for (const sock of sockets) if (sock !== acceptedSocket) sock.destroy();
|
|
@@ -14883,8 +14901,8 @@ function parseCapturedSupervisorResult(stdoutText) {
|
|
|
14883
14901
|
};
|
|
14884
14902
|
}
|
|
14885
14903
|
function runCapturedProcessSync(executable, args, opts) {
|
|
14886
|
-
const
|
|
14887
|
-
const configFile = join17(
|
|
14904
|
+
const stateDir2 = mkdtempSync2(join17(tmpdir3(), "tw-suite-capture-"));
|
|
14905
|
+
const configFile = join17(stateDir2, "config.json");
|
|
14888
14906
|
const backstop = opts.backstopMs ?? 3e4;
|
|
14889
14907
|
try {
|
|
14890
14908
|
writeFileSync8(
|
|
@@ -14932,7 +14950,7 @@ function runCapturedProcessSync(executable, args, opts) {
|
|
|
14932
14950
|
}
|
|
14933
14951
|
return parsed;
|
|
14934
14952
|
} finally {
|
|
14935
|
-
rmSync6(
|
|
14953
|
+
rmSync6(stateDir2, { recursive: true, force: true });
|
|
14936
14954
|
}
|
|
14937
14955
|
}
|
|
14938
14956
|
var DIAGNOSTIC_TAIL_BYTES, EMPTY_STREAM, CLEANUP_HELPERS_SRC, CAPTURE_SUPERVISOR;
|
|
@@ -15651,12 +15669,375 @@ var init_verifier_backend = __esm({
|
|
|
15651
15669
|
}
|
|
15652
15670
|
});
|
|
15653
15671
|
|
|
15654
|
-
// src/
|
|
15655
|
-
import { execFileSync as execFileSync8
|
|
15672
|
+
// src/verification-state.ts
|
|
15673
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
15656
15674
|
import { createHash as createHash12 } from "node:crypto";
|
|
15657
|
-
import {
|
|
15675
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
15676
|
+
import { dirname as dirname8, join as join19 } from "node:path";
|
|
15677
|
+
function git2(args, cwd) {
|
|
15678
|
+
return execFileSync8("git", args, {
|
|
15679
|
+
cwd,
|
|
15680
|
+
encoding: "utf8",
|
|
15681
|
+
maxBuffer: 1 << 28,
|
|
15682
|
+
env: trustedGitEnv()
|
|
15683
|
+
});
|
|
15684
|
+
}
|
|
15685
|
+
function resolveBaseRev(baseRef, cwd) {
|
|
15686
|
+
assertRev(baseRef);
|
|
15687
|
+
const rev2 = git2(["rev-parse", "--verify", `${baseRef}^{commit}`], cwd).trim();
|
|
15688
|
+
try {
|
|
15689
|
+
return git2(["merge-base", rev2, "HEAD"], cwd).trim();
|
|
15690
|
+
} catch {
|
|
15691
|
+
return rev2;
|
|
15692
|
+
}
|
|
15693
|
+
}
|
|
15694
|
+
function canonicalJson(value) {
|
|
15695
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
15696
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
15697
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
15698
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
15699
|
+
}
|
|
15700
|
+
function digest(value) {
|
|
15701
|
+
return createHash12("sha256").update(canonicalJson(value)).digest("hex");
|
|
15702
|
+
}
|
|
15703
|
+
function surfacePaths(base, cwd, policy) {
|
|
15704
|
+
let listing;
|
|
15705
|
+
try {
|
|
15706
|
+
listing = git2(["ls-tree", "-r", "--name-only", "-z", base], cwd);
|
|
15707
|
+
} catch {
|
|
15708
|
+
return [];
|
|
15709
|
+
}
|
|
15710
|
+
const inputs = policy.verify?.inputs ?? [];
|
|
15711
|
+
return listing.split("\0").filter(Boolean).filter((p) => isProtected(p, policy) || matchesAny(p, inputs)).sort();
|
|
15712
|
+
}
|
|
15713
|
+
function interventionWiring(cwd) {
|
|
15714
|
+
const dir = claudeConfigDir();
|
|
15715
|
+
const sources = [
|
|
15716
|
+
["repo", join19(cwd, ".claude", "settings.json")],
|
|
15717
|
+
["repo.local", join19(cwd, ".claude", "settings.local.json")],
|
|
15718
|
+
["user", join19(dir, "settings.json")],
|
|
15719
|
+
["user.local", join19(dir, "settings.local.json")]
|
|
15720
|
+
];
|
|
15721
|
+
return sources.map(([source, path]) => {
|
|
15722
|
+
let parsed;
|
|
15723
|
+
try {
|
|
15724
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
15725
|
+
} catch {
|
|
15726
|
+
return { source, present: false };
|
|
15727
|
+
}
|
|
15728
|
+
if (!isRecord4(parsed)) return { source, present: true, shape: "non-object" };
|
|
15729
|
+
return {
|
|
15730
|
+
source,
|
|
15731
|
+
present: true,
|
|
15732
|
+
hooks: parsed.hooks ?? null,
|
|
15733
|
+
disableAllHooks: parsed.disableAllHooks ?? null
|
|
15734
|
+
};
|
|
15735
|
+
});
|
|
15736
|
+
}
|
|
15737
|
+
function dependenciesBinding(root, policy, command) {
|
|
15738
|
+
const backend = policy.verify?.backend ?? "local";
|
|
15739
|
+
if (backend === "container") {
|
|
15740
|
+
return digest({ backend, image: policy.verify?.image ?? null });
|
|
15741
|
+
}
|
|
15742
|
+
const dep = discoverDependencyEnvironment(root, command);
|
|
15743
|
+
return digest({ backend, status: dep.status, fingerprint: dep.fingerprint ?? null });
|
|
15744
|
+
}
|
|
15745
|
+
function computeBinding(cwd, inputs, precomputed) {
|
|
15746
|
+
const root = repoRoot(cwd);
|
|
15747
|
+
const head = git2(["rev-parse", "--verify", "HEAD^{commit}"], root).trim();
|
|
15748
|
+
const base = resolveBaseRev(inputs.base_ref, root);
|
|
15749
|
+
const policy = inputs.explicit_base ? loadPolicyAt(base, root) ?? defaultPolicy() : loadPolicy(root);
|
|
15750
|
+
const command = inputs.command_source === "flag" ? inputs.command : policy.verify?.command ?? "";
|
|
15751
|
+
if (!command) {
|
|
15752
|
+
throw new Error("no verifier command is configured for the recorded verification");
|
|
15753
|
+
}
|
|
15754
|
+
const budget = inputs.budget_source === "flag" ? inputs.budget : policy.verify?.budget ?? 300;
|
|
15755
|
+
const protectedIgnored = (rel) => isProtected(rel, policy);
|
|
15756
|
+
return {
|
|
15757
|
+
// NOTE (#500/#600 follow-up): `tree` hashes every tracked and ignored-protected
|
|
15758
|
+
// file and `dependencies` walks the attested dependency roots, on EVERY call.
|
|
15759
|
+
// Right for `verify` (once per run) and reused there via `precomputed.tree`,
|
|
15760
|
+
// but `status` is meant to be polled continuously by a status bar/dashboard,
|
|
15761
|
+
// where a large repository makes each poll a full read. A stat-cache identity
|
|
15762
|
+
// (git's index plus hashing only what changed) is a deliberate follow-up, not
|
|
15763
|
+
// built here.
|
|
15764
|
+
tree: precomputed?.tree ?? treeFingerprint(root, protectedIgnored),
|
|
15765
|
+
head,
|
|
15766
|
+
base,
|
|
15767
|
+
policy: digest(policy),
|
|
15768
|
+
verifier: digest({
|
|
15769
|
+
command,
|
|
15770
|
+
budget,
|
|
15771
|
+
inputs: policy.verify?.inputs ?? [],
|
|
15772
|
+
backend: policy.verify?.backend ?? "local",
|
|
15773
|
+
image: policy.verify?.image ?? null
|
|
15774
|
+
}),
|
|
15775
|
+
surface: digest(surfacePaths(base, root, policy)),
|
|
15776
|
+
intervention: digest(interventionWiring(root)),
|
|
15777
|
+
dependencies: dependenciesBinding(root, policy, command)
|
|
15778
|
+
};
|
|
15779
|
+
}
|
|
15780
|
+
function stateDir(cwd) {
|
|
15781
|
+
const ctx = repoContext(cwd);
|
|
15782
|
+
return ctx ? join19(ctx.gitDir, "tamperward") : null;
|
|
15783
|
+
}
|
|
15784
|
+
function verificationRecordPath(cwd) {
|
|
15785
|
+
const dir = stateDir(cwd);
|
|
15786
|
+
return dir ? join19(dir, "verification-state.json") : null;
|
|
15787
|
+
}
|
|
15788
|
+
function verifyingMarkerPath(cwd) {
|
|
15789
|
+
const dir = stateDir(cwd);
|
|
15790
|
+
return dir ? join19(dir, "verifying.json") : null;
|
|
15791
|
+
}
|
|
15792
|
+
function recordVerification(cwd, inputs, treeFingerprintValue) {
|
|
15793
|
+
const path = verificationRecordPath(cwd);
|
|
15794
|
+
if (!path) return false;
|
|
15795
|
+
const binding = computeBinding(
|
|
15796
|
+
repoRoot(cwd),
|
|
15797
|
+
inputs,
|
|
15798
|
+
treeFingerprintValue ? { tree: treeFingerprintValue } : void 0
|
|
15799
|
+
);
|
|
15800
|
+
const record = {
|
|
15801
|
+
schema_version: VERIFICATION_STATE_SCHEMA_VERSION,
|
|
15802
|
+
verdict: "VERIFIED",
|
|
15803
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15804
|
+
tw_version: TW_VERSION,
|
|
15805
|
+
inputs,
|
|
15806
|
+
binding
|
|
15807
|
+
};
|
|
15808
|
+
mkdirSync8(dirname8(path), { recursive: true });
|
|
15809
|
+
writeFileSync9(path, JSON.stringify(record) + "\n");
|
|
15810
|
+
return true;
|
|
15811
|
+
}
|
|
15812
|
+
function invalidateVerificationRecordIfCurrent(cwd) {
|
|
15813
|
+
const record = readVerificationRecord(cwd);
|
|
15814
|
+
if (!record) return false;
|
|
15815
|
+
let live;
|
|
15816
|
+
try {
|
|
15817
|
+
live = computeBinding(cwd, record.inputs);
|
|
15818
|
+
} catch {
|
|
15819
|
+
return false;
|
|
15820
|
+
}
|
|
15821
|
+
if (firstMismatch(record.binding, live)) return false;
|
|
15822
|
+
const path = verificationRecordPath(cwd);
|
|
15823
|
+
if (!path) return false;
|
|
15824
|
+
try {
|
|
15825
|
+
rmSync7(path, { force: true });
|
|
15826
|
+
return true;
|
|
15827
|
+
} catch {
|
|
15828
|
+
return false;
|
|
15829
|
+
}
|
|
15830
|
+
}
|
|
15831
|
+
function isRecord4(v) {
|
|
15832
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15833
|
+
}
|
|
15834
|
+
function isBinding(v) {
|
|
15835
|
+
if (!isRecord4(v)) return false;
|
|
15836
|
+
return BINDING_INPUTS.every((k) => {
|
|
15837
|
+
const val = v[k];
|
|
15838
|
+
return typeof val === "string" && val.length > 0;
|
|
15839
|
+
});
|
|
15840
|
+
}
|
|
15841
|
+
function parseVerificationRecord(value) {
|
|
15842
|
+
if (!isRecord4(value)) return null;
|
|
15843
|
+
if (value.schema_version !== VERIFICATION_STATE_SCHEMA_VERSION) return null;
|
|
15844
|
+
if (value.verdict !== "VERIFIED") return null;
|
|
15845
|
+
if (typeof value.verified_at !== "string") return null;
|
|
15846
|
+
if (typeof value.tw_version !== "string") return null;
|
|
15847
|
+
const inputs = value.inputs;
|
|
15848
|
+
if (!isRecord4(inputs)) return null;
|
|
15849
|
+
if (typeof inputs.base_ref !== "string" || inputs.base_ref.length === 0) return null;
|
|
15850
|
+
if (typeof inputs.explicit_base !== "boolean") return null;
|
|
15851
|
+
if (inputs.command_source !== "policy" && inputs.command_source !== "flag") return null;
|
|
15852
|
+
if (typeof inputs.command !== "string") return null;
|
|
15853
|
+
if (inputs.budget_source !== "policy" && inputs.budget_source !== "flag") return null;
|
|
15854
|
+
if (typeof inputs.budget !== "number" || !Number.isFinite(inputs.budget)) return null;
|
|
15855
|
+
if (!isBinding(value.binding)) return null;
|
|
15856
|
+
return {
|
|
15857
|
+
schema_version: VERIFICATION_STATE_SCHEMA_VERSION,
|
|
15858
|
+
verdict: "VERIFIED",
|
|
15859
|
+
verified_at: value.verified_at,
|
|
15860
|
+
tw_version: value.tw_version,
|
|
15861
|
+
inputs: {
|
|
15862
|
+
base_ref: inputs.base_ref,
|
|
15863
|
+
explicit_base: inputs.explicit_base,
|
|
15864
|
+
command_source: inputs.command_source,
|
|
15865
|
+
command: inputs.command,
|
|
15866
|
+
budget_source: inputs.budget_source,
|
|
15867
|
+
budget: inputs.budget
|
|
15868
|
+
},
|
|
15869
|
+
binding: value.binding
|
|
15870
|
+
};
|
|
15871
|
+
}
|
|
15872
|
+
function readVerificationRecord(cwd) {
|
|
15873
|
+
const path = verificationRecordPath(cwd);
|
|
15874
|
+
if (!path) return null;
|
|
15875
|
+
let raw;
|
|
15876
|
+
try {
|
|
15877
|
+
raw = readFileSync16(path, "utf8");
|
|
15878
|
+
} catch {
|
|
15879
|
+
return null;
|
|
15880
|
+
}
|
|
15881
|
+
let parsed;
|
|
15882
|
+
try {
|
|
15883
|
+
parsed = JSON.parse(raw);
|
|
15884
|
+
} catch {
|
|
15885
|
+
return null;
|
|
15886
|
+
}
|
|
15887
|
+
return parseVerificationRecord(parsed);
|
|
15888
|
+
}
|
|
15889
|
+
function pidAlive3(pid) {
|
|
15890
|
+
try {
|
|
15891
|
+
process.kill(pid, 0);
|
|
15892
|
+
return true;
|
|
15893
|
+
} catch (e) {
|
|
15894
|
+
return isRecord4(e) && e.code === "EPERM";
|
|
15895
|
+
}
|
|
15896
|
+
}
|
|
15897
|
+
function beginVerifying(cwd) {
|
|
15898
|
+
const path = verifyingMarkerPath(cwd);
|
|
15899
|
+
if (!path) return false;
|
|
15900
|
+
try {
|
|
15901
|
+
const marker = {
|
|
15902
|
+
schema_version: VERIFICATION_STATE_SCHEMA_VERSION,
|
|
15903
|
+
pid: process.pid,
|
|
15904
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15905
|
+
};
|
|
15906
|
+
mkdirSync8(dirname8(path), { recursive: true });
|
|
15907
|
+
writeFileSync9(path, JSON.stringify(marker) + "\n");
|
|
15908
|
+
return true;
|
|
15909
|
+
} catch {
|
|
15910
|
+
return false;
|
|
15911
|
+
}
|
|
15912
|
+
}
|
|
15913
|
+
function endVerifying(cwd) {
|
|
15914
|
+
const path = verifyingMarkerPath(cwd);
|
|
15915
|
+
if (!path) return;
|
|
15916
|
+
try {
|
|
15917
|
+
rmSync7(path, { force: true });
|
|
15918
|
+
} catch {
|
|
15919
|
+
}
|
|
15920
|
+
}
|
|
15921
|
+
function readVerifyingMarker(cwd) {
|
|
15922
|
+
const path = verifyingMarkerPath(cwd);
|
|
15923
|
+
if (!path) return null;
|
|
15924
|
+
let parsed;
|
|
15925
|
+
try {
|
|
15926
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
15927
|
+
} catch {
|
|
15928
|
+
return null;
|
|
15929
|
+
}
|
|
15930
|
+
if (!isRecord4(parsed)) return null;
|
|
15931
|
+
if (parsed.schema_version !== VERIFICATION_STATE_SCHEMA_VERSION) return null;
|
|
15932
|
+
if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
15933
|
+
if (typeof parsed.started_at !== "string") return null;
|
|
15934
|
+
if (!pidAlive3(parsed.pid)) return null;
|
|
15935
|
+
return { schema_version: VERIFICATION_STATE_SCHEMA_VERSION, pid: parsed.pid, started_at: parsed.started_at };
|
|
15936
|
+
}
|
|
15937
|
+
function firstMismatch(recorded, live) {
|
|
15938
|
+
for (const input of MISMATCH_PRIORITY) {
|
|
15939
|
+
if (recorded[input] !== live[input]) return input;
|
|
15940
|
+
}
|
|
15941
|
+
return null;
|
|
15942
|
+
}
|
|
15943
|
+
function evaluateVerificationState(cwd) {
|
|
15944
|
+
if (readVerifyingMarker(cwd)) {
|
|
15945
|
+
return { state: "VERIFYING", reason: "a verification is in progress" };
|
|
15946
|
+
}
|
|
15947
|
+
const record = readVerificationRecord(cwd);
|
|
15948
|
+
if (!record) {
|
|
15949
|
+
return {
|
|
15950
|
+
state: "UNVERIFIED",
|
|
15951
|
+
reason: "no successful verification has been recorded for this repository"
|
|
15952
|
+
};
|
|
15953
|
+
}
|
|
15954
|
+
let live;
|
|
15955
|
+
try {
|
|
15956
|
+
live = computeBinding(cwd, record.inputs);
|
|
15957
|
+
} catch (e) {
|
|
15958
|
+
return {
|
|
15959
|
+
state: "BROKEN",
|
|
15960
|
+
reason: "verification authority wiring is invalid or unavailable",
|
|
15961
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
15962
|
+
verified_at: record.verified_at,
|
|
15963
|
+
verifier_command: record.inputs.command,
|
|
15964
|
+
record
|
|
15965
|
+
};
|
|
15966
|
+
}
|
|
15967
|
+
const changed = firstMismatch(record.binding, live);
|
|
15968
|
+
if (!changed) {
|
|
15969
|
+
return {
|
|
15970
|
+
state: "CURRENT",
|
|
15971
|
+
verified_at: record.verified_at,
|
|
15972
|
+
base: live.base,
|
|
15973
|
+
head: live.head,
|
|
15974
|
+
verifier_command: record.inputs.command,
|
|
15975
|
+
record
|
|
15976
|
+
};
|
|
15977
|
+
}
|
|
15978
|
+
return {
|
|
15979
|
+
state: "STALE",
|
|
15980
|
+
changed_input: changed,
|
|
15981
|
+
reason: STALE_REASON[changed],
|
|
15982
|
+
verified_at: record.verified_at,
|
|
15983
|
+
base: live.base,
|
|
15984
|
+
head: live.head,
|
|
15985
|
+
verifier_command: record.inputs.command,
|
|
15986
|
+
record
|
|
15987
|
+
};
|
|
15988
|
+
}
|
|
15989
|
+
var VERIFICATION_STATE_SCHEMA_VERSION, BINDING_INPUTS, STALE_REASON, MISMATCH_PRIORITY;
|
|
15990
|
+
var init_verification_state = __esm({
|
|
15991
|
+
"src/verification-state.ts"() {
|
|
15992
|
+
"use strict";
|
|
15993
|
+
init_build();
|
|
15994
|
+
init_trusted();
|
|
15995
|
+
init_fingerprint();
|
|
15996
|
+
init_policy();
|
|
15997
|
+
init_policy_load();
|
|
15998
|
+
init_repo_context();
|
|
15999
|
+
init_wiring();
|
|
16000
|
+
init_dependency_env();
|
|
16001
|
+
VERIFICATION_STATE_SCHEMA_VERSION = 1;
|
|
16002
|
+
BINDING_INPUTS = [
|
|
16003
|
+
"tree",
|
|
16004
|
+
"head",
|
|
16005
|
+
"base",
|
|
16006
|
+
"policy",
|
|
16007
|
+
"verifier",
|
|
16008
|
+
"surface",
|
|
16009
|
+
"intervention",
|
|
16010
|
+
"dependencies"
|
|
16011
|
+
];
|
|
16012
|
+
STALE_REASON = {
|
|
16013
|
+
tree: "candidate tree changed since verification",
|
|
16014
|
+
head: "HEAD commit changed since verification",
|
|
16015
|
+
base: "trusted base commit changed since verification",
|
|
16016
|
+
policy: "TamperWard policy changed since verification",
|
|
16017
|
+
verifier: "verifier configuration changed since verification",
|
|
16018
|
+
surface: "protected verification surface changed since verification",
|
|
16019
|
+
intervention: "runtime steering wiring changed since verification",
|
|
16020
|
+
dependencies: "dependency environment changed since verification"
|
|
16021
|
+
};
|
|
16022
|
+
MISMATCH_PRIORITY = [
|
|
16023
|
+
"head",
|
|
16024
|
+
"base",
|
|
16025
|
+
"verifier",
|
|
16026
|
+
"surface",
|
|
16027
|
+
"intervention",
|
|
16028
|
+
"dependencies",
|
|
16029
|
+
"policy",
|
|
16030
|
+
"tree"
|
|
16031
|
+
];
|
|
16032
|
+
}
|
|
16033
|
+
});
|
|
16034
|
+
|
|
16035
|
+
// src/cli/verify.ts
|
|
16036
|
+
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
16037
|
+
import { createHash as createHash13 } from "node:crypto";
|
|
16038
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, readlinkSync as readlinkSync3, rmSync as rmSync8, symlinkSync, writeFileSync as writeFileSync10, existsSync as existsSync14 } from "node:fs";
|
|
15658
16039
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
15659
|
-
import { dirname as
|
|
16040
|
+
import { dirname as dirname9, isAbsolute as isAbsolute11, join as join20, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
|
|
15660
16041
|
function oracleAssuranceReport() {
|
|
15661
16042
|
return {
|
|
15662
16043
|
level: "suite-exit-only",
|
|
@@ -15665,14 +16046,14 @@ function oracleAssuranceReport() {
|
|
|
15665
16046
|
limitation: "candidate source executes inside the configured suite process and may terminate or interpose on that in-process oracle; isolated-container is execution-domain isolation, not semantic/oracle isolation"
|
|
15666
16047
|
};
|
|
15667
16048
|
}
|
|
15668
|
-
function
|
|
15669
|
-
return
|
|
16049
|
+
function git3(args, cwd) {
|
|
16050
|
+
return execFileSync9("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
15670
16051
|
}
|
|
15671
16052
|
function resolveBase(base, cwd) {
|
|
15672
16053
|
assertRev(base);
|
|
15673
|
-
const rev2 =
|
|
16054
|
+
const rev2 = git3(["rev-parse", "--verify", `${base}^{commit}`], cwd).trim();
|
|
15674
16055
|
try {
|
|
15675
|
-
return
|
|
16056
|
+
return git3(["merge-base", rev2, "HEAD"], cwd).trim();
|
|
15676
16057
|
} catch {
|
|
15677
16058
|
return rev2;
|
|
15678
16059
|
}
|
|
@@ -15720,15 +16101,15 @@ function rootedLinkTarget(target) {
|
|
|
15720
16101
|
return isAbsolute11(target) || driveRelativeLinkTarget(target);
|
|
15721
16102
|
}
|
|
15722
16103
|
function safeSymlink(target, out3, root, label) {
|
|
15723
|
-
if (rootedLinkTarget(target) || !inside2(root, resolve9(
|
|
16104
|
+
if (rootedLinkTarget(target) || !inside2(root, resolve9(dirname9(out3), target))) {
|
|
15724
16105
|
throw linkEscape(label, target);
|
|
15725
16106
|
}
|
|
15726
|
-
|
|
16107
|
+
rmSync8(out3, { force: true });
|
|
15727
16108
|
symlinkSync(target, out3);
|
|
15728
16109
|
}
|
|
15729
16110
|
function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
15730
16111
|
let domain = "tree";
|
|
15731
|
-
let stack = linkParts(relative6(root,
|
|
16112
|
+
let stack = linkParts(relative6(root, dirname9(out3)));
|
|
15732
16113
|
const pending = linkParts(target);
|
|
15733
16114
|
const seen = /* @__PURE__ */ new Set();
|
|
15734
16115
|
let hops = 0;
|
|
@@ -15745,7 +16126,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
15745
16126
|
continue;
|
|
15746
16127
|
}
|
|
15747
16128
|
const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
|
|
15748
|
-
const current =
|
|
16129
|
+
const current = join20(base, ...stack);
|
|
15749
16130
|
let st;
|
|
15750
16131
|
try {
|
|
15751
16132
|
st = lstatSync9(current);
|
|
@@ -15787,7 +16168,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
15787
16168
|
const parts = rel.split("/").slice(0, -1);
|
|
15788
16169
|
let current = cwd;
|
|
15789
16170
|
for (const part of parts) {
|
|
15790
|
-
current =
|
|
16171
|
+
current = join20(current, part);
|
|
15791
16172
|
let st;
|
|
15792
16173
|
try {
|
|
15793
16174
|
st = lstatSync9(current);
|
|
@@ -15812,7 +16193,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
15812
16193
|
}
|
|
15813
16194
|
const links = [];
|
|
15814
16195
|
for (const rel of listed) {
|
|
15815
|
-
const src =
|
|
16196
|
+
const src = join20(cwd, rel);
|
|
15816
16197
|
rejectLinkedParent(cwd, rel);
|
|
15817
16198
|
let st;
|
|
15818
16199
|
try {
|
|
@@ -15820,8 +16201,8 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
15820
16201
|
} catch {
|
|
15821
16202
|
continue;
|
|
15822
16203
|
}
|
|
15823
|
-
const out3 =
|
|
15824
|
-
|
|
16204
|
+
const out3 = join20(dest, rel);
|
|
16205
|
+
mkdirSync9(dirname9(out3), { recursive: true });
|
|
15825
16206
|
dropSymlink(out3);
|
|
15826
16207
|
if (st.isSymbolicLink()) {
|
|
15827
16208
|
const target = readlinkSync3(src);
|
|
@@ -15840,19 +16221,19 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
15840
16221
|
for (const link of links) {
|
|
15841
16222
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
15842
16223
|
}
|
|
15843
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
16224
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join20(dest, "node_modules"), "dir");
|
|
15844
16225
|
}
|
|
15845
16226
|
function workingTreePaths(cwd) {
|
|
15846
|
-
return
|
|
16227
|
+
return git3(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
15847
16228
|
}
|
|
15848
16229
|
function probeFilesystemCaseSensitivity(dir) {
|
|
15849
|
-
const upper =
|
|
15850
|
-
const lower =
|
|
16230
|
+
const upper = join20(dir, ".tw-case-probe-A");
|
|
16231
|
+
const lower = join20(dir, ".tw-case-probe-a");
|
|
15851
16232
|
try {
|
|
15852
|
-
|
|
16233
|
+
writeFileSync10(upper, "", { flag: "wx" });
|
|
15853
16234
|
return !existsSync14(lower);
|
|
15854
16235
|
} finally {
|
|
15855
|
-
|
|
16236
|
+
rmSync8(upper, { force: true });
|
|
15856
16237
|
}
|
|
15857
16238
|
}
|
|
15858
16239
|
function foldCase(p) {
|
|
@@ -15870,7 +16251,7 @@ function caseFoldCollision(paths) {
|
|
|
15870
16251
|
}
|
|
15871
16252
|
function dropSymlink(p) {
|
|
15872
16253
|
try {
|
|
15873
|
-
if (lstatSync9(p).isSymbolicLink())
|
|
16254
|
+
if (lstatSync9(p).isSymbolicLink()) rmSync8(p, { force: true });
|
|
15874
16255
|
} catch {
|
|
15875
16256
|
}
|
|
15876
16257
|
}
|
|
@@ -15899,22 +16280,22 @@ function verifierCoveredInputs(cmd, atBase, policy) {
|
|
|
15899
16280
|
return covered;
|
|
15900
16281
|
}
|
|
15901
16282
|
function baseEntries(base, cwd) {
|
|
15902
|
-
return
|
|
16283
|
+
return git3(["ls-tree", "-r", "-z", base], cwd).split("\0").filter(Boolean).map((rec) => {
|
|
15903
16284
|
const tab = rec.indexOf(" ");
|
|
15904
16285
|
const [mode, type] = rec.slice(0, tab).split(" ");
|
|
15905
16286
|
return { mode, type, path: rec.slice(tab + 1) };
|
|
15906
16287
|
});
|
|
15907
16288
|
}
|
|
15908
16289
|
function overlayDigest(dest, paths) {
|
|
15909
|
-
const h =
|
|
16290
|
+
const h = createHash13("sha256");
|
|
15910
16291
|
for (const rel of [...paths].sort()) {
|
|
15911
16292
|
h.update(rel);
|
|
15912
16293
|
h.update("\0");
|
|
15913
|
-
const p =
|
|
16294
|
+
const p = join20(dest, rel);
|
|
15914
16295
|
try {
|
|
15915
16296
|
const st = lstatSync9(p);
|
|
15916
16297
|
h.update(String(st.mode));
|
|
15917
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
16298
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync17(p));
|
|
15918
16299
|
} catch {
|
|
15919
16300
|
h.update("<gone>");
|
|
15920
16301
|
}
|
|
@@ -15943,16 +16324,16 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
15943
16324
|
if (e.type === "commit") {
|
|
15944
16325
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
15945
16326
|
}
|
|
15946
|
-
const content =
|
|
15947
|
-
const out3 =
|
|
15948
|
-
|
|
15949
|
-
|
|
16327
|
+
const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
16328
|
+
const out3 = join20(dest, e.path);
|
|
16329
|
+
mkdirSync9(dirname9(out3), { recursive: true });
|
|
16330
|
+
rmSync8(out3, { force: true });
|
|
15950
16331
|
if (e.mode === "120000") {
|
|
15951
16332
|
const target = content.toString("utf8");
|
|
15952
16333
|
safeSymlink(target, out3, dest, `the base's ${e.path}`);
|
|
15953
16334
|
restoredLinks.push({ path: e.path, out: out3, target });
|
|
15954
16335
|
} else {
|
|
15955
|
-
|
|
16336
|
+
writeFileSync10(out3, content);
|
|
15956
16337
|
chmodSync2(out3, parseInt(e.mode.slice(-4), 8) & 511);
|
|
15957
16338
|
}
|
|
15958
16339
|
restored.push(e.path);
|
|
@@ -15961,7 +16342,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
15961
16342
|
const inCopy = workingTreePaths(cwd);
|
|
15962
16343
|
for (const rel of inCopy) {
|
|
15963
16344
|
if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
|
|
15964
|
-
|
|
16345
|
+
rmSync8(join20(dest, rel), { force: true });
|
|
15965
16346
|
removed++;
|
|
15966
16347
|
}
|
|
15967
16348
|
for (const link of restoredLinks) {
|
|
@@ -15975,9 +16356,9 @@ function suiteEnv(scratch) {
|
|
|
15975
16356
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
15976
16357
|
env[k] = v;
|
|
15977
16358
|
}
|
|
15978
|
-
const userRc =
|
|
15979
|
-
const globalRc =
|
|
15980
|
-
for (const f of [userRc, globalRc])
|
|
16359
|
+
const userRc = join20(scratch, "npmrc-user");
|
|
16360
|
+
const globalRc = join20(scratch, "npmrc-global");
|
|
16361
|
+
for (const f of [userRc, globalRc]) writeFileSync10(f, "", { mode: 292 });
|
|
15981
16362
|
env.npm_config_userconfig = userRc;
|
|
15982
16363
|
env.npm_config_globalconfig = globalRc;
|
|
15983
16364
|
env.npm_config_node_options = " ";
|
|
@@ -16008,7 +16389,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
16008
16389
|
reason: `checkpointed-local verifier is unsupported on ${process.platform}; no trusted local shell contract is defined`
|
|
16009
16390
|
};
|
|
16010
16391
|
}
|
|
16011
|
-
const scratch = mkdtempSync3(
|
|
16392
|
+
const scratch = mkdtempSync3(join20(tmpdir4(), "tw-verify-run-"));
|
|
16012
16393
|
try {
|
|
16013
16394
|
const r = runCapturedProcessSync(shell.executable, shell.args, {
|
|
16014
16395
|
cwd: dir,
|
|
@@ -16053,7 +16434,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
16053
16434
|
diagnostics: r.diagnostics
|
|
16054
16435
|
};
|
|
16055
16436
|
} finally {
|
|
16056
|
-
|
|
16437
|
+
rmSync8(scratch, { recursive: true, force: true });
|
|
16057
16438
|
}
|
|
16058
16439
|
}
|
|
16059
16440
|
function diagnosticsJson(diagnostics, includeTail) {
|
|
@@ -16098,6 +16479,15 @@ function verifyVerdictLine(verdict2, ctx) {
|
|
|
16098
16479
|
}
|
|
16099
16480
|
}
|
|
16100
16481
|
function runVerify(opts) {
|
|
16482
|
+
const markerCwd = repoRoot(opts.cwd ?? process.cwd());
|
|
16483
|
+
const marked = opts.silent ? false : beginVerifying(markerCwd);
|
|
16484
|
+
try {
|
|
16485
|
+
return runVerifyImpl(opts);
|
|
16486
|
+
} finally {
|
|
16487
|
+
if (marked) endVerifying(markerCwd);
|
|
16488
|
+
}
|
|
16489
|
+
}
|
|
16490
|
+
function runVerifyImpl(opts) {
|
|
16101
16491
|
const cwd = repoRoot(opts.cwd ?? process.cwd());
|
|
16102
16492
|
const colour = opts.silent ? false : colourEnabled(process.env, process.stdout);
|
|
16103
16493
|
const paintVerify = (s) => {
|
|
@@ -16137,7 +16527,7 @@ function runVerify(opts) {
|
|
|
16137
16527
|
return cannotVerify("POLICY_ERROR", `cannot load policy (${e instanceof Error ? e.message : String(e)})`);
|
|
16138
16528
|
}
|
|
16139
16529
|
if (opts.requireAncestor) {
|
|
16140
|
-
const requested =
|
|
16530
|
+
const requested = git3(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
|
|
16141
16531
|
if (!baseIsAncestorOfHead(requested, cwd)) {
|
|
16142
16532
|
return cannotVerify(
|
|
16143
16533
|
"BASE_NOT_ANCESTOR",
|
|
@@ -16245,17 +16635,17 @@ function runVerify(opts) {
|
|
|
16245
16635
|
} catch {
|
|
16246
16636
|
return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
|
|
16247
16637
|
}
|
|
16248
|
-
const visRoot = mkdtempSync3(
|
|
16249
|
-
const visDir =
|
|
16638
|
+
const visRoot = mkdtempSync3(join20(tmpdir4(), "tw-verify-vis-"));
|
|
16639
|
+
const visDir = join20(visRoot, "t");
|
|
16250
16640
|
const cleanup = (dirs) => {
|
|
16251
16641
|
if (opts.keep) return;
|
|
16252
|
-
for (const d of dirs)
|
|
16642
|
+
for (const d of dirs) rmSync8(d, { recursive: true, force: true });
|
|
16253
16643
|
};
|
|
16254
16644
|
filesystemCaseSensitive = (opts.probeCaseSensitivity ?? probeFilesystemCaseSensitivity)(visRoot);
|
|
16255
16645
|
if (!filesystemCaseSensitive) {
|
|
16256
16646
|
const present = (rel) => {
|
|
16257
16647
|
try {
|
|
16258
|
-
lstatSync9(
|
|
16648
|
+
lstatSync9(join20(cwd, rel));
|
|
16259
16649
|
return true;
|
|
16260
16650
|
} catch {
|
|
16261
16651
|
return false;
|
|
@@ -16287,7 +16677,7 @@ function runVerify(opts) {
|
|
|
16287
16677
|
);
|
|
16288
16678
|
}
|
|
16289
16679
|
try {
|
|
16290
|
-
|
|
16680
|
+
mkdirSync9(visDir);
|
|
16291
16681
|
materialize(cwd, visDir, frozenNodeModules);
|
|
16292
16682
|
} catch (e) {
|
|
16293
16683
|
cleanup([visRoot]);
|
|
@@ -16388,12 +16778,12 @@ function runVerify(opts) {
|
|
|
16388
16778
|
{ stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
|
|
16389
16779
|
);
|
|
16390
16780
|
}
|
|
16391
|
-
const priRoot = mkdtempSync3(
|
|
16392
|
-
const priDir =
|
|
16781
|
+
const priRoot = mkdtempSync3(join20(tmpdir4(), "tw-verify-pri-"));
|
|
16782
|
+
const priDir = join20(priRoot, "t");
|
|
16393
16783
|
let restored = [];
|
|
16394
16784
|
let removedAdded = 0;
|
|
16395
16785
|
try {
|
|
16396
|
-
|
|
16786
|
+
mkdirSync9(priDir);
|
|
16397
16787
|
materialize(cwd, priDir, frozenNodeModules);
|
|
16398
16788
|
({ restored, removed: removedAdded } = overlayPristine(
|
|
16399
16789
|
cwd,
|
|
@@ -16479,6 +16869,25 @@ function runVerify(opts) {
|
|
|
16479
16869
|
code2 = 1;
|
|
16480
16870
|
}
|
|
16481
16871
|
opts.onVerdict?.({ verdict: verdict2 });
|
|
16872
|
+
if (verdict2 === "VERIFIED") {
|
|
16873
|
+
try {
|
|
16874
|
+
const inputs = {
|
|
16875
|
+
base_ref: opts.base ?? "HEAD",
|
|
16876
|
+
explicit_base: opts.base !== void 0,
|
|
16877
|
+
command_source: opts.cmd !== void 0 ? "flag" : "policy",
|
|
16878
|
+
command: cmd,
|
|
16879
|
+
budget_source: opts.budget !== void 0 ? "flag" : "policy",
|
|
16880
|
+
budget
|
|
16881
|
+
};
|
|
16882
|
+
recordVerification(cwd, inputs, treeBefore);
|
|
16883
|
+
} catch {
|
|
16884
|
+
}
|
|
16885
|
+
} else {
|
|
16886
|
+
try {
|
|
16887
|
+
invalidateVerificationRecordIfCurrent(cwd);
|
|
16888
|
+
} catch {
|
|
16889
|
+
}
|
|
16890
|
+
}
|
|
16482
16891
|
const signedOff = verdict2 === "MASKED_FAILURE" ? oobToken("verify", oobFromEnv(), oobHeadFromEnv()) : null;
|
|
16483
16892
|
if (signedOff) code2 = 0;
|
|
16484
16893
|
if (opts.json) {
|
|
@@ -16584,6 +16993,7 @@ var init_verify = __esm({
|
|
|
16584
16993
|
init_verifier_backend();
|
|
16585
16994
|
init_machine_output();
|
|
16586
16995
|
init_signoff();
|
|
16996
|
+
init_verification_state();
|
|
16587
16997
|
init_suite_diagnostics();
|
|
16588
16998
|
init_text();
|
|
16589
16999
|
init_status();
|
|
@@ -16680,11 +17090,11 @@ var init_verify = __esm({
|
|
|
16680
17090
|
});
|
|
16681
17091
|
|
|
16682
17092
|
// src/cli/run.ts
|
|
16683
|
-
import { execFileSync as
|
|
17093
|
+
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
16684
17094
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
16685
|
-
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as
|
|
17095
|
+
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync18, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync9, statSync as statSync5, writeFileSync as writeFileSync11 } from "node:fs";
|
|
16686
17096
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
16687
|
-
import { dirname as
|
|
17097
|
+
import { dirname as dirname10, join as join21, resolve as resolve10 } from "node:path";
|
|
16688
17098
|
function authoritativeRunLifecyclePlatform(platform = process.platform) {
|
|
16689
17099
|
return platform === "linux";
|
|
16690
17100
|
}
|
|
@@ -16721,7 +17131,7 @@ function writableByCaller2(path) {
|
|
|
16721
17131
|
return true;
|
|
16722
17132
|
} catch {
|
|
16723
17133
|
}
|
|
16724
|
-
const parent =
|
|
17134
|
+
const parent = dirname10(cur);
|
|
16725
17135
|
if (parent === cur) break;
|
|
16726
17136
|
cur = parent;
|
|
16727
17137
|
}
|
|
@@ -16760,9 +17170,9 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
|
|
|
16760
17170
|
};
|
|
16761
17171
|
}
|
|
16762
17172
|
function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
|
|
16763
|
-
const
|
|
16764
|
-
const resultFile =
|
|
16765
|
-
const agentEnvFile =
|
|
17173
|
+
const stateDir2 = mkdtempSync4(join21(tmpdir5(), "tw-agent-supervisor-"));
|
|
17174
|
+
const resultFile = join21(stateDir2, "result.json");
|
|
17175
|
+
const agentEnvFile = join21(stateDir2, "agent-env.json");
|
|
16766
17176
|
try {
|
|
16767
17177
|
const linux = process.platform === "linux";
|
|
16768
17178
|
let executable;
|
|
@@ -16779,7 +17189,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
16779
17189
|
failure: trustedPython.reason ?? "trusted Linux python3 interpreter is unavailable"
|
|
16780
17190
|
};
|
|
16781
17191
|
}
|
|
16782
|
-
|
|
17192
|
+
writeFileSync11(agentEnvFile, JSON.stringify(process.env), { mode: 384 });
|
|
16783
17193
|
executable = trustedPython.path;
|
|
16784
17194
|
args = [
|
|
16785
17195
|
"-I",
|
|
@@ -16836,7 +17246,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
16836
17246
|
}
|
|
16837
17247
|
let state = {};
|
|
16838
17248
|
try {
|
|
16839
|
-
state = JSON.parse(
|
|
17249
|
+
state = JSON.parse(readFileSync18(resultFile, "utf8"));
|
|
16840
17250
|
} catch {
|
|
16841
17251
|
return {
|
|
16842
17252
|
exit: 1,
|
|
@@ -16861,13 +17271,13 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
16861
17271
|
...state.failure ? { failure: state.failure } : {}
|
|
16862
17272
|
};
|
|
16863
17273
|
} finally {
|
|
16864
|
-
|
|
17274
|
+
rmSync9(stateDir2, { recursive: true, force: true });
|
|
16865
17275
|
}
|
|
16866
17276
|
}
|
|
16867
17277
|
function waitMs(ms) {
|
|
16868
17278
|
Atomics.wait(waitCell, 0, 0, ms);
|
|
16869
17279
|
}
|
|
16870
|
-
function
|
|
17280
|
+
function pidAlive4(pid) {
|
|
16871
17281
|
if (!pid || !Number.isInteger(pid) || pid <= 0) return false;
|
|
16872
17282
|
try {
|
|
16873
17283
|
process.kill(pid, 0);
|
|
@@ -16877,10 +17287,10 @@ function pidAlive3(pid) {
|
|
|
16877
17287
|
}
|
|
16878
17288
|
}
|
|
16879
17289
|
function observerExited(pid) {
|
|
16880
|
-
if (!
|
|
17290
|
+
if (!pidAlive4(pid)) return true;
|
|
16881
17291
|
if (process.platform !== "linux") return false;
|
|
16882
17292
|
try {
|
|
16883
|
-
const stat =
|
|
17293
|
+
const stat = readFileSync18(`/proc/${pid}/stat`, "utf8");
|
|
16884
17294
|
const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0];
|
|
16885
17295
|
return state === "Z" || state === "X";
|
|
16886
17296
|
} catch (e) {
|
|
@@ -16888,8 +17298,8 @@ function observerExited(pid) {
|
|
|
16888
17298
|
}
|
|
16889
17299
|
}
|
|
16890
17300
|
function supervisedObserverLog(cwd) {
|
|
16891
|
-
const gd = gitDir(cwd) ??
|
|
16892
|
-
return
|
|
17301
|
+
const gd = gitDir(cwd) ?? join21(cwd, ".git");
|
|
17302
|
+
return join21(
|
|
16893
17303
|
gd,
|
|
16894
17304
|
"tamperward",
|
|
16895
17305
|
`run-observer-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}.jsonl`
|
|
@@ -16997,12 +17407,12 @@ function finishObserverAdvisory(observer, write = out) {
|
|
|
16997
17407
|
const telemetry = stopObserverProcess(observer);
|
|
16998
17408
|
observerSummary(observer, telemetry, write);
|
|
16999
17409
|
}
|
|
17000
|
-
function
|
|
17001
|
-
return
|
|
17410
|
+
function git4(args, cwd) {
|
|
17411
|
+
return execFileSync10("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
17002
17412
|
}
|
|
17003
17413
|
function nowTicks() {
|
|
17004
17414
|
try {
|
|
17005
|
-
return Math.floor(parseFloat(
|
|
17415
|
+
return Math.floor(parseFloat(readFileSync18("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
17006
17416
|
} catch {
|
|
17007
17417
|
return Number.POSITIVE_INFINITY;
|
|
17008
17418
|
}
|
|
@@ -17025,7 +17435,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
17025
17435
|
const n2 = Number(pid);
|
|
17026
17436
|
if (n2 === process.pid) continue;
|
|
17027
17437
|
try {
|
|
17028
|
-
const stat =
|
|
17438
|
+
const stat = readFileSync18(`/proc/${pid}/stat`, "utf8");
|
|
17029
17439
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
17030
17440
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
17031
17441
|
} catch {
|
|
@@ -17069,7 +17479,7 @@ function runEnvelope(opts) {
|
|
|
17069
17479
|
let base;
|
|
17070
17480
|
try {
|
|
17071
17481
|
const requestedBase = assertRev(opts.base ?? "HEAD");
|
|
17072
|
-
base =
|
|
17482
|
+
base = git4(["rev-parse", "--verify", `${requestedBase}^{commit}`], cwd).trim();
|
|
17073
17483
|
} catch {
|
|
17074
17484
|
err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
|
|
17075
17485
|
return 2;
|
|
@@ -17091,7 +17501,7 @@ function runEnvelope(opts) {
|
|
|
17091
17501
|
return 2;
|
|
17092
17502
|
}
|
|
17093
17503
|
if (!opts.allowDirty) {
|
|
17094
|
-
const dirty =
|
|
17504
|
+
const dirty = git4(["status", "--porcelain"], cwd).trim();
|
|
17095
17505
|
if (dirty) {
|
|
17096
17506
|
err("tamperward run: working tree is dirty before the agent starts \u2014 the envelope cannot");
|
|
17097
17507
|
err("attribute changes. Commit or stash first, or pass --allow-dirty to own the risk.");
|
|
@@ -17196,7 +17606,7 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
|
|
|
17196
17606
|
emitRunJson("OBJECT_REWRITE", 1);
|
|
17197
17607
|
return 1;
|
|
17198
17608
|
}
|
|
17199
|
-
const head =
|
|
17609
|
+
const head = git4(["rev-parse", "HEAD"], cwd).trim();
|
|
17200
17610
|
const isAncestor = spawnSync4("git", ["merge-base", "--is-ancestor", base, head], { cwd, env: trustedGitEnv() });
|
|
17201
17611
|
if (isAncestor.status !== 0) {
|
|
17202
17612
|
err(`tamperward run: HEAD ${head.slice(0, 10)} is not a descendant of the trusted base ${base.slice(0, 10)} \u2014`);
|
|
@@ -17597,9 +18007,9 @@ raise SystemExit(0)
|
|
|
17597
18007
|
});
|
|
17598
18008
|
|
|
17599
18009
|
// src/cli/doctor.ts
|
|
17600
|
-
import { execFileSync as
|
|
17601
|
-
import { existsSync as existsSync15, readFileSync as
|
|
17602
|
-
import { join as
|
|
18010
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
18011
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19, readdirSync as readdirSync5 } from "node:fs";
|
|
18012
|
+
import { join as join22, resolve as resolve11 } from "node:path";
|
|
17603
18013
|
function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
|
|
17604
18014
|
if (platform === "linux") {
|
|
17605
18015
|
if (linuxPython?.path) {
|
|
@@ -17657,7 +18067,7 @@ function pinsInFile(path) {
|
|
|
17657
18067
|
if (!existsSync15(path)) return [];
|
|
17658
18068
|
let src = "";
|
|
17659
18069
|
try {
|
|
17660
|
-
src =
|
|
18070
|
+
src = readFileSync19(path, "utf8");
|
|
17661
18071
|
} catch {
|
|
17662
18072
|
return [];
|
|
17663
18073
|
}
|
|
@@ -17684,7 +18094,7 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
|
|
|
17684
18094
|
}
|
|
17685
18095
|
let doc;
|
|
17686
18096
|
try {
|
|
17687
|
-
doc = yaml.parse(
|
|
18097
|
+
doc = yaml.parse(readFileSync19(path, "utf8"));
|
|
17688
18098
|
} catch (e) {
|
|
17689
18099
|
broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
|
|
17690
18100
|
continue;
|
|
@@ -17797,9 +18207,9 @@ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow =
|
|
|
17797
18207
|
addWiring("codeowners", "codeowners", true);
|
|
17798
18208
|
checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
|
|
17799
18209
|
const pins = [
|
|
17800
|
-
...pinsInFile(
|
|
17801
|
-
...pinsInFile(
|
|
17802
|
-
...pinsInFile(
|
|
18210
|
+
...pinsInFile(join22(cwd, ".claude", "settings.json")),
|
|
18211
|
+
...pinsInFile(join22(cwd, ".git", "hooks", "pre-commit")),
|
|
18212
|
+
...pinsInFile(join22(cwd, ".husky", "pre-commit"))
|
|
17803
18213
|
];
|
|
17804
18214
|
const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
|
|
17805
18215
|
const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
|
|
@@ -17917,7 +18327,7 @@ function evaluateGitHubProtection(snapshot2, requiredCheck = "tamperward", requi
|
|
|
17917
18327
|
}
|
|
17918
18328
|
function gitText(cwd, args) {
|
|
17919
18329
|
try {
|
|
17920
|
-
return
|
|
18330
|
+
return execFileSync11("git", args, {
|
|
17921
18331
|
cwd,
|
|
17922
18332
|
encoding: "utf8",
|
|
17923
18333
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -17951,7 +18361,7 @@ function githubApi(cwd, endpoint) {
|
|
|
17951
18361
|
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? "";
|
|
17952
18362
|
const invocation = githubApiInvocation(endpoint, token);
|
|
17953
18363
|
try {
|
|
17954
|
-
const stdout =
|
|
18364
|
+
const stdout = execFileSync11(
|
|
17955
18365
|
invocation.executable,
|
|
17956
18366
|
invocation.args,
|
|
17957
18367
|
{
|
|
@@ -18095,7 +18505,7 @@ function diagnose(opts = {}) {
|
|
|
18095
18505
|
`${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
|
|
18096
18506
|
);
|
|
18097
18507
|
}
|
|
18098
|
-
for (const name of entries) workflowRels.push(
|
|
18508
|
+
for (const name of entries) workflowRels.push(join22(workflowDirRel, name));
|
|
18099
18509
|
}
|
|
18100
18510
|
const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
|
|
18101
18511
|
const requiredMinutes = Math.ceil(requiredSecs / 60);
|
|
@@ -18108,7 +18518,7 @@ function diagnose(opts = {}) {
|
|
|
18108
18518
|
}
|
|
18109
18519
|
let doc;
|
|
18110
18520
|
try {
|
|
18111
|
-
doc = yaml.parse(
|
|
18521
|
+
doc = yaml.parse(readFileSync19(workflowPath, "utf8"));
|
|
18112
18522
|
} catch (e) {
|
|
18113
18523
|
return fail(
|
|
18114
18524
|
"ci-verifier",
|
|
@@ -18282,19 +18692,153 @@ var init_doctor = __esm({
|
|
|
18282
18692
|
}
|
|
18283
18693
|
});
|
|
18284
18694
|
|
|
18695
|
+
// src/cli/status.ts
|
|
18696
|
+
function authorityFromChecks(checks) {
|
|
18697
|
+
if (checks.some((c) => c.state === "BROKEN")) return "BROKEN";
|
|
18698
|
+
if (checks.some((c) => c.state === "WARN")) return "PARTIAL";
|
|
18699
|
+
return "ACTIVE";
|
|
18700
|
+
}
|
|
18701
|
+
function interventionFromCheck(check2) {
|
|
18702
|
+
if (!check2) return { state: "UNKNOWN", detail: "agent hook wiring could not be evaluated" };
|
|
18703
|
+
const map = {
|
|
18704
|
+
OK: "ACTIVE",
|
|
18705
|
+
WARN: "PARTIAL",
|
|
18706
|
+
BROKEN: "INACTIVE"
|
|
18707
|
+
};
|
|
18708
|
+
return { state: map[check2.state], detail: check2.detail };
|
|
18709
|
+
}
|
|
18710
|
+
function buildStatusModel(cwd) {
|
|
18711
|
+
let authority;
|
|
18712
|
+
let intervention;
|
|
18713
|
+
try {
|
|
18714
|
+
const outcome = diagnose({ cwd });
|
|
18715
|
+
authority = { state: authorityFromChecks(outcome.checks) };
|
|
18716
|
+
const summary = outcome.checks.filter((c) => c.state !== "OK").map((c) => `${c.id}: ${c.state}`);
|
|
18717
|
+
if (summary.length) authority.detail = summary.join("; ");
|
|
18718
|
+
intervention = interventionFromCheck(outcome.checks.find((c) => c.id === "claude-hooks"));
|
|
18719
|
+
} catch (e) {
|
|
18720
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
18721
|
+
authority = { state: "UNKNOWN", detail };
|
|
18722
|
+
intervention = { state: "UNKNOWN", detail };
|
|
18723
|
+
}
|
|
18724
|
+
const verification = evaluateVerificationState(cwd);
|
|
18725
|
+
const agent = intervention.state === "ACTIVE" || intervention.state === "PARTIAL" ? "claude-code" : "none";
|
|
18726
|
+
return { authority, intervention, verification, runtime: { agent } };
|
|
18727
|
+
}
|
|
18728
|
+
function shortId(id) {
|
|
18729
|
+
return id ? `${id.slice(0, 10)}...` : void 0;
|
|
18730
|
+
}
|
|
18731
|
+
function renderStatus(model, colour) {
|
|
18732
|
+
const v = model.verification;
|
|
18733
|
+
const lines = ["TamperWard", ""];
|
|
18734
|
+
lines.push(statusLine(AUTHORITY_SEVERITY[model.authority.state], "Authority", model.authority.state, colour));
|
|
18735
|
+
lines.push(statusLine(INTERVENTION_SEVERITY[model.intervention.state], "Intervention", model.intervention.state, colour));
|
|
18736
|
+
lines.push(statusLine(VERIFICATION_SEVERITY[v.state], "Verification", v.state, colour));
|
|
18737
|
+
const rows = [];
|
|
18738
|
+
const record = v.record;
|
|
18739
|
+
if (record) {
|
|
18740
|
+
const verifiedTree = shortId(record.binding.tree);
|
|
18741
|
+
if (verifiedTree) rows.push(["Verified tree", verifiedTree]);
|
|
18742
|
+
const baseShort = shortId(v.base ?? record.binding.base);
|
|
18743
|
+
if (baseShort) rows.push(["Base", `${record.inputs.base_ref}@${baseShort}`]);
|
|
18744
|
+
if (v.verifier_command) rows.push(["Verifier", v.verifier_command]);
|
|
18745
|
+
}
|
|
18746
|
+
rows.push(["Runtime", model.runtime.agent]);
|
|
18747
|
+
if (v.state === "STALE" && v.reason) rows.push(["Reason", v.reason]);
|
|
18748
|
+
else if (v.state === "BROKEN") rows.push(["Reason", v.detail ?? v.reason ?? "authority wiring is invalid"]);
|
|
18749
|
+
else if (v.state === "VERIFYING") rows.push(["Reason", v.reason ?? "a verification is in progress"]);
|
|
18750
|
+
else if (v.state === "UNVERIFIED") rows.push(["Next", "run `tamperward verify` to establish a verified baseline"]);
|
|
18751
|
+
if (rows.length) {
|
|
18752
|
+
lines.push("");
|
|
18753
|
+
const width = Math.max(...rows.map(([k]) => k.length));
|
|
18754
|
+
for (const [k, val] of rows) lines.push(`${k.padEnd(width)} ${val}`);
|
|
18755
|
+
}
|
|
18756
|
+
return lines.join("\n") + "\n";
|
|
18757
|
+
}
|
|
18758
|
+
function statusDocument(model) {
|
|
18759
|
+
const v = model.verification;
|
|
18760
|
+
const verification = { state: v.state };
|
|
18761
|
+
if (v.reason) verification.reason = v.reason;
|
|
18762
|
+
if (v.changed_input) verification.changed_input = v.changed_input;
|
|
18763
|
+
if (v.detail) verification.detail = v.detail;
|
|
18764
|
+
if (v.verified_at) verification.verified_at = v.verified_at;
|
|
18765
|
+
if (v.base) verification.base = v.base;
|
|
18766
|
+
if (v.head) verification.head = v.head;
|
|
18767
|
+
if (v.verifier_command) verification.verifier_command = v.verifier_command;
|
|
18768
|
+
if (v.record) {
|
|
18769
|
+
verification.tree = v.record.binding.tree;
|
|
18770
|
+
verification.binding = { ...v.record.binding };
|
|
18771
|
+
}
|
|
18772
|
+
return machineOutput({
|
|
18773
|
+
command: "status",
|
|
18774
|
+
authority: model.authority,
|
|
18775
|
+
intervention: model.intervention,
|
|
18776
|
+
verification,
|
|
18777
|
+
runtime: model.runtime
|
|
18778
|
+
});
|
|
18779
|
+
}
|
|
18780
|
+
function runStatus(opts = {}) {
|
|
18781
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
18782
|
+
if (!repoContext(cwd)) {
|
|
18783
|
+
const why = outsideRepository(cwd) ?? "cwd is not inside a repository the gate can read";
|
|
18784
|
+
process.stderr.write(`tamperward: status needs a git repository (${why})
|
|
18785
|
+
`);
|
|
18786
|
+
return 2;
|
|
18787
|
+
}
|
|
18788
|
+
const model = buildStatusModel(cwd);
|
|
18789
|
+
if (opts.json) {
|
|
18790
|
+
process.stdout.write(JSON.stringify(statusDocument(model)) + "\n");
|
|
18791
|
+
} else {
|
|
18792
|
+
process.stdout.write(renderStatus(model, colourEnabled(process.env, process.stdout)));
|
|
18793
|
+
}
|
|
18794
|
+
return 0;
|
|
18795
|
+
}
|
|
18796
|
+
var AUTHORITY_SEVERITY, INTERVENTION_SEVERITY, VERIFICATION_SEVERITY;
|
|
18797
|
+
var init_status2 = __esm({
|
|
18798
|
+
"src/cli/status.ts"() {
|
|
18799
|
+
"use strict";
|
|
18800
|
+
init_machine_output();
|
|
18801
|
+
init_repo_context();
|
|
18802
|
+
init_repo_context();
|
|
18803
|
+
init_doctor();
|
|
18804
|
+
init_verification_state();
|
|
18805
|
+
init_text();
|
|
18806
|
+
init_status();
|
|
18807
|
+
AUTHORITY_SEVERITY = {
|
|
18808
|
+
ACTIVE: "ok",
|
|
18809
|
+
PARTIAL: "warn",
|
|
18810
|
+
BROKEN: "bad",
|
|
18811
|
+
UNKNOWN: "info"
|
|
18812
|
+
};
|
|
18813
|
+
INTERVENTION_SEVERITY = {
|
|
18814
|
+
ACTIVE: "ok",
|
|
18815
|
+
PARTIAL: "warn",
|
|
18816
|
+
INACTIVE: "bad",
|
|
18817
|
+
UNKNOWN: "info"
|
|
18818
|
+
};
|
|
18819
|
+
VERIFICATION_SEVERITY = {
|
|
18820
|
+
CURRENT: "ok",
|
|
18821
|
+
STALE: "warn",
|
|
18822
|
+
VERIFYING: "info",
|
|
18823
|
+
BROKEN: "bad",
|
|
18824
|
+
UNVERIFIED: "info"
|
|
18825
|
+
};
|
|
18826
|
+
}
|
|
18827
|
+
});
|
|
18828
|
+
|
|
18285
18829
|
// src/cli/trace-verify.ts
|
|
18286
|
-
import { execFileSync as
|
|
18830
|
+
import { execFileSync as execFileSync12, spawnSync as spawnSync5 } from "node:child_process";
|
|
18287
18831
|
import {
|
|
18288
18832
|
existsSync as existsSync16,
|
|
18289
18833
|
mkdtempSync as mkdtempSync5,
|
|
18290
|
-
readFileSync as
|
|
18834
|
+
readFileSync as readFileSync20,
|
|
18291
18835
|
readdirSync as readdirSync6,
|
|
18292
18836
|
realpathSync as realpathSync9,
|
|
18293
|
-
rmSync as
|
|
18837
|
+
rmSync as rmSync10,
|
|
18294
18838
|
symlinkSync as symlinkSync2
|
|
18295
18839
|
} from "node:fs";
|
|
18296
18840
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
18297
|
-
import { isAbsolute as isAbsolute12, join as
|
|
18841
|
+
import { isAbsolute as isAbsolute12, join as join23, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
|
|
18298
18842
|
function unescapeStraceString(s) {
|
|
18299
18843
|
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, "\\");
|
|
18300
18844
|
}
|
|
@@ -18506,8 +19050,8 @@ function summarizeTraceRuns(opts) {
|
|
|
18506
19050
|
unresolved_accesses
|
|
18507
19051
|
};
|
|
18508
19052
|
}
|
|
18509
|
-
function
|
|
18510
|
-
return
|
|
19053
|
+
function git5(args, cwd) {
|
|
19054
|
+
return execFileSync12("git", args, {
|
|
18511
19055
|
cwd,
|
|
18512
19056
|
encoding: "utf8",
|
|
18513
19057
|
maxBuffer: 256 * 1024 * 1024,
|
|
@@ -18520,7 +19064,7 @@ function commandExists(command) {
|
|
|
18520
19064
|
return !r.error && r.status === 0;
|
|
18521
19065
|
}
|
|
18522
19066
|
function trackedAt(base, cwd) {
|
|
18523
|
-
return
|
|
19067
|
+
return git5(["ls-tree", "-r", "-z", "--name-only", base], cwd).split("\0").filter(Boolean);
|
|
18524
19068
|
}
|
|
18525
19069
|
function materializeBase(base, cwd, dest) {
|
|
18526
19070
|
const archive = spawnSync5("git", ["archive", "--format=tar", base], {
|
|
@@ -18542,8 +19086,8 @@ function materializeBase(base, cwd, dest) {
|
|
|
18542
19086
|
`tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
|
|
18543
19087
|
);
|
|
18544
19088
|
}
|
|
18545
|
-
const hostModules =
|
|
18546
|
-
const tracedModules =
|
|
19089
|
+
const hostModules = join23(cwd, "node_modules");
|
|
19090
|
+
const tracedModules = join23(dest, "node_modules");
|
|
18547
19091
|
if (existsSync16(hostModules) && !existsSync16(tracedModules)) {
|
|
18548
19092
|
symlinkSync2(resolve12(hostModules), tracedModules, "dir");
|
|
18549
19093
|
}
|
|
@@ -18551,7 +19095,7 @@ function materializeBase(base, cwd, dest) {
|
|
|
18551
19095
|
function traceFiles(prefix) {
|
|
18552
19096
|
const dir = resolve12(prefix, "..");
|
|
18553
19097
|
const base = prefix.split(sep5).at(-1) ?? prefix;
|
|
18554
|
-
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) =>
|
|
19098
|
+
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join23(dir, name)).sort();
|
|
18555
19099
|
}
|
|
18556
19100
|
function rewriteTraceRoot(accesses, actualRoot) {
|
|
18557
19101
|
return accesses.map((item) => {
|
|
@@ -18566,13 +19110,13 @@ function rewriteTraceRoot(accesses, actualRoot) {
|
|
|
18566
19110
|
if (!inside3(actualRoot, real)) return { ...item, path: real };
|
|
18567
19111
|
} catch {
|
|
18568
19112
|
}
|
|
18569
|
-
return { ...item, path:
|
|
19113
|
+
return { ...item, path: join23(TRACE_ROOT, ...rel.split("/")) };
|
|
18570
19114
|
});
|
|
18571
19115
|
}
|
|
18572
19116
|
function straceOnce(root, command, budget) {
|
|
18573
|
-
const traceDir = mkdtempSync5(
|
|
19117
|
+
const traceDir = mkdtempSync5(join23(tmpdir6(), "tw-trace-log-"));
|
|
18574
19118
|
try {
|
|
18575
|
-
const prefix =
|
|
19119
|
+
const prefix = join23(traceDir, "trace");
|
|
18576
19120
|
const traced = spawnSync5(
|
|
18577
19121
|
"strace",
|
|
18578
19122
|
[
|
|
@@ -18601,7 +19145,7 @@ function straceOnce(root, command, budget) {
|
|
|
18601
19145
|
}
|
|
18602
19146
|
);
|
|
18603
19147
|
const logs = traced.error ? [] : traceFiles(prefix);
|
|
18604
|
-
const accesses = logs.flatMap((path) => parseStraceFileAccess(
|
|
19148
|
+
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync20(path, "utf8"), TRACE_ROOT));
|
|
18605
19149
|
const raw = {
|
|
18606
19150
|
spawnError: traced.error ? traced.error.message : null,
|
|
18607
19151
|
status: traced.status,
|
|
@@ -18612,26 +19156,26 @@ function straceOnce(root, command, budget) {
|
|
|
18612
19156
|
};
|
|
18613
19157
|
return { raw, accesses };
|
|
18614
19158
|
} finally {
|
|
18615
|
-
|
|
19159
|
+
rmSync10(traceDir, { recursive: true, force: true });
|
|
18616
19160
|
}
|
|
18617
19161
|
}
|
|
18618
19162
|
function runOneTrace(cwd, base, command, budget) {
|
|
18619
|
-
const root = mkdtempSync5(
|
|
19163
|
+
const root = mkdtempSync5(join23(tmpdir6(), "tw-trace-base-"));
|
|
18620
19164
|
try {
|
|
18621
19165
|
materializeBase(base, cwd, root);
|
|
18622
19166
|
const { raw, accesses } = straceOnce(root, command, budget);
|
|
18623
19167
|
return { outcome: classifyTraceRun(raw), accesses: rewriteTraceRoot(accesses, root) };
|
|
18624
19168
|
} finally {
|
|
18625
|
-
|
|
19169
|
+
rmSync10(root, { recursive: true, force: true });
|
|
18626
19170
|
}
|
|
18627
19171
|
}
|
|
18628
19172
|
function tracerPreflight(budget = 10) {
|
|
18629
|
-
const probeRoot = mkdtempSync5(
|
|
19173
|
+
const probeRoot = mkdtempSync5(join23(tmpdir6(), "tw-trace-probe-"));
|
|
18630
19174
|
try {
|
|
18631
19175
|
const { raw } = straceOnce(probeRoot, "true", Math.max(1, Math.min(budget, 10)));
|
|
18632
19176
|
return classifyTraceRun(raw);
|
|
18633
19177
|
} finally {
|
|
18634
|
-
|
|
19178
|
+
rmSync10(probeRoot, { recursive: true, force: true });
|
|
18635
19179
|
}
|
|
18636
19180
|
}
|
|
18637
19181
|
function renderText2(report2) {
|
|
@@ -18696,7 +19240,7 @@ function runTraceVerify(opts = {}) {
|
|
|
18696
19240
|
let base;
|
|
18697
19241
|
try {
|
|
18698
19242
|
const baseArg = assertRev(requestedBase);
|
|
18699
|
-
base =
|
|
19243
|
+
base = git5(["rev-parse", "--verify", `${baseArg}^{commit}`], cwd).trim();
|
|
18700
19244
|
} catch (e) {
|
|
18701
19245
|
process.stderr.write(`tamperward trace-verify: cannot resolve trusted base ${JSON.stringify(requestedBase)} (${e instanceof Error ? e.message : String(e)}).
|
|
18702
19246
|
`);
|
|
@@ -18833,26 +19377,26 @@ var init_trace_verify = __esm({
|
|
|
18833
19377
|
});
|
|
18834
19378
|
|
|
18835
19379
|
// src/runtimes.ts
|
|
18836
|
-
import { existsSync as existsSync17, readFileSync as
|
|
18837
|
-
import { join as
|
|
18838
|
-
function
|
|
19380
|
+
import { existsSync as existsSync17, readFileSync as readFileSync21 } from "node:fs";
|
|
19381
|
+
import { join as join24 } from "node:path";
|
|
19382
|
+
function isRecord5(x) {
|
|
18839
19383
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
18840
19384
|
}
|
|
18841
19385
|
function hooksBeyondTamperward(hooks) {
|
|
18842
|
-
if (!
|
|
19386
|
+
if (!isRecord5(hooks)) return hooks != null;
|
|
18843
19387
|
for (const entries of Object.values(hooks)) {
|
|
18844
19388
|
if (!Array.isArray(entries)) {
|
|
18845
19389
|
if (entries != null) return true;
|
|
18846
19390
|
continue;
|
|
18847
19391
|
}
|
|
18848
19392
|
for (const entry of entries) {
|
|
18849
|
-
const hs =
|
|
19393
|
+
const hs = isRecord5(entry) ? entry.hooks : null;
|
|
18850
19394
|
if (!Array.isArray(hs)) {
|
|
18851
19395
|
if (hs != null) return true;
|
|
18852
19396
|
continue;
|
|
18853
19397
|
}
|
|
18854
19398
|
for (const h of hs) {
|
|
18855
|
-
const cmd =
|
|
19399
|
+
const cmd = isRecord5(h) ? String(h.command ?? "") : "";
|
|
18856
19400
|
if (!/tamperward/.test(cmd)) return true;
|
|
18857
19401
|
}
|
|
18858
19402
|
}
|
|
@@ -18860,11 +19404,11 @@ function hooksBeyondTamperward(hooks) {
|
|
|
18860
19404
|
return false;
|
|
18861
19405
|
}
|
|
18862
19406
|
function claudeSettingsBeyondTamperward(cwd) {
|
|
18863
|
-
const path =
|
|
19407
|
+
const path = join24(cwd, ".claude", "settings.json");
|
|
18864
19408
|
let raw;
|
|
18865
19409
|
try {
|
|
18866
19410
|
if (!existsSync17(path)) return false;
|
|
18867
|
-
raw =
|
|
19411
|
+
raw = readFileSync21(path, "utf8");
|
|
18868
19412
|
} catch {
|
|
18869
19413
|
return false;
|
|
18870
19414
|
}
|
|
@@ -18874,7 +19418,7 @@ function claudeSettingsBeyondTamperward(cwd) {
|
|
|
18874
19418
|
} catch {
|
|
18875
19419
|
return raw.trim().length > 0;
|
|
18876
19420
|
}
|
|
18877
|
-
if (!
|
|
19421
|
+
if (!isRecord5(parsed)) return raw.trim().length > 0;
|
|
18878
19422
|
for (const [key2, value] of Object.entries(parsed)) {
|
|
18879
19423
|
if (key2 === "disableAllHooks") continue;
|
|
18880
19424
|
if (key2 === "hooks") {
|
|
@@ -18890,7 +19434,7 @@ function detectRuntimes(cwd) {
|
|
|
18890
19434
|
for (const rt of KNOWN_RUNTIMES) {
|
|
18891
19435
|
const marker = rt.markers.find((m) => {
|
|
18892
19436
|
try {
|
|
18893
|
-
return existsSync17(
|
|
19437
|
+
return existsSync17(join24(cwd, m));
|
|
18894
19438
|
} catch {
|
|
18895
19439
|
return false;
|
|
18896
19440
|
}
|
|
@@ -18970,10 +19514,10 @@ var init_runtimes = __esm({
|
|
|
18970
19514
|
});
|
|
18971
19515
|
|
|
18972
19516
|
// src/cli/onboard.ts
|
|
18973
|
-
import { execFileSync as
|
|
18974
|
-
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as
|
|
19517
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
19518
|
+
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync22, realpathSync as realpathSync10, rmSync as rmSync11 } from "node:fs";
|
|
18975
19519
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
18976
|
-
import { join as
|
|
19520
|
+
import { join as join25, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
|
|
18977
19521
|
import { createInterface } from "node:readline";
|
|
18978
19522
|
import { parseDocument } from "yaml";
|
|
18979
19523
|
function readlineAsker(input, output) {
|
|
@@ -18982,9 +19526,9 @@ function readlineAsker(input, output) {
|
|
|
18982
19526
|
let pending = null;
|
|
18983
19527
|
rl.on("close", () => {
|
|
18984
19528
|
closed = true;
|
|
18985
|
-
const
|
|
19529
|
+
const resolve23 = pending;
|
|
18986
19530
|
pending = null;
|
|
18987
|
-
|
|
19531
|
+
resolve23?.(null);
|
|
18988
19532
|
});
|
|
18989
19533
|
rl.on("SIGINT", () => rl.close());
|
|
18990
19534
|
const ask = (question) => new Promise((resolvePromise) => {
|
|
@@ -19017,9 +19561,9 @@ function platformLabel(platform) {
|
|
|
19017
19561
|
if (platform === "linux") return "Linux";
|
|
19018
19562
|
return platform;
|
|
19019
19563
|
}
|
|
19020
|
-
function
|
|
19564
|
+
function git6(cwd, args) {
|
|
19021
19565
|
try {
|
|
19022
|
-
return
|
|
19566
|
+
return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
19023
19567
|
} catch {
|
|
19024
19568
|
return null;
|
|
19025
19569
|
}
|
|
@@ -19038,13 +19582,13 @@ function explainVerdict(v, code2) {
|
|
|
19038
19582
|
}
|
|
19039
19583
|
}
|
|
19040
19584
|
function writeVerifyCommand(cwd, command) {
|
|
19041
|
-
const path =
|
|
19585
|
+
const path = join25(cwd, POLICY_FILE);
|
|
19042
19586
|
const kind = writeTargetKind(path);
|
|
19043
19587
|
if (kind === "symlink" || kind === "irregular") {
|
|
19044
19588
|
return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
|
|
19045
19589
|
}
|
|
19046
19590
|
const mode = existingMode(path, 420);
|
|
19047
|
-
const original = kind === "file" ?
|
|
19591
|
+
const original = kind === "file" ? readFileSync22(path, "utf8") : null;
|
|
19048
19592
|
let next;
|
|
19049
19593
|
try {
|
|
19050
19594
|
const doc = parseDocument(original && original.trim() ? original : "version: 1\n");
|
|
@@ -19061,7 +19605,7 @@ function writeVerifyCommand(cwd, command) {
|
|
|
19061
19605
|
if (loaded2.verify?.command !== command) throw new Error("verify.command did not round-trip");
|
|
19062
19606
|
return null;
|
|
19063
19607
|
} catch (e) {
|
|
19064
|
-
if (original === null)
|
|
19608
|
+
if (original === null) rmSync11(path, { force: true });
|
|
19065
19609
|
else atomicReplaceFile(path, original, mode);
|
|
19066
19610
|
return `the updated ${POLICY_FILE} does not load (${errorMessage(e)}); restored the previous file`;
|
|
19067
19611
|
}
|
|
@@ -19096,7 +19640,7 @@ async function runOnboard(opts, io = {}) {
|
|
|
19096
19640
|
rawErr("or run `tamperward init` for the deterministic wiring step only.");
|
|
19097
19641
|
return 2;
|
|
19098
19642
|
}
|
|
19099
|
-
const rootText =
|
|
19643
|
+
const rootText = git6(requestedCwd, ["rev-parse", "--show-toplevel"])?.trim() ?? "";
|
|
19100
19644
|
if (!rootText) {
|
|
19101
19645
|
fail(requestedCwd + " is not inside a Git repository. Run `git init` first.");
|
|
19102
19646
|
return 2;
|
|
@@ -19162,8 +19706,8 @@ async function runOnboard(opts, io = {}) {
|
|
|
19162
19706
|
} else {
|
|
19163
19707
|
status("LIMITED", platformLabel(platform) + ": check works here; local verify and `run` are unavailable. Use the container verifier for final verification.", "warn");
|
|
19164
19708
|
}
|
|
19165
|
-
const head =
|
|
19166
|
-
const treeStatus =
|
|
19709
|
+
const head = git6(cwd, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])?.trim() ?? null;
|
|
19710
|
+
const treeStatus = git6(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
|
|
19167
19711
|
const dirty = treeStatus.split("\n").filter(Boolean).map((line2) => line2.slice(3).split(" -> ").at(-1) ?? "");
|
|
19168
19712
|
if (!head) {
|
|
19169
19713
|
const count = dirty.length;
|
|
@@ -19411,7 +19955,7 @@ function postureOf(outcome) {
|
|
|
19411
19955
|
return "READY";
|
|
19412
19956
|
}
|
|
19413
19957
|
function runDemo(cwd, head, runners, out3) {
|
|
19414
|
-
const files = (
|
|
19958
|
+
const files = (git6(cwd, ["ls-tree", "-r", "-z", head]) ?? "").split("\0").filter(Boolean).flatMap((row) => {
|
|
19415
19959
|
const tab = row.indexOf(" ");
|
|
19416
19960
|
if (tab < 0) return [];
|
|
19417
19961
|
const mode = row.slice(0, tab).split(" ")[0];
|
|
@@ -19420,7 +19964,7 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19420
19964
|
});
|
|
19421
19965
|
let target = null;
|
|
19422
19966
|
for (const path of files) {
|
|
19423
|
-
const before2 =
|
|
19967
|
+
const before2 = git6(cwd, ["show", `${head}:${path}`]);
|
|
19424
19968
|
if (before2 === null || !JS_TEST_BLOCK.test(before2)) continue;
|
|
19425
19969
|
target = { path, before: before2, after: before2.replace(JS_TEST_BLOCK, "$1$2.skip$3") };
|
|
19426
19970
|
break;
|
|
@@ -19431,13 +19975,13 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19431
19975
|
}
|
|
19432
19976
|
const before = treeFingerprint(cwd);
|
|
19433
19977
|
out3(`working tree fingerprint before: ${before.slice(0, 16)}`);
|
|
19434
|
-
const wt = mkdtempSync6(
|
|
19978
|
+
const wt = mkdtempSync6(join25(tmpdir7(), "tw-onboard-demo-"));
|
|
19435
19979
|
try {
|
|
19436
|
-
if (
|
|
19980
|
+
if (git6(cwd, ["worktree", "add", "--detach", wt, head]) === null) {
|
|
19437
19981
|
out3("Demo skipped: could not create a temporary worktree (git worktree add failed).");
|
|
19438
19982
|
return;
|
|
19439
19983
|
}
|
|
19440
|
-
const targetPath =
|
|
19984
|
+
const targetPath = join25(wt, target.path);
|
|
19441
19985
|
const targetStat = lstatSync10(targetPath, { throwIfNoEntry: false });
|
|
19442
19986
|
if (!targetStat || !targetStat.isFile() || targetStat.isSymbolicLink()) {
|
|
19443
19987
|
out3("Demo skipped: the selected test is not a regular file in the disposable worktree.");
|
|
@@ -19466,9 +20010,9 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
19466
20010
|
out3("");
|
|
19467
20011
|
out3(`check exited ${code2}: ${code2 === 1 ? "a blocking finding, which is what the hook, pre-commit and CI would all report" : "no blocking finding was reported"}.`);
|
|
19468
20012
|
} finally {
|
|
19469
|
-
|
|
19470
|
-
|
|
19471
|
-
|
|
20013
|
+
git6(cwd, ["worktree", "remove", "--force", wt]);
|
|
20014
|
+
git6(cwd, ["worktree", "prune"]);
|
|
20015
|
+
rmSync11(wt, { recursive: true, force: true });
|
|
19472
20016
|
}
|
|
19473
20017
|
const after = treeFingerprint(cwd);
|
|
19474
20018
|
out3(`working tree fingerprint after: ${after.slice(0, 16)}`);
|
|
@@ -19655,9 +20199,9 @@ var init_capture = __esm({
|
|
|
19655
20199
|
});
|
|
19656
20200
|
|
|
19657
20201
|
// src/research/manifest.ts
|
|
19658
|
-
import { createHash as
|
|
19659
|
-
import { readFileSync as
|
|
19660
|
-
import { dirname as
|
|
20202
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
20203
|
+
import { readFileSync as readFileSync23 } from "node:fs";
|
|
20204
|
+
import { dirname as dirname11, resolve as resolve15 } from "node:path";
|
|
19661
20205
|
function taskFrom(raw, index, manifestDir) {
|
|
19662
20206
|
const where = `task #${index + 1}`;
|
|
19663
20207
|
if (!isRecord(raw)) throw new ResearchError(`task manifest: ${where} is not an object`);
|
|
@@ -19690,7 +20234,7 @@ function readManifest(path) {
|
|
|
19690
20234
|
const abs = resolve15(path);
|
|
19691
20235
|
let bytes;
|
|
19692
20236
|
try {
|
|
19693
|
-
bytes =
|
|
20237
|
+
bytes = readFileSync23(abs);
|
|
19694
20238
|
} catch (e) {
|
|
19695
20239
|
throw new ResearchError(`cannot read task manifest ${abs}: ${e instanceof Error ? e.message : String(e)}`);
|
|
19696
20240
|
}
|
|
@@ -19707,13 +20251,13 @@ function readManifest(path) {
|
|
|
19707
20251
|
if (!Array.isArray(raw.tasks) || raw.tasks.length === 0) {
|
|
19708
20252
|
throw new ResearchError(`task manifest ${abs}: no tasks (tasks must be a non-empty array)`);
|
|
19709
20253
|
}
|
|
19710
|
-
const tasks = raw.tasks.map((t, i) => taskFrom(t, i,
|
|
20254
|
+
const tasks = raw.tasks.map((t, i) => taskFrom(t, i, dirname11(abs)));
|
|
19711
20255
|
const seen = /* @__PURE__ */ new Set();
|
|
19712
20256
|
for (const t of tasks) {
|
|
19713
20257
|
if (seen.has(t.id)) throw new ResearchError(`task manifest ${abs}: duplicate task id "${t.id}"`);
|
|
19714
20258
|
seen.add(t.id);
|
|
19715
20259
|
}
|
|
19716
|
-
return { path: abs, sha256:
|
|
20260
|
+
return { path: abs, sha256: createHash14("sha256").update(bytes).digest("hex"), tasks };
|
|
19717
20261
|
}
|
|
19718
20262
|
var MANIFEST_VERSION;
|
|
19719
20263
|
var init_manifest = __esm({
|
|
@@ -19987,22 +20531,22 @@ var init_record = __esm({
|
|
|
19987
20531
|
|
|
19988
20532
|
// src/research/run.ts
|
|
19989
20533
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
19990
|
-
import { execFileSync as
|
|
19991
|
-
import { closeSync as closeSync3, existsSync as existsSync18, linkSync, mkdirSync as
|
|
20534
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
20535
|
+
import { closeSync as closeSync3, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync10, openSync as openSync3, readFileSync as readFileSync24, renameSync as renameSync4, rmSync as rmSync12, unlinkSync, writeFileSync as writeFileSync12, writeSync } from "node:fs";
|
|
19992
20536
|
import { hostname } from "node:os";
|
|
19993
|
-
import { join as
|
|
19994
|
-
function
|
|
19995
|
-
return
|
|
20537
|
+
import { join as join26, resolve as resolve16 } from "node:path";
|
|
20538
|
+
function git7(args, cwd) {
|
|
20539
|
+
return execFileSync14("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
19996
20540
|
}
|
|
19997
20541
|
function pairRecordPath(ledger, task, pair) {
|
|
19998
|
-
return
|
|
20542
|
+
return join26(ledger, "pairs", `${task}--${pair}.json`);
|
|
19999
20543
|
}
|
|
20000
20544
|
function researchLockPath(ledger) {
|
|
20001
|
-
return
|
|
20545
|
+
return join26(ledger, "run.lock");
|
|
20002
20546
|
}
|
|
20003
20547
|
function readLockOwner(path) {
|
|
20004
20548
|
try {
|
|
20005
|
-
const parsed = JSON.parse(
|
|
20549
|
+
const parsed = JSON.parse(readFileSync24(path, "utf8"));
|
|
20006
20550
|
if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
20007
20551
|
if (typeof parsed.host !== "string" || typeof parsed.started_at !== "string" || typeof parsed.token !== "string") return null;
|
|
20008
20552
|
return { pid: parsed.pid, host: parsed.host, started_at: parsed.started_at, token: parsed.token };
|
|
@@ -20023,7 +20567,7 @@ function lockHolder(path) {
|
|
|
20023
20567
|
return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
|
|
20024
20568
|
}
|
|
20025
20569
|
function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
20026
|
-
|
|
20570
|
+
mkdirSync10(ledger, { recursive: true });
|
|
20027
20571
|
const path = researchLockPath(ledger);
|
|
20028
20572
|
if (breakStaleLock && existsSync18(path)) {
|
|
20029
20573
|
const owner2 = readLockOwner(path);
|
|
@@ -20087,7 +20631,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
20087
20631
|
if (released) return;
|
|
20088
20632
|
released = true;
|
|
20089
20633
|
try {
|
|
20090
|
-
if (
|
|
20634
|
+
if (readFileSync24(path, "utf8") === text) unlinkSync(path);
|
|
20091
20635
|
} catch {
|
|
20092
20636
|
}
|
|
20093
20637
|
}
|
|
@@ -20096,7 +20640,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
20096
20640
|
function resumableRecord(path, expected) {
|
|
20097
20641
|
let raw;
|
|
20098
20642
|
try {
|
|
20099
|
-
raw = JSON.parse(
|
|
20643
|
+
raw = JSON.parse(readFileSync24(path, "utf8"));
|
|
20100
20644
|
} catch (e) {
|
|
20101
20645
|
throw new ResearchError(`ledger record ${path} is malformed (${errorMessage(e).split("\n")[0]}); remove it to regenerate the pair, or use a new --out`);
|
|
20102
20646
|
}
|
|
@@ -20133,28 +20677,28 @@ function resumableRecord(path, expected) {
|
|
|
20133
20677
|
}
|
|
20134
20678
|
function writeRecordAtomically(path, text) {
|
|
20135
20679
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
20136
|
-
|
|
20680
|
+
writeFileSync12(tmp, text);
|
|
20137
20681
|
renameSync4(tmp, path);
|
|
20138
20682
|
}
|
|
20139
20683
|
function cloneArgs(repo, ws3) {
|
|
20140
20684
|
return ["-c", "protocol.ext.allow=never", "clone", "-q", "--no-hardlinks", "--", repo, ws3];
|
|
20141
20685
|
}
|
|
20142
20686
|
function freshWorkspace(ledger, task, pair, arm, sourceBase) {
|
|
20143
|
-
const ws3 =
|
|
20144
|
-
|
|
20145
|
-
|
|
20687
|
+
const ws3 = join26(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
|
|
20688
|
+
rmSync12(ws3, { recursive: true, force: true });
|
|
20689
|
+
mkdirSync10(join26(ledger, "workspaces"), { recursive: true });
|
|
20146
20690
|
try {
|
|
20147
|
-
|
|
20691
|
+
execFileSync14("git", cloneArgs(task.repo, ws3), { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
20148
20692
|
} catch (e) {
|
|
20149
20693
|
throw new ResearchError(`task "${task.id}": cannot clone ${task.repo}: ${errorMessage(e).split("\n")[0]}`);
|
|
20150
20694
|
}
|
|
20151
|
-
|
|
20152
|
-
|
|
20695
|
+
git7(["config", "user.name", "tamperward-research"], ws3);
|
|
20696
|
+
git7(["config", "user.email", "research@tamperward.invalid"], ws3);
|
|
20153
20697
|
const requestedBase = sourceBase ?? task.base;
|
|
20154
20698
|
const candidates = sourceBase !== void 0 ? [sourceBase] : task.base === "HEAD" ? ["HEAD"] : [task.base, `origin/${task.base}`];
|
|
20155
20699
|
for (const rev2 of candidates) {
|
|
20156
20700
|
try {
|
|
20157
|
-
|
|
20701
|
+
git7(["checkout", "-q", "--detach", rev2], ws3);
|
|
20158
20702
|
return ws3;
|
|
20159
20703
|
} catch {
|
|
20160
20704
|
}
|
|
@@ -20308,15 +20852,15 @@ function observeOutcome(ws3, base, head, task, trusted) {
|
|
|
20308
20852
|
}
|
|
20309
20853
|
function runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase) {
|
|
20310
20854
|
const ws3 = freshWorkspace(ledger, task, pair, arm, sourceBase);
|
|
20311
|
-
const prep = { id: task.id, prompt: task.prompt, cwd: ws3, base:
|
|
20855
|
+
const prep = { id: task.id, prompt: task.prompt, cwd: ws3, base: git7(["rev-parse", "HEAD"], ws3), arm, model: opts.model };
|
|
20312
20856
|
if (arm === "gated" && adapter.prepareGated) {
|
|
20313
20857
|
captureStdout(() => adapter.prepareGated?.(prep));
|
|
20314
|
-
if (
|
|
20315
|
-
|
|
20316
|
-
|
|
20858
|
+
if (git7(["status", "--porcelain"], ws3)) {
|
|
20859
|
+
git7(["add", "-A"], ws3);
|
|
20860
|
+
git7(["commit", "-qm", "tamperward research: wire enforcement (gated arm)"], ws3);
|
|
20317
20861
|
}
|
|
20318
20862
|
}
|
|
20319
|
-
const base =
|
|
20863
|
+
const base = git7(["rev-parse", "HEAD"], ws3);
|
|
20320
20864
|
const launch2 = adapter.launch({ ...prep, base });
|
|
20321
20865
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20322
20866
|
let agent;
|
|
@@ -20329,7 +20873,7 @@ function runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase) {
|
|
|
20329
20873
|
agent = runUngated(ws3, launch2.argv, launch2.env, opts.agentBudget);
|
|
20330
20874
|
}
|
|
20331
20875
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
20332
|
-
const head =
|
|
20876
|
+
const head = git7(["rev-parse", "HEAD"], ws3);
|
|
20333
20877
|
const trustedSurface = trustedProtectedOnly(base, ws3);
|
|
20334
20878
|
const fingerprintBefore = treeFingerprint(ws3, trustedSurface.protectedOnly);
|
|
20335
20879
|
const observed = observeOutcome(ws3, base, head, task, trustedSurface.trusted);
|
|
@@ -20391,7 +20935,7 @@ function runResearch(opts) {
|
|
|
20391
20935
|
}
|
|
20392
20936
|
try {
|
|
20393
20937
|
const pairs = opts.pairs ?? 1;
|
|
20394
|
-
|
|
20938
|
+
mkdirSync10(join26(ledger, "pairs"), { recursive: true });
|
|
20395
20939
|
for (const task of tasks) {
|
|
20396
20940
|
const existingRecords = /* @__PURE__ */ new Map();
|
|
20397
20941
|
let sourceBase = null;
|
|
@@ -20518,10 +21062,10 @@ var init_run2 = __esm({
|
|
|
20518
21062
|
});
|
|
20519
21063
|
|
|
20520
21064
|
// src/research/summarize.ts
|
|
20521
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
20522
|
-
import { join as
|
|
21065
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync25 } from "node:fs";
|
|
21066
|
+
import { join as join27, resolve as resolve17 } from "node:path";
|
|
20523
21067
|
function readLedger2(dir) {
|
|
20524
|
-
const pairsDir =
|
|
21068
|
+
const pairsDir = join27(resolve17(dir), "pairs");
|
|
20525
21069
|
let names;
|
|
20526
21070
|
try {
|
|
20527
21071
|
names = readdirSync7(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
|
|
@@ -20530,10 +21074,10 @@ function readLedger2(dir) {
|
|
|
20530
21074
|
}
|
|
20531
21075
|
if (names.length === 0) throw new ResearchError(`ledger ${pairsDir} holds no pair records`);
|
|
20532
21076
|
return names.map((n2) => {
|
|
20533
|
-
const path =
|
|
21077
|
+
const path = join27(pairsDir, n2);
|
|
20534
21078
|
let raw;
|
|
20535
21079
|
try {
|
|
20536
|
-
raw = JSON.parse(
|
|
21080
|
+
raw = JSON.parse(readFileSync25(path, "utf8"));
|
|
20537
21081
|
} catch (e) {
|
|
20538
21082
|
throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
|
|
20539
21083
|
}
|
|
@@ -20664,9 +21208,9 @@ var init_summarize = __esm({
|
|
|
20664
21208
|
});
|
|
20665
21209
|
|
|
20666
21210
|
// src/research/init.ts
|
|
20667
|
-
import { createHash as
|
|
20668
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
20669
|
-
import { basename as basename5, dirname as
|
|
21211
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
21212
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync26, writeFileSync as writeFileSync13 } from "node:fs";
|
|
21213
|
+
import { basename as basename5, dirname as dirname12, resolve as resolve18 } from "node:path";
|
|
20670
21214
|
function idFrom(repo) {
|
|
20671
21215
|
const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
|
|
20672
21216
|
return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
|
|
@@ -20694,8 +21238,8 @@ function createResearchManifest(opts) {
|
|
|
20694
21238
|
}]
|
|
20695
21239
|
};
|
|
20696
21240
|
const text = JSON.stringify(doc, null, 2) + "\n";
|
|
20697
|
-
|
|
20698
|
-
|
|
21241
|
+
mkdirSync11(dirname12(out3), { recursive: true });
|
|
21242
|
+
writeFileSync13(out3, text, { flag: "wx", mode: 384 });
|
|
20699
21243
|
const parsed = readManifest(out3);
|
|
20700
21244
|
if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
|
|
20701
21245
|
return out3;
|
|
@@ -20717,7 +21261,7 @@ function runResearchInit(opts) {
|
|
|
20717
21261
|
}
|
|
20718
21262
|
}
|
|
20719
21263
|
function requireHash(path) {
|
|
20720
|
-
return
|
|
21264
|
+
return createHash15("sha256").update(readFileSync26(path)).digest("hex");
|
|
20721
21265
|
}
|
|
20722
21266
|
var init_init2 = __esm({
|
|
20723
21267
|
"src/research/init.ts"() {
|
|
@@ -20782,10 +21326,10 @@ var init_report2 = __esm({
|
|
|
20782
21326
|
});
|
|
20783
21327
|
|
|
20784
21328
|
// src/research/bundle.ts
|
|
20785
|
-
import { createHash as
|
|
21329
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
20786
21330
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
20787
|
-
import { existsSync as existsSync20, mkdirSync as
|
|
20788
|
-
import { dirname as
|
|
21331
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync27, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
|
|
21332
|
+
import { dirname as dirname13, join as join28, resolve as resolve19 } from "node:path";
|
|
20789
21333
|
function octal(value, width) {
|
|
20790
21334
|
return value.toString(8).padStart(width - 1, "0") + "\0";
|
|
20791
21335
|
}
|
|
@@ -20831,14 +21375,14 @@ function parseTar(bytes) {
|
|
|
20831
21375
|
}
|
|
20832
21376
|
function walkLedger(ledger) {
|
|
20833
21377
|
const entries = [];
|
|
20834
|
-
const pairs =
|
|
21378
|
+
const pairs = join28(ledger, "pairs");
|
|
20835
21379
|
if (!existsSync20(pairs) || !statSync6(pairs).isDirectory()) throw new ResearchError(`ledger ${ledger} has no pairs directory`);
|
|
20836
21380
|
const names = readdirSync8(pairs).filter((n2) => n2.endsWith(".json")).sort();
|
|
20837
21381
|
if (names.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
|
|
20838
21382
|
for (const name of names) {
|
|
20839
|
-
const path =
|
|
21383
|
+
const path = join28(pairs, name);
|
|
20840
21384
|
if (!statSync6(path).isFile()) continue;
|
|
20841
|
-
entries.push({ name: `ledger/pairs/${name}`, bytes:
|
|
21385
|
+
entries.push({ name: `ledger/pairs/${name}`, bytes: readFileSync27(path) });
|
|
20842
21386
|
}
|
|
20843
21387
|
return entries;
|
|
20844
21388
|
}
|
|
@@ -20848,7 +21392,7 @@ function jsonBytes(value) {
|
|
|
20848
21392
|
function validateResearchBundle(path) {
|
|
20849
21393
|
let files;
|
|
20850
21394
|
try {
|
|
20851
|
-
files = parseTar(gunzipSync(
|
|
21395
|
+
files = parseTar(gunzipSync(readFileSync27(resolve19(path))));
|
|
20852
21396
|
} catch (e) {
|
|
20853
21397
|
throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
20854
21398
|
}
|
|
@@ -20871,7 +21415,7 @@ function validateResearchBundle(path) {
|
|
|
20871
21415
|
if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
|
|
20872
21416
|
throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
|
|
20873
21417
|
}
|
|
20874
|
-
if (manifest &&
|
|
21418
|
+
if (manifest && createHash16("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
|
|
20875
21419
|
throw new ResearchError("research bundle manifest does not match its recorded sha256");
|
|
20876
21420
|
}
|
|
20877
21421
|
const parsedSummary = summary;
|
|
@@ -20902,8 +21446,8 @@ function createResearchBundle(opts) {
|
|
|
20902
21446
|
const summary = summarizeLedger(ledger);
|
|
20903
21447
|
const entries = walkLedger(ledger);
|
|
20904
21448
|
if (opts.manifest) {
|
|
20905
|
-
const manifest =
|
|
20906
|
-
const sha3 =
|
|
21449
|
+
const manifest = readFileSync27(resolve19(opts.manifest));
|
|
21450
|
+
const sha3 = createHash16("sha256").update(manifest).digest("hex");
|
|
20907
21451
|
if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
|
|
20908
21452
|
entries.push({ name: "manifest.json", bytes: manifest });
|
|
20909
21453
|
}
|
|
@@ -20925,8 +21469,8 @@ function createResearchBundle(opts) {
|
|
|
20925
21469
|
entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
|
|
20926
21470
|
entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
|
|
20927
21471
|
const out3 = resolve19(opts.out);
|
|
20928
|
-
|
|
20929
|
-
|
|
21472
|
+
mkdirSync12(dirname13(out3), { recursive: true });
|
|
21473
|
+
writeFileSync14(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
|
|
20930
21474
|
return out3;
|
|
20931
21475
|
}
|
|
20932
21476
|
function runResearchBundle(opts) {
|
|
@@ -21050,39 +21594,2194 @@ var init_research = __esm({
|
|
|
21050
21594
|
}
|
|
21051
21595
|
});
|
|
21052
21596
|
|
|
21053
|
-
// src/
|
|
21054
|
-
function
|
|
21055
|
-
|
|
21056
|
-
|
|
21057
|
-
|
|
21058
|
-
|
|
21059
|
-
|
|
21060
|
-
|
|
21061
|
-
|
|
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;
|
|
21597
|
+
// src/adapters/contract.ts
|
|
21598
|
+
function steeringUnavailableFinding(detail) {
|
|
21599
|
+
return {
|
|
21600
|
+
rule: "tamperward-unavailable",
|
|
21601
|
+
severity: "block",
|
|
21602
|
+
message: `TamperWard could not evaluate this operation (${detail}), so it is denied rather than allowed.`,
|
|
21603
|
+
evidence: detail,
|
|
21604
|
+
remediation: "Repair the runtime steering path (hook config, transport, or event delivery), then retry. Do not work around the gate while it is down.",
|
|
21605
|
+
signoff: { required: true, command: 'tamperward allow --reason "..."' }
|
|
21606
|
+
};
|
|
21072
21607
|
}
|
|
21073
|
-
|
|
21074
|
-
|
|
21608
|
+
function failClosedResult(outcome, detail, phase, denyPayload) {
|
|
21609
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
21610
|
+
const wire = denyPayload(findings, phase);
|
|
21611
|
+
return { outcome, detail, wire, decision: { verdict: "deny", findings, reason: wire } };
|
|
21612
|
+
}
|
|
21613
|
+
var OPERATION_KINDS;
|
|
21614
|
+
var init_contract = __esm({
|
|
21615
|
+
"src/adapters/contract.ts"() {
|
|
21075
21616
|
"use strict";
|
|
21076
|
-
|
|
21617
|
+
OPERATION_KINDS = ["shell", "file-edit", "file-read", "mcp", "other"];
|
|
21077
21618
|
}
|
|
21078
21619
|
});
|
|
21079
21620
|
|
|
21080
|
-
// src/
|
|
21081
|
-
|
|
21082
|
-
|
|
21083
|
-
|
|
21084
|
-
|
|
21085
|
-
|
|
21621
|
+
// src/adapters/claude/adapter.ts
|
|
21622
|
+
function hookKindFor(phase) {
|
|
21623
|
+
if (phase === "end-of-turn") return "Stop";
|
|
21624
|
+
if (phase === "pre-action") return "PreToolUse";
|
|
21625
|
+
throw new Error(`no deny-capable Claude hook for the observation-only phase "${phase}"`);
|
|
21626
|
+
}
|
|
21627
|
+
function operationKind(toolName) {
|
|
21628
|
+
if (!toolName) return "other";
|
|
21629
|
+
if (toolName === "Bash" || toolName === "BashOutput" || toolName === "KillShell") return "shell";
|
|
21630
|
+
if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit" || toolName === "NotebookEdit") return "file-edit";
|
|
21631
|
+
if (toolName === "Read" || toolName === "Glob" || toolName === "Grep" || toolName === "NotebookRead") return "file-read";
|
|
21632
|
+
if (toolName.startsWith("mcp__")) return "mcp";
|
|
21633
|
+
return "other";
|
|
21634
|
+
}
|
|
21635
|
+
function eventFrom(input, phase) {
|
|
21636
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: operationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
|
|
21637
|
+
return {
|
|
21638
|
+
phase,
|
|
21639
|
+
operation,
|
|
21640
|
+
identity: { claimedCwd: input.cwd, sessionId: input.session_id }
|
|
21641
|
+
};
|
|
21642
|
+
}
|
|
21643
|
+
var ClaudeRuntimeAdapter, claudeAdapter;
|
|
21644
|
+
var init_adapter2 = __esm({
|
|
21645
|
+
"src/adapters/claude/adapter.ts"() {
|
|
21646
|
+
"use strict";
|
|
21647
|
+
init_deny();
|
|
21648
|
+
init_hook();
|
|
21649
|
+
init_repo_context();
|
|
21650
|
+
init_contract();
|
|
21651
|
+
ClaudeRuntimeAdapter = class {
|
|
21652
|
+
name = "claude-code";
|
|
21653
|
+
/**
|
|
21654
|
+
* Claude Code's live PreToolUse fires for EVERY tool and can synchronously deny it —
|
|
21655
|
+
* shell, file edit, file read, MCP, and anything else — even under
|
|
21656
|
+
* `--dangerously-skip-permissions`. The end-of-turn Stop sweep is live too. There is no
|
|
21657
|
+
* live per-tool PostToolUse veto (Stop is the post-turn reconciliation), so `postObserve`
|
|
21658
|
+
* is empty and `unsupported` names that gap explicitly rather than implying it.
|
|
21659
|
+
*/
|
|
21660
|
+
capabilities = {
|
|
21661
|
+
preDeny: OPERATION_KINDS,
|
|
21662
|
+
postObserve: [],
|
|
21663
|
+
endOfTurn: true,
|
|
21664
|
+
unsupported: [
|
|
21665
|
+
"per-operation post-action veto (end-of-turn Stop sweep is the post-turn reconciliation)",
|
|
21666
|
+
"network-egress control",
|
|
21667
|
+
"identity / authentication"
|
|
21668
|
+
]
|
|
21669
|
+
};
|
|
21670
|
+
parseEvent(raw, phase) {
|
|
21671
|
+
try {
|
|
21672
|
+
return eventFrom(parseInput(raw), phase);
|
|
21673
|
+
} catch (e) {
|
|
21674
|
+
if (e instanceof HookInputError) return { failure: "parse-failure", detail: e.message };
|
|
21675
|
+
throw e;
|
|
21676
|
+
}
|
|
21677
|
+
}
|
|
21678
|
+
/**
|
|
21679
|
+
* REVIEW POINT 5 — the runtime cwd is a CLAIM, not authority.
|
|
21680
|
+
*
|
|
21681
|
+
* The trusted repository root is derived from the RUNNER context (`defaultCwd`, else the
|
|
21682
|
+
* process cwd) — INDEPENDENTLY of the claim — via git (`repoContext` →
|
|
21683
|
+
* `git rev-parse --show-toplevel`, which resolves symlinks to a canonical path). The
|
|
21684
|
+
* claim is then validated against THAT root with the shared `validateClaimAgainstRoot`:
|
|
21685
|
+
* accepted only when it resolves to the same repository (its root, or a path inside it);
|
|
21686
|
+
* rejected for a malformed path, a non-repository, a different repository, or a symlink
|
|
21687
|
+
* that escapes into another repository. It never weakens what `preToolUseVerdict` does via
|
|
21688
|
+
* `repoRoot()` — it is the explicit boundary that `decide` enforces BEFORE evaluating.
|
|
21689
|
+
*/
|
|
21690
|
+
validateIdentity(claim, defaultCwd) {
|
|
21691
|
+
const base = defaultCwd ?? process.cwd();
|
|
21692
|
+
const runnerCtx = repoContext(base);
|
|
21693
|
+
if (!runnerCtx) {
|
|
21694
|
+
return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
|
|
21695
|
+
}
|
|
21696
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
21697
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
21698
|
+
}
|
|
21699
|
+
/** The exact deny wire bytes for `phase`, through the SAME `denyWire` the live
|
|
21700
|
+
* `verdict()` uses (src/cli/hook.ts). Reuses `formatDenial`, so the reason text is
|
|
21701
|
+
* identical to the live path. Byte-for-byte equal to what a live deny emits. */
|
|
21702
|
+
denyPayload(findings, phase) {
|
|
21703
|
+
return denyWire(formatDenial(findings), hookKindFor(phase));
|
|
21704
|
+
}
|
|
21705
|
+
/**
|
|
21706
|
+
* The authoritative synchronous path, enforced in order: parse → validate identity →
|
|
21707
|
+
* decide. Each step that fails does so CLOSED (deny), except `post-action`, which is
|
|
21708
|
+
* observation-only and returns `unsupported` (never a deny wire, never a PreToolUse
|
|
21709
|
+
* fallthrough — BLOCKER 2).
|
|
21710
|
+
*
|
|
21711
|
+
* 1. `post-action` → `unsupported` (Claude has no per-tool post-action veto).
|
|
21712
|
+
* 2. parse failure → `parse-failure`, byte-identical to the live fail-closed deny.
|
|
21713
|
+
* 3. identity claim rejected → a fail-closed DENY, BEFORE any content evaluation, so a
|
|
21714
|
+
* runtime-supplied cwd pointing at another repository can never reach the detectors.
|
|
21715
|
+
* 4. otherwise → delegate to the canonical `preToolUseFromRaw` / `stopFromRaw`, PASSING
|
|
21716
|
+
* the validated trusted root so the live path re-runs the same shared identity check.
|
|
21717
|
+
* `wire` is byte-identical to the live CLI — every deny/allow decision, the fail-closed
|
|
21718
|
+
* wrapping, repo-root resolution (#412), the turn baseline and the transport remain
|
|
21719
|
+
* exactly as src/cli/hook.ts computes them.
|
|
21720
|
+
*/
|
|
21721
|
+
decide(raw, phase, defaultCwd) {
|
|
21722
|
+
if (phase === "post-action") {
|
|
21723
|
+
return {
|
|
21724
|
+
outcome: "unsupported",
|
|
21725
|
+
detail: "Claude Code has no per-tool post-action veto; the end-of-turn Stop sweep is its post-turn reconciliation."
|
|
21726
|
+
};
|
|
21727
|
+
}
|
|
21728
|
+
const parsed = this.parseEvent(raw, phase);
|
|
21729
|
+
if ("failure" in parsed) {
|
|
21730
|
+
const live2 = phase === "end-of-turn" ? stopFromRaw(raw, defaultCwd) : preToolUseFromRaw(raw, defaultCwd);
|
|
21731
|
+
return {
|
|
21732
|
+
outcome: "parse-failure",
|
|
21733
|
+
detail: parsed.detail,
|
|
21734
|
+
wire: live2.stdout,
|
|
21735
|
+
decision: { verdict: "deny", findings: [], reason: live2.stdout }
|
|
21736
|
+
};
|
|
21737
|
+
}
|
|
21738
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
21739
|
+
if (!idv.ok) {
|
|
21740
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
21741
|
+
const wire2 = this.denyPayload(findings, phase);
|
|
21742
|
+
return { outcome: "ok", wire: wire2, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire2 } };
|
|
21743
|
+
}
|
|
21744
|
+
const live = phase === "end-of-turn" ? stopFromRaw(raw, defaultCwd, idv.trustedRoot) : preToolUseFromRaw(raw, defaultCwd, idv.trustedRoot);
|
|
21745
|
+
const wire = live.stdout;
|
|
21746
|
+
const denied = wire.length > 0;
|
|
21747
|
+
return {
|
|
21748
|
+
outcome: "ok",
|
|
21749
|
+
wire,
|
|
21750
|
+
decision: { verdict: denied ? "deny" : "allow", findings: [], reason: denied ? wire : void 0 }
|
|
21751
|
+
};
|
|
21752
|
+
}
|
|
21753
|
+
/**
|
|
21754
|
+
* Map a transport failure (or a required hook that did not fire) onto a fail-closed
|
|
21755
|
+
* DENY at the neutral seam. Claude's LIVE client additionally degrades a hook-service
|
|
21756
|
+
* transport failure to the in-process verdict (src/cli/index.ts / hook-client.ts) rather
|
|
21757
|
+
* than surfacing it here, so this method documents the contract guarantee for the seam;
|
|
21758
|
+
* a partial adapter with no in-process fallback denies through exactly this path.
|
|
21759
|
+
*/
|
|
21760
|
+
failClosed(outcome, detail, phase) {
|
|
21761
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
21762
|
+
}
|
|
21763
|
+
};
|
|
21764
|
+
claudeAdapter = new ClaudeRuntimeAdapter();
|
|
21765
|
+
}
|
|
21766
|
+
});
|
|
21767
|
+
|
|
21768
|
+
// src/adapters/apply-patch.ts
|
|
21769
|
+
import { isAbsolute as isAbsolute14, relative as relative9 } from "node:path";
|
|
21770
|
+
function readDisk2(path) {
|
|
21771
|
+
return textOf2(inspectResolved(path));
|
|
21772
|
+
}
|
|
21773
|
+
function relForDisplay2(path, cwd) {
|
|
21774
|
+
const rel = relative9(cwd, path);
|
|
21775
|
+
return rel && !rel.startsWith("..") && !isAbsolute14(rel) ? rel : path;
|
|
21776
|
+
}
|
|
21777
|
+
function parsePatchSections(patch) {
|
|
21778
|
+
const lines = patch.replace(/\r\n/g, "\n").split("\n");
|
|
21779
|
+
let i = 0;
|
|
21780
|
+
while (i < lines.length && !/^\*\*\* Begin Patch/.test(lines[i])) i++;
|
|
21781
|
+
if (i >= lines.length) throw new Error('apply_patch payload has no "*** Begin Patch" header');
|
|
21782
|
+
i++;
|
|
21783
|
+
const sections = [];
|
|
21784
|
+
let current = null;
|
|
21785
|
+
const flush = () => {
|
|
21786
|
+
if (current) sections.push(current);
|
|
21787
|
+
current = null;
|
|
21788
|
+
};
|
|
21789
|
+
for (; i < lines.length; i++) {
|
|
21790
|
+
const line = lines[i];
|
|
21791
|
+
if (/^\*\*\* End Patch/.test(line)) {
|
|
21792
|
+
flush();
|
|
21793
|
+
return sections;
|
|
21794
|
+
}
|
|
21795
|
+
const add = /^\*\*\* Add File: (.*)$/.exec(line);
|
|
21796
|
+
const upd = /^\*\*\* Update File: (.*)$/.exec(line);
|
|
21797
|
+
const del = /^\*\*\* Delete File: (.*)$/.exec(line);
|
|
21798
|
+
const move = /^\*\*\* Move to: (.*)$/.exec(line);
|
|
21799
|
+
if (add) {
|
|
21800
|
+
flush();
|
|
21801
|
+
current = { op: "add", path: add[1].trim(), body: [] };
|
|
21802
|
+
} else if (upd) {
|
|
21803
|
+
flush();
|
|
21804
|
+
current = { op: "update", path: upd[1].trim(), body: [] };
|
|
21805
|
+
} else if (del) {
|
|
21806
|
+
flush();
|
|
21807
|
+
current = { op: "delete", path: del[1].trim(), body: [] };
|
|
21808
|
+
} else if (move && current) {
|
|
21809
|
+
current.moveTo = move[1].trim();
|
|
21810
|
+
} else if (current) {
|
|
21811
|
+
current.body.push(line);
|
|
21812
|
+
}
|
|
21813
|
+
}
|
|
21814
|
+
throw new Error('apply_patch payload is missing its "*** End Patch" trailer');
|
|
21815
|
+
}
|
|
21816
|
+
function addedContent(body) {
|
|
21817
|
+
return body.filter((l) => l.startsWith("+")).map((l) => l.slice(1)).join("\n");
|
|
21818
|
+
}
|
|
21819
|
+
function applyUpdate(before, body) {
|
|
21820
|
+
const lines = before.split("\n");
|
|
21821
|
+
const hunks = [];
|
|
21822
|
+
let hunk = [];
|
|
21823
|
+
for (const l of body) {
|
|
21824
|
+
if (l.startsWith("@@")) {
|
|
21825
|
+
if (hunk.length) hunks.push(hunk);
|
|
21826
|
+
hunk = [];
|
|
21827
|
+
continue;
|
|
21828
|
+
}
|
|
21829
|
+
hunk.push(l);
|
|
21830
|
+
}
|
|
21831
|
+
if (hunk.length) hunks.push(hunk);
|
|
21832
|
+
let cursor = 0;
|
|
21833
|
+
for (const h of hunks) {
|
|
21834
|
+
const oldBlock = [];
|
|
21835
|
+
const newBlock = [];
|
|
21836
|
+
for (const l of h) {
|
|
21837
|
+
if (l === "") {
|
|
21838
|
+
oldBlock.push("");
|
|
21839
|
+
newBlock.push("");
|
|
21840
|
+
} else if (l.startsWith(" ")) {
|
|
21841
|
+
oldBlock.push(l.slice(1));
|
|
21842
|
+
newBlock.push(l.slice(1));
|
|
21843
|
+
} else if (l.startsWith("-")) {
|
|
21844
|
+
oldBlock.push(l.slice(1));
|
|
21845
|
+
} else if (l.startsWith("+")) {
|
|
21846
|
+
newBlock.push(l.slice(1));
|
|
21847
|
+
} else {
|
|
21848
|
+
return null;
|
|
21849
|
+
}
|
|
21850
|
+
}
|
|
21851
|
+
if (oldBlock.length === 0) return null;
|
|
21852
|
+
let at = -1;
|
|
21853
|
+
for (let s = cursor; s <= lines.length - oldBlock.length; s++) {
|
|
21854
|
+
let ok = true;
|
|
21855
|
+
for (let k = 0; k < oldBlock.length; k++) {
|
|
21856
|
+
if (lines[s + k] !== oldBlock[k]) {
|
|
21857
|
+
ok = false;
|
|
21858
|
+
break;
|
|
21859
|
+
}
|
|
21860
|
+
}
|
|
21861
|
+
if (ok) {
|
|
21862
|
+
at = s;
|
|
21863
|
+
break;
|
|
21864
|
+
}
|
|
21865
|
+
}
|
|
21866
|
+
if (at < 0) return null;
|
|
21867
|
+
lines.splice(at, oldBlock.length, ...newBlock);
|
|
21868
|
+
cursor = at + newBlock.length;
|
|
21869
|
+
}
|
|
21870
|
+
return lines.join("\n");
|
|
21871
|
+
}
|
|
21872
|
+
function applyPatchChanges(patch, abs, cwd) {
|
|
21873
|
+
const out3 = [];
|
|
21874
|
+
for (const section of parsePatchSections(patch)) {
|
|
21875
|
+
const targetRel = relForDisplay2(abs(section.moveTo ?? section.path), cwd);
|
|
21876
|
+
if (section.op === "add") {
|
|
21877
|
+
out3.push(...synthFileChange(targetRel, null, addedContent(section.body)));
|
|
21878
|
+
} else if (section.op === "delete") {
|
|
21879
|
+
out3.push(...synthFileChange(targetRel, readDisk2(abs(section.path)), null));
|
|
21880
|
+
} else {
|
|
21881
|
+
const before = readDisk2(abs(section.path));
|
|
21882
|
+
const after = applyUpdate(before ?? "", section.body);
|
|
21883
|
+
if (after === null) {
|
|
21884
|
+
throw new Error(
|
|
21885
|
+
`cannot reconstruct the apply_patch update to ${section.path}: its hunks did not locate against the file on disk`
|
|
21886
|
+
);
|
|
21887
|
+
}
|
|
21888
|
+
const synth = synthFileChange(targetRel, before, after);
|
|
21889
|
+
if (section.moveTo && section.moveTo !== section.path) {
|
|
21890
|
+
out3.push(...synth.map((c) => ({ ...c, op: "rename", oldPath: relForDisplay2(abs(section.path), cwd) })));
|
|
21891
|
+
} else {
|
|
21892
|
+
out3.push(...synth);
|
|
21893
|
+
}
|
|
21894
|
+
}
|
|
21895
|
+
}
|
|
21896
|
+
return out3;
|
|
21897
|
+
}
|
|
21898
|
+
var init_apply_patch = __esm({
|
|
21899
|
+
"src/adapters/apply-patch.ts"() {
|
|
21900
|
+
"use strict";
|
|
21901
|
+
init_changes();
|
|
21902
|
+
init_disk();
|
|
21903
|
+
}
|
|
21904
|
+
});
|
|
21905
|
+
|
|
21906
|
+
// src/adapters/codex/changes.ts
|
|
21907
|
+
import { isAbsolute as isAbsolute15, relative as relative10, resolve as resolve20 } from "node:path";
|
|
21908
|
+
function asStr2(v) {
|
|
21909
|
+
return typeof v === "string" ? v : "";
|
|
21910
|
+
}
|
|
21911
|
+
function readDisk3(path) {
|
|
21912
|
+
return textOf2(inspectResolved(path));
|
|
21913
|
+
}
|
|
21914
|
+
function relForDisplay3(path, cwd) {
|
|
21915
|
+
const rel = relative10(cwd, path);
|
|
21916
|
+
return rel && !rel.startsWith("..") && !isAbsolute15(rel) ? rel : path;
|
|
21917
|
+
}
|
|
21918
|
+
function shellChanges(args) {
|
|
21919
|
+
const cmd = args.command ?? args.argv;
|
|
21920
|
+
if (Array.isArray(cmd)) {
|
|
21921
|
+
const argv = cmd.map(asStr2).filter((s) => s.length > 0);
|
|
21922
|
+
if (!argv.length) throw new Error("shell event carries no command/argv to reconstruct");
|
|
21923
|
+
return [{ kind: "command", raw: argv.join(" "), argv }];
|
|
21924
|
+
}
|
|
21925
|
+
const raw = asStr2(cmd);
|
|
21926
|
+
if (!raw) throw new Error("shell event carries no command to reconstruct");
|
|
21927
|
+
return [{ kind: "command", raw, argv: raw.split(/\s+/) }];
|
|
21928
|
+
}
|
|
21929
|
+
function changesFromCodex(operation, cwd, base = cwd) {
|
|
21930
|
+
const args = operation.args ?? {};
|
|
21931
|
+
const abs = (path) => resolve20(base, path);
|
|
21932
|
+
if (operation.kind === "shell") return shellChanges(args);
|
|
21933
|
+
if (operation.kind !== "file-edit") return [];
|
|
21934
|
+
if (operation.name === "apply_patch" || operation.name === "patch") {
|
|
21935
|
+
const patch = asStr2(args.command) || asStr2(args.patch) || asStr2(args.input);
|
|
21936
|
+
if (!patch) throw new Error("apply_patch event carries no command/patch/input to reconstruct");
|
|
21937
|
+
return applyPatchChanges(patch, abs, cwd);
|
|
21938
|
+
}
|
|
21939
|
+
const fp = asStr2(args.path) || asStr2(args.file_path);
|
|
21940
|
+
if (!fp) throw new Error(`${operation.name || "file-edit"} event carries no path to reconstruct`);
|
|
21941
|
+
const before = readDisk3(abs(fp));
|
|
21942
|
+
const old = args.old_string ?? args.old_str ?? args.old;
|
|
21943
|
+
const next = args.new_string ?? args.new_str ?? args.new;
|
|
21944
|
+
if (typeof old === "string" || typeof next === "string") {
|
|
21945
|
+
const after = applyEdit(before, asStr2(old), asStr2(next), args.replace_all === true);
|
|
21946
|
+
return synthFileChange(relForDisplay3(abs(fp), cwd), before, after);
|
|
21947
|
+
}
|
|
21948
|
+
const content = args.content ?? args.text;
|
|
21949
|
+
if (typeof content === "string") {
|
|
21950
|
+
return synthFileChange(relForDisplay3(abs(fp), cwd), before, content);
|
|
21951
|
+
}
|
|
21952
|
+
throw new Error(`${operation.name || "file-edit"} event for ${fp} carries no old_string/new_string/content to reconstruct`);
|
|
21953
|
+
}
|
|
21954
|
+
var init_changes2 = __esm({
|
|
21955
|
+
"src/adapters/codex/changes.ts"() {
|
|
21956
|
+
"use strict";
|
|
21957
|
+
init_changes();
|
|
21958
|
+
init_apply_patch();
|
|
21959
|
+
init_disk();
|
|
21960
|
+
}
|
|
21961
|
+
});
|
|
21962
|
+
|
|
21963
|
+
// src/adapters/codex/deny.ts
|
|
21964
|
+
function assertCodexWirePhase(phase) {
|
|
21965
|
+
if (phase !== "pre-action" && phase !== "end-of-turn") {
|
|
21966
|
+
throw new Error(`unsupported Codex wire phase: ${phase}`);
|
|
21967
|
+
}
|
|
21968
|
+
}
|
|
21969
|
+
function codexWire(reason, phase) {
|
|
21970
|
+
assertCodexWirePhase(phase);
|
|
21971
|
+
const payload = phase === "end-of-turn" ? { decision: "block", reason } : {
|
|
21972
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
|
|
21973
|
+
decision: "block",
|
|
21974
|
+
reason
|
|
21975
|
+
};
|
|
21976
|
+
return JSON.stringify(payload) + "\n";
|
|
21977
|
+
}
|
|
21978
|
+
function codexDenyWire(findings, phase) {
|
|
21979
|
+
assertCodexWirePhase(phase);
|
|
21980
|
+
if (findings.length === 0) return "";
|
|
21981
|
+
return codexWire(formatDenial(findings), phase);
|
|
21982
|
+
}
|
|
21983
|
+
var init_deny2 = __esm({
|
|
21984
|
+
"src/adapters/codex/deny.ts"() {
|
|
21985
|
+
"use strict";
|
|
21986
|
+
init_deny();
|
|
21987
|
+
}
|
|
21988
|
+
});
|
|
21989
|
+
|
|
21990
|
+
// src/adapters/codex/schema.ts
|
|
21991
|
+
function codexOperationKind(toolName) {
|
|
21992
|
+
if (!toolName) return "other";
|
|
21993
|
+
if (SHELL_TOOLS.has(toolName)) return "shell";
|
|
21994
|
+
if (FILE_EDIT_TOOLS.has(toolName)) return "file-edit";
|
|
21995
|
+
if (toolName.startsWith(MCP_PREFIX)) return "mcp";
|
|
21996
|
+
if (FILE_READ_TOOLS.has(toolName)) return "file-read";
|
|
21997
|
+
return "other";
|
|
21998
|
+
}
|
|
21999
|
+
function inputFrom(parsed) {
|
|
22000
|
+
return {
|
|
22001
|
+
...typeof parsed.hook_event_name === "string" ? { hook_event_name: parsed.hook_event_name } : {},
|
|
22002
|
+
...typeof parsed.tool_name === "string" ? { tool_name: parsed.tool_name } : {},
|
|
22003
|
+
...isRecord(parsed.tool_input) ? { tool_input: parsed.tool_input } : {},
|
|
22004
|
+
...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
|
|
22005
|
+
...typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {},
|
|
22006
|
+
...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {}
|
|
22007
|
+
};
|
|
22008
|
+
}
|
|
22009
|
+
function eventFrom2(input, phase) {
|
|
22010
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: codexOperationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
|
|
22011
|
+
const identity2 = { claimedCwd: input.cwd, sessionId: input.session_id };
|
|
22012
|
+
return { phase, operation, identity: identity2 };
|
|
22013
|
+
}
|
|
22014
|
+
function normalizeCodexEvent(raw, phase) {
|
|
22015
|
+
if (!raw.trim()) return eventFrom2({}, phase);
|
|
22016
|
+
let parsed;
|
|
22017
|
+
try {
|
|
22018
|
+
parsed = JSON.parse(raw);
|
|
22019
|
+
} catch (e) {
|
|
22020
|
+
return { failure: "parse-failure", detail: `the Codex hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
22021
|
+
}
|
|
22022
|
+
if (!isRecord(parsed)) {
|
|
22023
|
+
return {
|
|
22024
|
+
failure: "parse-failure",
|
|
22025
|
+
detail: `the Codex hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})`
|
|
22026
|
+
};
|
|
22027
|
+
}
|
|
22028
|
+
return eventFrom2(inputFrom(parsed), phase);
|
|
22029
|
+
}
|
|
22030
|
+
var SHELL_TOOLS, FILE_EDIT_TOOLS, FILE_READ_TOOLS, MCP_PREFIX;
|
|
22031
|
+
var init_schema = __esm({
|
|
22032
|
+
"src/adapters/codex/schema.ts"() {
|
|
22033
|
+
"use strict";
|
|
22034
|
+
init_narrow();
|
|
22035
|
+
SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash"]);
|
|
22036
|
+
FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["apply_patch", "Write", "Edit"]);
|
|
22037
|
+
FILE_READ_TOOLS = /* @__PURE__ */ new Set(["view_image"]);
|
|
22038
|
+
MCP_PREFIX = "mcp__";
|
|
22039
|
+
}
|
|
22040
|
+
});
|
|
22041
|
+
|
|
22042
|
+
// src/adapters/codex/adapter.ts
|
|
22043
|
+
var POST_OBSERVE, CodexRuntimeAdapter, codexAdapter;
|
|
22044
|
+
var init_adapter3 = __esm({
|
|
22045
|
+
"src/adapters/codex/adapter.ts"() {
|
|
22046
|
+
"use strict";
|
|
22047
|
+
init_engine();
|
|
22048
|
+
init_policy_load();
|
|
22049
|
+
init_hook();
|
|
22050
|
+
init_session();
|
|
22051
|
+
init_repo_context();
|
|
22052
|
+
init_contract();
|
|
22053
|
+
init_changes2();
|
|
22054
|
+
init_deny2();
|
|
22055
|
+
init_schema();
|
|
22056
|
+
POST_OBSERVE = ["shell", "file-edit", "file-read", "mcp", "other"];
|
|
22057
|
+
CodexRuntimeAdapter = class {
|
|
22058
|
+
name = "codex";
|
|
22059
|
+
/**
|
|
22060
|
+
* CONSERVATIVE and honest. Pre-action deny ENFORCEMENT is not yet proven on a pinned
|
|
22061
|
+
* Codex build, so `preDeny` is empty — the adapter never claims a synchronous veto it
|
|
22062
|
+
* cannot demonstrate. Codex does surface post-execution tool outcomes and an end-of-turn
|
|
22063
|
+
* stop, so those are declared; every real gap is named in `unsupported`. The
|
|
22064
|
+
* `probe:codex-runtime` harness is what may later justify moving a kind into `preDeny`.
|
|
22065
|
+
*/
|
|
22066
|
+
capabilities = {
|
|
22067
|
+
preDeny: [],
|
|
22068
|
+
postObserve: POST_OBSERVE,
|
|
22069
|
+
endOfTurn: true,
|
|
22070
|
+
unsupported: [
|
|
22071
|
+
"pre-action deny enforcement not yet proven on a pinned Codex build (see probe:codex-runtime)",
|
|
22072
|
+
"fail-closed hook transport not yet proven (openai/codex#41979)",
|
|
22073
|
+
"network-egress control",
|
|
22074
|
+
"identity / authentication"
|
|
22075
|
+
]
|
|
22076
|
+
};
|
|
22077
|
+
parseEvent(raw, phase) {
|
|
22078
|
+
return normalizeCodexEvent(raw, phase);
|
|
22079
|
+
}
|
|
22080
|
+
/** Identity is a CLAIM validated against the runner's independently derived trusted root,
|
|
22081
|
+
* reusing `repoContext` / `validateClaimAgainstRoot` exactly as the Claude adapter does.
|
|
22082
|
+
* It never weakens the shared check. */
|
|
22083
|
+
validateIdentity(claim, defaultCwd) {
|
|
22084
|
+
const base = defaultCwd ?? process.cwd();
|
|
22085
|
+
const runnerCtx = repoContext(base);
|
|
22086
|
+
if (!runnerCtx) {
|
|
22087
|
+
return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
|
|
22088
|
+
}
|
|
22089
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
22090
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
22091
|
+
}
|
|
22092
|
+
denyPayload(findings, phase) {
|
|
22093
|
+
if (phase === "post-action") {
|
|
22094
|
+
throw new Error("Codex post-action is observation-only and cannot produce a deny wire");
|
|
22095
|
+
}
|
|
22096
|
+
return codexDenyWire(findings, phase);
|
|
22097
|
+
}
|
|
22098
|
+
/**
|
|
22099
|
+
* parse → validate identity → decide, every failure CLOSED (deny), in order:
|
|
22100
|
+
*
|
|
22101
|
+
* 1. `post-action` → `unsupported` (observation-only; never a deny wire).
|
|
22102
|
+
* 2. parse failure → a fail-closed deny in Codex's wire.
|
|
22103
|
+
* 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
|
|
22104
|
+
* 4. `end-of-turn` → delegate to the canonical git sweep, re-wrapped in Codex's wire.
|
|
22105
|
+
* 5. `pre-action` → reconstruct Change[] and run the SAME `evaluate` engine; a deny
|
|
22106
|
+
* carries the shared denial reason. A reconstruction that cannot be modelled fails
|
|
22107
|
+
* CLOSED to deny rather than allowing an unseen edit.
|
|
22108
|
+
*/
|
|
22109
|
+
decide(raw, phase, defaultCwd) {
|
|
22110
|
+
if (phase === "post-action") {
|
|
22111
|
+
return {
|
|
22112
|
+
outcome: "unsupported",
|
|
22113
|
+
detail: "Codex post-action is observation-only; the end-of-turn Stop sweep is the post-turn reconciliation."
|
|
22114
|
+
};
|
|
22115
|
+
}
|
|
22116
|
+
const parsed = this.parseEvent(raw, phase);
|
|
22117
|
+
if ("failure" in parsed) {
|
|
22118
|
+
const findings = [steeringUnavailableFinding(`unparseable Codex event: ${parsed.detail}`)];
|
|
22119
|
+
const wire = this.denyPayload(findings, phase);
|
|
22120
|
+
return { outcome: "parse-failure", detail: parsed.detail, wire, decision: { verdict: "deny", findings, reason: wire } };
|
|
22121
|
+
}
|
|
22122
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
22123
|
+
if (!idv.ok) {
|
|
22124
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
22125
|
+
const wire = this.denyPayload(findings, phase);
|
|
22126
|
+
return { outcome: "ok", wire, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire } };
|
|
22127
|
+
}
|
|
22128
|
+
if (phase === "end-of-turn") {
|
|
22129
|
+
const wire = stopFromRaw(raw, defaultCwd, idv.trustedRoot).stdout;
|
|
22130
|
+
return { outcome: "ok", wire, decision: { verdict: wire ? "deny" : "allow", findings: [], reason: wire || void 0 } };
|
|
22131
|
+
}
|
|
22132
|
+
try {
|
|
22133
|
+
const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
|
|
22134
|
+
const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
|
|
22135
|
+
turnBaseline(root, parsed.identity.sessionId);
|
|
22136
|
+
const policy = loadPolicy(root);
|
|
22137
|
+
const changes = changesFromCodex(parsed.operation, root, sessionCwd);
|
|
22138
|
+
const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22139
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22140
|
+
return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
|
|
22141
|
+
} catch (e) {
|
|
22142
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
22143
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
22144
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22145
|
+
return { outcome: "ok", wire, detail, decision: { verdict: "deny", findings, reason: wire } };
|
|
22146
|
+
}
|
|
22147
|
+
}
|
|
22148
|
+
/** A transport failure or a required hook that did not fire → a fail-closed deny at the
|
|
22149
|
+
* seam, through the adapter's own `denyPayload` so the wire is Codex's native envelope. */
|
|
22150
|
+
failClosed(outcome, detail, phase) {
|
|
22151
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
22152
|
+
}
|
|
22153
|
+
};
|
|
22154
|
+
codexAdapter = new CodexRuntimeAdapter();
|
|
22155
|
+
}
|
|
22156
|
+
});
|
|
22157
|
+
|
|
22158
|
+
// src/adapters/copilot/changes.ts
|
|
22159
|
+
import { isAbsolute as isAbsolute16, relative as relative11, resolve as resolve21 } from "node:path";
|
|
22160
|
+
function asStr3(v) {
|
|
22161
|
+
return typeof v === "string" ? v : "";
|
|
22162
|
+
}
|
|
22163
|
+
function readDisk4(path) {
|
|
22164
|
+
return textOf2(inspectResolved(path));
|
|
22165
|
+
}
|
|
22166
|
+
function relForDisplay4(path, cwd) {
|
|
22167
|
+
const rel = relative11(cwd, path);
|
|
22168
|
+
return rel && !rel.startsWith("..") && !isAbsolute16(rel) ? rel : path;
|
|
22169
|
+
}
|
|
22170
|
+
function shellChanges2(args) {
|
|
22171
|
+
const cmd = args.command ?? args.input ?? args.argv;
|
|
22172
|
+
if (Array.isArray(cmd)) {
|
|
22173
|
+
const argv = cmd.map(asStr3).filter((s) => s.length > 0);
|
|
22174
|
+
if (!argv.length) throw new Error("shell event carries no command/input/argv to reconstruct");
|
|
22175
|
+
return [{ kind: "command", raw: argv.join(" "), argv }];
|
|
22176
|
+
}
|
|
22177
|
+
const raw = asStr3(cmd);
|
|
22178
|
+
if (!raw) throw new Error("shell event carries no command/input to reconstruct");
|
|
22179
|
+
return [{ kind: "command", raw, argv: raw.split(/\s+/) }];
|
|
22180
|
+
}
|
|
22181
|
+
function changesFromCopilot(operation, cwd, base = cwd) {
|
|
22182
|
+
const args = operation.args ?? {};
|
|
22183
|
+
const abs = (path) => resolve21(base, path);
|
|
22184
|
+
if (operation.kind === "shell") return shellChanges2(args);
|
|
22185
|
+
if (operation.kind !== "file-edit") return [];
|
|
22186
|
+
const name = (operation.name || "").toLowerCase();
|
|
22187
|
+
if (name === "apply_patch") {
|
|
22188
|
+
const patch = asStr3(args.input) || asStr3(args.patch) || asStr3(args.command);
|
|
22189
|
+
if (!patch) throw new Error("apply_patch event carries no input/patch/command to reconstruct");
|
|
22190
|
+
return applyPatchChanges(patch, abs, cwd);
|
|
22191
|
+
}
|
|
22192
|
+
const fp = asStr3(args.path) || asStr3(args.file_path) || asStr3(args.filename);
|
|
22193
|
+
if (!fp) throw new Error(`${operation.name || "file-edit"} event carries no path to reconstruct`);
|
|
22194
|
+
const before = readDisk4(abs(fp));
|
|
22195
|
+
const display2 = relForDisplay4(abs(fp), cwd);
|
|
22196
|
+
if (name === "str_replace_editor") {
|
|
22197
|
+
const old2 = args.old_str ?? args.old_string ?? args.old;
|
|
22198
|
+
const next2 = args.new_str ?? args.new_string ?? args.new;
|
|
22199
|
+
if (typeof old2 === "string" || typeof next2 === "string") {
|
|
22200
|
+
return synthFileChange(display2, before, applyEdit(before, asStr3(old2), asStr3(next2), args.replace_all === true));
|
|
22201
|
+
}
|
|
22202
|
+
const fileText = args.file_text ?? args.content ?? args.text;
|
|
22203
|
+
if (typeof fileText === "string") return synthFileChange(display2, before, fileText);
|
|
22204
|
+
throw new Error(
|
|
22205
|
+
`str_replace_editor sub-op "${asStr3(args.command) || "unknown"}" for ${fp} cannot be reconstructed exactly (only str_replace/create are modelled; others fail closed pending a real payload)`
|
|
22206
|
+
);
|
|
22207
|
+
}
|
|
22208
|
+
if (Array.isArray(args.edits)) {
|
|
22209
|
+
let after = before;
|
|
22210
|
+
for (const raw of args.edits) {
|
|
22211
|
+
const ed = isRecord(raw) ? raw : {};
|
|
22212
|
+
after = applyEdit(after, asStr3(ed.old_string ?? ed.old_str), asStr3(ed.new_string ?? ed.new_str), ed.replace_all === true);
|
|
22213
|
+
}
|
|
22214
|
+
return synthFileChange(display2, before, after);
|
|
22215
|
+
}
|
|
22216
|
+
const old = args.old_string ?? args.old_str ?? args.old;
|
|
22217
|
+
const next = args.new_string ?? args.new_str ?? args.new;
|
|
22218
|
+
if (typeof old === "string" || typeof next === "string") {
|
|
22219
|
+
return synthFileChange(display2, before, applyEdit(before, asStr3(old), asStr3(next), args.replace_all === true));
|
|
22220
|
+
}
|
|
22221
|
+
const content = args.content ?? args.text ?? args.file_text;
|
|
22222
|
+
if (typeof content === "string") return synthFileChange(display2, before, content);
|
|
22223
|
+
throw new Error(
|
|
22224
|
+
`${operation.name || "file-edit"} event for ${fp} carries no old_string/new_string/content to reconstruct`
|
|
22225
|
+
);
|
|
22226
|
+
}
|
|
22227
|
+
var init_changes3 = __esm({
|
|
22228
|
+
"src/adapters/copilot/changes.ts"() {
|
|
22229
|
+
"use strict";
|
|
22230
|
+
init_changes();
|
|
22231
|
+
init_apply_patch();
|
|
22232
|
+
init_disk();
|
|
22233
|
+
init_narrow();
|
|
22234
|
+
}
|
|
22235
|
+
});
|
|
22236
|
+
|
|
22237
|
+
// src/adapters/copilot/deny.ts
|
|
22238
|
+
function assertCopilotWirePhase(phase) {
|
|
22239
|
+
const known = ["pre-action", "end-of-turn"];
|
|
22240
|
+
if (!known.includes(phase)) {
|
|
22241
|
+
throw new Error(`unsupported Copilot wire phase: ${phase}`);
|
|
22242
|
+
}
|
|
22243
|
+
}
|
|
22244
|
+
function copilotWire(reason, phase) {
|
|
22245
|
+
assertCopilotWirePhase(phase);
|
|
22246
|
+
const payload = phase === "end-of-turn" ? { decision: "block", reason } : { permissionDecision: "deny", permissionDecisionReason: reason };
|
|
22247
|
+
return JSON.stringify(payload) + "\n";
|
|
22248
|
+
}
|
|
22249
|
+
function copilotDenyWire(findings, phase) {
|
|
22250
|
+
assertCopilotWirePhase(phase);
|
|
22251
|
+
if (findings.length === 0) return "";
|
|
22252
|
+
return copilotWire(formatDenial(findings), phase);
|
|
22253
|
+
}
|
|
22254
|
+
var init_deny3 = __esm({
|
|
22255
|
+
"src/adapters/copilot/deny.ts"() {
|
|
22256
|
+
"use strict";
|
|
22257
|
+
init_deny();
|
|
22258
|
+
}
|
|
22259
|
+
});
|
|
22260
|
+
|
|
22261
|
+
// src/adapters/copilot/schema.ts
|
|
22262
|
+
function copilotOperationKind(toolName) {
|
|
22263
|
+
if (!toolName) return "other";
|
|
22264
|
+
const name = toolName.toLowerCase();
|
|
22265
|
+
if (SHELL_TOOLS2.has(name)) return "shell";
|
|
22266
|
+
if (FILE_EDIT_TOOLS2.has(name)) return "file-edit";
|
|
22267
|
+
if (name.startsWith(MCP_PREFIX2)) return "mcp";
|
|
22268
|
+
if (FILE_READ_TOOLS2.has(name)) return "file-read";
|
|
22269
|
+
return "other";
|
|
22270
|
+
}
|
|
22271
|
+
function argsFrom(parsed) {
|
|
22272
|
+
if (isRecord(parsed.tool_input)) return parsed.tool_input;
|
|
22273
|
+
const ta = parsed.toolArgs;
|
|
22274
|
+
if (isRecord(ta)) return ta;
|
|
22275
|
+
if (typeof ta === "string" && ta.trim()) {
|
|
22276
|
+
try {
|
|
22277
|
+
const decoded = JSON.parse(ta);
|
|
22278
|
+
return isRecord(decoded) ? decoded : void 0;
|
|
22279
|
+
} catch {
|
|
22280
|
+
return void 0;
|
|
22281
|
+
}
|
|
22282
|
+
}
|
|
22283
|
+
return void 0;
|
|
22284
|
+
}
|
|
22285
|
+
function inputFrom2(parsed) {
|
|
22286
|
+
const toolName = typeof parsed.tool_name === "string" ? parsed.tool_name : typeof parsed.toolName === "string" ? parsed.toolName : void 0;
|
|
22287
|
+
const sessionId = typeof parsed.session_id === "string" ? parsed.session_id : typeof parsed.sessionId === "string" ? parsed.sessionId : void 0;
|
|
22288
|
+
const args = argsFrom(parsed);
|
|
22289
|
+
return {
|
|
22290
|
+
...toolName !== void 0 ? { tool_name: toolName } : {},
|
|
22291
|
+
...args !== void 0 ? { tool_input: args } : {},
|
|
22292
|
+
...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
|
|
22293
|
+
...sessionId !== void 0 ? { session_id: sessionId } : {},
|
|
22294
|
+
...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {}
|
|
22295
|
+
};
|
|
22296
|
+
}
|
|
22297
|
+
function eventFrom3(input, phase) {
|
|
22298
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: copilotOperationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
|
|
22299
|
+
const identity2 = { claimedCwd: input.cwd, sessionId: input.session_id };
|
|
22300
|
+
return { phase, operation, identity: identity2 };
|
|
22301
|
+
}
|
|
22302
|
+
function parseRaw(raw) {
|
|
22303
|
+
if (!raw.trim()) return { kind: "absent" };
|
|
22304
|
+
let parsed;
|
|
22305
|
+
try {
|
|
22306
|
+
parsed = JSON.parse(raw);
|
|
22307
|
+
} catch (e) {
|
|
22308
|
+
return { kind: "fail", detail: `the Copilot hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
22309
|
+
}
|
|
22310
|
+
if (!isRecord(parsed)) {
|
|
22311
|
+
return { kind: "fail", detail: `the Copilot hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})` };
|
|
22312
|
+
}
|
|
22313
|
+
return { kind: "ok", record: parsed };
|
|
22314
|
+
}
|
|
22315
|
+
function normalizeCopilotEvent(raw, phase) {
|
|
22316
|
+
const parsed = parseRaw(raw);
|
|
22317
|
+
if (parsed.kind === "absent") return eventFrom3({}, phase);
|
|
22318
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22319
|
+
return eventFrom3(inputFrom2(parsed.record), phase);
|
|
22320
|
+
}
|
|
22321
|
+
function copilotStopInput(raw) {
|
|
22322
|
+
const parsed = parseRaw(raw);
|
|
22323
|
+
if (parsed.kind === "absent") return "{}";
|
|
22324
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22325
|
+
const input = inputFrom2(parsed.record);
|
|
22326
|
+
return JSON.stringify({
|
|
22327
|
+
...input.cwd !== void 0 ? { cwd: input.cwd } : {},
|
|
22328
|
+
...input.session_id !== void 0 ? { session_id: input.session_id } : {},
|
|
22329
|
+
...input.stop_hook_active !== void 0 ? { stop_hook_active: input.stop_hook_active } : {}
|
|
22330
|
+
});
|
|
22331
|
+
}
|
|
22332
|
+
var SHELL_TOOLS2, FILE_EDIT_TOOLS2, FILE_READ_TOOLS2, MCP_PREFIX2;
|
|
22333
|
+
var init_schema2 = __esm({
|
|
22334
|
+
"src/adapters/copilot/schema.ts"() {
|
|
22335
|
+
"use strict";
|
|
22336
|
+
init_narrow();
|
|
22337
|
+
SHELL_TOOLS2 = /* @__PURE__ */ new Set(["bash", "powershell", "write_bash", "write_powershell"]);
|
|
22338
|
+
FILE_EDIT_TOOLS2 = /* @__PURE__ */ new Set(["create", "edit", "write", "multiedit", "apply_patch", "str_replace_editor"]);
|
|
22339
|
+
FILE_READ_TOOLS2 = /* @__PURE__ */ new Set(["view", "read"]);
|
|
22340
|
+
MCP_PREFIX2 = "mcp__";
|
|
22341
|
+
}
|
|
22342
|
+
});
|
|
22343
|
+
|
|
22344
|
+
// src/adapters/copilot/adapter.ts
|
|
22345
|
+
var CopilotRuntimeAdapter, copilotAdapter;
|
|
22346
|
+
var init_adapter4 = __esm({
|
|
22347
|
+
"src/adapters/copilot/adapter.ts"() {
|
|
22348
|
+
"use strict";
|
|
22349
|
+
init_engine();
|
|
22350
|
+
init_policy_load();
|
|
22351
|
+
init_hook();
|
|
22352
|
+
init_session();
|
|
22353
|
+
init_repo_context();
|
|
22354
|
+
init_contract();
|
|
22355
|
+
init_changes3();
|
|
22356
|
+
init_deny3();
|
|
22357
|
+
init_schema2();
|
|
22358
|
+
CopilotRuntimeAdapter = class {
|
|
22359
|
+
name = "github-copilot-cli";
|
|
22360
|
+
/**
|
|
22361
|
+
* CONSERVATIVE and honest. Pre-action deny ENFORCEMENT is not yet proven on a pinned
|
|
22362
|
+
* Copilot CLI build, so `preDeny` is empty — the adapter never claims a synchronous veto
|
|
22363
|
+
* it has not demonstrated on the real runtime. `postObserve` is empty for milestone one:
|
|
22364
|
+
* Copilot does expose a `postToolUse` observation surface, but this adapter does not yet
|
|
22365
|
+
* consume it as a supported post-action path (`decide(..., 'post-action')` returns
|
|
22366
|
+
* `unsupported`), so it declares no post-observe capability rather than advertising kinds it
|
|
22367
|
+
* does not report. Every real gap is named in `unsupported`. The `probe:copilot-runtime`
|
|
22368
|
+
* harness is what may later justify moving a kind into `preDeny` or populating `postObserve`.
|
|
22369
|
+
*/
|
|
22370
|
+
capabilities = {
|
|
22371
|
+
preDeny: [],
|
|
22372
|
+
postObserve: [],
|
|
22373
|
+
endOfTurn: true,
|
|
22374
|
+
unsupported: [
|
|
22375
|
+
"pre-action deny enforcement not yet proven on a pinned Copilot CLI build (see probe:copilot-runtime)",
|
|
22376
|
+
"COMMAND preToolUse hook timeout fails OPEN on Copilot CLI: a timed-out hook lets the tool call proceed (crash / non-zero exit / exit 2 fail closed); an HTTP preToolUse hook fails OPEN on network error / timeout / non-2xx, so only the local command/exec transport is a qualification candidate",
|
|
22377
|
+
"preToolUse is the only PRE-EXECUTION tool veto; agentStop can only block turn completion and force continuation (a lifecycle control, not a filesystem veto), and Copilot overrides the hook after 8 consecutive blocks (stop_hook_active lifecycle); postToolUse observation is not yet consumed by this adapter",
|
|
22378
|
+
"apply_patch / str_replace_editor exact hook payloads are modelled from the published contract but not yet confirmed against a pinned real-run fixture (str_replace_editor sub-ops other than str_replace/create fail closed); a PascalCase Edit carrying a patch-style payload fails closed unless a fixture proves the Edit-field rewrite",
|
|
22379
|
+
"shell-session write tools (write_bash / write_powershell) are classified as mutation-capable shell ops; their exact hook payload is not yet confirmed against a pinned fixture, so an event carrying no reconstructable command/input fails closed",
|
|
22380
|
+
"network-egress control",
|
|
22381
|
+
"identity / authentication"
|
|
22382
|
+
]
|
|
22383
|
+
};
|
|
22384
|
+
parseEvent(raw, phase) {
|
|
22385
|
+
return normalizeCopilotEvent(raw, phase);
|
|
22386
|
+
}
|
|
22387
|
+
/** Identity is a CLAIM validated against the runner's independently derived trusted root,
|
|
22388
|
+
* reusing `repoContext` / `validateClaimAgainstRoot` exactly as the Claude and Codex
|
|
22389
|
+
* adapters do. It never weakens the shared check. */
|
|
22390
|
+
validateIdentity(claim, defaultCwd) {
|
|
22391
|
+
const base = defaultCwd ?? process.cwd();
|
|
22392
|
+
const runnerCtx = repoContext(base);
|
|
22393
|
+
if (!runnerCtx) {
|
|
22394
|
+
return {
|
|
22395
|
+
ok: false,
|
|
22396
|
+
rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against`
|
|
22397
|
+
};
|
|
22398
|
+
}
|
|
22399
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
22400
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
22401
|
+
}
|
|
22402
|
+
denyPayload(findings, phase) {
|
|
22403
|
+
if (phase === "post-action") {
|
|
22404
|
+
throw new Error("Copilot post-action is observation-only and cannot produce a deny wire");
|
|
22405
|
+
}
|
|
22406
|
+
return copilotDenyWire(findings, phase);
|
|
22407
|
+
}
|
|
22408
|
+
/**
|
|
22409
|
+
* parse → validate identity → decide, every failure CLOSED (deny), in order:
|
|
22410
|
+
*
|
|
22411
|
+
* 1. `post-action` → `unsupported` (observation-only; never a deny wire).
|
|
22412
|
+
* 2. parse failure → a fail-closed deny in Copilot's wire.
|
|
22413
|
+
* 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
|
|
22414
|
+
* 4. `end-of-turn` → delegate to the canonical git sweep, whose wire is already Copilot's
|
|
22415
|
+
* agentStop `{decision:"block",reason}` shape and passes through unchanged.
|
|
22416
|
+
* 5. `pre-action` → reconstruct Change[] and run the SAME `evaluate` engine; a deny
|
|
22417
|
+
* carries the shared denial reason. A reconstruction that cannot be modelled fails
|
|
22418
|
+
* CLOSED to deny rather than allowing an unseen edit.
|
|
22419
|
+
*/
|
|
22420
|
+
decide(raw, phase, defaultCwd) {
|
|
22421
|
+
if (phase === "post-action") {
|
|
22422
|
+
return {
|
|
22423
|
+
outcome: "unsupported",
|
|
22424
|
+
detail: "Copilot post-action is observation-only; the end-of-turn agentStop sweep is the post-turn reconciliation."
|
|
22425
|
+
};
|
|
22426
|
+
}
|
|
22427
|
+
const parsed = this.parseEvent(raw, phase);
|
|
22428
|
+
if ("failure" in parsed) {
|
|
22429
|
+
const findings = [steeringUnavailableFinding(`unparseable Copilot event: ${parsed.detail}`)];
|
|
22430
|
+
const wire = this.denyPayload(findings, phase);
|
|
22431
|
+
return { outcome: "parse-failure", detail: parsed.detail, wire, decision: { verdict: "deny", findings, reason: wire } };
|
|
22432
|
+
}
|
|
22433
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
22434
|
+
if (!idv.ok) {
|
|
22435
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
22436
|
+
const wire = this.denyPayload(findings, phase);
|
|
22437
|
+
return { outcome: "ok", wire, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire } };
|
|
22438
|
+
}
|
|
22439
|
+
if (phase === "end-of-turn") {
|
|
22440
|
+
const stopInput = copilotStopInput(raw);
|
|
22441
|
+
if (typeof stopInput !== "string") {
|
|
22442
|
+
const findings = [steeringUnavailableFinding(`unparseable Copilot event: ${stopInput.detail}`)];
|
|
22443
|
+
const wire2 = this.denyPayload(findings, "end-of-turn");
|
|
22444
|
+
return { outcome: "parse-failure", detail: stopInput.detail, wire: wire2, decision: { verdict: "deny", findings, reason: wire2 } };
|
|
22445
|
+
}
|
|
22446
|
+
const wire = stopFromRaw(stopInput, defaultCwd, idv.trustedRoot).stdout;
|
|
22447
|
+
return { outcome: "ok", wire, decision: { verdict: wire ? "deny" : "allow", findings: [], reason: wire || void 0 } };
|
|
22448
|
+
}
|
|
22449
|
+
try {
|
|
22450
|
+
const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
|
|
22451
|
+
const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
|
|
22452
|
+
turnBaseline(root, parsed.identity.sessionId);
|
|
22453
|
+
const policy = loadPolicy(root);
|
|
22454
|
+
const changes = changesFromCopilot(parsed.operation, root, sessionCwd);
|
|
22455
|
+
const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22456
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22457
|
+
return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
|
|
22458
|
+
} catch (e) {
|
|
22459
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
22460
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
22461
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22462
|
+
return { outcome: "ok", wire, detail, decision: { verdict: "deny", findings, reason: wire } };
|
|
22463
|
+
}
|
|
22464
|
+
}
|
|
22465
|
+
/** A transport failure or a required hook that did not fire → a fail-closed deny at the
|
|
22466
|
+
* seam, through the adapter's own `denyPayload` so the wire is Copilot's native envelope. */
|
|
22467
|
+
failClosed(outcome, detail, phase) {
|
|
22468
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
22469
|
+
}
|
|
22470
|
+
};
|
|
22471
|
+
copilotAdapter = new CopilotRuntimeAdapter();
|
|
22472
|
+
}
|
|
22473
|
+
});
|
|
22474
|
+
|
|
22475
|
+
// src/adapters/copilot-sdk/changes.ts
|
|
22476
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
22477
|
+
import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync15, readFileSync as readFileSync28, rmSync as rmSync13, existsSync as existsSync21, realpathSync as realpathSync11 } from "node:fs";
|
|
22478
|
+
import { tmpdir as tmpdir8 } from "node:os";
|
|
22479
|
+
import { basename as basename6, dirname as dirname14, isAbsolute as isAbsolute17, relative as relative12, resolve as resolve22, join as join29 } from "node:path";
|
|
22480
|
+
function asStr4(v) {
|
|
22481
|
+
return typeof v === "string" ? v : "";
|
|
22482
|
+
}
|
|
22483
|
+
function relForDisplay5(path, cwd) {
|
|
22484
|
+
const rel = relative12(cwd, path);
|
|
22485
|
+
return rel && !rel.startsWith("..") && !isAbsolute17(rel) ? rel : path;
|
|
22486
|
+
}
|
|
22487
|
+
function canonicalContainedTarget(abs, root) {
|
|
22488
|
+
let realRoot;
|
|
22489
|
+
try {
|
|
22490
|
+
realRoot = realpathSync11(root);
|
|
22491
|
+
} catch (e) {
|
|
22492
|
+
throw new Error(`trusted repository root ${root} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
22493
|
+
}
|
|
22494
|
+
let dir = abs;
|
|
22495
|
+
const tail = [];
|
|
22496
|
+
while (!existsSync21(dir)) {
|
|
22497
|
+
const parent = dirname14(dir);
|
|
22498
|
+
if (parent === dir) break;
|
|
22499
|
+
tail.unshift(basename6(dir));
|
|
22500
|
+
dir = parent;
|
|
22501
|
+
}
|
|
22502
|
+
let realDir;
|
|
22503
|
+
try {
|
|
22504
|
+
realDir = realpathSync11(dir);
|
|
22505
|
+
} catch (e) {
|
|
22506
|
+
throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
22507
|
+
}
|
|
22508
|
+
const real = tail.length ? join29(realDir, ...tail) : realDir;
|
|
22509
|
+
const rel = relative12(realRoot, real);
|
|
22510
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute17(rel)) {
|
|
22511
|
+
throw new Error(`write target ${abs} resolves outside the trusted repository root ${realRoot} \u2014 refusing to judge a write beyond the repository`);
|
|
22512
|
+
}
|
|
22513
|
+
return { real, rel };
|
|
22514
|
+
}
|
|
22515
|
+
function sdkFileEditChanges(args, cwd, base = cwd) {
|
|
22516
|
+
const path = asStr4(args.path) || asStr4(args.fileName) || asStr4(args.file_path);
|
|
22517
|
+
if (!path) throw new Error("write event carries no fileName/path to reconstruct");
|
|
22518
|
+
const abs = resolve22(base, path);
|
|
22519
|
+
const canonical2 = canonicalContainedTarget(abs, cwd);
|
|
22520
|
+
const claimedResolved = asStr4(args.resolvedPath);
|
|
22521
|
+
if (claimedResolved && canonicalContainedTarget(resolve22(base, claimedResolved), cwd).real !== canonical2.real) {
|
|
22522
|
+
throw new Error(`the runtime-claimed resolvedPath (${claimedResolved}) does not match the host-derived target for ${path} \u2014 refusing to judge a different path`);
|
|
22523
|
+
}
|
|
22524
|
+
const display2 = canonical2.rel;
|
|
22525
|
+
const requestRel = relForDisplay5(abs, cwd);
|
|
22526
|
+
const entry = inspectResolved(canonical2.real);
|
|
22527
|
+
const isCreate = entry.kind === "absent";
|
|
22528
|
+
const before = textOf2(entry);
|
|
22529
|
+
if (!isCreate && before === null) {
|
|
22530
|
+
throw new Error(`write target ${display2} exists but cannot be read to judge it (${entry.kind})`);
|
|
22531
|
+
}
|
|
22532
|
+
const newFileContents = typeof args.newFileContents === "string" ? args.newFileContents : void 0;
|
|
22533
|
+
const rawDiff = asStr4(args.diff);
|
|
22534
|
+
const afterFromDiff = rawDiff.trim() ? reconstructAfterViaGit(rawDiff, requestRel, before, isCreate) : void 0;
|
|
22535
|
+
if (newFileContents !== void 0) {
|
|
22536
|
+
if (afterFromDiff !== void 0 && afterFromDiff !== newFileContents) {
|
|
22537
|
+
throw new Error(`write supplies both newFileContents and a diff that DISAGREE for ${display2}; ambiguous request \u2014 refusing to judge only one representation`);
|
|
22538
|
+
}
|
|
22539
|
+
return synthFileChange(display2, before, newFileContents);
|
|
22540
|
+
}
|
|
22541
|
+
if (afterFromDiff !== void 0) return synthFileChange(display2, before, afterFromDiff);
|
|
22542
|
+
return null;
|
|
22543
|
+
}
|
|
22544
|
+
function stripPrefix(p) {
|
|
22545
|
+
return /^[ab]\//.test(p) ? p.slice(2) : p;
|
|
22546
|
+
}
|
|
22547
|
+
function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
|
|
22548
|
+
const bytes = Buffer.byteLength(rawDiff);
|
|
22549
|
+
if (bytes > DIFF_MAX_BYTES()) throw new Error(`write diff for ${display2} is ${bytes} bytes (over the ${DIFF_MAX_BYTES()}-byte reconstruction budget)`);
|
|
22550
|
+
const { op, patch } = canonicalPatch(rawDiff, display2, isCreate);
|
|
22551
|
+
const dir = mkdtempSync7(join29(tmpdir8(), "hf-sdk-apply-"));
|
|
22552
|
+
try {
|
|
22553
|
+
const targetPath = join29(dir, TARGET);
|
|
22554
|
+
if (op !== "create") writeFileSync15(targetPath, before ?? "");
|
|
22555
|
+
const patchPath = join29(dir, "change.patch");
|
|
22556
|
+
writeFileSync15(patchPath, patch);
|
|
22557
|
+
const git8 = (extra) => execFileSync15("git", ["apply", ...extra, patchPath], {
|
|
22558
|
+
cwd: dir,
|
|
22559
|
+
encoding: "utf8",
|
|
22560
|
+
timeout: APPLY_TIMEOUT_MS(),
|
|
22561
|
+
maxBuffer: APPLY_MAXBUFFER(),
|
|
22562
|
+
killSignal: "SIGKILL"
|
|
22563
|
+
});
|
|
22564
|
+
const stat = git8(["--numstat"]).trim();
|
|
22565
|
+
const rows = stat ? stat.split("\n") : [];
|
|
22566
|
+
if (rows.length !== 1) throw new Error(`write diff spans ${rows.length} files; a single permission target (${display2}) was expected`);
|
|
22567
|
+
const statPath = rows[0].split(" ")[2];
|
|
22568
|
+
if (statPath !== TARGET) throw new Error(`write diff reconstructs a different file (${statPath}) than the bound target`);
|
|
22569
|
+
git8(["--check"]);
|
|
22570
|
+
git8([]);
|
|
22571
|
+
return op === "delete" ? null : readFileSync28(targetPath, "utf8");
|
|
22572
|
+
} catch (e) {
|
|
22573
|
+
throw new Error(`could not reconstruct the write to ${display2} from its diff via git apply: ${e instanceof Error ? e.message : String(e)}`);
|
|
22574
|
+
} finally {
|
|
22575
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
22576
|
+
}
|
|
22577
|
+
}
|
|
22578
|
+
function canonicalPatch(raw, display2, isCreate) {
|
|
22579
|
+
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
|
22580
|
+
if (lines.length > DIFF_MAX_LINES()) {
|
|
22581
|
+
throw new Error(`write diff for ${display2} is ${lines.length} lines (over the ${DIFF_MAX_LINES()}-line reconstruction budget)`);
|
|
22582
|
+
}
|
|
22583
|
+
const at = lines.findIndex((l) => l.startsWith("@@ "));
|
|
22584
|
+
if (at < 0) throw new Error(`write diff for ${display2} has no hunk to reconstruct`);
|
|
22585
|
+
const header = lines.slice(0, at);
|
|
22586
|
+
const body = lines.slice(at);
|
|
22587
|
+
let src;
|
|
22588
|
+
let dst;
|
|
22589
|
+
let hasGitLine = false;
|
|
22590
|
+
let sawCreateMode = false;
|
|
22591
|
+
let sawDeleteMode = false;
|
|
22592
|
+
let sawSemanticHeader = false;
|
|
22593
|
+
for (const line of header) {
|
|
22594
|
+
if (line.trim() === "") continue;
|
|
22595
|
+
if (line.startsWith("diff --git ")) {
|
|
22596
|
+
if (hasGitLine) throw new Error(`write diff for ${display2} carries more than one diff --git header (contradictory / multi-file) \u2014 refusing to reconstruct`);
|
|
22597
|
+
hasGitLine = true;
|
|
22598
|
+
sawSemanticHeader = true;
|
|
22599
|
+
for (const p of line.slice("diff --git ".length).trim().split(/\s+/)) assertBound(stripPrefix(p), display2);
|
|
22600
|
+
} else if (line.startsWith("--- ")) {
|
|
22601
|
+
if (src !== void 0) throw new Error(`write diff for ${display2} carries more than one --- endpoint (contradictory header) \u2014 refusing to reconstruct`);
|
|
22602
|
+
src = line.slice(4).split(" ")[0].trim();
|
|
22603
|
+
} else if (line.startsWith("+++ ")) {
|
|
22604
|
+
if (dst !== void 0) throw new Error(`write diff for ${display2} carries more than one +++ endpoint (contradictory header) \u2014 refusing to reconstruct`);
|
|
22605
|
+
dst = line.slice(4).split(" ")[0].trim();
|
|
22606
|
+
} else if (line.startsWith("index ")) {
|
|
22607
|
+
sawSemanticHeader = true;
|
|
22608
|
+
} else if (/^new file mode\b/.test(line)) {
|
|
22609
|
+
if (sawCreateMode) throw new Error(`write diff for ${display2} carries duplicate new file mode metadata \u2014 refusing to reconstruct`);
|
|
22610
|
+
sawCreateMode = true;
|
|
22611
|
+
sawSemanticHeader = true;
|
|
22612
|
+
} else if (/^deleted file mode\b/.test(line)) {
|
|
22613
|
+
if (sawDeleteMode) throw new Error(`write diff for ${display2} carries duplicate deleted file mode metadata \u2014 refusing to reconstruct`);
|
|
22614
|
+
sawDeleteMode = true;
|
|
22615
|
+
sawSemanticHeader = true;
|
|
22616
|
+
} else {
|
|
22617
|
+
throw new Error(`write diff for ${display2} carries an unsupported or unrecognized pre-hunk header line ("${line.trim().slice(0, 60)}") \u2014 refusing to reconstruct`);
|
|
22618
|
+
}
|
|
22619
|
+
}
|
|
22620
|
+
let op;
|
|
22621
|
+
if (src === void 0 && dst === void 0) {
|
|
22622
|
+
if (sawSemanticHeader) throw new Error(`write diff for ${display2} carries Git file metadata but no --- / +++ endpoints (malformed)`);
|
|
22623
|
+
op = isCreate ? "create" : "modify";
|
|
22624
|
+
} else if (src !== void 0 && dst !== void 0) {
|
|
22625
|
+
const srcNull = src === "/dev/null";
|
|
22626
|
+
const dstNull = dst === "/dev/null";
|
|
22627
|
+
if (srcNull && dstNull) throw new Error(`write diff for ${display2} has /dev/null on both sides`);
|
|
22628
|
+
if (!srcNull) assertBound(stripPrefix(src), display2);
|
|
22629
|
+
if (!dstNull) assertBound(stripPrefix(dst), display2);
|
|
22630
|
+
op = srcNull ? "create" : dstNull ? "delete" : "modify";
|
|
22631
|
+
} else {
|
|
22632
|
+
throw new Error(`write diff for ${display2} has a one-sided --- / +++ endpoint pair (malformed) \u2014 refusing to repair it into a valid operation`);
|
|
22633
|
+
}
|
|
22634
|
+
if (sawCreateMode && op !== "create") throw new Error(`write diff for ${display2} carries new file mode metadata but its endpoints describe a ${op} \u2014 contradictory, refusing to reconstruct`);
|
|
22635
|
+
if (sawDeleteMode && op !== "delete") throw new Error(`write diff for ${display2} carries deleted file mode metadata but its endpoints describe a ${op} \u2014 contradictory, refusing to reconstruct`);
|
|
22636
|
+
if (op === "create" && !isCreate) throw new Error(`write diff for ${display2} proposes a create but the target already exists`);
|
|
22637
|
+
if (op !== "create" && isCreate) throw new Error(`write diff for ${display2} proposes a ${op} but the target is absent`);
|
|
22638
|
+
const source = op === "create" ? "/dev/null" : `a/${TARGET}`;
|
|
22639
|
+
const dest = op === "delete" ? "/dev/null" : `b/${TARGET}`;
|
|
22640
|
+
const joined = body.join("\n");
|
|
22641
|
+
const patch = `--- ${source}
|
|
22642
|
+
+++ ${dest}
|
|
22643
|
+
` + (joined.endsWith("\n") ? joined : joined + "\n");
|
|
22644
|
+
return { op, patch };
|
|
22645
|
+
}
|
|
22646
|
+
function assertBound(declared, display2) {
|
|
22647
|
+
if (declared !== display2) {
|
|
22648
|
+
throw new Error(`write diff path (${declared}) does not match the permission request target (${display2}) \u2014 refusing to judge a different file`);
|
|
22649
|
+
}
|
|
22650
|
+
}
|
|
22651
|
+
var num2, APPLY_TIMEOUT_MS, APPLY_MAXBUFFER, DIFF_MAX_BYTES, DIFF_MAX_LINES, TARGET;
|
|
22652
|
+
var init_changes4 = __esm({
|
|
22653
|
+
"src/adapters/copilot-sdk/changes.ts"() {
|
|
22654
|
+
"use strict";
|
|
22655
|
+
init_changes();
|
|
22656
|
+
init_disk();
|
|
22657
|
+
num2 = (name, fallback) => {
|
|
22658
|
+
const raw = process.env[name];
|
|
22659
|
+
const n2 = raw === void 0 ? NaN : Number(raw);
|
|
22660
|
+
return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
|
|
22661
|
+
};
|
|
22662
|
+
APPLY_TIMEOUT_MS = () => num2("TAMPERWARD_RECONSTRUCT_TIMEOUT_MS", 5e3);
|
|
22663
|
+
APPLY_MAXBUFFER = () => num2("TAMPERWARD_RECONSTRUCT_MAXBUFFER", 32 * 1024 * 1024);
|
|
22664
|
+
DIFF_MAX_BYTES = () => num2("TAMPERWARD_RECONSTRUCT_MAX_BYTES", 384 * 1024);
|
|
22665
|
+
DIFF_MAX_LINES = () => num2("TAMPERWARD_RECONSTRUCT_MAX_LINES", 4e3);
|
|
22666
|
+
TARGET = "target";
|
|
22667
|
+
}
|
|
22668
|
+
});
|
|
22669
|
+
|
|
22670
|
+
// src/adapters/copilot-sdk/deny.ts
|
|
22671
|
+
function assertPhase(phase) {
|
|
22672
|
+
const known = ["pre-action", "end-of-turn"];
|
|
22673
|
+
if (!known.includes(phase)) throw new Error(`unsupported Copilot SDK wire phase: ${phase}`);
|
|
22674
|
+
}
|
|
22675
|
+
function copilotSdkWire(reason, phase) {
|
|
22676
|
+
assertPhase(phase);
|
|
22677
|
+
const payload = phase === "end-of-turn" ? { decision: "block", reason } : { kind: "reject", feedback: reason };
|
|
22678
|
+
return JSON.stringify(payload) + "\n";
|
|
22679
|
+
}
|
|
22680
|
+
function copilotSdkDenyWire(findings, phase) {
|
|
22681
|
+
assertPhase(phase);
|
|
22682
|
+
if (findings.length === 0) return "";
|
|
22683
|
+
return copilotSdkWire(formatDenial(findings), phase);
|
|
22684
|
+
}
|
|
22685
|
+
var init_deny4 = __esm({
|
|
22686
|
+
"src/adapters/copilot-sdk/deny.ts"() {
|
|
22687
|
+
"use strict";
|
|
22688
|
+
init_deny();
|
|
22689
|
+
}
|
|
22690
|
+
});
|
|
22691
|
+
|
|
22692
|
+
// src/adapters/copilot-sdk/schema.ts
|
|
22693
|
+
function copilotSdkOperationKind(kind) {
|
|
22694
|
+
switch ((kind ?? "").toLowerCase()) {
|
|
22695
|
+
case "shell":
|
|
22696
|
+
return "shell";
|
|
22697
|
+
case "write":
|
|
22698
|
+
return "file-edit";
|
|
22699
|
+
case "read":
|
|
22700
|
+
return "file-read";
|
|
22701
|
+
case "mcp":
|
|
22702
|
+
return "mcp";
|
|
22703
|
+
default:
|
|
22704
|
+
return "other";
|
|
22705
|
+
}
|
|
22706
|
+
}
|
|
22707
|
+
function str3(v) {
|
|
22708
|
+
return typeof v === "string" ? v : void 0;
|
|
22709
|
+
}
|
|
22710
|
+
function requestFrom2(parsed) {
|
|
22711
|
+
const s = (k) => str3(parsed[k]);
|
|
22712
|
+
return {
|
|
22713
|
+
...s("kind") !== void 0 ? { kind: s("kind") } : {},
|
|
22714
|
+
...s("toolName") !== void 0 ? { toolName: s("toolName") } : {},
|
|
22715
|
+
...s("fileName") !== void 0 ? { fileName: s("fileName") } : {},
|
|
22716
|
+
...s("resolvedPath") !== void 0 ? { resolvedPath: s("resolvedPath") } : {},
|
|
22717
|
+
...s("fullCommandText") !== void 0 ? { fullCommandText: s("fullCommandText") } : {},
|
|
22718
|
+
...s("diff") !== void 0 ? { diff: s("diff") } : {},
|
|
22719
|
+
...s("newFileContents") !== void 0 ? { newFileContents: s("newFileContents") } : {},
|
|
22720
|
+
...s("intention") !== void 0 ? { intention: s("intention") } : {},
|
|
22721
|
+
...s("cwd") !== void 0 ? { cwd: s("cwd") } : {},
|
|
22722
|
+
...s("sessionId") !== void 0 ? { sessionId: s("sessionId") } : {},
|
|
22723
|
+
...typeof parsed.stopHookActive === "boolean" ? { stopHookActive: parsed.stopHookActive } : {}
|
|
22724
|
+
};
|
|
22725
|
+
}
|
|
22726
|
+
function argsFor(req2) {
|
|
22727
|
+
const kind = copilotSdkOperationKind(req2.kind);
|
|
22728
|
+
if (kind === "shell") return req2.fullCommandText !== void 0 ? { command: req2.fullCommandText } : {};
|
|
22729
|
+
if (kind === "file-edit") {
|
|
22730
|
+
return {
|
|
22731
|
+
...req2.fileName !== void 0 ? { path: req2.fileName } : {},
|
|
22732
|
+
// The SDK's experimental runtime-resolved canonical path. Retained as an UNTRUSTED claim: the
|
|
22733
|
+
// host derives its own canonical target and denies on mismatch — resolvedPath is never authority.
|
|
22734
|
+
...req2.resolvedPath !== void 0 ? { resolvedPath: req2.resolvedPath } : {},
|
|
22735
|
+
...req2.diff !== void 0 ? { diff: req2.diff } : {},
|
|
22736
|
+
...req2.newFileContents !== void 0 ? { newFileContents: req2.newFileContents } : {},
|
|
22737
|
+
...req2.intention !== void 0 ? { intention: req2.intention } : {}
|
|
22738
|
+
};
|
|
22739
|
+
}
|
|
22740
|
+
return {};
|
|
22741
|
+
}
|
|
22742
|
+
function eventFrom4(req2, phase) {
|
|
22743
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: copilotSdkOperationKind(req2.kind), name: req2.toolName ?? "", args: argsFor(req2) };
|
|
22744
|
+
const identity2 = { claimedCwd: req2.cwd, sessionId: req2.sessionId };
|
|
22745
|
+
return { phase, operation, identity: identity2 };
|
|
22746
|
+
}
|
|
22747
|
+
function parseRaw2(raw) {
|
|
22748
|
+
if (!raw.trim()) return { kind: "absent" };
|
|
22749
|
+
let parsed;
|
|
22750
|
+
try {
|
|
22751
|
+
parsed = JSON.parse(raw);
|
|
22752
|
+
} catch (e) {
|
|
22753
|
+
return { kind: "fail", detail: `the Copilot SDK permission request is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
22754
|
+
}
|
|
22755
|
+
if (!isRecord(parsed)) {
|
|
22756
|
+
return { kind: "fail", detail: `the Copilot SDK permission request is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})` };
|
|
22757
|
+
}
|
|
22758
|
+
return { kind: "ok", record: parsed };
|
|
22759
|
+
}
|
|
22760
|
+
function normalizeCopilotSdkEvent(raw, phase) {
|
|
22761
|
+
const parsed = parseRaw2(raw);
|
|
22762
|
+
if (parsed.kind === "absent") return eventFrom4({}, phase);
|
|
22763
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22764
|
+
return eventFrom4(requestFrom2(parsed.record), phase);
|
|
22765
|
+
}
|
|
22766
|
+
function copilotSdkStopInput(raw) {
|
|
22767
|
+
const parsed = parseRaw2(raw);
|
|
22768
|
+
if (parsed.kind === "absent") return "{}";
|
|
22769
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22770
|
+
const req2 = requestFrom2(parsed.record);
|
|
22771
|
+
return JSON.stringify({
|
|
22772
|
+
...req2.cwd !== void 0 ? { cwd: req2.cwd } : {},
|
|
22773
|
+
...req2.sessionId !== void 0 ? { session_id: req2.sessionId } : {},
|
|
22774
|
+
...req2.stopHookActive !== void 0 ? { stop_hook_active: req2.stopHookActive } : {}
|
|
22775
|
+
});
|
|
22776
|
+
}
|
|
22777
|
+
var init_schema3 = __esm({
|
|
22778
|
+
"src/adapters/copilot-sdk/schema.ts"() {
|
|
22779
|
+
"use strict";
|
|
22780
|
+
init_narrow();
|
|
22781
|
+
}
|
|
22782
|
+
});
|
|
22783
|
+
|
|
22784
|
+
// src/adapters/copilot-sdk/adapter.ts
|
|
22785
|
+
var FILE_EDIT_NO_CONTENT_DETAIL, CopilotSdkHostedAdapter, copilotSdkAdapter;
|
|
22786
|
+
var init_adapter5 = __esm({
|
|
22787
|
+
"src/adapters/copilot-sdk/adapter.ts"() {
|
|
22788
|
+
"use strict";
|
|
22789
|
+
init_engine();
|
|
22790
|
+
init_policy_load();
|
|
22791
|
+
init_hook();
|
|
22792
|
+
init_session();
|
|
22793
|
+
init_repo_context();
|
|
22794
|
+
init_contract();
|
|
22795
|
+
init_changes3();
|
|
22796
|
+
init_changes4();
|
|
22797
|
+
init_deny4();
|
|
22798
|
+
init_schema3();
|
|
22799
|
+
FILE_EDIT_NO_CONTENT_DETAIL = "this Copilot SDK write surfaced no usable content (no diff, no newFileContents), so content-aware pre-denial cannot be established for this measured configuration; the end-of-turn git sweep is the authority for the landed write. This adapter does not blanket-deny writes.";
|
|
22800
|
+
CopilotSdkHostedAdapter = class {
|
|
22801
|
+
name = "github-copilot-sdk-hosted";
|
|
22802
|
+
/**
|
|
22803
|
+
* CONSERVATIVE and honest. `preDeny` is empty: even shell pre-deny is only a CANDIDATE until the
|
|
22804
|
+
* spike proves, on a pinned SDK, that a rejected/thrown/timed-out permission callback does NOT
|
|
22805
|
+
* dispatch the tool (the decisive unknown #611 must measure). `postObserve` is empty (the SDK has
|
|
22806
|
+
* no post-execution veto). `endOfTurn` is true via `onAgentStop`. Every real gap is named in
|
|
22807
|
+
* `unsupported`, at the granularity #611 asks for.
|
|
22808
|
+
*/
|
|
22809
|
+
capabilities = {
|
|
22810
|
+
preDeny: [],
|
|
22811
|
+
postObserve: [],
|
|
22812
|
+
endOfTurn: true,
|
|
22813
|
+
unsupported: [
|
|
22814
|
+
"shell pre-deny enforcement is only a CANDIDATE until the spike proves on a pinned @github/copilot-sdk that a rejected permission decision actually blocks tool dispatch (see spike:copilot-sdk)",
|
|
22815
|
+
"content-aware file-edit pre-deny is CONDITIONAL on the pinned runtime surfacing usable write content: the adapter reconstructs and content-judges a write carrying a diff / newFileContents, but a write that surfaces neither is unsupported for that measured configuration (allow-through; the end-of-turn sweep is authority) \u2014 never a blanket path deny",
|
|
22816
|
+
"the SDK permission-handler failure semantics (synchronous throw / rejected Promise / timeout) are UNMEASURED: whether a broken decision path fails open (dispatches the tool) or closed is exactly what the spike must establish; a single observed fail-open makes the path ineligible for #482 / Round 4.1",
|
|
22817
|
+
'onAgentStop is a lifecycle continuation control ({decision:"block"} forces another turn, guarded by stopHookActive), not a filesystem veto',
|
|
22818
|
+
"network-egress control",
|
|
22819
|
+
"identity / authentication"
|
|
22820
|
+
]
|
|
22821
|
+
};
|
|
22822
|
+
parseEvent(raw, phase) {
|
|
22823
|
+
return normalizeCopilotSdkEvent(raw, phase);
|
|
22824
|
+
}
|
|
22825
|
+
/** Identity is a CLAIM validated against the runner's independently derived trusted root,
|
|
22826
|
+
* reusing `repoContext` / `validateClaimAgainstRoot` exactly as the other adapters do. */
|
|
22827
|
+
validateIdentity(claim, defaultCwd) {
|
|
22828
|
+
const base = defaultCwd ?? process.cwd();
|
|
22829
|
+
const runnerCtx = repoContext(base);
|
|
22830
|
+
if (!runnerCtx) {
|
|
22831
|
+
return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
|
|
22832
|
+
}
|
|
22833
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
22834
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
22835
|
+
}
|
|
22836
|
+
denyPayload(findings, phase) {
|
|
22837
|
+
if (phase === "post-action") {
|
|
22838
|
+
throw new Error("Copilot SDK post-action is observation-only and cannot produce a deny wire");
|
|
22839
|
+
}
|
|
22840
|
+
return copilotSdkDenyWire(findings, phase);
|
|
22841
|
+
}
|
|
22842
|
+
/**
|
|
22843
|
+
* parse → validate identity → decide, every failure CLOSED (deny), in order:
|
|
22844
|
+
* 1. `post-action` → `unsupported` (the SDK has no post-execution veto).
|
|
22845
|
+
* 2. parse failure → a fail-closed deny in the SDK reject wire.
|
|
22846
|
+
* 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
|
|
22847
|
+
* 4. `end-of-turn` → delegate to the canonical git sweep, whose wire is already the SDK
|
|
22848
|
+
* `onAgentStop` `{decision:"block",reason}` shape.
|
|
22849
|
+
* 5. `pre-action` + shell → the SAME `evaluate` engine over the reconstructed command.
|
|
22850
|
+
* 6. `pre-action` + file-edit → `unsupported` (content not surfaced; sweep is authority).
|
|
22851
|
+
* 7. `pre-action` + read/mcp/other → no Change → allow.
|
|
22852
|
+
*/
|
|
22853
|
+
decide(raw, phase, defaultCwd) {
|
|
22854
|
+
if (phase === "post-action") {
|
|
22855
|
+
return { outcome: "unsupported", detail: "Copilot SDK has no post-execution veto; the end-of-turn onAgentStop sweep is the post-turn reconciliation." };
|
|
22856
|
+
}
|
|
22857
|
+
const parsed = this.parseEvent(raw, phase);
|
|
22858
|
+
if ("failure" in parsed) {
|
|
22859
|
+
const findings = [steeringUnavailableFinding(`unparseable Copilot SDK permission request: ${parsed.detail}`)];
|
|
22860
|
+
const wire = this.denyPayload(findings, phase);
|
|
22861
|
+
return { outcome: "parse-failure", detail: parsed.detail, wire, unavailableReason: "parse-failure", decision: { verdict: "deny", findings, reason: wire } };
|
|
22862
|
+
}
|
|
22863
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
22864
|
+
if (!idv.ok) {
|
|
22865
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
22866
|
+
const wire = this.denyPayload(findings, phase);
|
|
22867
|
+
return { outcome: "ok", wire, detail: idv.rejected, unavailableReason: "identity-rejected", decision: { verdict: "deny", findings, reason: wire } };
|
|
22868
|
+
}
|
|
22869
|
+
if (phase === "end-of-turn") {
|
|
22870
|
+
const stopInput = copilotSdkStopInput(raw);
|
|
22871
|
+
if (typeof stopInput !== "string") {
|
|
22872
|
+
const findings2 = [steeringUnavailableFinding(`unparseable Copilot SDK permission request: ${stopInput.detail}`)];
|
|
22873
|
+
const wire = this.denyPayload(findings2, "end-of-turn");
|
|
22874
|
+
return { outcome: "parse-failure", detail: stopInput.detail, wire, unavailableReason: "parse-failure", decision: { verdict: "deny", findings: findings2, reason: wire } };
|
|
22875
|
+
}
|
|
22876
|
+
const swept = stopFromRaw(stopInput, defaultCwd, idv.trustedRoot);
|
|
22877
|
+
const findings = swept.findings ? [...swept.findings] : [];
|
|
22878
|
+
return { outcome: "ok", wire: swept.stdout, decision: { verdict: swept.stdout ? "deny" : "allow", findings, reason: swept.stdout || void 0 } };
|
|
22879
|
+
}
|
|
22880
|
+
let stage = "repo-context";
|
|
22881
|
+
try {
|
|
22882
|
+
const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
|
|
22883
|
+
stage = "baseline";
|
|
22884
|
+
turnBaseline(root, parsed.identity.sessionId);
|
|
22885
|
+
const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
|
|
22886
|
+
stage = "policy-load";
|
|
22887
|
+
const policy = loadPolicy(root);
|
|
22888
|
+
if (parsed.operation.kind === "file-edit") {
|
|
22889
|
+
stage = "reconstruction";
|
|
22890
|
+
const changes2 = sdkFileEditChanges(parsed.operation.args, root, sessionCwd);
|
|
22891
|
+
if (changes2 === null) return { outcome: "unsupported", detail: FILE_EDIT_NO_CONTENT_DETAIL };
|
|
22892
|
+
stage = "evaluate";
|
|
22893
|
+
const findings2 = evaluate(changes2, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22894
|
+
const wire2 = this.denyPayload(findings2, "pre-action");
|
|
22895
|
+
return { outcome: "ok", wire: wire2, decision: { verdict: findings2.length ? "deny" : "allow", findings: findings2, reason: wire2 || void 0 } };
|
|
22896
|
+
}
|
|
22897
|
+
stage = "reconstruction";
|
|
22898
|
+
const changes = changesFromCopilot(parsed.operation, root, sessionCwd);
|
|
22899
|
+
stage = "evaluate";
|
|
22900
|
+
const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22901
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22902
|
+
return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
|
|
22903
|
+
} catch (e) {
|
|
22904
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
22905
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
22906
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22907
|
+
return { outcome: "ok", wire, detail, unavailableReason: stage, decision: { verdict: "deny", findings, reason: wire } };
|
|
22908
|
+
}
|
|
22909
|
+
}
|
|
22910
|
+
/** A transport failure or a required callback that did not fire → a fail-closed deny at the
|
|
22911
|
+
* seam, through the adapter's own `denyPayload` so the wire is the SDK's native envelope. */
|
|
22912
|
+
failClosed(outcome, detail, phase) {
|
|
22913
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
22914
|
+
}
|
|
22915
|
+
};
|
|
22916
|
+
copilotSdkAdapter = new CopilotSdkHostedAdapter();
|
|
22917
|
+
}
|
|
22918
|
+
});
|
|
22919
|
+
|
|
22920
|
+
// src/adapters/registry.ts
|
|
22921
|
+
function adapterFor(id) {
|
|
22922
|
+
const name = ADAPTER_ALIASES[id] ?? id;
|
|
22923
|
+
return RUNTIME_ADAPTERS.find((a) => a.name === name) ?? null;
|
|
22924
|
+
}
|
|
22925
|
+
function labelFor(name) {
|
|
22926
|
+
return ADAPTER_LABELS[name] ?? name;
|
|
22927
|
+
}
|
|
22928
|
+
var RUNTIME_ADAPTERS, ADAPTER_LABELS, ADAPTER_ALIASES;
|
|
22929
|
+
var init_registry = __esm({
|
|
22930
|
+
"src/adapters/registry.ts"() {
|
|
22931
|
+
"use strict";
|
|
22932
|
+
init_adapter2();
|
|
22933
|
+
init_adapter3();
|
|
22934
|
+
init_adapter4();
|
|
22935
|
+
init_adapter5();
|
|
22936
|
+
RUNTIME_ADAPTERS = [
|
|
22937
|
+
claudeAdapter,
|
|
22938
|
+
codexAdapter,
|
|
22939
|
+
copilotAdapter,
|
|
22940
|
+
copilotSdkAdapter
|
|
22941
|
+
];
|
|
22942
|
+
ADAPTER_LABELS = {
|
|
22943
|
+
"claude-code": "Claude Code",
|
|
22944
|
+
codex: "Codex",
|
|
22945
|
+
"github-copilot-cli": "GitHub Copilot CLI",
|
|
22946
|
+
"github-copilot-sdk-hosted": "GitHub Copilot SDK (hosted)"
|
|
22947
|
+
};
|
|
22948
|
+
ADAPTER_ALIASES = {
|
|
22949
|
+
claude: "claude-code",
|
|
22950
|
+
copilot: "github-copilot-cli",
|
|
22951
|
+
"copilot-cli": "github-copilot-cli",
|
|
22952
|
+
"copilot-sdk": "github-copilot-sdk-hosted"
|
|
22953
|
+
};
|
|
22954
|
+
}
|
|
22955
|
+
});
|
|
22956
|
+
|
|
22957
|
+
// src/adapters/evidence.ts
|
|
22958
|
+
function sameIdSet(a, b) {
|
|
22959
|
+
const ua = [...new Set(a)].sort();
|
|
22960
|
+
const ub = [...new Set(b)].sort();
|
|
22961
|
+
return ua.length === ub.length && ua.every((x, i) => x === ub[i]);
|
|
22962
|
+
}
|
|
22963
|
+
function evidenceBindingMatches(record, current) {
|
|
22964
|
+
return record.runtime_id === current.runtime_id && record.runtime_version === current.runtime_version && record.model === current.model && record.platform === current.platform && record.execution_mode === current.execution_mode && record.hook_config_hash === current.hook_config_hash && // Pinned SDK/protocol component versions — order-insensitive set comparison.
|
|
22965
|
+
sameIdSet(record.component_versions, current.component_versions) && // The TamperWard build (version+commit): a different TamperWard build is a different binding.
|
|
22966
|
+
record.tamperward_version === current.tamperward_version && // Adapter capability hash — REQUIRED (fail-closed) on both sides. A `null` recorded hash
|
|
22967
|
+
// (unrecoverable, never fabricated) can never equal a concrete current hash.
|
|
22968
|
+
record.adapter_capability_hash !== null && current.adapter_capability_hash !== null && record.adapter_capability_hash === current.adapter_capability_hash && // The capability surface the probe tested — order-insensitive set comparison.
|
|
22969
|
+
sameIdSet(record.tested_capabilities, current.tested_capabilities);
|
|
22970
|
+
}
|
|
22971
|
+
function matchRetainedEvidence(current, catalogue = RETAINED_EVIDENCE) {
|
|
22972
|
+
return catalogue.find((r) => evidenceBindingMatches(r.binding, current)) ?? null;
|
|
22973
|
+
}
|
|
22974
|
+
var RETAINED_EVIDENCE;
|
|
22975
|
+
var init_evidence = __esm({
|
|
22976
|
+
"src/adapters/evidence.ts"() {
|
|
22977
|
+
"use strict";
|
|
22978
|
+
RETAINED_EVIDENCE = [
|
|
22979
|
+
{
|
|
22980
|
+
ref: "copilot-sdk-capture-2026-09-20",
|
|
22981
|
+
source: "harness/adapters/copilot-sdk/evidence/capture-2026-09-20.json",
|
|
22982
|
+
source_artifact_sha256: "c46fba989dc0e772012c5fdb1291cf099326d24da2b551788be2e1e85113e49a",
|
|
22983
|
+
binding: {
|
|
22984
|
+
runtime_id: "github-copilot-sdk-hosted",
|
|
22985
|
+
runtime_version: "copilot-runtime@1.0.85",
|
|
22986
|
+
component_versions: ["@github/copilot-sdk@1.0.14", "copilot-protocol@3"],
|
|
22987
|
+
// The capture's own provenance: tamperward@2.31.0+3671a5e6 (capture-2026-09-20.json).
|
|
22988
|
+
tamperward_version: "2.31.0+3671a5e6",
|
|
22989
|
+
// NOT authentically recoverable from the committed capture: the sanitized artifact records
|
|
22990
|
+
// no adapter capability-declaration hash. We do NOT invent a historical hash. `null` makes
|
|
22991
|
+
// the applicability match fail CLOSED (a null recorded hash can never equal a concrete
|
|
22992
|
+
// current hash), so this record can never promote — the honest, safe outcome.
|
|
22993
|
+
adapter_capability_hash: null,
|
|
22994
|
+
// The capability ids the capture actually observed (the `observations` below).
|
|
22995
|
+
tested_capabilities: ["pre-deny:shell", "hook-not-invoked", "transport:timeout"],
|
|
22996
|
+
model: "gpt-5.4",
|
|
22997
|
+
platform: "darwin-arm64",
|
|
22998
|
+
execution_mode: "headless",
|
|
22999
|
+
hook_config_hash: null
|
|
23000
|
+
},
|
|
23001
|
+
observations: [
|
|
23002
|
+
{
|
|
23003
|
+
id: "pre-deny:shell",
|
|
23004
|
+
result: "proven",
|
|
23005
|
+
detail: "returned-reject path (permission.completed result.kind denied): a rejected permission decision blocked shell tool dispatch \u2014 protected state not mutated (shell-pre-deny observation, boundary seq 2 < completion seq 3)"
|
|
23006
|
+
},
|
|
23007
|
+
{
|
|
23008
|
+
id: "hook-not-invoked",
|
|
23009
|
+
result: "proven",
|
|
23010
|
+
detail: "callback-failure path: a broken permission callback (sync-throw, reject and adapter-throw, independently) failed CLOSED \u2014 the tool did not dispatch and protected state stayed intact"
|
|
23011
|
+
},
|
|
23012
|
+
{
|
|
23013
|
+
id: "transport:timeout",
|
|
23014
|
+
result: "inconclusive",
|
|
23015
|
+
detail: "callback-timeout path: a hung permission handler emits no permission.completed (the pinned SDK has no handler timeout), so non-dispatch is intrinsically unobservable \u2014 timeout is not proven either way"
|
|
23016
|
+
}
|
|
23017
|
+
]
|
|
23018
|
+
}
|
|
23019
|
+
];
|
|
23020
|
+
}
|
|
23021
|
+
});
|
|
23022
|
+
|
|
23023
|
+
// src/runtime-qualification.ts
|
|
23024
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
23025
|
+
function includesKind(set, kind) {
|
|
23026
|
+
return set.includes(kind);
|
|
23027
|
+
}
|
|
23028
|
+
function find(unsupported, re) {
|
|
23029
|
+
return unsupported.find((u) => re.test(u));
|
|
23030
|
+
}
|
|
23031
|
+
function indexEvidence(evidence) {
|
|
23032
|
+
const m = /* @__PURE__ */ new Map();
|
|
23033
|
+
if (!evidence) return m;
|
|
23034
|
+
for (const o of evidence.observations) m.set(o.id, o);
|
|
23035
|
+
return m;
|
|
23036
|
+
}
|
|
23037
|
+
function assessFromEvidence(id, index, evidence) {
|
|
23038
|
+
const obs = index.get(id);
|
|
23039
|
+
if (!obs || !evidence) return null;
|
|
23040
|
+
const state = obs.result === "proven" ? "PROVEN" : obs.result === "fail-open" ? "FAIL-OPEN" : "INCONCLUSIVE";
|
|
23041
|
+
return {
|
|
23042
|
+
id,
|
|
23043
|
+
state,
|
|
23044
|
+
evidence: {
|
|
23045
|
+
source: "committed-evidence",
|
|
23046
|
+
detail: `${obs.detail} [retained evidence ${evidence.ref} \xB7 ${evidence.source} \xB7 sha256 ${evidence.source_artifact_sha256.slice(0, 12)}]`
|
|
23047
|
+
}
|
|
23048
|
+
};
|
|
23049
|
+
}
|
|
23050
|
+
function assessPreDeny(caps, id, kind, kindNote, index, evidence) {
|
|
23051
|
+
const proven = assessFromEvidence(id, index, evidence);
|
|
23052
|
+
if (proven) return proven;
|
|
23053
|
+
if (includesKind(caps.preDeny, kind)) {
|
|
23054
|
+
return {
|
|
23055
|
+
id,
|
|
23056
|
+
state: "PARTIAL",
|
|
23057
|
+
evidence: {
|
|
23058
|
+
source: "adapter-declaration",
|
|
23059
|
+
detail: `declared preDeny includes '${kind}' (${kindNote}); the contract denies it at the adapter boundary, but no retained real-runtime probe proves this runtime/version invokes and honors the hook live`
|
|
23060
|
+
}
|
|
23061
|
+
};
|
|
23062
|
+
}
|
|
23063
|
+
const note = find(caps.unsupported, PRE_DENY_UNPROVEN_RE);
|
|
23064
|
+
if (note) return { id, state: "UNPROVEN", evidence: { source: "adapter-unsupported", detail: note } };
|
|
23065
|
+
return {
|
|
23066
|
+
id,
|
|
23067
|
+
state: "UNPROVEN",
|
|
23068
|
+
evidence: {
|
|
23069
|
+
source: "not-declared",
|
|
23070
|
+
detail: `operation kind '${kind}' (${kindNote}) is absent from the declared preDeny set and unnamed in unsupported`
|
|
23071
|
+
}
|
|
23072
|
+
};
|
|
23073
|
+
}
|
|
23074
|
+
function assessTransport(caps, id, index, evidence) {
|
|
23075
|
+
const observed = assessFromEvidence(id, index, evidence);
|
|
23076
|
+
if (observed) return observed;
|
|
23077
|
+
const kw = TRANSPORT_KEYWORDS[id];
|
|
23078
|
+
const failOpen = caps.unsupported.find((u) => FAIL_OPEN_RE.test(u) && kw.test(u));
|
|
23079
|
+
if (failOpen) return { id, state: "FAIL-OPEN", evidence: { source: "adapter-unsupported", detail: failOpen } };
|
|
23080
|
+
const unproven = find(caps.unsupported, TRANSPORT_UNPROVEN_RE);
|
|
23081
|
+
if (unproven) return { id, state: "UNPROVEN", evidence: { source: "adapter-unsupported", detail: unproven } };
|
|
23082
|
+
return {
|
|
23083
|
+
id,
|
|
23084
|
+
state: "PARTIAL",
|
|
23085
|
+
evidence: {
|
|
23086
|
+
source: "contract",
|
|
23087
|
+
detail: "the neutral steering contract maps this transport failure to a fail-closed deny at the adapter boundary (failsClosed); full live-runtime honoring across execution modes is not separately proven by static qualification"
|
|
23088
|
+
}
|
|
23089
|
+
};
|
|
23090
|
+
}
|
|
23091
|
+
function assessCapabilities(caps, evidence) {
|
|
23092
|
+
const out3 = [];
|
|
23093
|
+
const ev = indexEvidence(evidence);
|
|
23094
|
+
const fromEvidence = (id) => assessFromEvidence(id, ev, evidence);
|
|
23095
|
+
out3.push(assessPreDeny(caps, "pre-deny:shell", "shell", "a shell/exec tool call", ev, evidence));
|
|
23096
|
+
out3.push(assessPreDeny(caps, "pre-deny:native-edit", "file-edit", "a native file-edit tool", ev, evidence));
|
|
23097
|
+
out3.push(assessPreDeny(caps, "pre-deny:delete", "file-edit", "a delete proposed as a file-edit/native tool", ev, evidence));
|
|
23098
|
+
out3.push(assessPreDeny(caps, "pre-deny:rename", "file-edit", "a rename proposed as a file-edit/native tool", ev, evidence));
|
|
23099
|
+
out3.push(assessPreDeny(caps, "pre-deny:git-mutation", "shell", "a git mutation proposed as a shell command", ev, evidence));
|
|
23100
|
+
out3.push(assessPreDeny(caps, "pre-deny:mcp", "mcp", "an MCP tool call", ev, evidence));
|
|
23101
|
+
const postObserveEvidence = fromEvidence("post-observe");
|
|
23102
|
+
if (postObserveEvidence) {
|
|
23103
|
+
out3.push(postObserveEvidence);
|
|
23104
|
+
} else if (caps.postObserve.length > 0) {
|
|
23105
|
+
out3.push({
|
|
23106
|
+
id: "post-observe",
|
|
23107
|
+
state: "PARTIAL",
|
|
23108
|
+
evidence: { source: "adapter-declaration", detail: `declared postObserve: [${[...caps.postObserve].sort().join(", ")}]; no retained real-runtime probe proves live post-action observation on this runtime/version` }
|
|
23109
|
+
});
|
|
23110
|
+
} else {
|
|
23111
|
+
const note = find(caps.unsupported, POST_ACTION_RE);
|
|
23112
|
+
out3.push(
|
|
23113
|
+
note ? { id: "post-observe", state: "UNSUPPORTED", evidence: { source: "adapter-unsupported", detail: note } } : {
|
|
23114
|
+
id: "post-observe",
|
|
23115
|
+
state: "UNPROVEN",
|
|
23116
|
+
evidence: { source: "not-declared", detail: "no post-action observation kind is declared and none is named unsupported" }
|
|
23117
|
+
}
|
|
23118
|
+
);
|
|
23119
|
+
}
|
|
23120
|
+
const endOfTurnEvidence = fromEvidence("end-of-turn");
|
|
23121
|
+
if (endOfTurnEvidence) {
|
|
23122
|
+
out3.push(endOfTurnEvidence);
|
|
23123
|
+
} else if (!caps.endOfTurn) {
|
|
23124
|
+
out3.push({ id: "end-of-turn", state: "UNSUPPORTED", evidence: { source: "adapter-declaration", detail: "endOfTurn is declared false" } });
|
|
23125
|
+
} else {
|
|
23126
|
+
const caveat = find(caps.unsupported, LIFECYCLE_ONLY_RE);
|
|
23127
|
+
out3.push(
|
|
23128
|
+
caveat ? { id: "end-of-turn", state: "PARTIAL", evidence: { source: "adapter-unsupported", detail: caveat } } : { id: "end-of-turn", state: "PARTIAL", evidence: { source: "adapter-declaration", detail: "endOfTurn is declared true (a stop event can run the mandatory sweep); no retained real-runtime probe proves this runtime/version delivers and honors the stop sweep live" } }
|
|
23129
|
+
);
|
|
23130
|
+
}
|
|
23131
|
+
const hasDenyPath = caps.endOfTurn || caps.preDeny.length > 0;
|
|
23132
|
+
out3.push(
|
|
23133
|
+
hasDenyPath ? {
|
|
23134
|
+
id: "denial-reason-delivery",
|
|
23135
|
+
state: "PARTIAL",
|
|
23136
|
+
evidence: {
|
|
23137
|
+
source: "contract",
|
|
23138
|
+
detail: "a deny carries a human/agent-facing reason on the wire (SteeringDecision.reason); that the runtime delivers it to the agent is not separately proven by static qualification"
|
|
23139
|
+
}
|
|
23140
|
+
} : {
|
|
23141
|
+
id: "denial-reason-delivery",
|
|
23142
|
+
state: "UNPROVEN",
|
|
23143
|
+
evidence: { source: "not-declared", detail: "the adapter declares no pre-deny or end-of-turn deny path, so no reason is delivered" }
|
|
23144
|
+
}
|
|
23145
|
+
);
|
|
23146
|
+
out3.push({
|
|
23147
|
+
id: "continue-after-denial",
|
|
23148
|
+
state: "UNPROVEN",
|
|
23149
|
+
evidence: { source: "not-declared", detail: "whether the agent can continue after a denial is not encoded in the neutral capability declaration and requires live evidence" }
|
|
23150
|
+
});
|
|
23151
|
+
for (const id of ["transport:missing-executable", "transport:non-zero", "transport:timeout", "transport:malformed", "transport:empty"]) {
|
|
23152
|
+
out3.push(assessTransport(caps, id, ev, evidence));
|
|
23153
|
+
}
|
|
23154
|
+
out3.push(
|
|
23155
|
+
fromEvidence("hook-not-invoked") ?? {
|
|
23156
|
+
id: "hook-not-invoked",
|
|
23157
|
+
state: "PARTIAL",
|
|
23158
|
+
evidence: {
|
|
23159
|
+
source: "contract",
|
|
23160
|
+
detail: "the neutral contract fails a required not-invoked hook CLOSED (failsClosed); that a configured hook is actually invoked/honored on this runtime/version is not proven by static qualification"
|
|
23161
|
+
}
|
|
23162
|
+
}
|
|
23163
|
+
);
|
|
23164
|
+
out3.push(
|
|
23165
|
+
caps.endOfTurn ? {
|
|
23166
|
+
id: "detached/quiescence",
|
|
23167
|
+
state: "PARTIAL",
|
|
23168
|
+
evidence: {
|
|
23169
|
+
source: "contract",
|
|
23170
|
+
detail: "the end-of-turn sweep reconciles the net tree at turn end, catching a detached/deferred effect the in-loop phase could only observe; continuous in-turn quiescence is not an in-loop guarantee"
|
|
23171
|
+
}
|
|
23172
|
+
} : {
|
|
23173
|
+
id: "detached/quiescence",
|
|
23174
|
+
state: "UNPROVEN",
|
|
23175
|
+
evidence: { source: "not-declared", detail: "no end-of-turn sweep is declared, so a detached/deferred effect is not reconciled in-loop" }
|
|
23176
|
+
}
|
|
23177
|
+
);
|
|
23178
|
+
return out3;
|
|
23179
|
+
}
|
|
23180
|
+
function aggregateInLoop(assessments) {
|
|
23181
|
+
const byId = new Map(assessments.map((a) => [a.id, a]));
|
|
23182
|
+
const required = REQUIRED_IN_LOOP_IDS.map((id) => byId.get(id)).filter((a) => a !== void 0);
|
|
23183
|
+
const anyDangerous = required.some((a) => a.state === "FAIL-OPEN" || a.state === "INCONCLUSIVE");
|
|
23184
|
+
const present = (id) => {
|
|
23185
|
+
const s = byId.get(id)?.state;
|
|
23186
|
+
return s === "PROVEN" || s === "PARTIAL";
|
|
23187
|
+
};
|
|
23188
|
+
const anyPreDenyPresent = PRE_DENY_IDS.some(present);
|
|
23189
|
+
const endOfTurnPresent = present("end-of-turn");
|
|
23190
|
+
if (!anyPreDenyPresent && !endOfTurnPresent) return "NONE";
|
|
23191
|
+
if (!anyDangerous && required.every((a) => a.state === "PROVEN")) return "FULL";
|
|
23192
|
+
return "PARTIAL";
|
|
23193
|
+
}
|
|
23194
|
+
function sha16(s) {
|
|
23195
|
+
return createHash17("sha256").update(s).digest("hex").slice(0, 16);
|
|
23196
|
+
}
|
|
23197
|
+
function capabilityHash(caps) {
|
|
23198
|
+
const stable = JSON.stringify({
|
|
23199
|
+
preDeny: [...caps.preDeny].sort(),
|
|
23200
|
+
postObserve: [...caps.postObserve].sort(),
|
|
23201
|
+
endOfTurn: caps.endOfTurn,
|
|
23202
|
+
unsupported: [...caps.unsupported].sort()
|
|
23203
|
+
});
|
|
23204
|
+
return sha16(stable);
|
|
23205
|
+
}
|
|
23206
|
+
function loadBearing(b) {
|
|
23207
|
+
return {
|
|
23208
|
+
"runtime.id": b.runtime.id,
|
|
23209
|
+
"runtime.version": b.runtime.version ?? "\u2205",
|
|
23210
|
+
"tamperward.version": b.tamperward.version,
|
|
23211
|
+
"tamperward.commit": b.tamperward.commit ?? "\u2205",
|
|
23212
|
+
"adapter.capability_hash": b.adapter.capability_hash,
|
|
23213
|
+
"hook_config_hash": b.hook_config_hash ?? "\u2205",
|
|
23214
|
+
"execution_mode": b.execution_mode,
|
|
23215
|
+
"platform": b.platform,
|
|
23216
|
+
"model": b.model ?? "\u2205",
|
|
23217
|
+
"tested_capabilities": [...b.tested_capabilities].sort().join(",")
|
|
23218
|
+
};
|
|
23219
|
+
}
|
|
23220
|
+
function qualificationStaleness(stored, current) {
|
|
23221
|
+
const a = loadBearing(stored);
|
|
23222
|
+
const b = loadBearing(current);
|
|
23223
|
+
const changed = [];
|
|
23224
|
+
for (const key2 of Object.keys(b)) {
|
|
23225
|
+
if (a[key2] !== b[key2]) changed.push(`${key2}: ${a[key2]} \u2192 ${b[key2]}`);
|
|
23226
|
+
}
|
|
23227
|
+
return { stale: changed.length > 0, changed };
|
|
23228
|
+
}
|
|
23229
|
+
function evidenceId(binding, assessments) {
|
|
23230
|
+
const projection = {
|
|
23231
|
+
load_bearing: loadBearing(binding),
|
|
23232
|
+
states: assessments.map((a) => [a.id, a.state])
|
|
23233
|
+
};
|
|
23234
|
+
return sha16(JSON.stringify(projection));
|
|
23235
|
+
}
|
|
23236
|
+
var CAPABILITY_STATES, RUNTIME_CAPABILITY_IDS, EVIDENCE_SOURCES, FINAL_AUTHORITY, FAIL_OPEN_RE, PRE_DENY_UNPROVEN_RE, TRANSPORT_UNPROVEN_RE, LIFECYCLE_ONLY_RE, POST_ACTION_RE, TRANSPORT_KEYWORDS, PRE_DENY_IDS, REQUIRED_IN_LOOP_IDS;
|
|
23237
|
+
var init_runtime_qualification = __esm({
|
|
23238
|
+
"src/runtime-qualification.ts"() {
|
|
23239
|
+
"use strict";
|
|
23240
|
+
CAPABILITY_STATES = [
|
|
23241
|
+
"PROVEN",
|
|
23242
|
+
"PARTIAL",
|
|
23243
|
+
"UNPROVEN",
|
|
23244
|
+
"UNSUPPORTED",
|
|
23245
|
+
"FAIL-OPEN",
|
|
23246
|
+
"INCONCLUSIVE"
|
|
23247
|
+
];
|
|
23248
|
+
RUNTIME_CAPABILITY_IDS = [
|
|
23249
|
+
"pre-deny:shell",
|
|
23250
|
+
"pre-deny:native-edit",
|
|
23251
|
+
"pre-deny:delete",
|
|
23252
|
+
"pre-deny:rename",
|
|
23253
|
+
"pre-deny:git-mutation",
|
|
23254
|
+
"pre-deny:mcp",
|
|
23255
|
+
"post-observe",
|
|
23256
|
+
"end-of-turn",
|
|
23257
|
+
"denial-reason-delivery",
|
|
23258
|
+
"continue-after-denial",
|
|
23259
|
+
"transport:missing-executable",
|
|
23260
|
+
"transport:non-zero",
|
|
23261
|
+
"transport:timeout",
|
|
23262
|
+
"transport:malformed",
|
|
23263
|
+
"transport:empty",
|
|
23264
|
+
"hook-not-invoked",
|
|
23265
|
+
"detached/quiescence"
|
|
23266
|
+
];
|
|
23267
|
+
EVIDENCE_SOURCES = [
|
|
23268
|
+
"adapter-declaration",
|
|
23269
|
+
// structural preDeny / postObserve / endOfTurn membership
|
|
23270
|
+
"adapter-unsupported",
|
|
23271
|
+
// named verbatim in the adapter's `unsupported[]` prose
|
|
23272
|
+
"contract",
|
|
23273
|
+
// the neutral steering contract guarantees this at the adapter boundary
|
|
23274
|
+
"committed-evidence",
|
|
23275
|
+
// a committed real-runtime evidence record proved it
|
|
23276
|
+
"not-declared"
|
|
23277
|
+
// absent from every declaration — no proof either way
|
|
23278
|
+
];
|
|
23279
|
+
FINAL_AUTHORITY = "AVAILABLE";
|
|
23280
|
+
FAIL_OPEN_RE = /fails?\s+open/i;
|
|
23281
|
+
PRE_DENY_UNPROVEN_RE = /pre-action deny|pre-deny|shell pre-deny/i;
|
|
23282
|
+
TRANSPORT_UNPROVEN_RE = /(?:fail-?closed|transport)[^.]*\b(?:not (?:yet )?proven|unproven|unmeasured)/i;
|
|
23283
|
+
LIFECYCLE_ONLY_RE = /lifecycle continuation|not a filesystem veto|only block turn completion|block turn completion and force continuation/i;
|
|
23284
|
+
POST_ACTION_RE = /post-action|post-observe|posttooluse|stop sweep is the post-turn/i;
|
|
23285
|
+
TRANSPORT_KEYWORDS = {
|
|
23286
|
+
"transport:missing-executable": /missing exec|executable|enoent|not found/i,
|
|
23287
|
+
"transport:non-zero": /non-?zero|exit code|exit 2|non-?2xx/i,
|
|
23288
|
+
"transport:timeout": /time-?out|timed[- ]out/i,
|
|
23289
|
+
"transport:malformed": /malformed|parse/i,
|
|
23290
|
+
"transport:empty": /\bempty\b/i
|
|
23291
|
+
};
|
|
23292
|
+
PRE_DENY_IDS = [
|
|
23293
|
+
"pre-deny:shell",
|
|
23294
|
+
"pre-deny:native-edit",
|
|
23295
|
+
"pre-deny:delete",
|
|
23296
|
+
"pre-deny:rename",
|
|
23297
|
+
"pre-deny:git-mutation",
|
|
23298
|
+
"pre-deny:mcp"
|
|
23299
|
+
];
|
|
23300
|
+
REQUIRED_IN_LOOP_IDS = [
|
|
23301
|
+
...PRE_DENY_IDS,
|
|
23302
|
+
"end-of-turn",
|
|
23303
|
+
"transport:missing-executable",
|
|
23304
|
+
"transport:non-zero",
|
|
23305
|
+
"transport:timeout",
|
|
23306
|
+
"transport:malformed",
|
|
23307
|
+
"transport:empty",
|
|
23308
|
+
"hook-not-invoked"
|
|
23309
|
+
];
|
|
23310
|
+
}
|
|
23311
|
+
});
|
|
23312
|
+
|
|
23313
|
+
// src/cli/runtime.ts
|
|
23314
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
23315
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync29, writeFileSync as writeFileSync16 } from "node:fs";
|
|
23316
|
+
import { join as join30, dirname as dirname15 } from "node:path";
|
|
23317
|
+
function storePath(cwd) {
|
|
23318
|
+
const dir = gitDir(cwd);
|
|
23319
|
+
return dir ? join30(dir, "tamperward", "runtime-qualification.json") : null;
|
|
23320
|
+
}
|
|
23321
|
+
function readStore(cwd) {
|
|
23322
|
+
const path = storePath(cwd);
|
|
23323
|
+
if (!path || !existsSync22(path)) return null;
|
|
23324
|
+
try {
|
|
23325
|
+
const parsed = JSON.parse(readFileSync29(path, "utf8"));
|
|
23326
|
+
if (parsed && typeof parsed === "object" && parsed.records && typeof parsed.records === "object") return parsed;
|
|
23327
|
+
} catch {
|
|
23328
|
+
return null;
|
|
23329
|
+
}
|
|
23330
|
+
return null;
|
|
23331
|
+
}
|
|
23332
|
+
function isRec(v) {
|
|
23333
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23334
|
+
}
|
|
23335
|
+
function asStringOrNull(v) {
|
|
23336
|
+
return v === null || typeof v === "string" ? v : void 0;
|
|
23337
|
+
}
|
|
23338
|
+
function validateStoredReport(value) {
|
|
23339
|
+
if (!isRec(value)) return { reason: "record is not an object" };
|
|
23340
|
+
if (value.schema_version !== 1) return { reason: "unrecognized schema_version" };
|
|
23341
|
+
if (value.command !== "runtime") return { reason: 'command is not "runtime"' };
|
|
23342
|
+
const subcommand = value.subcommand;
|
|
23343
|
+
if (subcommand !== "verify" && subcommand !== "status") return { reason: "invalid subcommand" };
|
|
23344
|
+
const recorded = value.recorded;
|
|
23345
|
+
const stale = value.stale;
|
|
23346
|
+
if (typeof recorded !== "boolean" || typeof stale !== "boolean") return { reason: "invalid recorded/stale flags" };
|
|
23347
|
+
const changedInputs = value.changed_inputs;
|
|
23348
|
+
if (!Array.isArray(changedInputs) || !changedInputs.every((s) => typeof s === "string")) return { reason: "invalid changed_inputs" };
|
|
23349
|
+
const runtime = value.runtime;
|
|
23350
|
+
if (!isRec(runtime)) return { reason: "invalid runtime binding" };
|
|
23351
|
+
const rId = runtime.id;
|
|
23352
|
+
const rLabel = runtime.label;
|
|
23353
|
+
const rVersion = asStringOrNull(runtime.version);
|
|
23354
|
+
if (typeof rId !== "string" || !rId || typeof rLabel !== "string" || !rLabel || rVersion === void 0)
|
|
23355
|
+
return { reason: "invalid runtime binding" };
|
|
23356
|
+
const tw = value.tamperward;
|
|
23357
|
+
if (!isRec(tw)) return { reason: "invalid tamperward binding" };
|
|
23358
|
+
const twVersion = tw.version;
|
|
23359
|
+
const twCommit = asStringOrNull(tw.commit);
|
|
23360
|
+
if (typeof twVersion !== "string" || !twVersion || twCommit === void 0) return { reason: "invalid tamperward binding" };
|
|
23361
|
+
const adapter = value.adapter;
|
|
23362
|
+
if (!isRec(adapter)) return { reason: "invalid adapter binding" };
|
|
23363
|
+
const adName = adapter.name;
|
|
23364
|
+
const adHash = adapter.capability_hash;
|
|
23365
|
+
if (typeof adName !== "string" || !adName || typeof adHash !== "string" || !adHash) return { reason: "invalid adapter binding" };
|
|
23366
|
+
const hookConfigHash = asStringOrNull(value.hook_config_hash);
|
|
23367
|
+
if (hookConfigHash === void 0) return { reason: "invalid hook_config_hash" };
|
|
23368
|
+
const executionMode = value.execution_mode;
|
|
23369
|
+
if (executionMode !== "headless" && executionMode !== "interactive") return { reason: "invalid execution_mode" };
|
|
23370
|
+
const platform = value.platform;
|
|
23371
|
+
if (typeof platform !== "string" || !platform) return { reason: "invalid platform" };
|
|
23372
|
+
const model = asStringOrNull(value.model);
|
|
23373
|
+
if (model === void 0) return { reason: "invalid model" };
|
|
23374
|
+
const testedCapabilities = value.tested_capabilities;
|
|
23375
|
+
if (!Array.isArray(testedCapabilities) || !testedCapabilities.every((c) => typeof c === "string" && CAP_ID_SET.has(c)))
|
|
23376
|
+
return { reason: "invalid tested_capabilities" };
|
|
23377
|
+
const timestamp = value.timestamp;
|
|
23378
|
+
if (typeof timestamp !== "string" || !timestamp) return { reason: "invalid timestamp" };
|
|
23379
|
+
const storedEvidenceId = value.evidence_id;
|
|
23380
|
+
if (typeof storedEvidenceId !== "string" || !storedEvidenceId) return { reason: "invalid evidence_id" };
|
|
23381
|
+
if (!Array.isArray(value.capabilities)) return { reason: "capabilities is not an array" };
|
|
23382
|
+
const capabilities = [];
|
|
23383
|
+
for (const c of value.capabilities) {
|
|
23384
|
+
if (!isRec(c)) return { reason: "invalid capability entry" };
|
|
23385
|
+
const id = RUNTIME_CAPABILITY_IDS.find((x) => x === c.id);
|
|
23386
|
+
const state = CAPABILITY_STATES.find((x) => x === c.state);
|
|
23387
|
+
if (!id || !state) return { reason: "invalid capability entry" };
|
|
23388
|
+
const ev = c.evidence;
|
|
23389
|
+
if (!isRec(ev)) return { reason: "invalid capability evidence" };
|
|
23390
|
+
const source = EVIDENCE_SOURCES.find((x) => x === ev.source);
|
|
23391
|
+
const detail = ev.detail;
|
|
23392
|
+
if (!source || typeof detail !== "string") return { reason: "invalid capability evidence" };
|
|
23393
|
+
capabilities.push({ id, state, evidence: { source, detail } });
|
|
23394
|
+
}
|
|
23395
|
+
const inLoop = value.in_loop_protection;
|
|
23396
|
+
if (inLoop !== "FULL" && inLoop !== "PARTIAL" && inLoop !== "NONE") return { reason: "invalid in_loop_protection" };
|
|
23397
|
+
if (value.final_authority !== FINAL_AUTHORITY) return { reason: "invalid final_authority" };
|
|
23398
|
+
const note = value.note;
|
|
23399
|
+
if (typeof note !== "string") return { reason: "invalid note" };
|
|
23400
|
+
const base = {
|
|
23401
|
+
runtime: { id: rId, label: rLabel, version: rVersion },
|
|
23402
|
+
tamperward: { version: twVersion, commit: twCommit },
|
|
23403
|
+
adapter: { name: adName, capability_hash: adHash },
|
|
23404
|
+
hook_config_hash: hookConfigHash,
|
|
23405
|
+
execution_mode: executionMode,
|
|
23406
|
+
platform,
|
|
23407
|
+
model,
|
|
23408
|
+
tested_capabilities: testedCapabilities
|
|
23409
|
+
};
|
|
23410
|
+
const recomputedId = evidenceId(base, capabilities);
|
|
23411
|
+
if (recomputedId !== storedEvidenceId) return { reason: `evidence_id mismatch (recorded ${storedEvidenceId}, recomputed ${recomputedId})` };
|
|
23412
|
+
const recomputedAgg = aggregateInLoop(capabilities);
|
|
23413
|
+
if (inLoop !== recomputedAgg) return { reason: `in_loop_protection mismatch (recorded ${inLoop}, recomputed ${recomputedAgg})` };
|
|
23414
|
+
const report2 = {
|
|
23415
|
+
schema_version: 1,
|
|
23416
|
+
command: "runtime",
|
|
23417
|
+
subcommand,
|
|
23418
|
+
recorded,
|
|
23419
|
+
stale,
|
|
23420
|
+
changed_inputs: changedInputs,
|
|
23421
|
+
runtime: base.runtime,
|
|
23422
|
+
tamperward: base.tamperward,
|
|
23423
|
+
adapter: base.adapter,
|
|
23424
|
+
hook_config_hash: hookConfigHash,
|
|
23425
|
+
execution_mode: executionMode,
|
|
23426
|
+
platform,
|
|
23427
|
+
model,
|
|
23428
|
+
tested_capabilities: testedCapabilities,
|
|
23429
|
+
timestamp,
|
|
23430
|
+
evidence_id: storedEvidenceId,
|
|
23431
|
+
capabilities,
|
|
23432
|
+
in_loop_protection: inLoop,
|
|
23433
|
+
final_authority: FINAL_AUTHORITY,
|
|
23434
|
+
note
|
|
23435
|
+
};
|
|
23436
|
+
return { report: report2 };
|
|
23437
|
+
}
|
|
23438
|
+
function writeRecord(cwd, id, report2) {
|
|
23439
|
+
const path = storePath(cwd);
|
|
23440
|
+
if (!path) return false;
|
|
23441
|
+
const existing = readStore(cwd);
|
|
23442
|
+
const store = existing ?? { schema_version: report2.schema_version, updated_at: "", records: {} };
|
|
23443
|
+
store.records[id] = report2;
|
|
23444
|
+
store.updated_at = report2.timestamp;
|
|
23445
|
+
try {
|
|
23446
|
+
mkdirSync13(dirname15(path), { recursive: true });
|
|
23447
|
+
writeFileSync16(path, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
23448
|
+
return true;
|
|
23449
|
+
} catch {
|
|
23450
|
+
return false;
|
|
23451
|
+
}
|
|
23452
|
+
}
|
|
23453
|
+
function resolveRuntimeVersion(canonicalId) {
|
|
23454
|
+
const override = process.env.TAMPERWARD_RUNTIME_VERSION;
|
|
23455
|
+
if (override && override.trim()) return override.trim();
|
|
23456
|
+
const bins = {
|
|
23457
|
+
"claude-code": "claude",
|
|
23458
|
+
codex: "codex",
|
|
23459
|
+
"github-copilot-cli": "copilot"
|
|
23460
|
+
};
|
|
23461
|
+
const bin = bins[canonicalId];
|
|
23462
|
+
if (!bin) return null;
|
|
23463
|
+
try {
|
|
23464
|
+
const out3 = execFileSync16(bin, ["--version"], { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "ignore"] });
|
|
23465
|
+
const m = out3.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
23466
|
+
return m ? m[0] : out3.trim() || null;
|
|
23467
|
+
} catch {
|
|
23468
|
+
return null;
|
|
23469
|
+
}
|
|
23470
|
+
}
|
|
23471
|
+
function tamperwardBuildTag(version, commit) {
|
|
23472
|
+
return commit ? `${version}+${commit.slice(0, 8)}` : version;
|
|
23473
|
+
}
|
|
23474
|
+
function resolveComponentVersions(_canonicalId) {
|
|
23475
|
+
return [];
|
|
23476
|
+
}
|
|
23477
|
+
function resolveHookConfigHash(canonicalId, cwd) {
|
|
23478
|
+
if (canonicalId === "claude-code") {
|
|
23479
|
+
return sha16(JSON.stringify(interventionWiring(cwd)));
|
|
23480
|
+
}
|
|
23481
|
+
const files = {
|
|
23482
|
+
"github-copilot-cli": join30(cwd, ".github", "hooks", "tamperward.json")
|
|
23483
|
+
};
|
|
23484
|
+
const file = files[canonicalId];
|
|
23485
|
+
if (!file || !existsSync22(file)) return null;
|
|
23486
|
+
try {
|
|
23487
|
+
const raw = readFileSync29(file, "utf8");
|
|
23488
|
+
let hooks = raw;
|
|
23489
|
+
try {
|
|
23490
|
+
const parsed = JSON.parse(raw);
|
|
23491
|
+
hooks = parsed && typeof parsed === "object" && "hooks" in parsed ? parsed.hooks : parsed;
|
|
23492
|
+
} catch {
|
|
23493
|
+
}
|
|
23494
|
+
return sha16(JSON.stringify(hooks ?? null));
|
|
23495
|
+
} catch {
|
|
23496
|
+
return null;
|
|
23497
|
+
}
|
|
23498
|
+
}
|
|
23499
|
+
function buildQualification(adapter, opts, cwd) {
|
|
23500
|
+
const canonical2 = adapter.name;
|
|
23501
|
+
const capHash = capabilityHash(adapter.capabilities);
|
|
23502
|
+
const mode = opts.mode ?? "headless";
|
|
23503
|
+
const version = resolveRuntimeVersion(canonical2);
|
|
23504
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
23505
|
+
const model = opts.model ?? null;
|
|
23506
|
+
const hookConfigHash = resolveHookConfigHash(canonical2, cwd);
|
|
23507
|
+
const commit = TW_COMMIT;
|
|
23508
|
+
const testedRaw = [...RUNTIME_CAPABILITY_IDS];
|
|
23509
|
+
const matchKey = {
|
|
23510
|
+
runtime_id: canonical2,
|
|
23511
|
+
runtime_version: version,
|
|
23512
|
+
// No live resolver for a runtime's SDK/protocol component versions ships yet, so the
|
|
23513
|
+
// current side is honestly the empty (unresolved) set. An unresolved component set never
|
|
23514
|
+
// matches a probed non-empty one — it fails closed, never promotes. We do not fabricate one.
|
|
23515
|
+
component_versions: resolveComponentVersions(canonical2),
|
|
23516
|
+
tamperward_version: tamperwardBuildTag(TW_VERSION, commit),
|
|
23517
|
+
adapter_capability_hash: capHash,
|
|
23518
|
+
tested_capabilities: testedRaw,
|
|
23519
|
+
model,
|
|
23520
|
+
platform,
|
|
23521
|
+
execution_mode: mode,
|
|
23522
|
+
hook_config_hash: hookConfigHash
|
|
23523
|
+
};
|
|
23524
|
+
const evidence = matchRetainedEvidence(matchKey);
|
|
23525
|
+
const assessments = assessCapabilities(adapter.capabilities, evidence);
|
|
23526
|
+
const base = {
|
|
23527
|
+
runtime: { id: canonical2, label: labelFor(canonical2), version },
|
|
23528
|
+
tamperward: { version: TW_VERSION, commit },
|
|
23529
|
+
adapter: { name: canonical2, capability_hash: capHash },
|
|
23530
|
+
hook_config_hash: hookConfigHash,
|
|
23531
|
+
execution_mode: mode,
|
|
23532
|
+
platform,
|
|
23533
|
+
model,
|
|
23534
|
+
tested_capabilities: testedRaw
|
|
23535
|
+
};
|
|
23536
|
+
const eid = evidenceId(base, assessments);
|
|
23537
|
+
const binding = { ...base, timestamp: (/* @__PURE__ */ new Date()).toISOString(), evidence_id: eid };
|
|
23538
|
+
return { binding, assessments };
|
|
23539
|
+
}
|
|
23540
|
+
function reportFrom(subcommand, binding, assessments) {
|
|
23541
|
+
return machineOutput({
|
|
23542
|
+
command: "runtime",
|
|
23543
|
+
subcommand,
|
|
23544
|
+
recorded: true,
|
|
23545
|
+
stale: false,
|
|
23546
|
+
changed_inputs: [],
|
|
23547
|
+
runtime: binding.runtime,
|
|
23548
|
+
tamperward: binding.tamperward,
|
|
23549
|
+
adapter: binding.adapter,
|
|
23550
|
+
hook_config_hash: binding.hook_config_hash,
|
|
23551
|
+
execution_mode: binding.execution_mode,
|
|
23552
|
+
platform: binding.platform,
|
|
23553
|
+
model: binding.model,
|
|
23554
|
+
tested_capabilities: binding.tested_capabilities,
|
|
23555
|
+
timestamp: binding.timestamp,
|
|
23556
|
+
evidence_id: binding.evidence_id,
|
|
23557
|
+
capabilities: assessments,
|
|
23558
|
+
in_loop_protection: aggregateInLoop(assessments),
|
|
23559
|
+
final_authority: FINAL_AUTHORITY,
|
|
23560
|
+
note: HONESTY_NOTE
|
|
23561
|
+
});
|
|
23562
|
+
}
|
|
23563
|
+
function resolveTarget(opts, cwd) {
|
|
23564
|
+
if (opts.runtime) {
|
|
23565
|
+
const a = adapterFor(opts.runtime);
|
|
23566
|
+
return a ?? { error: `no shipped runtime adapter for "${opts.runtime}"` };
|
|
23567
|
+
}
|
|
23568
|
+
const detected = detectRuntimes(cwd);
|
|
23569
|
+
for (const rt of detected) {
|
|
23570
|
+
const a = adapterFor(rt.id);
|
|
23571
|
+
if (a) return a;
|
|
23572
|
+
}
|
|
23573
|
+
const reason = detected.length > 0 ? "no adapter-backed runtime detected in this repository" : "no runtime detected in this repository";
|
|
23574
|
+
return { error: `${reason}; pass --runtime <id> to qualify a specific runtime` };
|
|
23575
|
+
}
|
|
23576
|
+
function renderText3(report2, cwd) {
|
|
23577
|
+
const colour = colourEnabled(process.env, process.stdout);
|
|
23578
|
+
const w = (s) => process.stdout.write(s + "\n");
|
|
23579
|
+
const dim = (s) => paint(s, DIM, colour);
|
|
23580
|
+
w(`${paint(report2.runtime.label, BOLD, colour)} \u2014 runtime protection qualification`);
|
|
23581
|
+
w(dim(` runtime ${report2.runtime.version ?? "(version unresolved)"} \xB7 tamperward ${report2.tamperward.version} \xB7 adapter ${report2.adapter.capability_hash} \xB7 ${report2.execution_mode} \xB7 ${report2.platform}`));
|
|
23582
|
+
w(dim(` evidence ${report2.evidence_id} \xB7 ${report2.timestamp}`));
|
|
23583
|
+
if (!report2.recorded) {
|
|
23584
|
+
w("");
|
|
23585
|
+
w(`${paint("[UNQUALIFIED]", severityColour("bad") + BOLD, colour)} no qualification recorded for this runtime.`);
|
|
23586
|
+
if (report2.note) w(dim(` ${report2.note.replace(/—/g, "-")}`));
|
|
23587
|
+
w(`Run: ${paint("tamperward runtime verify", BOLD, colour)}`);
|
|
23588
|
+
return;
|
|
23589
|
+
}
|
|
23590
|
+
if (report2.stale) {
|
|
23591
|
+
w("");
|
|
23592
|
+
w(`${paint("[STALE]", severityColour("bad") + BOLD, colour)} a load-bearing input changed \u2014 this qualification no longer applies:`);
|
|
23593
|
+
for (const c of report2.changed_inputs) w(` \xB7 ${c}`);
|
|
23594
|
+
w(`Run: ${paint("tamperward runtime verify", BOLD, colour)}`);
|
|
23595
|
+
}
|
|
23596
|
+
w("");
|
|
23597
|
+
const width = Math.max(...report2.capabilities.map((c) => c.id.length));
|
|
23598
|
+
for (const cap of report2.capabilities) {
|
|
23599
|
+
const sev = STATE_SEVERITY[cap.state];
|
|
23600
|
+
const tag = paint(cap.state.padEnd(12), (sev === "bad" ? BOLD : "") + severityColour(sev), colour);
|
|
23601
|
+
w(` ${cap.id.padEnd(width)} ${tag}`);
|
|
23602
|
+
}
|
|
23603
|
+
w("");
|
|
23604
|
+
const agg = report2.in_loop_protection;
|
|
23605
|
+
const aggTag = paint(agg, (AGGREGATE_SEVERITY[agg] === "bad" ? BOLD : "") + severityColour(AGGREGATE_SEVERITY[agg]), colour);
|
|
23606
|
+
w(` ${"In-loop protection".padEnd(width)} ${aggTag}`);
|
|
23607
|
+
w(` ${"Final authority".padEnd(width)} ${paint(report2.final_authority, severityColour("ok"), colour)}`);
|
|
23608
|
+
w("");
|
|
23609
|
+
w(dim(" Final authority (CI / pristine verification) is independent of the runtime hook: a weak in-loop"));
|
|
23610
|
+
w(dim(" capability never weakens adjudication. Steering and authority remain separate."));
|
|
23611
|
+
w("");
|
|
23612
|
+
w(dim(" " + report2.note.replace(/—/g, "-")));
|
|
23613
|
+
w("");
|
|
23614
|
+
w(dim(" Evidence:"));
|
|
23615
|
+
for (const cap of report2.capabilities) {
|
|
23616
|
+
w(dim(` ${cap.id} [${cap.state}] (${cap.evidence.source}): ${cap.evidence.detail}`));
|
|
23617
|
+
}
|
|
23618
|
+
}
|
|
23619
|
+
function emit2(report2, opts, cwd) {
|
|
23620
|
+
if (opts.json) {
|
|
23621
|
+
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
23622
|
+
return;
|
|
23623
|
+
}
|
|
23624
|
+
renderText3(report2, cwd);
|
|
23625
|
+
}
|
|
23626
|
+
function runRuntime(sub, opts) {
|
|
23627
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
23628
|
+
if (sub === "verify") {
|
|
23629
|
+
const target = resolveTarget(opts, cwd);
|
|
23630
|
+
if ("error" in target) {
|
|
23631
|
+
process.stderr.write(`tamperward: ${target.error}
|
|
23632
|
+
`);
|
|
23633
|
+
return 2;
|
|
23634
|
+
}
|
|
23635
|
+
const { binding, assessments } = buildQualification(target, opts, cwd);
|
|
23636
|
+
const report2 = reportFrom("verify", binding, assessments);
|
|
23637
|
+
const wrote = writeRecord(cwd, target.name, report2);
|
|
23638
|
+
if (!wrote) {
|
|
23639
|
+
const dest = storePath(cwd);
|
|
23640
|
+
const reason = `could not persist the qualification to ${dest ?? "the git-local store (no repository found)"}; nothing was recorded, so \`tamperward runtime status\` will report UNQUALIFIED`;
|
|
23641
|
+
const failure = {
|
|
23642
|
+
...report2,
|
|
23643
|
+
recorded: false,
|
|
23644
|
+
capabilities: [],
|
|
23645
|
+
in_loop_protection: "NONE",
|
|
23646
|
+
note: reason
|
|
23647
|
+
};
|
|
23648
|
+
emit2(failure, opts, cwd);
|
|
23649
|
+
process.stderr.write(`tamperward: ${reason}
|
|
23650
|
+
`);
|
|
23651
|
+
return 1;
|
|
23652
|
+
}
|
|
23653
|
+
emit2(report2, opts, cwd);
|
|
23654
|
+
return 0;
|
|
23655
|
+
}
|
|
23656
|
+
if (sub === "status") {
|
|
23657
|
+
const target = resolveTarget(opts, cwd);
|
|
23658
|
+
if ("error" in target) {
|
|
23659
|
+
process.stderr.write(`tamperward: ${target.error}
|
|
23660
|
+
`);
|
|
23661
|
+
return 2;
|
|
23662
|
+
}
|
|
23663
|
+
const store = readStore(cwd);
|
|
23664
|
+
const raw = store?.records[target.name];
|
|
23665
|
+
const validated = raw === void 0 ? { reason: "none recorded" } : validateStoredReport(raw);
|
|
23666
|
+
if ("reason" in validated) {
|
|
23667
|
+
const { binding, assessments } = buildQualification(target, opts, cwd);
|
|
23668
|
+
const note = raw === void 0 ? `No qualification recorded for ${binding.runtime.label}. Run: tamperward runtime verify` : `Stored qualification for ${binding.runtime.label} was rejected (${validated.reason}) and treated as unrecorded. Run: tamperward runtime verify`;
|
|
23669
|
+
const empty2 = {
|
|
23670
|
+
...reportFrom("status", binding, assessments),
|
|
23671
|
+
recorded: false,
|
|
23672
|
+
capabilities: [],
|
|
23673
|
+
in_loop_protection: "NONE",
|
|
23674
|
+
note
|
|
23675
|
+
};
|
|
23676
|
+
emit2(empty2, opts, cwd);
|
|
23677
|
+
return 0;
|
|
23678
|
+
}
|
|
23679
|
+
const stored = validated.report;
|
|
23680
|
+
const { binding: current } = buildQualification(target, opts, cwd);
|
|
23681
|
+
const storedBinding = {
|
|
23682
|
+
runtime: stored.runtime,
|
|
23683
|
+
tamperward: stored.tamperward,
|
|
23684
|
+
adapter: stored.adapter,
|
|
23685
|
+
hook_config_hash: stored.hook_config_hash,
|
|
23686
|
+
execution_mode: stored.execution_mode,
|
|
23687
|
+
platform: stored.platform,
|
|
23688
|
+
model: stored.model,
|
|
23689
|
+
tested_capabilities: stored.tested_capabilities,
|
|
23690
|
+
timestamp: stored.timestamp,
|
|
23691
|
+
evidence_id: stored.evidence_id
|
|
23692
|
+
};
|
|
23693
|
+
const staleness = qualificationStaleness(storedBinding, current);
|
|
23694
|
+
const report2 = {
|
|
23695
|
+
...stored,
|
|
23696
|
+
subcommand: "status",
|
|
23697
|
+
stale: staleness.stale,
|
|
23698
|
+
changed_inputs: staleness.changed
|
|
23699
|
+
};
|
|
23700
|
+
emit2(report2, opts, cwd);
|
|
23701
|
+
return 0;
|
|
23702
|
+
}
|
|
23703
|
+
process.stderr.write(`tamperward: runtime requires one of verify | status (got "${sub ?? ""}")
|
|
23704
|
+
`);
|
|
23705
|
+
return 2;
|
|
23706
|
+
}
|
|
23707
|
+
function parseRuntime(args) {
|
|
23708
|
+
const [sub, ...rest] = args;
|
|
23709
|
+
const opts = {};
|
|
23710
|
+
for (let i = 0; i < rest.length; i++) {
|
|
23711
|
+
const a = rest[i];
|
|
23712
|
+
if (a === "--cwd") opts.cwd = rest[++i];
|
|
23713
|
+
else if (a === "--runtime") opts.runtime = rest[++i];
|
|
23714
|
+
else if (a === "--model") opts.model = rest[++i];
|
|
23715
|
+
else if (a === "--json") opts.json = true;
|
|
23716
|
+
else if (a === "--mode") {
|
|
23717
|
+
const v = rest[++i];
|
|
23718
|
+
if (v === "headless" || v === "interactive") opts.mode = v;
|
|
23719
|
+
}
|
|
23720
|
+
}
|
|
23721
|
+
return { sub, opts };
|
|
23722
|
+
}
|
|
23723
|
+
var HONESTY_NOTE, CAP_ID_SET, STATE_SEVERITY, AGGREGATE_SEVERITY, RUNTIME_SUBCOMMANDS;
|
|
23724
|
+
var init_runtime = __esm({
|
|
23725
|
+
"src/cli/runtime.ts"() {
|
|
23726
|
+
"use strict";
|
|
23727
|
+
init_build();
|
|
23728
|
+
init_wiring();
|
|
23729
|
+
init_machine_output();
|
|
23730
|
+
init_text();
|
|
23731
|
+
init_status();
|
|
23732
|
+
init_registry();
|
|
23733
|
+
init_runtimes();
|
|
23734
|
+
init_evidence();
|
|
23735
|
+
init_verification_state();
|
|
23736
|
+
init_runtime_qualification();
|
|
23737
|
+
HONESTY_NOTE = "PROVEN is reserved for a capability a retained real-runtime probe observed holding under THIS exact runtime/version/config binding; a declaration alone grades PARTIAL (declared at the contract boundary, unproven live) and an absent capability UNPROVEN/UNSUPPORTED. No live in-process probe runs here \u2014 grading is against committed, sanitized real-runtime evidence, and absent a matching record nothing is PROVEN. This reports existing facts only \u2014 no promotion, no Round 4.1 eligibility claim.";
|
|
23738
|
+
CAP_ID_SET = new Set(RUNTIME_CAPABILITY_IDS);
|
|
23739
|
+
STATE_SEVERITY = {
|
|
23740
|
+
PROVEN: "ok",
|
|
23741
|
+
PARTIAL: "warn",
|
|
23742
|
+
UNPROVEN: "warn",
|
|
23743
|
+
UNSUPPORTED: "info",
|
|
23744
|
+
"FAIL-OPEN": "bad",
|
|
23745
|
+
INCONCLUSIVE: "bad"
|
|
23746
|
+
};
|
|
23747
|
+
AGGREGATE_SEVERITY = { FULL: "ok", PARTIAL: "warn", NONE: "bad" };
|
|
23748
|
+
RUNTIME_SUBCOMMANDS = ["verify", "status"];
|
|
23749
|
+
}
|
|
23750
|
+
});
|
|
23751
|
+
|
|
23752
|
+
// src/cli/signoff-label.ts
|
|
23753
|
+
function runSignoffLabel(opts) {
|
|
23754
|
+
if (!opts.rule) {
|
|
23755
|
+
process.stderr.write("tamperward signoff-label --rule <rule> --head <full-head-sha> [--file <path>]\n");
|
|
23756
|
+
return 2;
|
|
23757
|
+
}
|
|
23758
|
+
if (!opts.head) {
|
|
23759
|
+
process.stderr.write("tamperward: --head is required and must be a full 40- or 64-character object id.\n");
|
|
23760
|
+
return 2;
|
|
23761
|
+
}
|
|
23762
|
+
const want = opts.file ? `${opts.rule}:${opts.file}` : opts.rule;
|
|
23763
|
+
const token = compactOobToken(want, opts.head);
|
|
23764
|
+
if (!token) {
|
|
23765
|
+
process.stderr.write("tamperward: --head must be a full 40- or 64-character hexadecimal object id.\n");
|
|
23766
|
+
return 2;
|
|
23767
|
+
}
|
|
23768
|
+
process.stdout.write(`${token}
|
|
23769
|
+
`);
|
|
23770
|
+
return 0;
|
|
23771
|
+
}
|
|
23772
|
+
var init_signoff_label = __esm({
|
|
23773
|
+
"src/cli/signoff-label.ts"() {
|
|
23774
|
+
"use strict";
|
|
23775
|
+
init_signoff();
|
|
23776
|
+
}
|
|
23777
|
+
});
|
|
23778
|
+
|
|
23779
|
+
// src/cli/main.ts
|
|
23780
|
+
var main_exports = {};
|
|
23781
|
+
__export(main_exports, {
|
|
23782
|
+
guardedMain: () => guardedMain,
|
|
23783
|
+
main: () => main,
|
|
23784
|
+
runHookFromRaw: () => runHookFromRaw,
|
|
21086
23785
|
validateCliArgs: () => validateCliArgs
|
|
21087
23786
|
});
|
|
21088
23787
|
function parseAllow(args) {
|
|
@@ -21145,6 +23844,15 @@ function parseDoctor(args) {
|
|
|
21145
23844
|
}
|
|
21146
23845
|
return o;
|
|
21147
23846
|
}
|
|
23847
|
+
function parseStatus(args) {
|
|
23848
|
+
const o = {};
|
|
23849
|
+
for (let i = 0; i < args.length; i++) {
|
|
23850
|
+
const a = args[i];
|
|
23851
|
+
if (a === "--json") o.json = true;
|
|
23852
|
+
else if (a === "--cwd") o.cwd = args[++i];
|
|
23853
|
+
}
|
|
23854
|
+
return o;
|
|
23855
|
+
}
|
|
21148
23856
|
function parseStats(args) {
|
|
21149
23857
|
const o = {};
|
|
21150
23858
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -21256,6 +23964,12 @@ function validateFlatArgs(args, grammar) {
|
|
|
21256
23964
|
seen,
|
|
21257
23965
|
positionals
|
|
21258
23966
|
};
|
|
23967
|
+
} else if (rule === "mode" && v !== "headless" && v !== "interactive") {
|
|
23968
|
+
return {
|
|
23969
|
+
error: `${a} needs one of headless | interactive (got "${v}")`,
|
|
23970
|
+
seen,
|
|
23971
|
+
positionals
|
|
23972
|
+
};
|
|
21259
23973
|
}
|
|
21260
23974
|
continue;
|
|
21261
23975
|
}
|
|
@@ -21353,6 +24067,12 @@ function validateCliArgs(cmd, args) {
|
|
|
21353
24067
|
}
|
|
21354
24068
|
}).error;
|
|
21355
24069
|
}
|
|
24070
|
+
if (cmd === "status") {
|
|
24071
|
+
return validateFlatArgs(args, {
|
|
24072
|
+
flags: ["--json"],
|
|
24073
|
+
values: { "--cwd": "string" }
|
|
24074
|
+
}).error;
|
|
24075
|
+
}
|
|
21356
24076
|
if (cmd === "onboard") {
|
|
21357
24077
|
const parsed = validateFlatArgs(args, {
|
|
21358
24078
|
flags: ["--yes", "--skip-demo", "--demo", "--no-github"],
|
|
@@ -21401,6 +24121,19 @@ function validateCliArgs(cmd, args) {
|
|
|
21401
24121
|
values: { "--dir": "string", "--log": "string", "--base": "string" }
|
|
21402
24122
|
}).error;
|
|
21403
24123
|
}
|
|
24124
|
+
if (cmd === "runtime") {
|
|
24125
|
+
const [sub, ...rest] = args;
|
|
24126
|
+
if (sub === void 0) return `runtime requires a subcommand (${RUNTIME_SUBCOMMANDS.join(" | ")})`;
|
|
24127
|
+
if (sub !== "verify" && sub !== "status") {
|
|
24128
|
+
return `unknown runtime subcommand "${sub}" (${RUNTIME_SUBCOMMANDS.join(" | ")})`;
|
|
24129
|
+
}
|
|
24130
|
+
const parsed = validateFlatArgs(rest, {
|
|
24131
|
+
flags: ["--json"],
|
|
24132
|
+
values: { "--cwd": "string", "--runtime": "string", "--mode": "mode", "--model": "string" }
|
|
24133
|
+
});
|
|
24134
|
+
if (parsed.error) return parsed.error;
|
|
24135
|
+
return void 0;
|
|
24136
|
+
}
|
|
21404
24137
|
if (cmd === "research") {
|
|
21405
24138
|
const [sub, ...rest] = args;
|
|
21406
24139
|
if (sub === void 0) return `research requires a subcommand (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
@@ -21598,6 +24331,36 @@ Formats:
|
|
|
21598
24331
|
every tool the gate must see.
|
|
21599
24332
|
--force-workflow replaces a workflow it
|
|
21600
24333
|
did not write, or one you have edited.
|
|
24334
|
+
tamperward status [--json] [--cwd D] verification posture in three distinct
|
|
24335
|
+
lanes \u2014 Authority (repository/final
|
|
24336
|
+
adjudication), Intervention (runtime
|
|
24337
|
+
steering), and Verification: whether the
|
|
24338
|
+
last successful verify still applies to
|
|
24339
|
+
the EXACT current state (CURRENT / STALE /
|
|
24340
|
+
VERIFYING / BROKEN / UNVERIFIED). --json
|
|
24341
|
+
emits the versioned status document for
|
|
24342
|
+
editors, CI and dashboards. Local CURRENT
|
|
24343
|
+
is posture, never repository/CI authority.
|
|
24344
|
+
tamperward runtime verify [--runtime ID] qualify the in-loop runtime: an operation-
|
|
24345
|
+
[--mode headless|interactive] specific capability matrix (pre-deny:shell,
|
|
24346
|
+
[--model M] [--json] [--cwd D] native-edit, mcp, end-of-turn, transport:*,
|
|
24347
|
+
\u2026) with an explicit state per capability
|
|
24348
|
+
(PROVEN | PARTIAL | UNPROVEN | UNSUPPORTED |
|
|
24349
|
+
FAIL-OPEN | INCONCLUSIVE; never a score),
|
|
24350
|
+
derived from the shipped adapter's declared
|
|
24351
|
+
capabilities and committed evidence. Bound to
|
|
24352
|
+
the runtime version, TamperWard version/commit,
|
|
24353
|
+
adapter hash, hook-config hash, execution mode,
|
|
24354
|
+
platform and model; persisted git-locally.
|
|
24355
|
+
Reports EXISTING facts only \u2014 no promotion, no
|
|
24356
|
+
Round 4.1 claim. Final authority (CI / pristine
|
|
24357
|
+
verify) stays separate from in-loop steering.
|
|
24358
|
+
tamperward runtime status [--runtime ID] render the latest recorded qualification
|
|
24359
|
+
[--mode headless|interactive] WITHOUT rerunning it, and mark it STALE when a
|
|
24360
|
+
[--model M] [--json] [--cwd D] load-bearing input (version/config/adapter/mode/
|
|
24361
|
+
model/\u2026) changed since it was recorded. --mode and
|
|
24362
|
+
--model select the binding staleness is compared
|
|
24363
|
+
against, as for verify.
|
|
21601
24364
|
tamperward doctor [--base R] report installation + authority posture
|
|
21602
24365
|
[--workflow F] [--cwd D] and validate the CI verifier's outer-time
|
|
21603
24366
|
[--json] envelope against the trusted policy.
|
|
@@ -21650,6 +24413,8 @@ function main(argv) {
|
|
|
21650
24413
|
return runDoctor(parseDoctor(rest));
|
|
21651
24414
|
case "stats":
|
|
21652
24415
|
return runStats(parseStats(rest));
|
|
24416
|
+
case "status":
|
|
24417
|
+
return runStatus(parseStatus(rest));
|
|
21653
24418
|
case "onboard":
|
|
21654
24419
|
return runOnboard(parseOnboard(rest));
|
|
21655
24420
|
case "watch":
|
|
@@ -21662,6 +24427,10 @@ function main(argv) {
|
|
|
21662
24427
|
return runTraceVerify(parseTraceVerify(rest));
|
|
21663
24428
|
case "research":
|
|
21664
24429
|
return runResearchCommand(rest);
|
|
24430
|
+
case "runtime": {
|
|
24431
|
+
const { sub, opts } = parseRuntime(rest);
|
|
24432
|
+
return runRuntime(sub, opts);
|
|
24433
|
+
}
|
|
21665
24434
|
case "run":
|
|
21666
24435
|
return runEnvelope({
|
|
21667
24436
|
...parseRun(rest),
|
|
@@ -21705,12 +24474,14 @@ var init_main = __esm({
|
|
|
21705
24474
|
init_allow();
|
|
21706
24475
|
init_init();
|
|
21707
24476
|
init_doctor();
|
|
24477
|
+
init_status2();
|
|
21708
24478
|
init_verify();
|
|
21709
24479
|
init_trace_verify();
|
|
21710
24480
|
init_run();
|
|
21711
24481
|
init_watch();
|
|
21712
24482
|
init_onboard();
|
|
21713
24483
|
init_research();
|
|
24484
|
+
init_runtime();
|
|
21714
24485
|
init_audit();
|
|
21715
24486
|
init_signoff_label();
|
|
21716
24487
|
}
|
|
@@ -21719,7 +24490,7 @@ var init_main = __esm({
|
|
|
21719
24490
|
// src/cli/index.ts
|
|
21720
24491
|
init_hook_client();
|
|
21721
24492
|
init_exit();
|
|
21722
|
-
import { readFileSync as
|
|
24493
|
+
import { readFileSync as readFileSync30 } from "node:fs";
|
|
21723
24494
|
function loadMain() {
|
|
21724
24495
|
return Promise.resolve().then(() => (init_main(), main_exports));
|
|
21725
24496
|
}
|
|
@@ -21732,7 +24503,7 @@ async function launch(argv) {
|
|
|
21732
24503
|
if (kind && hookServiceEnabled()) {
|
|
21733
24504
|
let raw = null;
|
|
21734
24505
|
try {
|
|
21735
|
-
raw =
|
|
24506
|
+
raw = readFileSync30(0, "utf8");
|
|
21736
24507
|
} catch {
|
|
21737
24508
|
raw = null;
|
|
21738
24509
|
}
|