tamperward 2.36.0 → 2.37.1
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 +2545 -267
- package/package.json +1 -1
- package/schemas/runtime-qualification-v1.schema.json +184 -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
|
}
|
|
@@ -12183,7 +12201,7 @@ var init_fs_events = __esm({
|
|
|
12183
12201
|
|
|
12184
12202
|
// src/cli/audit.ts
|
|
12185
12203
|
import { createHash as createHash6, randomBytes } from "node:crypto";
|
|
12186
|
-
import { appendFileSync as appendFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync5,
|
|
12204
|
+
import { appendFileSync as appendFileSync4, closeSync as closeSync3, existsSync as existsSync9, mkdirSync as mkdirSync5, openSync as openSync3, readSync as readSync3 } from "node:fs";
|
|
12187
12205
|
import { dirname as dirname4, isAbsolute as isAbsolute8, join as join13, resolve as resolve4 } from "node:path";
|
|
12188
12206
|
function sessionHash(sessionId) {
|
|
12189
12207
|
if (!sessionId) return void 0;
|
|
@@ -12293,25 +12311,62 @@ function parseAuditEvent(value, line = 0) {
|
|
|
12293
12311
|
...typeof value.session === "string" ? { session: value.session } : {}
|
|
12294
12312
|
};
|
|
12295
12313
|
}
|
|
12296
|
-
function
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12314
|
+
function parseAuditLine(line, lineNumber) {
|
|
12315
|
+
let value;
|
|
12316
|
+
try {
|
|
12317
|
+
value = JSON.parse(line);
|
|
12318
|
+
} catch {
|
|
12319
|
+
throw new Error(`audit line ${lineNumber} is not valid JSON`);
|
|
12320
|
+
}
|
|
12321
|
+
return parseAuditEvent(value, lineNumber);
|
|
12322
|
+
}
|
|
12323
|
+
function forEachAuditLine(file, onLine, maxLineBytes = MAX_AUDIT_LINE_BYTES) {
|
|
12324
|
+
const fd = openSync3(file, "r");
|
|
12325
|
+
try {
|
|
12326
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
12327
|
+
let pending = [];
|
|
12328
|
+
let pendingBytes = 0;
|
|
12329
|
+
let lineNumber = 0;
|
|
12330
|
+
const emit3 = (parts) => {
|
|
12331
|
+
lineNumber++;
|
|
12332
|
+
let line = parts.length === 1 ? parts[0].toString("utf8") : Buffer.concat(parts).toString("utf8");
|
|
12333
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
12334
|
+
if (line.trim()) onLine(line, lineNumber);
|
|
12335
|
+
};
|
|
12336
|
+
for (; ; ) {
|
|
12337
|
+
const read = readSync3(fd, chunk, 0, chunk.length, null);
|
|
12338
|
+
if (read === 0) break;
|
|
12339
|
+
let start = 0;
|
|
12340
|
+
for (let i = 0; i < read; i++) {
|
|
12341
|
+
if (chunk[i] !== 10) continue;
|
|
12342
|
+
const part = chunk.subarray(start, i);
|
|
12343
|
+
if (pendingBytes + part.length > maxLineBytes) {
|
|
12344
|
+
throw new Error(`audit line ${lineNumber + 1} exceeds ${maxLineBytes} bytes (not an audit-v1 event)`);
|
|
12345
|
+
}
|
|
12346
|
+
emit3(pending.length ? [...pending, part] : [part]);
|
|
12347
|
+
pending = [];
|
|
12348
|
+
pendingBytes = 0;
|
|
12349
|
+
start = i + 1;
|
|
12350
|
+
}
|
|
12351
|
+
if (start < read) {
|
|
12352
|
+
const rest = Buffer.from(chunk.subarray(start, read));
|
|
12353
|
+
pendingBytes += rest.length;
|
|
12354
|
+
if (pendingBytes > maxLineBytes) {
|
|
12355
|
+
throw new Error(`audit line ${lineNumber + 1} exceeds ${maxLineBytes} bytes (not an audit-v1 event)`);
|
|
12356
|
+
}
|
|
12357
|
+
pending.push(rest);
|
|
12358
|
+
}
|
|
12305
12359
|
}
|
|
12306
|
-
|
|
12307
|
-
}
|
|
12308
|
-
|
|
12360
|
+
if (pending.length) emit3(pending);
|
|
12361
|
+
} finally {
|
|
12362
|
+
closeSync3(fd);
|
|
12363
|
+
}
|
|
12309
12364
|
}
|
|
12310
12365
|
function parseSince(value, nowMs = Date.now()) {
|
|
12311
|
-
const
|
|
12312
|
-
if (
|
|
12313
|
-
const count = Number(
|
|
12314
|
-
const unit =
|
|
12366
|
+
const relative13 = /^(\d+)(m|h|d)$/.exec(value);
|
|
12367
|
+
if (relative13) {
|
|
12368
|
+
const count = Number(relative13[1]);
|
|
12369
|
+
const unit = relative13[2] === "m" ? 6e4 : relative13[2] === "h" ? 36e5 : 864e5;
|
|
12315
12370
|
if (!Number.isSafeInteger(count) || count <= 0) throw new Error(`invalid --since value "${value}"`);
|
|
12316
12371
|
return nowMs - count * unit;
|
|
12317
12372
|
}
|
|
@@ -12321,37 +12376,58 @@ function parseSince(value, nowMs = Date.now()) {
|
|
|
12321
12376
|
}
|
|
12322
12377
|
return parsed;
|
|
12323
12378
|
}
|
|
12324
|
-
function
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12379
|
+
function createAuditAggregator() {
|
|
12380
|
+
let events = 0;
|
|
12381
|
+
let blocked = 0;
|
|
12382
|
+
let warnings = 0;
|
|
12328
12383
|
const rules = /* @__PURE__ */ new Map();
|
|
12329
12384
|
const surfaces = /* @__PURE__ */ new Map();
|
|
12330
12385
|
const sessions = /* @__PURE__ */ new Set();
|
|
12331
|
-
|
|
12332
|
-
const bucket = rules.get(event.rule) ?? { events: 0, blocked: 0, warnings: 0 };
|
|
12333
|
-
bucket.events++;
|
|
12334
|
-
if (event.severity === "block") bucket.blocked++;
|
|
12335
|
-
else bucket.warnings++;
|
|
12336
|
-
rules.set(event.rule, bucket);
|
|
12337
|
-
surfaces.set(event.surface, (surfaces.get(event.surface) ?? 0) + 1);
|
|
12338
|
-
if (event.session) sessions.add(event.session);
|
|
12339
|
-
}
|
|
12340
|
-
const by_rule = [...rules.entries()].map(([rule, counts]) => ({ rule, ...counts })).sort((a, b) => b.events - a.events || a.rule.localeCompare(b.rule));
|
|
12341
|
-
const by_surface = [...surfaces.entries()].map(([surface, count]) => ({ surface, events: count })).sort((a, b) => b.events - a.events || a.surface.localeCompare(b.surface));
|
|
12386
|
+
const bounds = { first: null, last: null };
|
|
12342
12387
|
return {
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12388
|
+
add(event) {
|
|
12389
|
+
events++;
|
|
12390
|
+
if (event.severity === "block") blocked++;
|
|
12391
|
+
else warnings++;
|
|
12392
|
+
const bucket = rules.get(event.rule) ?? { events: 0, blocked: 0, warnings: 0 };
|
|
12393
|
+
bucket.events++;
|
|
12394
|
+
if (event.severity === "block") bucket.blocked++;
|
|
12395
|
+
else bucket.warnings++;
|
|
12396
|
+
rules.set(event.rule, bucket);
|
|
12397
|
+
surfaces.set(event.surface, (surfaces.get(event.surface) ?? 0) + 1);
|
|
12398
|
+
if (event.session) sessions.add(event.session);
|
|
12399
|
+
const ms = Date.parse(event.timestamp);
|
|
12400
|
+
const { first, last } = bounds;
|
|
12401
|
+
if (first === null || ms < first.ms || ms === first.ms && event.id.localeCompare(first.id) < 0) {
|
|
12402
|
+
bounds.first = { ms, id: event.id, timestamp: event.timestamp };
|
|
12403
|
+
}
|
|
12404
|
+
if (last === null || ms > last.ms || ms === last.ms && event.id.localeCompare(last.id) > 0) {
|
|
12405
|
+
bounds.last = { ms, id: event.id, timestamp: event.timestamp };
|
|
12406
|
+
}
|
|
12407
|
+
},
|
|
12408
|
+
finish() {
|
|
12409
|
+
const by_rule = [...rules.entries()].map(([rule, counts]) => ({ rule, ...counts })).sort((a, b) => b.events - a.events || a.rule.localeCompare(b.rule));
|
|
12410
|
+
const by_surface = [...surfaces.entries()].map(([surface, count]) => ({ surface, events: count })).sort((a, b) => b.events - a.events || a.surface.localeCompare(b.surface));
|
|
12411
|
+
return {
|
|
12412
|
+
schema_version: AUDIT_SCHEMA_VERSION,
|
|
12413
|
+
events,
|
|
12414
|
+
blocked,
|
|
12415
|
+
warnings,
|
|
12416
|
+
sessions: sessions.size,
|
|
12417
|
+
first_event: bounds.first === null ? null : bounds.first.timestamp,
|
|
12418
|
+
last_event: bounds.last === null ? null : bounds.last.timestamp,
|
|
12419
|
+
by_rule,
|
|
12420
|
+
by_surface,
|
|
12421
|
+
interpretation: "finding-is-not-proof-of-intent"
|
|
12422
|
+
};
|
|
12423
|
+
}
|
|
12353
12424
|
};
|
|
12354
12425
|
}
|
|
12426
|
+
function summarizeAudit(events) {
|
|
12427
|
+
const aggregator = createAuditAggregator();
|
|
12428
|
+
for (const event of events) aggregator.add(event);
|
|
12429
|
+
return aggregator.finish();
|
|
12430
|
+
}
|
|
12355
12431
|
function renderAuditStats(summary) {
|
|
12356
12432
|
const out3 = [
|
|
12357
12433
|
"TamperWard audit stats",
|
|
@@ -12390,21 +12466,24 @@ function runStats(opts = {}) {
|
|
|
12390
12466
|
process.stdout.write(opts.json ? JSON.stringify(empty2) + "\n" : renderAuditStats(empty2));
|
|
12391
12467
|
return 0;
|
|
12392
12468
|
}
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12469
|
+
const aggregator = createAuditAggregator();
|
|
12470
|
+
forEachAuditLine(file, (line, lineNumber) => {
|
|
12471
|
+
const event = parseAuditLine(line, lineNumber);
|
|
12472
|
+
if (cutoff !== null && Date.parse(event.timestamp) < cutoff) return;
|
|
12473
|
+
aggregator.add(event);
|
|
12474
|
+
});
|
|
12475
|
+
const summary = aggregator.finish();
|
|
12398
12476
|
process.stdout.write(opts.json ? JSON.stringify(summary) + "\n" : renderAuditStats(summary));
|
|
12399
12477
|
return 0;
|
|
12400
12478
|
}
|
|
12401
|
-
var AUDIT_SCHEMA_VERSION, AUDIT_LOG_ENV, ALLOWED_KEYS;
|
|
12479
|
+
var AUDIT_SCHEMA_VERSION, AUDIT_LOG_ENV, MAX_AUDIT_LINE_BYTES, ALLOWED_KEYS;
|
|
12402
12480
|
var init_audit = __esm({
|
|
12403
12481
|
"src/cli/audit.ts"() {
|
|
12404
12482
|
"use strict";
|
|
12405
12483
|
init_repo_context();
|
|
12406
12484
|
AUDIT_SCHEMA_VERSION = 1;
|
|
12407
12485
|
AUDIT_LOG_ENV = "TAMPERWARD_AUDIT_LOG";
|
|
12486
|
+
MAX_AUDIT_LINE_BYTES = 16 * 1024;
|
|
12408
12487
|
ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
12409
12488
|
"schema_version",
|
|
12410
12489
|
"id",
|
|
@@ -12420,14 +12499,14 @@ var init_audit = __esm({
|
|
|
12420
12499
|
});
|
|
12421
12500
|
|
|
12422
12501
|
// src/cli/hook.ts
|
|
12423
|
-
import { readFileSync as
|
|
12502
|
+
import { readFileSync as readFileSync10, appendFileSync as appendFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
|
|
12424
12503
|
import { createHash as createHash7 } from "node:crypto";
|
|
12425
12504
|
function setSnapshotCache(cache3) {
|
|
12426
12505
|
snapshotCache = cache3;
|
|
12427
12506
|
}
|
|
12428
12507
|
function readStdin() {
|
|
12429
12508
|
try {
|
|
12430
|
-
return
|
|
12509
|
+
return readFileSync10(0, "utf8");
|
|
12431
12510
|
} catch (e) {
|
|
12432
12511
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
12433
12512
|
}
|
|
@@ -12720,7 +12799,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
12720
12799
|
if (!existsSync10(log)) return none;
|
|
12721
12800
|
let offset = 0;
|
|
12722
12801
|
if (cp && existsSync10(cp)) {
|
|
12723
|
-
const n2 = Number(
|
|
12802
|
+
const n2 = Number(readFileSync10(cp, "utf8"));
|
|
12724
12803
|
offset = Number.isFinite(n2) ? n2 : 0;
|
|
12725
12804
|
}
|
|
12726
12805
|
const drained = drainEvents(log, offset);
|
|
@@ -12994,13 +13073,13 @@ var init_ptree_cache = __esm({
|
|
|
12994
13073
|
});
|
|
12995
13074
|
|
|
12996
13075
|
// src/cli/hook-service.ts
|
|
12997
|
-
import { chmodSync, existsSync as existsSync11, lstatSync as lstatSync6, mkdirSync as mkdirSync6, readFileSync as
|
|
13076
|
+
import { chmodSync, existsSync as existsSync11, lstatSync as lstatSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
12998
13077
|
import { createConnection as createConnection2, createServer } from "node:net";
|
|
12999
13078
|
import { realpathSync as realpathSync4 } from "node:fs";
|
|
13000
13079
|
import { sep } from "node:path";
|
|
13001
13080
|
function readServiceState(paths) {
|
|
13002
13081
|
try {
|
|
13003
|
-
const v = JSON.parse(
|
|
13082
|
+
const v = JSON.parse(readFileSync11(paths.state, "utf8"));
|
|
13004
13083
|
if (!isRecord(v)) return null;
|
|
13005
13084
|
const { pid, version, root, started_at } = v;
|
|
13006
13085
|
if (typeof pid !== "number" || typeof version !== "string" || typeof root !== "string" || typeof started_at !== "string") return null;
|
|
@@ -13019,12 +13098,12 @@ function pidAlive2(pid) {
|
|
|
13019
13098
|
}
|
|
13020
13099
|
}
|
|
13021
13100
|
function listening(socket) {
|
|
13022
|
-
return new Promise((
|
|
13101
|
+
return new Promise((resolve23) => {
|
|
13023
13102
|
const sock = createConnection2(socket);
|
|
13024
13103
|
const done = (v) => {
|
|
13025
13104
|
clearTimeout(timer);
|
|
13026
13105
|
sock.destroy();
|
|
13027
|
-
|
|
13106
|
+
resolve23(v);
|
|
13028
13107
|
};
|
|
13029
13108
|
const timer = setTimeout(() => done(false), 1e3);
|
|
13030
13109
|
sock.once("connect", () => done(true));
|
|
@@ -13203,11 +13282,11 @@ async function startHookService(opts) {
|
|
|
13203
13282
|
});
|
|
13204
13283
|
const umask = process.umask(63);
|
|
13205
13284
|
try {
|
|
13206
|
-
await new Promise((
|
|
13285
|
+
await new Promise((resolve23, reject) => {
|
|
13207
13286
|
server.once("error", reject);
|
|
13208
13287
|
server.listen(paths.socket, () => {
|
|
13209
13288
|
server.off("error", reject);
|
|
13210
|
-
|
|
13289
|
+
resolve23();
|
|
13211
13290
|
});
|
|
13212
13291
|
});
|
|
13213
13292
|
} finally {
|
|
@@ -13224,8 +13303,8 @@ async function startHookService(opts) {
|
|
|
13224
13303
|
get served() {
|
|
13225
13304
|
return served;
|
|
13226
13305
|
},
|
|
13227
|
-
close: () => new Promise((
|
|
13228
|
-
if (closed) return
|
|
13306
|
+
close: () => new Promise((resolve23) => {
|
|
13307
|
+
if (closed) return resolve23();
|
|
13229
13308
|
closed = true;
|
|
13230
13309
|
setSnapshotCache(null);
|
|
13231
13310
|
let settled = false;
|
|
@@ -13236,7 +13315,7 @@ async function startHookService(opts) {
|
|
|
13236
13315
|
clearTimeout(drain);
|
|
13237
13316
|
removeQuietly(paths.socket);
|
|
13238
13317
|
removeQuietly(paths.state);
|
|
13239
|
-
|
|
13318
|
+
resolve23();
|
|
13240
13319
|
};
|
|
13241
13320
|
server.close(finalize);
|
|
13242
13321
|
for (const sock of sockets) if (sock !== acceptedSocket) sock.destroy();
|
|
@@ -13561,7 +13640,7 @@ var init_safe_write = __esm({
|
|
|
13561
13640
|
// src/cli/init.ts
|
|
13562
13641
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
13563
13642
|
import { createHash as createHash8 } from "node:crypto";
|
|
13564
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as
|
|
13643
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync12, realpathSync as realpathSync5 } from "node:fs";
|
|
13565
13644
|
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join15, relative as relative4, resolve as resolve6 } from "node:path";
|
|
13566
13645
|
function gitConfig(cwd, key2) {
|
|
13567
13646
|
try {
|
|
@@ -13655,34 +13734,34 @@ function verifierCandidates(cwd) {
|
|
|
13655
13734
|
if (!candidates.includes(command)) candidates.push(command);
|
|
13656
13735
|
};
|
|
13657
13736
|
try {
|
|
13658
|
-
const pkg = JSON.parse(
|
|
13737
|
+
const pkg = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
|
|
13659
13738
|
const test = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts.test : void 0;
|
|
13660
13739
|
if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
|
|
13661
13740
|
} catch {
|
|
13662
13741
|
}
|
|
13663
13742
|
try {
|
|
13664
|
-
const pyproject =
|
|
13743
|
+
const pyproject = readFileSync12(join15(cwd, "pyproject.toml"), "utf8");
|
|
13665
13744
|
if (/^\s*\[tool\.pytest(?:\.ini_options)?\]\s*$/m.test(pyproject)) add("pytest");
|
|
13666
13745
|
} catch {
|
|
13667
13746
|
}
|
|
13668
13747
|
if (existsSync12(join15(cwd, "pytest.ini"))) add("pytest");
|
|
13669
13748
|
try {
|
|
13670
|
-
const setupCfg =
|
|
13749
|
+
const setupCfg = readFileSync12(join15(cwd, "setup.cfg"), "utf8");
|
|
13671
13750
|
if (/^\s*\[tool:pytest\]\s*$/m.test(setupCfg)) add("pytest");
|
|
13672
13751
|
} catch {
|
|
13673
13752
|
}
|
|
13674
13753
|
try {
|
|
13675
|
-
const tox =
|
|
13754
|
+
const tox = readFileSync12(join15(cwd, "tox.ini"), "utf8");
|
|
13676
13755
|
if (/^\s*\[tox\]\s*$/m.test(tox)) add("tox");
|
|
13677
13756
|
} catch {
|
|
13678
13757
|
}
|
|
13679
13758
|
try {
|
|
13680
|
-
const cargo =
|
|
13759
|
+
const cargo = readFileSync12(join15(cwd, "Cargo.toml"), "utf8");
|
|
13681
13760
|
if (/^\s*\[(?:package|workspace)\]\s*(?:#.*)?$/m.test(cargo)) add("cargo test");
|
|
13682
13761
|
} catch {
|
|
13683
13762
|
}
|
|
13684
13763
|
try {
|
|
13685
|
-
const goMod =
|
|
13764
|
+
const goMod = readFileSync12(join15(cwd, "go.mod"), "utf8");
|
|
13686
13765
|
if (/^\s*module\s+\S+\s*(?:\/\/.*)?$/m.test(goMod)) add("go test ./...");
|
|
13687
13766
|
} catch {
|
|
13688
13767
|
}
|
|
@@ -13754,7 +13833,7 @@ function planGitignore(cwd) {
|
|
|
13754
13833
|
}
|
|
13755
13834
|
const refused = refuseNonRegular(path);
|
|
13756
13835
|
if (refused) return { item: "gitignore", path: rel, status: "error", detail: refused };
|
|
13757
|
-
if (kind === "file") existing =
|
|
13836
|
+
if (kind === "file") existing = readFileSync12(path, "utf8");
|
|
13758
13837
|
try {
|
|
13759
13838
|
const tracked = execFileSync6("git", ["ls-files", "--cached", "--", "node_modules"], {
|
|
13760
13839
|
cwd,
|
|
@@ -13812,7 +13891,7 @@ function planClaudeHooks(cwd) {
|
|
|
13812
13891
|
if (existsSync12(path)) {
|
|
13813
13892
|
let parsed;
|
|
13814
13893
|
try {
|
|
13815
|
-
parsed = JSON.parse(
|
|
13894
|
+
parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
13816
13895
|
} catch {
|
|
13817
13896
|
return {
|
|
13818
13897
|
item: "agent",
|
|
@@ -13954,7 +14033,7 @@ function planPreCommit(cwd) {
|
|
|
13954
14033
|
const rel = display(cwd, path);
|
|
13955
14034
|
const refused = refuseNonRegular(path);
|
|
13956
14035
|
if (refused) return { item: "pre-commit", path: rel, status: "error", detail: refused };
|
|
13957
|
-
const existing = existsSync12(path) ?
|
|
14036
|
+
const existing = existsSync12(path) ? readFileSync12(path, "utf8") : null;
|
|
13958
14037
|
const hookMode = () => existingMode(path, 493) | 73;
|
|
13959
14038
|
if (existing === null) {
|
|
13960
14039
|
return {
|
|
@@ -14045,7 +14124,7 @@ function planCodeowners(cwd) {
|
|
|
14045
14124
|
const path = join15(cwd, existingRel ?? rel);
|
|
14046
14125
|
const refused = refuseNonRegular(path);
|
|
14047
14126
|
if (refused) return { item: "codeowners", path: existingRel ?? rel, status: "error", detail: refused };
|
|
14048
|
-
const existing = existingRel ?
|
|
14127
|
+
const existing = existingRel ? readFileSync12(path, "utf8") : null;
|
|
14049
14128
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
14050
14129
|
if (existingRel !== void 0 && missing.length === 0) {
|
|
14051
14130
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -14096,7 +14175,7 @@ function planWorkflow(cwd, force) {
|
|
|
14096
14175
|
}
|
|
14097
14176
|
let src;
|
|
14098
14177
|
try {
|
|
14099
|
-
src =
|
|
14178
|
+
src = readFileSync12(path, "utf8");
|
|
14100
14179
|
} catch {
|
|
14101
14180
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
14102
14181
|
}
|
|
@@ -14396,7 +14475,7 @@ import {
|
|
|
14396
14475
|
existsSync as existsSync13,
|
|
14397
14476
|
lstatSync as lstatSync8,
|
|
14398
14477
|
readdirSync as readdirSync3,
|
|
14399
|
-
readFileSync as
|
|
14478
|
+
readFileSync as readFileSync13,
|
|
14400
14479
|
readlinkSync as readlinkSync2,
|
|
14401
14480
|
realpathSync as realpathSync6,
|
|
14402
14481
|
statSync as statSync3
|
|
@@ -14454,7 +14533,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
14454
14533
|
st = statSync3(path);
|
|
14455
14534
|
if (!st.isFile()) throw new Error("not a regular file");
|
|
14456
14535
|
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
14457
|
-
h.update(
|
|
14536
|
+
h.update(readFileSync13(path));
|
|
14458
14537
|
h.update("\0");
|
|
14459
14538
|
} catch (e) {
|
|
14460
14539
|
throw new Unattestable(
|
|
@@ -14488,7 +14567,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
14488
14567
|
h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
|
|
14489
14568
|
if (ident.kind === "file") {
|
|
14490
14569
|
try {
|
|
14491
|
-
h.update(
|
|
14570
|
+
h.update(readFileSync13(path));
|
|
14492
14571
|
h.update("\0");
|
|
14493
14572
|
} catch (e) {
|
|
14494
14573
|
throw new Unattestable(
|
|
@@ -14659,7 +14738,7 @@ function venvProblem(root) {
|
|
|
14659
14738
|
const cfg = join16(root, "pyvenv.cfg");
|
|
14660
14739
|
if (!existsSync13(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
|
|
14661
14740
|
try {
|
|
14662
|
-
const body =
|
|
14741
|
+
const body = readFileSync13(cfg, "utf8");
|
|
14663
14742
|
if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
|
|
14664
14743
|
return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
|
|
14665
14744
|
}
|
|
@@ -15175,7 +15254,7 @@ if (child) {
|
|
|
15175
15254
|
// src/verifier-backend.ts
|
|
15176
15255
|
import { createHash as createHash11, randomUUID } from "node:crypto";
|
|
15177
15256
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
15178
|
-
import { accessSync as accessSync2, constants as constants3, readFileSync as
|
|
15257
|
+
import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync14, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
|
|
15179
15258
|
import { delimiter as delimiter2, dirname as dirname7, join as join18, resolve as resolve8 } from "node:path";
|
|
15180
15259
|
function identity() {
|
|
15181
15260
|
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
@@ -15220,7 +15299,7 @@ function findExecutable(name) {
|
|
|
15220
15299
|
return null;
|
|
15221
15300
|
}
|
|
15222
15301
|
function fileSha256(path) {
|
|
15223
|
-
return createHash11("sha256").update(
|
|
15302
|
+
return createHash11("sha256").update(readFileSync14(path)).digest("hex");
|
|
15224
15303
|
}
|
|
15225
15304
|
function engineClientEnv() {
|
|
15226
15305
|
return {
|
|
@@ -15654,7 +15733,7 @@ var init_verifier_backend = __esm({
|
|
|
15654
15733
|
// src/verification-state.ts
|
|
15655
15734
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
15656
15735
|
import { createHash as createHash12 } from "node:crypto";
|
|
15657
|
-
import { mkdirSync as mkdirSync8, readFileSync as
|
|
15736
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
15658
15737
|
import { dirname as dirname8, join as join19 } from "node:path";
|
|
15659
15738
|
function git2(args, cwd) {
|
|
15660
15739
|
return execFileSync8("git", args, {
|
|
@@ -15703,7 +15782,7 @@ function interventionWiring(cwd) {
|
|
|
15703
15782
|
return sources.map(([source, path]) => {
|
|
15704
15783
|
let parsed;
|
|
15705
15784
|
try {
|
|
15706
|
-
parsed = JSON.parse(
|
|
15785
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
15707
15786
|
} catch {
|
|
15708
15787
|
return { source, present: false };
|
|
15709
15788
|
}
|
|
@@ -15856,7 +15935,7 @@ function readVerificationRecord(cwd) {
|
|
|
15856
15935
|
if (!path) return null;
|
|
15857
15936
|
let raw;
|
|
15858
15937
|
try {
|
|
15859
|
-
raw =
|
|
15938
|
+
raw = readFileSync15(path, "utf8");
|
|
15860
15939
|
} catch {
|
|
15861
15940
|
return null;
|
|
15862
15941
|
}
|
|
@@ -15905,7 +15984,7 @@ function readVerifyingMarker(cwd) {
|
|
|
15905
15984
|
if (!path) return null;
|
|
15906
15985
|
let parsed;
|
|
15907
15986
|
try {
|
|
15908
|
-
parsed = JSON.parse(
|
|
15987
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
15909
15988
|
} catch {
|
|
15910
15989
|
return null;
|
|
15911
15990
|
}
|
|
@@ -16017,7 +16096,7 @@ var init_verification_state = __esm({
|
|
|
16017
16096
|
// src/cli/verify.ts
|
|
16018
16097
|
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
16019
16098
|
import { createHash as createHash13 } from "node:crypto";
|
|
16020
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as
|
|
16099
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync8, symlinkSync, writeFileSync as writeFileSync10, existsSync as existsSync14 } from "node:fs";
|
|
16021
16100
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
16022
16101
|
import { dirname as dirname9, isAbsolute as isAbsolute11, join as join20, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
|
|
16023
16102
|
function oracleAssuranceReport() {
|
|
@@ -16277,7 +16356,7 @@ function overlayDigest(dest, paths) {
|
|
|
16277
16356
|
try {
|
|
16278
16357
|
const st = lstatSync9(p);
|
|
16279
16358
|
h.update(String(st.mode));
|
|
16280
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
16359
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
|
|
16281
16360
|
} catch {
|
|
16282
16361
|
h.update("<gone>");
|
|
16283
16362
|
}
|
|
@@ -17074,7 +17153,7 @@ var init_verify = __esm({
|
|
|
17074
17153
|
// src/cli/run.ts
|
|
17075
17154
|
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
17076
17155
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
17077
|
-
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as
|
|
17156
|
+
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync9, statSync as statSync5, writeFileSync as writeFileSync11 } from "node:fs";
|
|
17078
17157
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
17079
17158
|
import { dirname as dirname10, join as join21, resolve as resolve10 } from "node:path";
|
|
17080
17159
|
function authoritativeRunLifecyclePlatform(platform = process.platform) {
|
|
@@ -17228,7 +17307,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
17228
17307
|
}
|
|
17229
17308
|
let state = {};
|
|
17230
17309
|
try {
|
|
17231
|
-
state = JSON.parse(
|
|
17310
|
+
state = JSON.parse(readFileSync17(resultFile, "utf8"));
|
|
17232
17311
|
} catch {
|
|
17233
17312
|
return {
|
|
17234
17313
|
exit: 1,
|
|
@@ -17272,7 +17351,7 @@ function observerExited(pid) {
|
|
|
17272
17351
|
if (!pidAlive4(pid)) return true;
|
|
17273
17352
|
if (process.platform !== "linux") return false;
|
|
17274
17353
|
try {
|
|
17275
|
-
const stat =
|
|
17354
|
+
const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
|
|
17276
17355
|
const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0];
|
|
17277
17356
|
return state === "Z" || state === "X";
|
|
17278
17357
|
} catch (e) {
|
|
@@ -17394,7 +17473,7 @@ function git4(args, cwd) {
|
|
|
17394
17473
|
}
|
|
17395
17474
|
function nowTicks() {
|
|
17396
17475
|
try {
|
|
17397
|
-
return Math.floor(parseFloat(
|
|
17476
|
+
return Math.floor(parseFloat(readFileSync17("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
17398
17477
|
} catch {
|
|
17399
17478
|
return Number.POSITIVE_INFINITY;
|
|
17400
17479
|
}
|
|
@@ -17417,7 +17496,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
17417
17496
|
const n2 = Number(pid);
|
|
17418
17497
|
if (n2 === process.pid) continue;
|
|
17419
17498
|
try {
|
|
17420
|
-
const stat =
|
|
17499
|
+
const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
|
|
17421
17500
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
17422
17501
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
17423
17502
|
} catch {
|
|
@@ -17990,7 +18069,7 @@ raise SystemExit(0)
|
|
|
17990
18069
|
|
|
17991
18070
|
// src/cli/doctor.ts
|
|
17992
18071
|
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
17993
|
-
import { existsSync as existsSync15, readFileSync as
|
|
18072
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
|
|
17994
18073
|
import { join as join22, resolve as resolve11 } from "node:path";
|
|
17995
18074
|
function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
|
|
17996
18075
|
if (platform === "linux") {
|
|
@@ -18049,7 +18128,7 @@ function pinsInFile(path) {
|
|
|
18049
18128
|
if (!existsSync15(path)) return [];
|
|
18050
18129
|
let src = "";
|
|
18051
18130
|
try {
|
|
18052
|
-
src =
|
|
18131
|
+
src = readFileSync18(path, "utf8");
|
|
18053
18132
|
} catch {
|
|
18054
18133
|
return [];
|
|
18055
18134
|
}
|
|
@@ -18076,7 +18155,7 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
|
|
|
18076
18155
|
}
|
|
18077
18156
|
let doc;
|
|
18078
18157
|
try {
|
|
18079
|
-
doc = yaml.parse(
|
|
18158
|
+
doc = yaml.parse(readFileSync18(path, "utf8"));
|
|
18080
18159
|
} catch (e) {
|
|
18081
18160
|
broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
|
|
18082
18161
|
continue;
|
|
@@ -18500,7 +18579,7 @@ function diagnose(opts = {}) {
|
|
|
18500
18579
|
}
|
|
18501
18580
|
let doc;
|
|
18502
18581
|
try {
|
|
18503
|
-
doc = yaml.parse(
|
|
18582
|
+
doc = yaml.parse(readFileSync18(workflowPath, "utf8"));
|
|
18504
18583
|
} catch (e) {
|
|
18505
18584
|
return fail(
|
|
18506
18585
|
"ci-verifier",
|
|
@@ -18813,7 +18892,7 @@ import { execFileSync as execFileSync12, spawnSync as spawnSync5 } from "node:ch
|
|
|
18813
18892
|
import {
|
|
18814
18893
|
existsSync as existsSync16,
|
|
18815
18894
|
mkdtempSync as mkdtempSync5,
|
|
18816
|
-
readFileSync as
|
|
18895
|
+
readFileSync as readFileSync19,
|
|
18817
18896
|
readdirSync as readdirSync6,
|
|
18818
18897
|
realpathSync as realpathSync9,
|
|
18819
18898
|
rmSync as rmSync10,
|
|
@@ -19127,7 +19206,7 @@ function straceOnce(root, command, budget) {
|
|
|
19127
19206
|
}
|
|
19128
19207
|
);
|
|
19129
19208
|
const logs = traced.error ? [] : traceFiles(prefix);
|
|
19130
|
-
const accesses = logs.flatMap((path) => parseStraceFileAccess(
|
|
19209
|
+
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync19(path, "utf8"), TRACE_ROOT));
|
|
19131
19210
|
const raw = {
|
|
19132
19211
|
spawnError: traced.error ? traced.error.message : null,
|
|
19133
19212
|
status: traced.status,
|
|
@@ -19359,7 +19438,7 @@ var init_trace_verify = __esm({
|
|
|
19359
19438
|
});
|
|
19360
19439
|
|
|
19361
19440
|
// src/runtimes.ts
|
|
19362
|
-
import { existsSync as existsSync17, readFileSync as
|
|
19441
|
+
import { existsSync as existsSync17, readFileSync as readFileSync20 } from "node:fs";
|
|
19363
19442
|
import { join as join24 } from "node:path";
|
|
19364
19443
|
function isRecord5(x) {
|
|
19365
19444
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
@@ -19390,7 +19469,7 @@ function claudeSettingsBeyondTamperward(cwd) {
|
|
|
19390
19469
|
let raw;
|
|
19391
19470
|
try {
|
|
19392
19471
|
if (!existsSync17(path)) return false;
|
|
19393
|
-
raw =
|
|
19472
|
+
raw = readFileSync20(path, "utf8");
|
|
19394
19473
|
} catch {
|
|
19395
19474
|
return false;
|
|
19396
19475
|
}
|
|
@@ -19497,7 +19576,7 @@ var init_runtimes = __esm({
|
|
|
19497
19576
|
|
|
19498
19577
|
// src/cli/onboard.ts
|
|
19499
19578
|
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
19500
|
-
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as
|
|
19579
|
+
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync10, rmSync as rmSync11 } from "node:fs";
|
|
19501
19580
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
19502
19581
|
import { join as join25, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
|
|
19503
19582
|
import { createInterface } from "node:readline";
|
|
@@ -19508,9 +19587,9 @@ function readlineAsker(input, output) {
|
|
|
19508
19587
|
let pending = null;
|
|
19509
19588
|
rl.on("close", () => {
|
|
19510
19589
|
closed = true;
|
|
19511
|
-
const
|
|
19590
|
+
const resolve23 = pending;
|
|
19512
19591
|
pending = null;
|
|
19513
|
-
|
|
19592
|
+
resolve23?.(null);
|
|
19514
19593
|
});
|
|
19515
19594
|
rl.on("SIGINT", () => rl.close());
|
|
19516
19595
|
const ask = (question) => new Promise((resolvePromise) => {
|
|
@@ -19570,7 +19649,7 @@ function writeVerifyCommand(cwd, command) {
|
|
|
19570
19649
|
return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
|
|
19571
19650
|
}
|
|
19572
19651
|
const mode = existingMode(path, 420);
|
|
19573
|
-
const original = kind === "file" ?
|
|
19652
|
+
const original = kind === "file" ? readFileSync21(path, "utf8") : null;
|
|
19574
19653
|
let next;
|
|
19575
19654
|
try {
|
|
19576
19655
|
const doc = parseDocument(original && original.trim() ? original : "version: 1\n");
|
|
@@ -20182,7 +20261,7 @@ var init_capture = __esm({
|
|
|
20182
20261
|
|
|
20183
20262
|
// src/research/manifest.ts
|
|
20184
20263
|
import { createHash as createHash14 } from "node:crypto";
|
|
20185
|
-
import { readFileSync as
|
|
20264
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
20186
20265
|
import { dirname as dirname11, resolve as resolve15 } from "node:path";
|
|
20187
20266
|
function taskFrom(raw, index, manifestDir) {
|
|
20188
20267
|
const where = `task #${index + 1}`;
|
|
@@ -20216,7 +20295,7 @@ function readManifest(path) {
|
|
|
20216
20295
|
const abs = resolve15(path);
|
|
20217
20296
|
let bytes;
|
|
20218
20297
|
try {
|
|
20219
|
-
bytes =
|
|
20298
|
+
bytes = readFileSync22(abs);
|
|
20220
20299
|
} catch (e) {
|
|
20221
20300
|
throw new ResearchError(`cannot read task manifest ${abs}: ${e instanceof Error ? e.message : String(e)}`);
|
|
20222
20301
|
}
|
|
@@ -20514,7 +20593,7 @@ var init_record = __esm({
|
|
|
20514
20593
|
// src/research/run.ts
|
|
20515
20594
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20516
20595
|
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
20517
|
-
import { closeSync as
|
|
20596
|
+
import { closeSync as closeSync4, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync10, openSync as openSync4, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync12, unlinkSync, writeFileSync as writeFileSync12, writeSync } from "node:fs";
|
|
20518
20597
|
import { hostname } from "node:os";
|
|
20519
20598
|
import { join as join26, resolve as resolve16 } from "node:path";
|
|
20520
20599
|
function git7(args, cwd) {
|
|
@@ -20528,7 +20607,7 @@ function researchLockPath(ledger) {
|
|
|
20528
20607
|
}
|
|
20529
20608
|
function readLockOwner(path) {
|
|
20530
20609
|
try {
|
|
20531
|
-
const parsed = JSON.parse(
|
|
20610
|
+
const parsed = JSON.parse(readFileSync23(path, "utf8"));
|
|
20532
20611
|
if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
20533
20612
|
if (typeof parsed.host !== "string" || typeof parsed.started_at !== "string" || typeof parsed.token !== "string") return null;
|
|
20534
20613
|
return { pid: parsed.pid, host: parsed.host, started_at: parsed.started_at, token: parsed.token };
|
|
@@ -20594,12 +20673,12 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
20594
20673
|
const text = JSON.stringify(owner) + "\n";
|
|
20595
20674
|
let fd = -1;
|
|
20596
20675
|
try {
|
|
20597
|
-
fd =
|
|
20676
|
+
fd = openSync4(path, "wx", 384);
|
|
20598
20677
|
writeSync(fd, text);
|
|
20599
|
-
|
|
20678
|
+
closeSync4(fd);
|
|
20600
20679
|
fd = -1;
|
|
20601
20680
|
} catch (e) {
|
|
20602
|
-
if (fd !== -1)
|
|
20681
|
+
if (fd !== -1) closeSync4(fd);
|
|
20603
20682
|
if (e.code === "EEXIST") {
|
|
20604
20683
|
throw new ResearchError(
|
|
20605
20684
|
`research output ${ledger} is already locked by ${lockHolder(path)}; refusing concurrent writers. Verify the owner is not running, then remove ${path} deliberately or rerun with --break-lock`
|
|
@@ -20613,7 +20692,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
20613
20692
|
if (released) return;
|
|
20614
20693
|
released = true;
|
|
20615
20694
|
try {
|
|
20616
|
-
if (
|
|
20695
|
+
if (readFileSync23(path, "utf8") === text) unlinkSync(path);
|
|
20617
20696
|
} catch {
|
|
20618
20697
|
}
|
|
20619
20698
|
}
|
|
@@ -20622,7 +20701,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
20622
20701
|
function resumableRecord(path, expected) {
|
|
20623
20702
|
let raw;
|
|
20624
20703
|
try {
|
|
20625
|
-
raw = JSON.parse(
|
|
20704
|
+
raw = JSON.parse(readFileSync23(path, "utf8"));
|
|
20626
20705
|
} catch (e) {
|
|
20627
20706
|
throw new ResearchError(`ledger record ${path} is malformed (${errorMessage(e).split("\n")[0]}); remove it to regenerate the pair, or use a new --out`);
|
|
20628
20707
|
}
|
|
@@ -21044,7 +21123,7 @@ var init_run2 = __esm({
|
|
|
21044
21123
|
});
|
|
21045
21124
|
|
|
21046
21125
|
// src/research/summarize.ts
|
|
21047
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
21126
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync24 } from "node:fs";
|
|
21048
21127
|
import { join as join27, resolve as resolve17 } from "node:path";
|
|
21049
21128
|
function readLedger2(dir) {
|
|
21050
21129
|
const pairsDir = join27(resolve17(dir), "pairs");
|
|
@@ -21059,7 +21138,7 @@ function readLedger2(dir) {
|
|
|
21059
21138
|
const path = join27(pairsDir, n2);
|
|
21060
21139
|
let raw;
|
|
21061
21140
|
try {
|
|
21062
|
-
raw = JSON.parse(
|
|
21141
|
+
raw = JSON.parse(readFileSync24(path, "utf8"));
|
|
21063
21142
|
} catch (e) {
|
|
21064
21143
|
throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
|
|
21065
21144
|
}
|
|
@@ -21191,7 +21270,7 @@ var init_summarize = __esm({
|
|
|
21191
21270
|
|
|
21192
21271
|
// src/research/init.ts
|
|
21193
21272
|
import { createHash as createHash15 } from "node:crypto";
|
|
21194
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as
|
|
21273
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
|
|
21195
21274
|
import { basename as basename5, dirname as dirname12, resolve as resolve18 } from "node:path";
|
|
21196
21275
|
function idFrom(repo) {
|
|
21197
21276
|
const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
|
|
@@ -21243,7 +21322,7 @@ function runResearchInit(opts) {
|
|
|
21243
21322
|
}
|
|
21244
21323
|
}
|
|
21245
21324
|
function requireHash(path) {
|
|
21246
|
-
return createHash15("sha256").update(
|
|
21325
|
+
return createHash15("sha256").update(readFileSync25(path)).digest("hex");
|
|
21247
21326
|
}
|
|
21248
21327
|
var init_init2 = __esm({
|
|
21249
21328
|
"src/research/init.ts"() {
|
|
@@ -21310,7 +21389,7 @@ var init_report2 = __esm({
|
|
|
21310
21389
|
// src/research/bundle.ts
|
|
21311
21390
|
import { createHash as createHash16 } from "node:crypto";
|
|
21312
21391
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
21313
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as
|
|
21392
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync26, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
|
|
21314
21393
|
import { dirname as dirname13, join as join28, resolve as resolve19 } from "node:path";
|
|
21315
21394
|
function octal(value, width) {
|
|
21316
21395
|
return value.toString(8).padStart(width - 1, "0") + "\0";
|
|
@@ -21364,7 +21443,7 @@ function walkLedger(ledger) {
|
|
|
21364
21443
|
for (const name of names) {
|
|
21365
21444
|
const path = join28(pairs, name);
|
|
21366
21445
|
if (!statSync6(path).isFile()) continue;
|
|
21367
|
-
entries.push({ name: `ledger/pairs/${name}`, bytes:
|
|
21446
|
+
entries.push({ name: `ledger/pairs/${name}`, bytes: readFileSync26(path) });
|
|
21368
21447
|
}
|
|
21369
21448
|
return entries;
|
|
21370
21449
|
}
|
|
@@ -21374,7 +21453,7 @@ function jsonBytes(value) {
|
|
|
21374
21453
|
function validateResearchBundle(path) {
|
|
21375
21454
|
let files;
|
|
21376
21455
|
try {
|
|
21377
|
-
files = parseTar(gunzipSync(
|
|
21456
|
+
files = parseTar(gunzipSync(readFileSync26(resolve19(path))));
|
|
21378
21457
|
} catch (e) {
|
|
21379
21458
|
throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
21380
21459
|
}
|
|
@@ -21428,7 +21507,7 @@ function createResearchBundle(opts) {
|
|
|
21428
21507
|
const summary = summarizeLedger(ledger);
|
|
21429
21508
|
const entries = walkLedger(ledger);
|
|
21430
21509
|
if (opts.manifest) {
|
|
21431
|
-
const manifest =
|
|
21510
|
+
const manifest = readFileSync26(resolve19(opts.manifest));
|
|
21432
21511
|
const sha3 = createHash16("sha256").update(manifest).digest("hex");
|
|
21433
21512
|
if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
|
|
21434
21513
|
entries.push({ name: "manifest.json", bytes: manifest });
|
|
@@ -21576,150 +21655,2305 @@ var init_research = __esm({
|
|
|
21576
21655
|
}
|
|
21577
21656
|
});
|
|
21578
21657
|
|
|
21579
|
-
// src/
|
|
21580
|
-
function
|
|
21581
|
-
|
|
21582
|
-
|
|
21583
|
-
|
|
21584
|
-
|
|
21585
|
-
|
|
21586
|
-
|
|
21587
|
-
|
|
21588
|
-
}
|
|
21589
|
-
const want = opts.file ? `${opts.rule}:${opts.file}` : opts.rule;
|
|
21590
|
-
const token = compactOobToken(want, opts.head);
|
|
21591
|
-
if (!token) {
|
|
21592
|
-
process.stderr.write("tamperward: --head must be a full 40- or 64-character hexadecimal object id.\n");
|
|
21593
|
-
return 2;
|
|
21594
|
-
}
|
|
21595
|
-
process.stdout.write(`${token}
|
|
21596
|
-
`);
|
|
21597
|
-
return 0;
|
|
21658
|
+
// src/adapters/contract.ts
|
|
21659
|
+
function steeringUnavailableFinding(detail) {
|
|
21660
|
+
return {
|
|
21661
|
+
rule: "tamperward-unavailable",
|
|
21662
|
+
severity: "block",
|
|
21663
|
+
message: `TamperWard could not evaluate this operation (${detail}), so it is denied rather than allowed.`,
|
|
21664
|
+
evidence: detail,
|
|
21665
|
+
remediation: "Repair the runtime steering path (hook config, transport, or event delivery), then retry. Do not work around the gate while it is down.",
|
|
21666
|
+
signoff: { required: true, command: 'tamperward allow --reason "..."' }
|
|
21667
|
+
};
|
|
21598
21668
|
}
|
|
21599
|
-
|
|
21600
|
-
|
|
21669
|
+
function failClosedResult(outcome, detail, phase, denyPayload) {
|
|
21670
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
21671
|
+
const wire = denyPayload(findings, phase);
|
|
21672
|
+
return { outcome, detail, wire, decision: { verdict: "deny", findings, reason: wire } };
|
|
21673
|
+
}
|
|
21674
|
+
var OPERATION_KINDS;
|
|
21675
|
+
var init_contract = __esm({
|
|
21676
|
+
"src/adapters/contract.ts"() {
|
|
21601
21677
|
"use strict";
|
|
21602
|
-
|
|
21678
|
+
OPERATION_KINDS = ["shell", "file-edit", "file-read", "mcp", "other"];
|
|
21603
21679
|
}
|
|
21604
21680
|
});
|
|
21605
21681
|
|
|
21606
|
-
// src/
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21610
|
-
|
|
21611
|
-
|
|
21612
|
-
|
|
21613
|
-
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
21617
|
-
|
|
21618
|
-
|
|
21619
|
-
|
|
21620
|
-
|
|
21621
|
-
|
|
21622
|
-
|
|
21623
|
-
|
|
21682
|
+
// src/adapters/claude/adapter.ts
|
|
21683
|
+
function hookKindFor(phase) {
|
|
21684
|
+
if (phase === "end-of-turn") return "Stop";
|
|
21685
|
+
if (phase === "pre-action") return "PreToolUse";
|
|
21686
|
+
throw new Error(`no deny-capable Claude hook for the observation-only phase "${phase}"`);
|
|
21687
|
+
}
|
|
21688
|
+
function operationKind(toolName) {
|
|
21689
|
+
if (!toolName) return "other";
|
|
21690
|
+
if (toolName === "Bash" || toolName === "BashOutput" || toolName === "KillShell") return "shell";
|
|
21691
|
+
if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit" || toolName === "NotebookEdit") return "file-edit";
|
|
21692
|
+
if (toolName === "Read" || toolName === "Glob" || toolName === "Grep" || toolName === "NotebookRead") return "file-read";
|
|
21693
|
+
if (toolName.startsWith("mcp__")) return "mcp";
|
|
21694
|
+
return "other";
|
|
21695
|
+
}
|
|
21696
|
+
function eventFrom(input, phase) {
|
|
21697
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: operationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
|
|
21698
|
+
return {
|
|
21699
|
+
phase,
|
|
21700
|
+
operation,
|
|
21701
|
+
identity: { claimedCwd: input.cwd, sessionId: input.session_id }
|
|
21702
|
+
};
|
|
21624
21703
|
}
|
|
21625
|
-
|
|
21626
|
-
|
|
21627
|
-
|
|
21628
|
-
|
|
21629
|
-
|
|
21630
|
-
|
|
21704
|
+
var ClaudeRuntimeAdapter, claudeAdapter;
|
|
21705
|
+
var init_adapter2 = __esm({
|
|
21706
|
+
"src/adapters/claude/adapter.ts"() {
|
|
21707
|
+
"use strict";
|
|
21708
|
+
init_deny();
|
|
21709
|
+
init_hook();
|
|
21710
|
+
init_repo_context();
|
|
21711
|
+
init_contract();
|
|
21712
|
+
ClaudeRuntimeAdapter = class {
|
|
21713
|
+
name = "claude-code";
|
|
21714
|
+
/**
|
|
21715
|
+
* Claude Code's live PreToolUse fires for EVERY tool and can synchronously deny it —
|
|
21716
|
+
* shell, file edit, file read, MCP, and anything else — even under
|
|
21717
|
+
* `--dangerously-skip-permissions`. The end-of-turn Stop sweep is live too. There is no
|
|
21718
|
+
* live per-tool PostToolUse veto (Stop is the post-turn reconciliation), so `postObserve`
|
|
21719
|
+
* is empty and `unsupported` names that gap explicitly rather than implying it.
|
|
21720
|
+
*/
|
|
21721
|
+
capabilities = {
|
|
21722
|
+
preDeny: OPERATION_KINDS,
|
|
21723
|
+
postObserve: [],
|
|
21724
|
+
endOfTurn: true,
|
|
21725
|
+
unsupported: [
|
|
21726
|
+
"per-operation post-action veto (end-of-turn Stop sweep is the post-turn reconciliation)",
|
|
21727
|
+
"network-egress control",
|
|
21728
|
+
"identity / authentication"
|
|
21729
|
+
]
|
|
21730
|
+
};
|
|
21731
|
+
parseEvent(raw, phase) {
|
|
21732
|
+
try {
|
|
21733
|
+
return eventFrom(parseInput(raw), phase);
|
|
21734
|
+
} catch (e) {
|
|
21735
|
+
if (e instanceof HookInputError) return { failure: "parse-failure", detail: e.message };
|
|
21736
|
+
throw e;
|
|
21737
|
+
}
|
|
21738
|
+
}
|
|
21739
|
+
/**
|
|
21740
|
+
* REVIEW POINT 5 — the runtime cwd is a CLAIM, not authority.
|
|
21741
|
+
*
|
|
21742
|
+
* The trusted repository root is derived from the RUNNER context (`defaultCwd`, else the
|
|
21743
|
+
* process cwd) — INDEPENDENTLY of the claim — via git (`repoContext` →
|
|
21744
|
+
* `git rev-parse --show-toplevel`, which resolves symlinks to a canonical path). The
|
|
21745
|
+
* claim is then validated against THAT root with the shared `validateClaimAgainstRoot`:
|
|
21746
|
+
* accepted only when it resolves to the same repository (its root, or a path inside it);
|
|
21747
|
+
* rejected for a malformed path, a non-repository, a different repository, or a symlink
|
|
21748
|
+
* that escapes into another repository. It never weakens what `preToolUseVerdict` does via
|
|
21749
|
+
* `repoRoot()` — it is the explicit boundary that `decide` enforces BEFORE evaluating.
|
|
21750
|
+
*/
|
|
21751
|
+
validateIdentity(claim, defaultCwd) {
|
|
21752
|
+
const base = defaultCwd ?? process.cwd();
|
|
21753
|
+
const runnerCtx = repoContext(base);
|
|
21754
|
+
if (!runnerCtx) {
|
|
21755
|
+
return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
|
|
21756
|
+
}
|
|
21757
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
21758
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
21759
|
+
}
|
|
21760
|
+
/** The exact deny wire bytes for `phase`, through the SAME `denyWire` the live
|
|
21761
|
+
* `verdict()` uses (src/cli/hook.ts). Reuses `formatDenial`, so the reason text is
|
|
21762
|
+
* identical to the live path. Byte-for-byte equal to what a live deny emits. */
|
|
21763
|
+
denyPayload(findings, phase) {
|
|
21764
|
+
return denyWire(formatDenial(findings), hookKindFor(phase));
|
|
21765
|
+
}
|
|
21766
|
+
/**
|
|
21767
|
+
* The authoritative synchronous path, enforced in order: parse → validate identity →
|
|
21768
|
+
* decide. Each step that fails does so CLOSED (deny), except `post-action`, which is
|
|
21769
|
+
* observation-only and returns `unsupported` (never a deny wire, never a PreToolUse
|
|
21770
|
+
* fallthrough — BLOCKER 2).
|
|
21771
|
+
*
|
|
21772
|
+
* 1. `post-action` → `unsupported` (Claude has no per-tool post-action veto).
|
|
21773
|
+
* 2. parse failure → `parse-failure`, byte-identical to the live fail-closed deny.
|
|
21774
|
+
* 3. identity claim rejected → a fail-closed DENY, BEFORE any content evaluation, so a
|
|
21775
|
+
* runtime-supplied cwd pointing at another repository can never reach the detectors.
|
|
21776
|
+
* 4. otherwise → delegate to the canonical `preToolUseFromRaw` / `stopFromRaw`, PASSING
|
|
21777
|
+
* the validated trusted root so the live path re-runs the same shared identity check.
|
|
21778
|
+
* `wire` is byte-identical to the live CLI — every deny/allow decision, the fail-closed
|
|
21779
|
+
* wrapping, repo-root resolution (#412), the turn baseline and the transport remain
|
|
21780
|
+
* exactly as src/cli/hook.ts computes them.
|
|
21781
|
+
*/
|
|
21782
|
+
decide(raw, phase, defaultCwd) {
|
|
21783
|
+
if (phase === "post-action") {
|
|
21784
|
+
return {
|
|
21785
|
+
outcome: "unsupported",
|
|
21786
|
+
detail: "Claude Code has no per-tool post-action veto; the end-of-turn Stop sweep is its post-turn reconciliation."
|
|
21787
|
+
};
|
|
21788
|
+
}
|
|
21789
|
+
const parsed = this.parseEvent(raw, phase);
|
|
21790
|
+
if ("failure" in parsed) {
|
|
21791
|
+
const live2 = phase === "end-of-turn" ? stopFromRaw(raw, defaultCwd) : preToolUseFromRaw(raw, defaultCwd);
|
|
21792
|
+
return {
|
|
21793
|
+
outcome: "parse-failure",
|
|
21794
|
+
detail: parsed.detail,
|
|
21795
|
+
wire: live2.stdout,
|
|
21796
|
+
decision: { verdict: "deny", findings: [], reason: live2.stdout }
|
|
21797
|
+
};
|
|
21798
|
+
}
|
|
21799
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
21800
|
+
if (!idv.ok) {
|
|
21801
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
21802
|
+
const wire2 = this.denyPayload(findings, phase);
|
|
21803
|
+
return { outcome: "ok", wire: wire2, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire2 } };
|
|
21804
|
+
}
|
|
21805
|
+
const live = phase === "end-of-turn" ? stopFromRaw(raw, defaultCwd, idv.trustedRoot) : preToolUseFromRaw(raw, defaultCwd, idv.trustedRoot);
|
|
21806
|
+
const wire = live.stdout;
|
|
21807
|
+
const denied = wire.length > 0;
|
|
21808
|
+
return {
|
|
21809
|
+
outcome: "ok",
|
|
21810
|
+
wire,
|
|
21811
|
+
decision: { verdict: denied ? "deny" : "allow", findings: [], reason: denied ? wire : void 0 }
|
|
21812
|
+
};
|
|
21813
|
+
}
|
|
21814
|
+
/**
|
|
21815
|
+
* Map a transport failure (or a required hook that did not fire) onto a fail-closed
|
|
21816
|
+
* DENY at the neutral seam. Claude's LIVE client additionally degrades a hook-service
|
|
21817
|
+
* transport failure to the in-process verdict (src/cli/index.ts / hook-client.ts) rather
|
|
21818
|
+
* than surfacing it here, so this method documents the contract guarantee for the seam;
|
|
21819
|
+
* a partial adapter with no in-process fallback denies through exactly this path.
|
|
21820
|
+
*/
|
|
21821
|
+
failClosed(outcome, detail, phase) {
|
|
21822
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
21823
|
+
}
|
|
21824
|
+
};
|
|
21825
|
+
claudeAdapter = new ClaudeRuntimeAdapter();
|
|
21631
21826
|
}
|
|
21632
|
-
|
|
21827
|
+
});
|
|
21828
|
+
|
|
21829
|
+
// src/adapters/apply-patch.ts
|
|
21830
|
+
import { isAbsolute as isAbsolute14, relative as relative9 } from "node:path";
|
|
21831
|
+
function readDisk2(path) {
|
|
21832
|
+
return textOf2(inspectResolved(path));
|
|
21633
21833
|
}
|
|
21634
|
-
function
|
|
21635
|
-
const
|
|
21636
|
-
|
|
21637
|
-
const a = args[i];
|
|
21638
|
-
if (a === "--cwd") o.cwd = args[++i];
|
|
21639
|
-
else if (a === "--dry-run") o.dryRun = true;
|
|
21640
|
-
else if (a === "--force-workflow") o.forceWorkflow = true;
|
|
21641
|
-
}
|
|
21642
|
-
return o;
|
|
21834
|
+
function relForDisplay2(path, cwd) {
|
|
21835
|
+
const rel = relative9(cwd, path);
|
|
21836
|
+
return rel && !rel.startsWith("..") && !isAbsolute14(rel) ? rel : path;
|
|
21643
21837
|
}
|
|
21644
|
-
function
|
|
21645
|
-
const
|
|
21646
|
-
|
|
21647
|
-
|
|
21648
|
-
|
|
21649
|
-
|
|
21650
|
-
|
|
21651
|
-
|
|
21652
|
-
|
|
21653
|
-
|
|
21654
|
-
|
|
21655
|
-
|
|
21656
|
-
|
|
21838
|
+
function parsePatchSections(patch) {
|
|
21839
|
+
const lines = patch.replace(/\r\n/g, "\n").split("\n");
|
|
21840
|
+
let i = 0;
|
|
21841
|
+
while (i < lines.length && !/^\*\*\* Begin Patch/.test(lines[i])) i++;
|
|
21842
|
+
if (i >= lines.length) throw new Error('apply_patch payload has no "*** Begin Patch" header');
|
|
21843
|
+
i++;
|
|
21844
|
+
const sections = [];
|
|
21845
|
+
let current = null;
|
|
21846
|
+
const flush = () => {
|
|
21847
|
+
if (current) sections.push(current);
|
|
21848
|
+
current = null;
|
|
21849
|
+
};
|
|
21850
|
+
for (; i < lines.length; i++) {
|
|
21851
|
+
const line = lines[i];
|
|
21852
|
+
if (/^\*\*\* End Patch/.test(line)) {
|
|
21853
|
+
flush();
|
|
21854
|
+
return sections;
|
|
21855
|
+
}
|
|
21856
|
+
const add = /^\*\*\* Add File: (.*)$/.exec(line);
|
|
21857
|
+
const upd = /^\*\*\* Update File: (.*)$/.exec(line);
|
|
21858
|
+
const del = /^\*\*\* Delete File: (.*)$/.exec(line);
|
|
21859
|
+
const move = /^\*\*\* Move to: (.*)$/.exec(line);
|
|
21860
|
+
if (add) {
|
|
21861
|
+
flush();
|
|
21862
|
+
current = { op: "add", path: add[1].trim(), body: [] };
|
|
21863
|
+
} else if (upd) {
|
|
21864
|
+
flush();
|
|
21865
|
+
current = { op: "update", path: upd[1].trim(), body: [] };
|
|
21866
|
+
} else if (del) {
|
|
21867
|
+
flush();
|
|
21868
|
+
current = { op: "delete", path: del[1].trim(), body: [] };
|
|
21869
|
+
} else if (move && current) {
|
|
21870
|
+
current.moveTo = move[1].trim();
|
|
21871
|
+
} else if (current) {
|
|
21872
|
+
current.body.push(line);
|
|
21873
|
+
}
|
|
21874
|
+
}
|
|
21875
|
+
throw new Error('apply_patch payload is missing its "*** End Patch" trailer');
|
|
21876
|
+
}
|
|
21877
|
+
function addedContent(body) {
|
|
21878
|
+
return body.filter((l) => l.startsWith("+")).map((l) => l.slice(1)).join("\n");
|
|
21879
|
+
}
|
|
21880
|
+
function applyUpdate(before, body) {
|
|
21881
|
+
const lines = before.split("\n");
|
|
21882
|
+
const hunks = [];
|
|
21883
|
+
let hunk = [];
|
|
21884
|
+
for (const l of body) {
|
|
21885
|
+
if (l.startsWith("@@")) {
|
|
21886
|
+
if (hunk.length) hunks.push(hunk);
|
|
21887
|
+
hunk = [];
|
|
21888
|
+
continue;
|
|
21889
|
+
}
|
|
21890
|
+
hunk.push(l);
|
|
21891
|
+
}
|
|
21892
|
+
if (hunk.length) hunks.push(hunk);
|
|
21893
|
+
let cursor = 0;
|
|
21894
|
+
for (const h of hunks) {
|
|
21895
|
+
const oldBlock = [];
|
|
21896
|
+
const newBlock = [];
|
|
21897
|
+
for (const l of h) {
|
|
21898
|
+
if (l === "") {
|
|
21899
|
+
oldBlock.push("");
|
|
21900
|
+
newBlock.push("");
|
|
21901
|
+
} else if (l.startsWith(" ")) {
|
|
21902
|
+
oldBlock.push(l.slice(1));
|
|
21903
|
+
newBlock.push(l.slice(1));
|
|
21904
|
+
} else if (l.startsWith("-")) {
|
|
21905
|
+
oldBlock.push(l.slice(1));
|
|
21906
|
+
} else if (l.startsWith("+")) {
|
|
21907
|
+
newBlock.push(l.slice(1));
|
|
21908
|
+
} else {
|
|
21909
|
+
return null;
|
|
21910
|
+
}
|
|
21911
|
+
}
|
|
21912
|
+
if (oldBlock.length === 0) return null;
|
|
21913
|
+
let at = -1;
|
|
21914
|
+
for (let s = cursor; s <= lines.length - oldBlock.length; s++) {
|
|
21915
|
+
let ok = true;
|
|
21916
|
+
for (let k = 0; k < oldBlock.length; k++) {
|
|
21917
|
+
if (lines[s + k] !== oldBlock[k]) {
|
|
21918
|
+
ok = false;
|
|
21919
|
+
break;
|
|
21920
|
+
}
|
|
21921
|
+
}
|
|
21922
|
+
if (ok) {
|
|
21923
|
+
at = s;
|
|
21924
|
+
break;
|
|
21925
|
+
}
|
|
21926
|
+
}
|
|
21927
|
+
if (at < 0) return null;
|
|
21928
|
+
lines.splice(at, oldBlock.length, ...newBlock);
|
|
21929
|
+
cursor = at + newBlock.length;
|
|
21657
21930
|
}
|
|
21658
|
-
return
|
|
21931
|
+
return lines.join("\n");
|
|
21659
21932
|
}
|
|
21660
|
-
function
|
|
21661
|
-
const
|
|
21662
|
-
for (
|
|
21663
|
-
const
|
|
21664
|
-
if (
|
|
21665
|
-
|
|
21666
|
-
else if (
|
|
21667
|
-
|
|
21668
|
-
else
|
|
21669
|
-
|
|
21670
|
-
|
|
21933
|
+
function applyPatchChanges(patch, abs, cwd) {
|
|
21934
|
+
const out3 = [];
|
|
21935
|
+
for (const section of parsePatchSections(patch)) {
|
|
21936
|
+
const targetRel = relForDisplay2(abs(section.moveTo ?? section.path), cwd);
|
|
21937
|
+
if (section.op === "add") {
|
|
21938
|
+
out3.push(...synthFileChange(targetRel, null, addedContent(section.body)));
|
|
21939
|
+
} else if (section.op === "delete") {
|
|
21940
|
+
out3.push(...synthFileChange(targetRel, readDisk2(abs(section.path)), null));
|
|
21941
|
+
} else {
|
|
21942
|
+
const before = readDisk2(abs(section.path));
|
|
21943
|
+
const after = applyUpdate(before ?? "", section.body);
|
|
21944
|
+
if (after === null) {
|
|
21945
|
+
throw new Error(
|
|
21946
|
+
`cannot reconstruct the apply_patch update to ${section.path}: its hunks did not locate against the file on disk`
|
|
21947
|
+
);
|
|
21948
|
+
}
|
|
21949
|
+
const synth = synthFileChange(targetRel, before, after);
|
|
21950
|
+
if (section.moveTo && section.moveTo !== section.path) {
|
|
21951
|
+
out3.push(...synth.map((c) => ({ ...c, op: "rename", oldPath: relForDisplay2(abs(section.path), cwd) })));
|
|
21952
|
+
} else {
|
|
21953
|
+
out3.push(...synth);
|
|
21954
|
+
}
|
|
21955
|
+
}
|
|
21671
21956
|
}
|
|
21672
|
-
return
|
|
21957
|
+
return out3;
|
|
21673
21958
|
}
|
|
21674
|
-
|
|
21675
|
-
|
|
21676
|
-
|
|
21677
|
-
|
|
21678
|
-
|
|
21679
|
-
else if (a === "--cwd") o.cwd = args[++i];
|
|
21959
|
+
var init_apply_patch = __esm({
|
|
21960
|
+
"src/adapters/apply-patch.ts"() {
|
|
21961
|
+
"use strict";
|
|
21962
|
+
init_changes();
|
|
21963
|
+
init_disk();
|
|
21680
21964
|
}
|
|
21681
|
-
|
|
21965
|
+
});
|
|
21966
|
+
|
|
21967
|
+
// src/adapters/codex/changes.ts
|
|
21968
|
+
import { isAbsolute as isAbsolute15, relative as relative10, resolve as resolve20 } from "node:path";
|
|
21969
|
+
function asStr2(v) {
|
|
21970
|
+
return typeof v === "string" ? v : "";
|
|
21682
21971
|
}
|
|
21683
|
-
function
|
|
21684
|
-
|
|
21685
|
-
for (let i = 0; i < args.length; i++) {
|
|
21686
|
-
const a = args[i];
|
|
21687
|
-
if (a === "--cwd") o.cwd = args[++i];
|
|
21688
|
-
else if (a === "--file") o.file = args[++i];
|
|
21689
|
-
else if (a === "--since") o.since = args[++i];
|
|
21690
|
-
else if (a === "--json") o.json = true;
|
|
21691
|
-
}
|
|
21692
|
-
return o;
|
|
21972
|
+
function readDisk3(path) {
|
|
21973
|
+
return textOf2(inspectResolved(path));
|
|
21693
21974
|
}
|
|
21694
|
-
function
|
|
21695
|
-
const
|
|
21696
|
-
|
|
21697
|
-
|
|
21698
|
-
|
|
21699
|
-
|
|
21700
|
-
|
|
21975
|
+
function relForDisplay3(path, cwd) {
|
|
21976
|
+
const rel = relative10(cwd, path);
|
|
21977
|
+
return rel && !rel.startsWith("..") && !isAbsolute15(rel) ? rel : path;
|
|
21978
|
+
}
|
|
21979
|
+
function shellChanges(args) {
|
|
21980
|
+
const cmd = args.command ?? args.argv;
|
|
21981
|
+
if (Array.isArray(cmd)) {
|
|
21982
|
+
const argv = cmd.map(asStr2).filter((s) => s.length > 0);
|
|
21983
|
+
if (!argv.length) throw new Error("shell event carries no command/argv to reconstruct");
|
|
21984
|
+
return [{ kind: "command", raw: argv.join(" "), argv }];
|
|
21985
|
+
}
|
|
21986
|
+
const raw = asStr2(cmd);
|
|
21987
|
+
if (!raw) throw new Error("shell event carries no command to reconstruct");
|
|
21988
|
+
return [{ kind: "command", raw, argv: raw.split(/\s+/) }];
|
|
21989
|
+
}
|
|
21990
|
+
function changesFromCodex(operation, cwd, base = cwd) {
|
|
21991
|
+
const args = operation.args ?? {};
|
|
21992
|
+
const abs = (path) => resolve20(base, path);
|
|
21993
|
+
if (operation.kind === "shell") return shellChanges(args);
|
|
21994
|
+
if (operation.kind !== "file-edit") return [];
|
|
21995
|
+
if (operation.name === "apply_patch" || operation.name === "patch") {
|
|
21996
|
+
const patch = asStr2(args.command) || asStr2(args.patch) || asStr2(args.input);
|
|
21997
|
+
if (!patch) throw new Error("apply_patch event carries no command/patch/input to reconstruct");
|
|
21998
|
+
return applyPatchChanges(patch, abs, cwd);
|
|
21999
|
+
}
|
|
22000
|
+
const fp = asStr2(args.path) || asStr2(args.file_path);
|
|
22001
|
+
if (!fp) throw new Error(`${operation.name || "file-edit"} event carries no path to reconstruct`);
|
|
22002
|
+
const before = readDisk3(abs(fp));
|
|
22003
|
+
const old = args.old_string ?? args.old_str ?? args.old;
|
|
22004
|
+
const next = args.new_string ?? args.new_str ?? args.new;
|
|
22005
|
+
if (typeof old === "string" || typeof next === "string") {
|
|
22006
|
+
const after = applyEdit(before, asStr2(old), asStr2(next), args.replace_all === true);
|
|
22007
|
+
return synthFileChange(relForDisplay3(abs(fp), cwd), before, after);
|
|
22008
|
+
}
|
|
22009
|
+
const content = args.content ?? args.text;
|
|
22010
|
+
if (typeof content === "string") {
|
|
22011
|
+
return synthFileChange(relForDisplay3(abs(fp), cwd), before, content);
|
|
22012
|
+
}
|
|
22013
|
+
throw new Error(`${operation.name || "file-edit"} event for ${fp} carries no old_string/new_string/content to reconstruct`);
|
|
22014
|
+
}
|
|
22015
|
+
var init_changes2 = __esm({
|
|
22016
|
+
"src/adapters/codex/changes.ts"() {
|
|
22017
|
+
"use strict";
|
|
22018
|
+
init_changes();
|
|
22019
|
+
init_apply_patch();
|
|
22020
|
+
init_disk();
|
|
21701
22021
|
}
|
|
21702
|
-
|
|
22022
|
+
});
|
|
22023
|
+
|
|
22024
|
+
// src/adapters/codex/deny.ts
|
|
22025
|
+
function assertCodexWirePhase(phase) {
|
|
22026
|
+
if (phase !== "pre-action" && phase !== "end-of-turn") {
|
|
22027
|
+
throw new Error(`unsupported Codex wire phase: ${phase}`);
|
|
22028
|
+
}
|
|
22029
|
+
}
|
|
22030
|
+
function codexWire(reason, phase) {
|
|
22031
|
+
assertCodexWirePhase(phase);
|
|
22032
|
+
const payload = phase === "end-of-turn" ? { decision: "block", reason } : {
|
|
22033
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
|
|
22034
|
+
decision: "block",
|
|
22035
|
+
reason
|
|
22036
|
+
};
|
|
22037
|
+
return JSON.stringify(payload) + "\n";
|
|
21703
22038
|
}
|
|
21704
|
-
function
|
|
21705
|
-
|
|
21706
|
-
|
|
21707
|
-
|
|
21708
|
-
if (a === "--staged") o.staged = true;
|
|
21709
|
-
else if (a === "--worktree") o.worktree = true;
|
|
21710
|
-
else if (a === "--json") o.json = true;
|
|
21711
|
-
else if (a === "--format") {
|
|
21712
|
-
const v = args[++i];
|
|
21713
|
-
if (v !== void 0 && isFormat(v)) o.format = v;
|
|
21714
|
-
} else if (a === "--diff") o.diff = args[++i];
|
|
21715
|
-
else if (a === "--cwd") o.cwd = args[++i];
|
|
21716
|
-
}
|
|
21717
|
-
return o;
|
|
22039
|
+
function codexDenyWire(findings, phase) {
|
|
22040
|
+
assertCodexWirePhase(phase);
|
|
22041
|
+
if (findings.length === 0) return "";
|
|
22042
|
+
return codexWire(formatDenial(findings), phase);
|
|
21718
22043
|
}
|
|
21719
|
-
|
|
21720
|
-
|
|
21721
|
-
|
|
21722
|
-
|
|
22044
|
+
var init_deny2 = __esm({
|
|
22045
|
+
"src/adapters/codex/deny.ts"() {
|
|
22046
|
+
"use strict";
|
|
22047
|
+
init_deny();
|
|
22048
|
+
}
|
|
22049
|
+
});
|
|
22050
|
+
|
|
22051
|
+
// src/adapters/codex/schema.ts
|
|
22052
|
+
function codexOperationKind(toolName) {
|
|
22053
|
+
if (!toolName) return "other";
|
|
22054
|
+
if (SHELL_TOOLS.has(toolName)) return "shell";
|
|
22055
|
+
if (FILE_EDIT_TOOLS.has(toolName)) return "file-edit";
|
|
22056
|
+
if (toolName.startsWith(MCP_PREFIX)) return "mcp";
|
|
22057
|
+
if (FILE_READ_TOOLS.has(toolName)) return "file-read";
|
|
22058
|
+
return "other";
|
|
22059
|
+
}
|
|
22060
|
+
function inputFrom(parsed) {
|
|
22061
|
+
return {
|
|
22062
|
+
...typeof parsed.hook_event_name === "string" ? { hook_event_name: parsed.hook_event_name } : {},
|
|
22063
|
+
...typeof parsed.tool_name === "string" ? { tool_name: parsed.tool_name } : {},
|
|
22064
|
+
...isRecord(parsed.tool_input) ? { tool_input: parsed.tool_input } : {},
|
|
22065
|
+
...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
|
|
22066
|
+
...typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {},
|
|
22067
|
+
...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {}
|
|
22068
|
+
};
|
|
22069
|
+
}
|
|
22070
|
+
function eventFrom2(input, phase) {
|
|
22071
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: codexOperationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
|
|
22072
|
+
const identity2 = { claimedCwd: input.cwd, sessionId: input.session_id };
|
|
22073
|
+
return { phase, operation, identity: identity2 };
|
|
22074
|
+
}
|
|
22075
|
+
function normalizeCodexEvent(raw, phase) {
|
|
22076
|
+
if (!raw.trim()) return eventFrom2({}, phase);
|
|
22077
|
+
let parsed;
|
|
22078
|
+
try {
|
|
22079
|
+
parsed = JSON.parse(raw);
|
|
22080
|
+
} catch (e) {
|
|
22081
|
+
return { failure: "parse-failure", detail: `the Codex hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
22082
|
+
}
|
|
22083
|
+
if (!isRecord(parsed)) {
|
|
22084
|
+
return {
|
|
22085
|
+
failure: "parse-failure",
|
|
22086
|
+
detail: `the Codex hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})`
|
|
22087
|
+
};
|
|
22088
|
+
}
|
|
22089
|
+
return eventFrom2(inputFrom(parsed), phase);
|
|
22090
|
+
}
|
|
22091
|
+
var SHELL_TOOLS, FILE_EDIT_TOOLS, FILE_READ_TOOLS, MCP_PREFIX;
|
|
22092
|
+
var init_schema = __esm({
|
|
22093
|
+
"src/adapters/codex/schema.ts"() {
|
|
22094
|
+
"use strict";
|
|
22095
|
+
init_narrow();
|
|
22096
|
+
SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash"]);
|
|
22097
|
+
FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["apply_patch", "Write", "Edit"]);
|
|
22098
|
+
FILE_READ_TOOLS = /* @__PURE__ */ new Set(["view_image"]);
|
|
22099
|
+
MCP_PREFIX = "mcp__";
|
|
22100
|
+
}
|
|
22101
|
+
});
|
|
22102
|
+
|
|
22103
|
+
// src/adapters/codex/adapter.ts
|
|
22104
|
+
var POST_OBSERVE, CodexRuntimeAdapter, codexAdapter;
|
|
22105
|
+
var init_adapter3 = __esm({
|
|
22106
|
+
"src/adapters/codex/adapter.ts"() {
|
|
22107
|
+
"use strict";
|
|
22108
|
+
init_engine();
|
|
22109
|
+
init_policy_load();
|
|
22110
|
+
init_hook();
|
|
22111
|
+
init_session();
|
|
22112
|
+
init_repo_context();
|
|
22113
|
+
init_contract();
|
|
22114
|
+
init_changes2();
|
|
22115
|
+
init_deny2();
|
|
22116
|
+
init_schema();
|
|
22117
|
+
POST_OBSERVE = ["shell", "file-edit", "file-read", "mcp", "other"];
|
|
22118
|
+
CodexRuntimeAdapter = class {
|
|
22119
|
+
name = "codex";
|
|
22120
|
+
/**
|
|
22121
|
+
* CONSERVATIVE and honest. Pre-action deny ENFORCEMENT is not yet proven on a pinned
|
|
22122
|
+
* Codex build, so `preDeny` is empty — the adapter never claims a synchronous veto it
|
|
22123
|
+
* cannot demonstrate. Codex does surface post-execution tool outcomes and an end-of-turn
|
|
22124
|
+
* stop, so those are declared; every real gap is named in `unsupported`. The
|
|
22125
|
+
* `probe:codex-runtime` harness is what may later justify moving a kind into `preDeny`.
|
|
22126
|
+
*/
|
|
22127
|
+
capabilities = {
|
|
22128
|
+
preDeny: [],
|
|
22129
|
+
postObserve: POST_OBSERVE,
|
|
22130
|
+
endOfTurn: true,
|
|
22131
|
+
unsupported: [
|
|
22132
|
+
"pre-action deny enforcement not yet proven on a pinned Codex build (see probe:codex-runtime)",
|
|
22133
|
+
"fail-closed hook transport not yet proven (openai/codex#41979)",
|
|
22134
|
+
"network-egress control",
|
|
22135
|
+
"identity / authentication"
|
|
22136
|
+
]
|
|
22137
|
+
};
|
|
22138
|
+
parseEvent(raw, phase) {
|
|
22139
|
+
return normalizeCodexEvent(raw, phase);
|
|
22140
|
+
}
|
|
22141
|
+
/** Identity is a CLAIM validated against the runner's independently derived trusted root,
|
|
22142
|
+
* reusing `repoContext` / `validateClaimAgainstRoot` exactly as the Claude adapter does.
|
|
22143
|
+
* It never weakens the shared check. */
|
|
22144
|
+
validateIdentity(claim, defaultCwd) {
|
|
22145
|
+
const base = defaultCwd ?? process.cwd();
|
|
22146
|
+
const runnerCtx = repoContext(base);
|
|
22147
|
+
if (!runnerCtx) {
|
|
22148
|
+
return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
|
|
22149
|
+
}
|
|
22150
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
22151
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
22152
|
+
}
|
|
22153
|
+
denyPayload(findings, phase) {
|
|
22154
|
+
if (phase === "post-action") {
|
|
22155
|
+
throw new Error("Codex post-action is observation-only and cannot produce a deny wire");
|
|
22156
|
+
}
|
|
22157
|
+
return codexDenyWire(findings, phase);
|
|
22158
|
+
}
|
|
22159
|
+
/**
|
|
22160
|
+
* parse → validate identity → decide, every failure CLOSED (deny), in order:
|
|
22161
|
+
*
|
|
22162
|
+
* 1. `post-action` → `unsupported` (observation-only; never a deny wire).
|
|
22163
|
+
* 2. parse failure → a fail-closed deny in Codex's wire.
|
|
22164
|
+
* 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
|
|
22165
|
+
* 4. `end-of-turn` → delegate to the canonical git sweep, re-wrapped in Codex's wire.
|
|
22166
|
+
* 5. `pre-action` → reconstruct Change[] and run the SAME `evaluate` engine; a deny
|
|
22167
|
+
* carries the shared denial reason. A reconstruction that cannot be modelled fails
|
|
22168
|
+
* CLOSED to deny rather than allowing an unseen edit.
|
|
22169
|
+
*/
|
|
22170
|
+
decide(raw, phase, defaultCwd) {
|
|
22171
|
+
if (phase === "post-action") {
|
|
22172
|
+
return {
|
|
22173
|
+
outcome: "unsupported",
|
|
22174
|
+
detail: "Codex post-action is observation-only; the end-of-turn Stop sweep is the post-turn reconciliation."
|
|
22175
|
+
};
|
|
22176
|
+
}
|
|
22177
|
+
const parsed = this.parseEvent(raw, phase);
|
|
22178
|
+
if ("failure" in parsed) {
|
|
22179
|
+
const findings = [steeringUnavailableFinding(`unparseable Codex event: ${parsed.detail}`)];
|
|
22180
|
+
const wire = this.denyPayload(findings, phase);
|
|
22181
|
+
return { outcome: "parse-failure", detail: parsed.detail, wire, decision: { verdict: "deny", findings, reason: wire } };
|
|
22182
|
+
}
|
|
22183
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
22184
|
+
if (!idv.ok) {
|
|
22185
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
22186
|
+
const wire = this.denyPayload(findings, phase);
|
|
22187
|
+
return { outcome: "ok", wire, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire } };
|
|
22188
|
+
}
|
|
22189
|
+
if (phase === "end-of-turn") {
|
|
22190
|
+
const wire = stopFromRaw(raw, defaultCwd, idv.trustedRoot).stdout;
|
|
22191
|
+
return { outcome: "ok", wire, decision: { verdict: wire ? "deny" : "allow", findings: [], reason: wire || void 0 } };
|
|
22192
|
+
}
|
|
22193
|
+
try {
|
|
22194
|
+
const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
|
|
22195
|
+
const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
|
|
22196
|
+
turnBaseline(root, parsed.identity.sessionId);
|
|
22197
|
+
const policy = loadPolicy(root);
|
|
22198
|
+
const changes = changesFromCodex(parsed.operation, root, sessionCwd);
|
|
22199
|
+
const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22200
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22201
|
+
return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
|
|
22202
|
+
} catch (e) {
|
|
22203
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
22204
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
22205
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22206
|
+
return { outcome: "ok", wire, detail, decision: { verdict: "deny", findings, reason: wire } };
|
|
22207
|
+
}
|
|
22208
|
+
}
|
|
22209
|
+
/** A transport failure or a required hook that did not fire → a fail-closed deny at the
|
|
22210
|
+
* seam, through the adapter's own `denyPayload` so the wire is Codex's native envelope. */
|
|
22211
|
+
failClosed(outcome, detail, phase) {
|
|
22212
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
22213
|
+
}
|
|
22214
|
+
};
|
|
22215
|
+
codexAdapter = new CodexRuntimeAdapter();
|
|
22216
|
+
}
|
|
22217
|
+
});
|
|
22218
|
+
|
|
22219
|
+
// src/adapters/copilot/changes.ts
|
|
22220
|
+
import { isAbsolute as isAbsolute16, relative as relative11, resolve as resolve21 } from "node:path";
|
|
22221
|
+
function asStr3(v) {
|
|
22222
|
+
return typeof v === "string" ? v : "";
|
|
22223
|
+
}
|
|
22224
|
+
function readDisk4(path) {
|
|
22225
|
+
return textOf2(inspectResolved(path));
|
|
22226
|
+
}
|
|
22227
|
+
function relForDisplay4(path, cwd) {
|
|
22228
|
+
const rel = relative11(cwd, path);
|
|
22229
|
+
return rel && !rel.startsWith("..") && !isAbsolute16(rel) ? rel : path;
|
|
22230
|
+
}
|
|
22231
|
+
function shellChanges2(args) {
|
|
22232
|
+
const cmd = args.command ?? args.input ?? args.argv;
|
|
22233
|
+
if (Array.isArray(cmd)) {
|
|
22234
|
+
const argv = cmd.map(asStr3).filter((s) => s.length > 0);
|
|
22235
|
+
if (!argv.length) throw new Error("shell event carries no command/input/argv to reconstruct");
|
|
22236
|
+
return [{ kind: "command", raw: argv.join(" "), argv }];
|
|
22237
|
+
}
|
|
22238
|
+
const raw = asStr3(cmd);
|
|
22239
|
+
if (!raw) throw new Error("shell event carries no command/input to reconstruct");
|
|
22240
|
+
return [{ kind: "command", raw, argv: raw.split(/\s+/) }];
|
|
22241
|
+
}
|
|
22242
|
+
function changesFromCopilot(operation, cwd, base = cwd) {
|
|
22243
|
+
const args = operation.args ?? {};
|
|
22244
|
+
const abs = (path) => resolve21(base, path);
|
|
22245
|
+
if (operation.kind === "shell") return shellChanges2(args);
|
|
22246
|
+
if (operation.kind !== "file-edit") return [];
|
|
22247
|
+
const name = (operation.name || "").toLowerCase();
|
|
22248
|
+
if (name === "apply_patch") {
|
|
22249
|
+
const patch = asStr3(args.input) || asStr3(args.patch) || asStr3(args.command);
|
|
22250
|
+
if (!patch) throw new Error("apply_patch event carries no input/patch/command to reconstruct");
|
|
22251
|
+
return applyPatchChanges(patch, abs, cwd);
|
|
22252
|
+
}
|
|
22253
|
+
const fp = asStr3(args.path) || asStr3(args.file_path) || asStr3(args.filename);
|
|
22254
|
+
if (!fp) throw new Error(`${operation.name || "file-edit"} event carries no path to reconstruct`);
|
|
22255
|
+
const before = readDisk4(abs(fp));
|
|
22256
|
+
const display2 = relForDisplay4(abs(fp), cwd);
|
|
22257
|
+
if (name === "str_replace_editor") {
|
|
22258
|
+
const old2 = args.old_str ?? args.old_string ?? args.old;
|
|
22259
|
+
const next2 = args.new_str ?? args.new_string ?? args.new;
|
|
22260
|
+
if (typeof old2 === "string" || typeof next2 === "string") {
|
|
22261
|
+
return synthFileChange(display2, before, applyEdit(before, asStr3(old2), asStr3(next2), args.replace_all === true));
|
|
22262
|
+
}
|
|
22263
|
+
const fileText = args.file_text ?? args.content ?? args.text;
|
|
22264
|
+
if (typeof fileText === "string") return synthFileChange(display2, before, fileText);
|
|
22265
|
+
throw new Error(
|
|
22266
|
+
`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)`
|
|
22267
|
+
);
|
|
22268
|
+
}
|
|
22269
|
+
if (Array.isArray(args.edits)) {
|
|
22270
|
+
let after = before;
|
|
22271
|
+
for (const raw of args.edits) {
|
|
22272
|
+
const ed = isRecord(raw) ? raw : {};
|
|
22273
|
+
after = applyEdit(after, asStr3(ed.old_string ?? ed.old_str), asStr3(ed.new_string ?? ed.new_str), ed.replace_all === true);
|
|
22274
|
+
}
|
|
22275
|
+
return synthFileChange(display2, before, after);
|
|
22276
|
+
}
|
|
22277
|
+
const old = args.old_string ?? args.old_str ?? args.old;
|
|
22278
|
+
const next = args.new_string ?? args.new_str ?? args.new;
|
|
22279
|
+
if (typeof old === "string" || typeof next === "string") {
|
|
22280
|
+
return synthFileChange(display2, before, applyEdit(before, asStr3(old), asStr3(next), args.replace_all === true));
|
|
22281
|
+
}
|
|
22282
|
+
const content = args.content ?? args.text ?? args.file_text;
|
|
22283
|
+
if (typeof content === "string") return synthFileChange(display2, before, content);
|
|
22284
|
+
throw new Error(
|
|
22285
|
+
`${operation.name || "file-edit"} event for ${fp} carries no old_string/new_string/content to reconstruct`
|
|
22286
|
+
);
|
|
22287
|
+
}
|
|
22288
|
+
var init_changes3 = __esm({
|
|
22289
|
+
"src/adapters/copilot/changes.ts"() {
|
|
22290
|
+
"use strict";
|
|
22291
|
+
init_changes();
|
|
22292
|
+
init_apply_patch();
|
|
22293
|
+
init_disk();
|
|
22294
|
+
init_narrow();
|
|
22295
|
+
}
|
|
22296
|
+
});
|
|
22297
|
+
|
|
22298
|
+
// src/adapters/copilot/deny.ts
|
|
22299
|
+
function assertCopilotWirePhase(phase) {
|
|
22300
|
+
const known = ["pre-action", "end-of-turn"];
|
|
22301
|
+
if (!known.includes(phase)) {
|
|
22302
|
+
throw new Error(`unsupported Copilot wire phase: ${phase}`);
|
|
22303
|
+
}
|
|
22304
|
+
}
|
|
22305
|
+
function copilotWire(reason, phase) {
|
|
22306
|
+
assertCopilotWirePhase(phase);
|
|
22307
|
+
const payload = phase === "end-of-turn" ? { decision: "block", reason } : { permissionDecision: "deny", permissionDecisionReason: reason };
|
|
22308
|
+
return JSON.stringify(payload) + "\n";
|
|
22309
|
+
}
|
|
22310
|
+
function copilotDenyWire(findings, phase) {
|
|
22311
|
+
assertCopilotWirePhase(phase);
|
|
22312
|
+
if (findings.length === 0) return "";
|
|
22313
|
+
return copilotWire(formatDenial(findings), phase);
|
|
22314
|
+
}
|
|
22315
|
+
var init_deny3 = __esm({
|
|
22316
|
+
"src/adapters/copilot/deny.ts"() {
|
|
22317
|
+
"use strict";
|
|
22318
|
+
init_deny();
|
|
22319
|
+
}
|
|
22320
|
+
});
|
|
22321
|
+
|
|
22322
|
+
// src/adapters/copilot/schema.ts
|
|
22323
|
+
function copilotOperationKind(toolName) {
|
|
22324
|
+
if (!toolName) return "other";
|
|
22325
|
+
const name = toolName.toLowerCase();
|
|
22326
|
+
if (SHELL_TOOLS2.has(name)) return "shell";
|
|
22327
|
+
if (FILE_EDIT_TOOLS2.has(name)) return "file-edit";
|
|
22328
|
+
if (name.startsWith(MCP_PREFIX2)) return "mcp";
|
|
22329
|
+
if (FILE_READ_TOOLS2.has(name)) return "file-read";
|
|
22330
|
+
return "other";
|
|
22331
|
+
}
|
|
22332
|
+
function argsFrom(parsed) {
|
|
22333
|
+
if (isRecord(parsed.tool_input)) return parsed.tool_input;
|
|
22334
|
+
const ta = parsed.toolArgs;
|
|
22335
|
+
if (isRecord(ta)) return ta;
|
|
22336
|
+
if (typeof ta === "string" && ta.trim()) {
|
|
22337
|
+
try {
|
|
22338
|
+
const decoded = JSON.parse(ta);
|
|
22339
|
+
return isRecord(decoded) ? decoded : void 0;
|
|
22340
|
+
} catch {
|
|
22341
|
+
return void 0;
|
|
22342
|
+
}
|
|
22343
|
+
}
|
|
22344
|
+
return void 0;
|
|
22345
|
+
}
|
|
22346
|
+
function inputFrom2(parsed) {
|
|
22347
|
+
const toolName = typeof parsed.tool_name === "string" ? parsed.tool_name : typeof parsed.toolName === "string" ? parsed.toolName : void 0;
|
|
22348
|
+
const sessionId = typeof parsed.session_id === "string" ? parsed.session_id : typeof parsed.sessionId === "string" ? parsed.sessionId : void 0;
|
|
22349
|
+
const args = argsFrom(parsed);
|
|
22350
|
+
return {
|
|
22351
|
+
...toolName !== void 0 ? { tool_name: toolName } : {},
|
|
22352
|
+
...args !== void 0 ? { tool_input: args } : {},
|
|
22353
|
+
...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
|
|
22354
|
+
...sessionId !== void 0 ? { session_id: sessionId } : {},
|
|
22355
|
+
...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {}
|
|
22356
|
+
};
|
|
22357
|
+
}
|
|
22358
|
+
function eventFrom3(input, phase) {
|
|
22359
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: copilotOperationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
|
|
22360
|
+
const identity2 = { claimedCwd: input.cwd, sessionId: input.session_id };
|
|
22361
|
+
return { phase, operation, identity: identity2 };
|
|
22362
|
+
}
|
|
22363
|
+
function parseRaw(raw) {
|
|
22364
|
+
if (!raw.trim()) return { kind: "absent" };
|
|
22365
|
+
let parsed;
|
|
22366
|
+
try {
|
|
22367
|
+
parsed = JSON.parse(raw);
|
|
22368
|
+
} catch (e) {
|
|
22369
|
+
return { kind: "fail", detail: `the Copilot hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
22370
|
+
}
|
|
22371
|
+
if (!isRecord(parsed)) {
|
|
22372
|
+
return { kind: "fail", detail: `the Copilot hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})` };
|
|
22373
|
+
}
|
|
22374
|
+
return { kind: "ok", record: parsed };
|
|
22375
|
+
}
|
|
22376
|
+
function normalizeCopilotEvent(raw, phase) {
|
|
22377
|
+
const parsed = parseRaw(raw);
|
|
22378
|
+
if (parsed.kind === "absent") return eventFrom3({}, phase);
|
|
22379
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22380
|
+
return eventFrom3(inputFrom2(parsed.record), phase);
|
|
22381
|
+
}
|
|
22382
|
+
function copilotStopInput(raw) {
|
|
22383
|
+
const parsed = parseRaw(raw);
|
|
22384
|
+
if (parsed.kind === "absent") return "{}";
|
|
22385
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22386
|
+
const input = inputFrom2(parsed.record);
|
|
22387
|
+
return JSON.stringify({
|
|
22388
|
+
...input.cwd !== void 0 ? { cwd: input.cwd } : {},
|
|
22389
|
+
...input.session_id !== void 0 ? { session_id: input.session_id } : {},
|
|
22390
|
+
...input.stop_hook_active !== void 0 ? { stop_hook_active: input.stop_hook_active } : {}
|
|
22391
|
+
});
|
|
22392
|
+
}
|
|
22393
|
+
var SHELL_TOOLS2, FILE_EDIT_TOOLS2, FILE_READ_TOOLS2, MCP_PREFIX2;
|
|
22394
|
+
var init_schema2 = __esm({
|
|
22395
|
+
"src/adapters/copilot/schema.ts"() {
|
|
22396
|
+
"use strict";
|
|
22397
|
+
init_narrow();
|
|
22398
|
+
SHELL_TOOLS2 = /* @__PURE__ */ new Set(["bash", "powershell", "write_bash", "write_powershell"]);
|
|
22399
|
+
FILE_EDIT_TOOLS2 = /* @__PURE__ */ new Set(["create", "edit", "write", "multiedit", "apply_patch", "str_replace_editor"]);
|
|
22400
|
+
FILE_READ_TOOLS2 = /* @__PURE__ */ new Set(["view", "read"]);
|
|
22401
|
+
MCP_PREFIX2 = "mcp__";
|
|
22402
|
+
}
|
|
22403
|
+
});
|
|
22404
|
+
|
|
22405
|
+
// src/adapters/copilot/adapter.ts
|
|
22406
|
+
var CopilotRuntimeAdapter, copilotAdapter;
|
|
22407
|
+
var init_adapter4 = __esm({
|
|
22408
|
+
"src/adapters/copilot/adapter.ts"() {
|
|
22409
|
+
"use strict";
|
|
22410
|
+
init_engine();
|
|
22411
|
+
init_policy_load();
|
|
22412
|
+
init_hook();
|
|
22413
|
+
init_session();
|
|
22414
|
+
init_repo_context();
|
|
22415
|
+
init_contract();
|
|
22416
|
+
init_changes3();
|
|
22417
|
+
init_deny3();
|
|
22418
|
+
init_schema2();
|
|
22419
|
+
CopilotRuntimeAdapter = class {
|
|
22420
|
+
name = "github-copilot-cli";
|
|
22421
|
+
/**
|
|
22422
|
+
* CONSERVATIVE and honest. Pre-action deny ENFORCEMENT is not yet proven on a pinned
|
|
22423
|
+
* Copilot CLI build, so `preDeny` is empty — the adapter never claims a synchronous veto
|
|
22424
|
+
* it has not demonstrated on the real runtime. `postObserve` is empty for milestone one:
|
|
22425
|
+
* Copilot does expose a `postToolUse` observation surface, but this adapter does not yet
|
|
22426
|
+
* consume it as a supported post-action path (`decide(..., 'post-action')` returns
|
|
22427
|
+
* `unsupported`), so it declares no post-observe capability rather than advertising kinds it
|
|
22428
|
+
* does not report. Every real gap is named in `unsupported`. The `probe:copilot-runtime`
|
|
22429
|
+
* harness is what may later justify moving a kind into `preDeny` or populating `postObserve`.
|
|
22430
|
+
*/
|
|
22431
|
+
capabilities = {
|
|
22432
|
+
preDeny: [],
|
|
22433
|
+
postObserve: [],
|
|
22434
|
+
endOfTurn: true,
|
|
22435
|
+
unsupported: [
|
|
22436
|
+
"pre-action deny enforcement not yet proven on a pinned Copilot CLI build (see probe:copilot-runtime)",
|
|
22437
|
+
"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",
|
|
22438
|
+
"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",
|
|
22439
|
+
"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",
|
|
22440
|
+
"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",
|
|
22441
|
+
"network-egress control",
|
|
22442
|
+
"identity / authentication"
|
|
22443
|
+
]
|
|
22444
|
+
};
|
|
22445
|
+
parseEvent(raw, phase) {
|
|
22446
|
+
return normalizeCopilotEvent(raw, phase);
|
|
22447
|
+
}
|
|
22448
|
+
/** Identity is a CLAIM validated against the runner's independently derived trusted root,
|
|
22449
|
+
* reusing `repoContext` / `validateClaimAgainstRoot` exactly as the Claude and Codex
|
|
22450
|
+
* adapters do. It never weakens the shared check. */
|
|
22451
|
+
validateIdentity(claim, defaultCwd) {
|
|
22452
|
+
const base = defaultCwd ?? process.cwd();
|
|
22453
|
+
const runnerCtx = repoContext(base);
|
|
22454
|
+
if (!runnerCtx) {
|
|
22455
|
+
return {
|
|
22456
|
+
ok: false,
|
|
22457
|
+
rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against`
|
|
22458
|
+
};
|
|
22459
|
+
}
|
|
22460
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
22461
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
22462
|
+
}
|
|
22463
|
+
denyPayload(findings, phase) {
|
|
22464
|
+
if (phase === "post-action") {
|
|
22465
|
+
throw new Error("Copilot post-action is observation-only and cannot produce a deny wire");
|
|
22466
|
+
}
|
|
22467
|
+
return copilotDenyWire(findings, phase);
|
|
22468
|
+
}
|
|
22469
|
+
/**
|
|
22470
|
+
* parse → validate identity → decide, every failure CLOSED (deny), in order:
|
|
22471
|
+
*
|
|
22472
|
+
* 1. `post-action` → `unsupported` (observation-only; never a deny wire).
|
|
22473
|
+
* 2. parse failure → a fail-closed deny in Copilot's wire.
|
|
22474
|
+
* 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
|
|
22475
|
+
* 4. `end-of-turn` → delegate to the canonical git sweep, whose wire is already Copilot's
|
|
22476
|
+
* agentStop `{decision:"block",reason}` shape and passes through unchanged.
|
|
22477
|
+
* 5. `pre-action` → reconstruct Change[] and run the SAME `evaluate` engine; a deny
|
|
22478
|
+
* carries the shared denial reason. A reconstruction that cannot be modelled fails
|
|
22479
|
+
* CLOSED to deny rather than allowing an unseen edit.
|
|
22480
|
+
*/
|
|
22481
|
+
decide(raw, phase, defaultCwd) {
|
|
22482
|
+
if (phase === "post-action") {
|
|
22483
|
+
return {
|
|
22484
|
+
outcome: "unsupported",
|
|
22485
|
+
detail: "Copilot post-action is observation-only; the end-of-turn agentStop sweep is the post-turn reconciliation."
|
|
22486
|
+
};
|
|
22487
|
+
}
|
|
22488
|
+
const parsed = this.parseEvent(raw, phase);
|
|
22489
|
+
if ("failure" in parsed) {
|
|
22490
|
+
const findings = [steeringUnavailableFinding(`unparseable Copilot event: ${parsed.detail}`)];
|
|
22491
|
+
const wire = this.denyPayload(findings, phase);
|
|
22492
|
+
return { outcome: "parse-failure", detail: parsed.detail, wire, decision: { verdict: "deny", findings, reason: wire } };
|
|
22493
|
+
}
|
|
22494
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
22495
|
+
if (!idv.ok) {
|
|
22496
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
22497
|
+
const wire = this.denyPayload(findings, phase);
|
|
22498
|
+
return { outcome: "ok", wire, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire } };
|
|
22499
|
+
}
|
|
22500
|
+
if (phase === "end-of-turn") {
|
|
22501
|
+
const stopInput = copilotStopInput(raw);
|
|
22502
|
+
if (typeof stopInput !== "string") {
|
|
22503
|
+
const findings = [steeringUnavailableFinding(`unparseable Copilot event: ${stopInput.detail}`)];
|
|
22504
|
+
const wire2 = this.denyPayload(findings, "end-of-turn");
|
|
22505
|
+
return { outcome: "parse-failure", detail: stopInput.detail, wire: wire2, decision: { verdict: "deny", findings, reason: wire2 } };
|
|
22506
|
+
}
|
|
22507
|
+
const wire = stopFromRaw(stopInput, defaultCwd, idv.trustedRoot).stdout;
|
|
22508
|
+
return { outcome: "ok", wire, decision: { verdict: wire ? "deny" : "allow", findings: [], reason: wire || void 0 } };
|
|
22509
|
+
}
|
|
22510
|
+
try {
|
|
22511
|
+
const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
|
|
22512
|
+
const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
|
|
22513
|
+
turnBaseline(root, parsed.identity.sessionId);
|
|
22514
|
+
const policy = loadPolicy(root);
|
|
22515
|
+
const changes = changesFromCopilot(parsed.operation, root, sessionCwd);
|
|
22516
|
+
const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22517
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22518
|
+
return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
|
|
22519
|
+
} catch (e) {
|
|
22520
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
22521
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
22522
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22523
|
+
return { outcome: "ok", wire, detail, decision: { verdict: "deny", findings, reason: wire } };
|
|
22524
|
+
}
|
|
22525
|
+
}
|
|
22526
|
+
/** A transport failure or a required hook that did not fire → a fail-closed deny at the
|
|
22527
|
+
* seam, through the adapter's own `denyPayload` so the wire is Copilot's native envelope. */
|
|
22528
|
+
failClosed(outcome, detail, phase) {
|
|
22529
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
22530
|
+
}
|
|
22531
|
+
};
|
|
22532
|
+
copilotAdapter = new CopilotRuntimeAdapter();
|
|
22533
|
+
}
|
|
22534
|
+
});
|
|
22535
|
+
|
|
22536
|
+
// src/adapters/copilot-sdk/changes.ts
|
|
22537
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
22538
|
+
import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync15, readFileSync as readFileSync27, rmSync as rmSync13, existsSync as existsSync21, realpathSync as realpathSync11 } from "node:fs";
|
|
22539
|
+
import { tmpdir as tmpdir8 } from "node:os";
|
|
22540
|
+
import { basename as basename6, dirname as dirname14, isAbsolute as isAbsolute17, relative as relative12, resolve as resolve22, join as join29 } from "node:path";
|
|
22541
|
+
function asStr4(v) {
|
|
22542
|
+
return typeof v === "string" ? v : "";
|
|
22543
|
+
}
|
|
22544
|
+
function relForDisplay5(path, cwd) {
|
|
22545
|
+
const rel = relative12(cwd, path);
|
|
22546
|
+
return rel && !rel.startsWith("..") && !isAbsolute17(rel) ? rel : path;
|
|
22547
|
+
}
|
|
22548
|
+
function canonicalContainedTarget(abs, root) {
|
|
22549
|
+
let realRoot;
|
|
22550
|
+
try {
|
|
22551
|
+
realRoot = realpathSync11(root);
|
|
22552
|
+
} catch (e) {
|
|
22553
|
+
throw new Error(`trusted repository root ${root} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
22554
|
+
}
|
|
22555
|
+
let dir = abs;
|
|
22556
|
+
const tail = [];
|
|
22557
|
+
while (!existsSync21(dir)) {
|
|
22558
|
+
const parent = dirname14(dir);
|
|
22559
|
+
if (parent === dir) break;
|
|
22560
|
+
tail.unshift(basename6(dir));
|
|
22561
|
+
dir = parent;
|
|
22562
|
+
}
|
|
22563
|
+
let realDir;
|
|
22564
|
+
try {
|
|
22565
|
+
realDir = realpathSync11(dir);
|
|
22566
|
+
} catch (e) {
|
|
22567
|
+
throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
|
|
22568
|
+
}
|
|
22569
|
+
const real = tail.length ? join29(realDir, ...tail) : realDir;
|
|
22570
|
+
const rel = relative12(realRoot, real);
|
|
22571
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute17(rel)) {
|
|
22572
|
+
throw new Error(`write target ${abs} resolves outside the trusted repository root ${realRoot} \u2014 refusing to judge a write beyond the repository`);
|
|
22573
|
+
}
|
|
22574
|
+
return { real, rel };
|
|
22575
|
+
}
|
|
22576
|
+
function sdkFileEditChanges(args, cwd, base = cwd) {
|
|
22577
|
+
const path = asStr4(args.path) || asStr4(args.fileName) || asStr4(args.file_path);
|
|
22578
|
+
if (!path) throw new Error("write event carries no fileName/path to reconstruct");
|
|
22579
|
+
const abs = resolve22(base, path);
|
|
22580
|
+
const canonical2 = canonicalContainedTarget(abs, cwd);
|
|
22581
|
+
const claimedResolved = asStr4(args.resolvedPath);
|
|
22582
|
+
if (claimedResolved && canonicalContainedTarget(resolve22(base, claimedResolved), cwd).real !== canonical2.real) {
|
|
22583
|
+
throw new Error(`the runtime-claimed resolvedPath (${claimedResolved}) does not match the host-derived target for ${path} \u2014 refusing to judge a different path`);
|
|
22584
|
+
}
|
|
22585
|
+
const display2 = canonical2.rel;
|
|
22586
|
+
const requestRel = relForDisplay5(abs, cwd);
|
|
22587
|
+
const entry = inspectResolved(canonical2.real);
|
|
22588
|
+
const isCreate = entry.kind === "absent";
|
|
22589
|
+
const before = textOf2(entry);
|
|
22590
|
+
if (!isCreate && before === null) {
|
|
22591
|
+
throw new Error(`write target ${display2} exists but cannot be read to judge it (${entry.kind})`);
|
|
22592
|
+
}
|
|
22593
|
+
const newFileContents = typeof args.newFileContents === "string" ? args.newFileContents : void 0;
|
|
22594
|
+
const rawDiff = asStr4(args.diff);
|
|
22595
|
+
const afterFromDiff = rawDiff.trim() ? reconstructAfterViaGit(rawDiff, requestRel, before, isCreate) : void 0;
|
|
22596
|
+
if (newFileContents !== void 0) {
|
|
22597
|
+
if (afterFromDiff !== void 0 && afterFromDiff !== newFileContents) {
|
|
22598
|
+
throw new Error(`write supplies both newFileContents and a diff that DISAGREE for ${display2}; ambiguous request \u2014 refusing to judge only one representation`);
|
|
22599
|
+
}
|
|
22600
|
+
return synthFileChange(display2, before, newFileContents);
|
|
22601
|
+
}
|
|
22602
|
+
if (afterFromDiff !== void 0) return synthFileChange(display2, before, afterFromDiff);
|
|
22603
|
+
return null;
|
|
22604
|
+
}
|
|
22605
|
+
function stripPrefix(p) {
|
|
22606
|
+
return /^[ab]\//.test(p) ? p.slice(2) : p;
|
|
22607
|
+
}
|
|
22608
|
+
function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
|
|
22609
|
+
const bytes = Buffer.byteLength(rawDiff);
|
|
22610
|
+
if (bytes > DIFF_MAX_BYTES()) throw new Error(`write diff for ${display2} is ${bytes} bytes (over the ${DIFF_MAX_BYTES()}-byte reconstruction budget)`);
|
|
22611
|
+
const { op, patch } = canonicalPatch(rawDiff, display2, isCreate);
|
|
22612
|
+
const dir = mkdtempSync7(join29(tmpdir8(), "hf-sdk-apply-"));
|
|
22613
|
+
try {
|
|
22614
|
+
const targetPath = join29(dir, TARGET);
|
|
22615
|
+
if (op !== "create") writeFileSync15(targetPath, before ?? "");
|
|
22616
|
+
const patchPath = join29(dir, "change.patch");
|
|
22617
|
+
writeFileSync15(patchPath, patch);
|
|
22618
|
+
const git8 = (extra) => execFileSync15("git", ["apply", ...extra, patchPath], {
|
|
22619
|
+
cwd: dir,
|
|
22620
|
+
encoding: "utf8",
|
|
22621
|
+
timeout: APPLY_TIMEOUT_MS(),
|
|
22622
|
+
maxBuffer: APPLY_MAXBUFFER(),
|
|
22623
|
+
killSignal: "SIGKILL"
|
|
22624
|
+
});
|
|
22625
|
+
const stat = git8(["--numstat"]).trim();
|
|
22626
|
+
const rows = stat ? stat.split("\n") : [];
|
|
22627
|
+
if (rows.length !== 1) throw new Error(`write diff spans ${rows.length} files; a single permission target (${display2}) was expected`);
|
|
22628
|
+
const statPath = rows[0].split(" ")[2];
|
|
22629
|
+
if (statPath !== TARGET) throw new Error(`write diff reconstructs a different file (${statPath}) than the bound target`);
|
|
22630
|
+
git8(["--check"]);
|
|
22631
|
+
git8([]);
|
|
22632
|
+
return op === "delete" ? null : readFileSync27(targetPath, "utf8");
|
|
22633
|
+
} catch (e) {
|
|
22634
|
+
throw new Error(`could not reconstruct the write to ${display2} from its diff via git apply: ${e instanceof Error ? e.message : String(e)}`);
|
|
22635
|
+
} finally {
|
|
22636
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
22637
|
+
}
|
|
22638
|
+
}
|
|
22639
|
+
function canonicalPatch(raw, display2, isCreate) {
|
|
22640
|
+
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
|
22641
|
+
if (lines.length > DIFF_MAX_LINES()) {
|
|
22642
|
+
throw new Error(`write diff for ${display2} is ${lines.length} lines (over the ${DIFF_MAX_LINES()}-line reconstruction budget)`);
|
|
22643
|
+
}
|
|
22644
|
+
const at = lines.findIndex((l) => l.startsWith("@@ "));
|
|
22645
|
+
if (at < 0) throw new Error(`write diff for ${display2} has no hunk to reconstruct`);
|
|
22646
|
+
const header = lines.slice(0, at);
|
|
22647
|
+
const body = lines.slice(at);
|
|
22648
|
+
let src;
|
|
22649
|
+
let dst;
|
|
22650
|
+
let hasGitLine = false;
|
|
22651
|
+
let sawCreateMode = false;
|
|
22652
|
+
let sawDeleteMode = false;
|
|
22653
|
+
let sawSemanticHeader = false;
|
|
22654
|
+
for (const line of header) {
|
|
22655
|
+
if (line.trim() === "") continue;
|
|
22656
|
+
if (line.startsWith("diff --git ")) {
|
|
22657
|
+
if (hasGitLine) throw new Error(`write diff for ${display2} carries more than one diff --git header (contradictory / multi-file) \u2014 refusing to reconstruct`);
|
|
22658
|
+
hasGitLine = true;
|
|
22659
|
+
sawSemanticHeader = true;
|
|
22660
|
+
for (const p of line.slice("diff --git ".length).trim().split(/\s+/)) assertBound(stripPrefix(p), display2);
|
|
22661
|
+
} else if (line.startsWith("--- ")) {
|
|
22662
|
+
if (src !== void 0) throw new Error(`write diff for ${display2} carries more than one --- endpoint (contradictory header) \u2014 refusing to reconstruct`);
|
|
22663
|
+
src = line.slice(4).split(" ")[0].trim();
|
|
22664
|
+
} else if (line.startsWith("+++ ")) {
|
|
22665
|
+
if (dst !== void 0) throw new Error(`write diff for ${display2} carries more than one +++ endpoint (contradictory header) \u2014 refusing to reconstruct`);
|
|
22666
|
+
dst = line.slice(4).split(" ")[0].trim();
|
|
22667
|
+
} else if (line.startsWith("index ")) {
|
|
22668
|
+
sawSemanticHeader = true;
|
|
22669
|
+
} else if (/^new file mode\b/.test(line)) {
|
|
22670
|
+
if (sawCreateMode) throw new Error(`write diff for ${display2} carries duplicate new file mode metadata \u2014 refusing to reconstruct`);
|
|
22671
|
+
sawCreateMode = true;
|
|
22672
|
+
sawSemanticHeader = true;
|
|
22673
|
+
} else if (/^deleted file mode\b/.test(line)) {
|
|
22674
|
+
if (sawDeleteMode) throw new Error(`write diff for ${display2} carries duplicate deleted file mode metadata \u2014 refusing to reconstruct`);
|
|
22675
|
+
sawDeleteMode = true;
|
|
22676
|
+
sawSemanticHeader = true;
|
|
22677
|
+
} else {
|
|
22678
|
+
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`);
|
|
22679
|
+
}
|
|
22680
|
+
}
|
|
22681
|
+
let op;
|
|
22682
|
+
if (src === void 0 && dst === void 0) {
|
|
22683
|
+
if (sawSemanticHeader) throw new Error(`write diff for ${display2} carries Git file metadata but no --- / +++ endpoints (malformed)`);
|
|
22684
|
+
op = isCreate ? "create" : "modify";
|
|
22685
|
+
} else if (src !== void 0 && dst !== void 0) {
|
|
22686
|
+
const srcNull = src === "/dev/null";
|
|
22687
|
+
const dstNull = dst === "/dev/null";
|
|
22688
|
+
if (srcNull && dstNull) throw new Error(`write diff for ${display2} has /dev/null on both sides`);
|
|
22689
|
+
if (!srcNull) assertBound(stripPrefix(src), display2);
|
|
22690
|
+
if (!dstNull) assertBound(stripPrefix(dst), display2);
|
|
22691
|
+
op = srcNull ? "create" : dstNull ? "delete" : "modify";
|
|
22692
|
+
} else {
|
|
22693
|
+
throw new Error(`write diff for ${display2} has a one-sided --- / +++ endpoint pair (malformed) \u2014 refusing to repair it into a valid operation`);
|
|
22694
|
+
}
|
|
22695
|
+
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`);
|
|
22696
|
+
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`);
|
|
22697
|
+
if (op === "create" && !isCreate) throw new Error(`write diff for ${display2} proposes a create but the target already exists`);
|
|
22698
|
+
if (op !== "create" && isCreate) throw new Error(`write diff for ${display2} proposes a ${op} but the target is absent`);
|
|
22699
|
+
const source = op === "create" ? "/dev/null" : `a/${TARGET}`;
|
|
22700
|
+
const dest = op === "delete" ? "/dev/null" : `b/${TARGET}`;
|
|
22701
|
+
const joined = body.join("\n");
|
|
22702
|
+
const patch = `--- ${source}
|
|
22703
|
+
+++ ${dest}
|
|
22704
|
+
` + (joined.endsWith("\n") ? joined : joined + "\n");
|
|
22705
|
+
return { op, patch };
|
|
22706
|
+
}
|
|
22707
|
+
function assertBound(declared, display2) {
|
|
22708
|
+
if (declared !== display2) {
|
|
22709
|
+
throw new Error(`write diff path (${declared}) does not match the permission request target (${display2}) \u2014 refusing to judge a different file`);
|
|
22710
|
+
}
|
|
22711
|
+
}
|
|
22712
|
+
var num2, APPLY_TIMEOUT_MS, APPLY_MAXBUFFER, DIFF_MAX_BYTES, DIFF_MAX_LINES, TARGET;
|
|
22713
|
+
var init_changes4 = __esm({
|
|
22714
|
+
"src/adapters/copilot-sdk/changes.ts"() {
|
|
22715
|
+
"use strict";
|
|
22716
|
+
init_changes();
|
|
22717
|
+
init_disk();
|
|
22718
|
+
num2 = (name, fallback) => {
|
|
22719
|
+
const raw = process.env[name];
|
|
22720
|
+
const n2 = raw === void 0 ? NaN : Number(raw);
|
|
22721
|
+
return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
|
|
22722
|
+
};
|
|
22723
|
+
APPLY_TIMEOUT_MS = () => num2("TAMPERWARD_RECONSTRUCT_TIMEOUT_MS", 5e3);
|
|
22724
|
+
APPLY_MAXBUFFER = () => num2("TAMPERWARD_RECONSTRUCT_MAXBUFFER", 32 * 1024 * 1024);
|
|
22725
|
+
DIFF_MAX_BYTES = () => num2("TAMPERWARD_RECONSTRUCT_MAX_BYTES", 384 * 1024);
|
|
22726
|
+
DIFF_MAX_LINES = () => num2("TAMPERWARD_RECONSTRUCT_MAX_LINES", 4e3);
|
|
22727
|
+
TARGET = "target";
|
|
22728
|
+
}
|
|
22729
|
+
});
|
|
22730
|
+
|
|
22731
|
+
// src/adapters/copilot-sdk/deny.ts
|
|
22732
|
+
function assertPhase(phase) {
|
|
22733
|
+
const known = ["pre-action", "end-of-turn"];
|
|
22734
|
+
if (!known.includes(phase)) throw new Error(`unsupported Copilot SDK wire phase: ${phase}`);
|
|
22735
|
+
}
|
|
22736
|
+
function copilotSdkWire(reason, phase) {
|
|
22737
|
+
assertPhase(phase);
|
|
22738
|
+
const payload = phase === "end-of-turn" ? { decision: "block", reason } : { kind: "reject", feedback: reason };
|
|
22739
|
+
return JSON.stringify(payload) + "\n";
|
|
22740
|
+
}
|
|
22741
|
+
function copilotSdkDenyWire(findings, phase) {
|
|
22742
|
+
assertPhase(phase);
|
|
22743
|
+
if (findings.length === 0) return "";
|
|
22744
|
+
return copilotSdkWire(formatDenial(findings), phase);
|
|
22745
|
+
}
|
|
22746
|
+
var init_deny4 = __esm({
|
|
22747
|
+
"src/adapters/copilot-sdk/deny.ts"() {
|
|
22748
|
+
"use strict";
|
|
22749
|
+
init_deny();
|
|
22750
|
+
}
|
|
22751
|
+
});
|
|
22752
|
+
|
|
22753
|
+
// src/adapters/copilot-sdk/schema.ts
|
|
22754
|
+
function copilotSdkOperationKind(kind) {
|
|
22755
|
+
switch ((kind ?? "").toLowerCase()) {
|
|
22756
|
+
case "shell":
|
|
22757
|
+
return "shell";
|
|
22758
|
+
case "write":
|
|
22759
|
+
return "file-edit";
|
|
22760
|
+
case "read":
|
|
22761
|
+
return "file-read";
|
|
22762
|
+
case "mcp":
|
|
22763
|
+
return "mcp";
|
|
22764
|
+
default:
|
|
22765
|
+
return "other";
|
|
22766
|
+
}
|
|
22767
|
+
}
|
|
22768
|
+
function str3(v) {
|
|
22769
|
+
return typeof v === "string" ? v : void 0;
|
|
22770
|
+
}
|
|
22771
|
+
function requestFrom2(parsed) {
|
|
22772
|
+
const s = (k) => str3(parsed[k]);
|
|
22773
|
+
return {
|
|
22774
|
+
...s("kind") !== void 0 ? { kind: s("kind") } : {},
|
|
22775
|
+
...s("toolName") !== void 0 ? { toolName: s("toolName") } : {},
|
|
22776
|
+
...s("fileName") !== void 0 ? { fileName: s("fileName") } : {},
|
|
22777
|
+
...s("resolvedPath") !== void 0 ? { resolvedPath: s("resolvedPath") } : {},
|
|
22778
|
+
...s("fullCommandText") !== void 0 ? { fullCommandText: s("fullCommandText") } : {},
|
|
22779
|
+
...s("diff") !== void 0 ? { diff: s("diff") } : {},
|
|
22780
|
+
...s("newFileContents") !== void 0 ? { newFileContents: s("newFileContents") } : {},
|
|
22781
|
+
...s("intention") !== void 0 ? { intention: s("intention") } : {},
|
|
22782
|
+
...s("cwd") !== void 0 ? { cwd: s("cwd") } : {},
|
|
22783
|
+
...s("sessionId") !== void 0 ? { sessionId: s("sessionId") } : {},
|
|
22784
|
+
...typeof parsed.stopHookActive === "boolean" ? { stopHookActive: parsed.stopHookActive } : {}
|
|
22785
|
+
};
|
|
22786
|
+
}
|
|
22787
|
+
function argsFor(req2) {
|
|
22788
|
+
const kind = copilotSdkOperationKind(req2.kind);
|
|
22789
|
+
if (kind === "shell") return req2.fullCommandText !== void 0 ? { command: req2.fullCommandText } : {};
|
|
22790
|
+
if (kind === "file-edit") {
|
|
22791
|
+
return {
|
|
22792
|
+
...req2.fileName !== void 0 ? { path: req2.fileName } : {},
|
|
22793
|
+
// The SDK's experimental runtime-resolved canonical path. Retained as an UNTRUSTED claim: the
|
|
22794
|
+
// host derives its own canonical target and denies on mismatch — resolvedPath is never authority.
|
|
22795
|
+
...req2.resolvedPath !== void 0 ? { resolvedPath: req2.resolvedPath } : {},
|
|
22796
|
+
...req2.diff !== void 0 ? { diff: req2.diff } : {},
|
|
22797
|
+
...req2.newFileContents !== void 0 ? { newFileContents: req2.newFileContents } : {},
|
|
22798
|
+
...req2.intention !== void 0 ? { intention: req2.intention } : {}
|
|
22799
|
+
};
|
|
22800
|
+
}
|
|
22801
|
+
return {};
|
|
22802
|
+
}
|
|
22803
|
+
function eventFrom4(req2, phase) {
|
|
22804
|
+
const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: copilotSdkOperationKind(req2.kind), name: req2.toolName ?? "", args: argsFor(req2) };
|
|
22805
|
+
const identity2 = { claimedCwd: req2.cwd, sessionId: req2.sessionId };
|
|
22806
|
+
return { phase, operation, identity: identity2 };
|
|
22807
|
+
}
|
|
22808
|
+
function parseRaw2(raw) {
|
|
22809
|
+
if (!raw.trim()) return { kind: "absent" };
|
|
22810
|
+
let parsed;
|
|
22811
|
+
try {
|
|
22812
|
+
parsed = JSON.parse(raw);
|
|
22813
|
+
} catch (e) {
|
|
22814
|
+
return { kind: "fail", detail: `the Copilot SDK permission request is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
22815
|
+
}
|
|
22816
|
+
if (!isRecord(parsed)) {
|
|
22817
|
+
return { kind: "fail", detail: `the Copilot SDK permission request is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})` };
|
|
22818
|
+
}
|
|
22819
|
+
return { kind: "ok", record: parsed };
|
|
22820
|
+
}
|
|
22821
|
+
function normalizeCopilotSdkEvent(raw, phase) {
|
|
22822
|
+
const parsed = parseRaw2(raw);
|
|
22823
|
+
if (parsed.kind === "absent") return eventFrom4({}, phase);
|
|
22824
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22825
|
+
return eventFrom4(requestFrom2(parsed.record), phase);
|
|
22826
|
+
}
|
|
22827
|
+
function copilotSdkStopInput(raw) {
|
|
22828
|
+
const parsed = parseRaw2(raw);
|
|
22829
|
+
if (parsed.kind === "absent") return "{}";
|
|
22830
|
+
if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
|
|
22831
|
+
const req2 = requestFrom2(parsed.record);
|
|
22832
|
+
return JSON.stringify({
|
|
22833
|
+
...req2.cwd !== void 0 ? { cwd: req2.cwd } : {},
|
|
22834
|
+
...req2.sessionId !== void 0 ? { session_id: req2.sessionId } : {},
|
|
22835
|
+
...req2.stopHookActive !== void 0 ? { stop_hook_active: req2.stopHookActive } : {}
|
|
22836
|
+
});
|
|
22837
|
+
}
|
|
22838
|
+
var init_schema3 = __esm({
|
|
22839
|
+
"src/adapters/copilot-sdk/schema.ts"() {
|
|
22840
|
+
"use strict";
|
|
22841
|
+
init_narrow();
|
|
22842
|
+
}
|
|
22843
|
+
});
|
|
22844
|
+
|
|
22845
|
+
// src/adapters/copilot-sdk/adapter.ts
|
|
22846
|
+
var FILE_EDIT_NO_CONTENT_DETAIL, CopilotSdkHostedAdapter, copilotSdkAdapter;
|
|
22847
|
+
var init_adapter5 = __esm({
|
|
22848
|
+
"src/adapters/copilot-sdk/adapter.ts"() {
|
|
22849
|
+
"use strict";
|
|
22850
|
+
init_engine();
|
|
22851
|
+
init_policy_load();
|
|
22852
|
+
init_hook();
|
|
22853
|
+
init_session();
|
|
22854
|
+
init_repo_context();
|
|
22855
|
+
init_contract();
|
|
22856
|
+
init_changes3();
|
|
22857
|
+
init_changes4();
|
|
22858
|
+
init_deny4();
|
|
22859
|
+
init_schema3();
|
|
22860
|
+
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.";
|
|
22861
|
+
CopilotSdkHostedAdapter = class {
|
|
22862
|
+
name = "github-copilot-sdk-hosted";
|
|
22863
|
+
/**
|
|
22864
|
+
* CONSERVATIVE and honest. `preDeny` is empty: even shell pre-deny is only a CANDIDATE until the
|
|
22865
|
+
* spike proves, on a pinned SDK, that a rejected/thrown/timed-out permission callback does NOT
|
|
22866
|
+
* dispatch the tool (the decisive unknown #611 must measure). `postObserve` is empty (the SDK has
|
|
22867
|
+
* no post-execution veto). `endOfTurn` is true via `onAgentStop`. Every real gap is named in
|
|
22868
|
+
* `unsupported`, at the granularity #611 asks for.
|
|
22869
|
+
*/
|
|
22870
|
+
capabilities = {
|
|
22871
|
+
preDeny: [],
|
|
22872
|
+
postObserve: [],
|
|
22873
|
+
endOfTurn: true,
|
|
22874
|
+
unsupported: [
|
|
22875
|
+
"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)",
|
|
22876
|
+
"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",
|
|
22877
|
+
"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",
|
|
22878
|
+
'onAgentStop is a lifecycle continuation control ({decision:"block"} forces another turn, guarded by stopHookActive), not a filesystem veto',
|
|
22879
|
+
"network-egress control",
|
|
22880
|
+
"identity / authentication"
|
|
22881
|
+
]
|
|
22882
|
+
};
|
|
22883
|
+
parseEvent(raw, phase) {
|
|
22884
|
+
return normalizeCopilotSdkEvent(raw, phase);
|
|
22885
|
+
}
|
|
22886
|
+
/** Identity is a CLAIM validated against the runner's independently derived trusted root,
|
|
22887
|
+
* reusing `repoContext` / `validateClaimAgainstRoot` exactly as the other adapters do. */
|
|
22888
|
+
validateIdentity(claim, defaultCwd) {
|
|
22889
|
+
const base = defaultCwd ?? process.cwd();
|
|
22890
|
+
const runnerCtx = repoContext(base);
|
|
22891
|
+
if (!runnerCtx) {
|
|
22892
|
+
return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
|
|
22893
|
+
}
|
|
22894
|
+
const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
|
|
22895
|
+
return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
|
|
22896
|
+
}
|
|
22897
|
+
denyPayload(findings, phase) {
|
|
22898
|
+
if (phase === "post-action") {
|
|
22899
|
+
throw new Error("Copilot SDK post-action is observation-only and cannot produce a deny wire");
|
|
22900
|
+
}
|
|
22901
|
+
return copilotSdkDenyWire(findings, phase);
|
|
22902
|
+
}
|
|
22903
|
+
/**
|
|
22904
|
+
* parse → validate identity → decide, every failure CLOSED (deny), in order:
|
|
22905
|
+
* 1. `post-action` → `unsupported` (the SDK has no post-execution veto).
|
|
22906
|
+
* 2. parse failure → a fail-closed deny in the SDK reject wire.
|
|
22907
|
+
* 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
|
|
22908
|
+
* 4. `end-of-turn` → delegate to the canonical git sweep, whose wire is already the SDK
|
|
22909
|
+
* `onAgentStop` `{decision:"block",reason}` shape.
|
|
22910
|
+
* 5. `pre-action` + shell → the SAME `evaluate` engine over the reconstructed command.
|
|
22911
|
+
* 6. `pre-action` + file-edit → `unsupported` (content not surfaced; sweep is authority).
|
|
22912
|
+
* 7. `pre-action` + read/mcp/other → no Change → allow.
|
|
22913
|
+
*/
|
|
22914
|
+
decide(raw, phase, defaultCwd) {
|
|
22915
|
+
if (phase === "post-action") {
|
|
22916
|
+
return { outcome: "unsupported", detail: "Copilot SDK has no post-execution veto; the end-of-turn onAgentStop sweep is the post-turn reconciliation." };
|
|
22917
|
+
}
|
|
22918
|
+
const parsed = this.parseEvent(raw, phase);
|
|
22919
|
+
if ("failure" in parsed) {
|
|
22920
|
+
const findings = [steeringUnavailableFinding(`unparseable Copilot SDK permission request: ${parsed.detail}`)];
|
|
22921
|
+
const wire = this.denyPayload(findings, phase);
|
|
22922
|
+
return { outcome: "parse-failure", detail: parsed.detail, wire, unavailableReason: "parse-failure", decision: { verdict: "deny", findings, reason: wire } };
|
|
22923
|
+
}
|
|
22924
|
+
const idv = this.validateIdentity(parsed.identity, defaultCwd);
|
|
22925
|
+
if (!idv.ok) {
|
|
22926
|
+
const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
|
|
22927
|
+
const wire = this.denyPayload(findings, phase);
|
|
22928
|
+
return { outcome: "ok", wire, detail: idv.rejected, unavailableReason: "identity-rejected", decision: { verdict: "deny", findings, reason: wire } };
|
|
22929
|
+
}
|
|
22930
|
+
if (phase === "end-of-turn") {
|
|
22931
|
+
const stopInput = copilotSdkStopInput(raw);
|
|
22932
|
+
if (typeof stopInput !== "string") {
|
|
22933
|
+
const findings2 = [steeringUnavailableFinding(`unparseable Copilot SDK permission request: ${stopInput.detail}`)];
|
|
22934
|
+
const wire = this.denyPayload(findings2, "end-of-turn");
|
|
22935
|
+
return { outcome: "parse-failure", detail: stopInput.detail, wire, unavailableReason: "parse-failure", decision: { verdict: "deny", findings: findings2, reason: wire } };
|
|
22936
|
+
}
|
|
22937
|
+
const swept = stopFromRaw(stopInput, defaultCwd, idv.trustedRoot);
|
|
22938
|
+
const findings = swept.findings ? [...swept.findings] : [];
|
|
22939
|
+
return { outcome: "ok", wire: swept.stdout, decision: { verdict: swept.stdout ? "deny" : "allow", findings, reason: swept.stdout || void 0 } };
|
|
22940
|
+
}
|
|
22941
|
+
let stage = "repo-context";
|
|
22942
|
+
try {
|
|
22943
|
+
const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
|
|
22944
|
+
stage = "baseline";
|
|
22945
|
+
turnBaseline(root, parsed.identity.sessionId);
|
|
22946
|
+
const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
|
|
22947
|
+
stage = "policy-load";
|
|
22948
|
+
const policy = loadPolicy(root);
|
|
22949
|
+
if (parsed.operation.kind === "file-edit") {
|
|
22950
|
+
stage = "reconstruction";
|
|
22951
|
+
const changes2 = sdkFileEditChanges(parsed.operation.args, root, sessionCwd);
|
|
22952
|
+
if (changes2 === null) return { outcome: "unsupported", detail: FILE_EDIT_NO_CONTENT_DETAIL };
|
|
22953
|
+
stage = "evaluate";
|
|
22954
|
+
const findings2 = evaluate(changes2, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22955
|
+
const wire2 = this.denyPayload(findings2, "pre-action");
|
|
22956
|
+
return { outcome: "ok", wire: wire2, decision: { verdict: findings2.length ? "deny" : "allow", findings: findings2, reason: wire2 || void 0 } };
|
|
22957
|
+
}
|
|
22958
|
+
stage = "reconstruction";
|
|
22959
|
+
const changes = changesFromCopilot(parsed.operation, root, sessionCwd);
|
|
22960
|
+
stage = "evaluate";
|
|
22961
|
+
const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
|
|
22962
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22963
|
+
return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
|
|
22964
|
+
} catch (e) {
|
|
22965
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
22966
|
+
const findings = [steeringUnavailableFinding(detail)];
|
|
22967
|
+
const wire = this.denyPayload(findings, "pre-action");
|
|
22968
|
+
return { outcome: "ok", wire, detail, unavailableReason: stage, decision: { verdict: "deny", findings, reason: wire } };
|
|
22969
|
+
}
|
|
22970
|
+
}
|
|
22971
|
+
/** A transport failure or a required callback that did not fire → a fail-closed deny at the
|
|
22972
|
+
* seam, through the adapter's own `denyPayload` so the wire is the SDK's native envelope. */
|
|
22973
|
+
failClosed(outcome, detail, phase) {
|
|
22974
|
+
return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
|
|
22975
|
+
}
|
|
22976
|
+
};
|
|
22977
|
+
copilotSdkAdapter = new CopilotSdkHostedAdapter();
|
|
22978
|
+
}
|
|
22979
|
+
});
|
|
22980
|
+
|
|
22981
|
+
// src/adapters/registry.ts
|
|
22982
|
+
function adapterFor(id) {
|
|
22983
|
+
const name = ADAPTER_ALIASES[id] ?? id;
|
|
22984
|
+
return RUNTIME_ADAPTERS.find((a) => a.name === name) ?? null;
|
|
22985
|
+
}
|
|
22986
|
+
function labelFor(name) {
|
|
22987
|
+
return ADAPTER_LABELS[name] ?? name;
|
|
22988
|
+
}
|
|
22989
|
+
var RUNTIME_ADAPTERS, ADAPTER_LABELS, ADAPTER_ALIASES;
|
|
22990
|
+
var init_registry = __esm({
|
|
22991
|
+
"src/adapters/registry.ts"() {
|
|
22992
|
+
"use strict";
|
|
22993
|
+
init_adapter2();
|
|
22994
|
+
init_adapter3();
|
|
22995
|
+
init_adapter4();
|
|
22996
|
+
init_adapter5();
|
|
22997
|
+
RUNTIME_ADAPTERS = [
|
|
22998
|
+
claudeAdapter,
|
|
22999
|
+
codexAdapter,
|
|
23000
|
+
copilotAdapter,
|
|
23001
|
+
copilotSdkAdapter
|
|
23002
|
+
];
|
|
23003
|
+
ADAPTER_LABELS = {
|
|
23004
|
+
"claude-code": "Claude Code",
|
|
23005
|
+
codex: "Codex",
|
|
23006
|
+
"github-copilot-cli": "GitHub Copilot CLI",
|
|
23007
|
+
"github-copilot-sdk-hosted": "GitHub Copilot SDK (hosted)"
|
|
23008
|
+
};
|
|
23009
|
+
ADAPTER_ALIASES = {
|
|
23010
|
+
claude: "claude-code",
|
|
23011
|
+
copilot: "github-copilot-cli",
|
|
23012
|
+
"copilot-cli": "github-copilot-cli",
|
|
23013
|
+
"copilot-sdk": "github-copilot-sdk-hosted"
|
|
23014
|
+
};
|
|
23015
|
+
}
|
|
23016
|
+
});
|
|
23017
|
+
|
|
23018
|
+
// src/adapters/evidence.ts
|
|
23019
|
+
function sameIdSet(a, b) {
|
|
23020
|
+
const ua = [...new Set(a)].sort();
|
|
23021
|
+
const ub = [...new Set(b)].sort();
|
|
23022
|
+
return ua.length === ub.length && ua.every((x, i) => x === ub[i]);
|
|
23023
|
+
}
|
|
23024
|
+
function evidenceBindingMatches(record, current) {
|
|
23025
|
+
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.
|
|
23026
|
+
sameIdSet(record.component_versions, current.component_versions) && // The TamperWard build (version+commit): a different TamperWard build is a different binding.
|
|
23027
|
+
record.tamperward_version === current.tamperward_version && // Adapter capability hash — REQUIRED (fail-closed) on both sides. A `null` recorded hash
|
|
23028
|
+
// (unrecoverable, never fabricated) can never equal a concrete current hash.
|
|
23029
|
+
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.
|
|
23030
|
+
sameIdSet(record.tested_capabilities, current.tested_capabilities);
|
|
23031
|
+
}
|
|
23032
|
+
function matchRetainedEvidence(current, catalogue = RETAINED_EVIDENCE) {
|
|
23033
|
+
return catalogue.find((r) => evidenceBindingMatches(r.binding, current)) ?? null;
|
|
23034
|
+
}
|
|
23035
|
+
var RETAINED_EVIDENCE;
|
|
23036
|
+
var init_evidence = __esm({
|
|
23037
|
+
"src/adapters/evidence.ts"() {
|
|
23038
|
+
"use strict";
|
|
23039
|
+
RETAINED_EVIDENCE = [
|
|
23040
|
+
{
|
|
23041
|
+
ref: "copilot-sdk-capture-2026-09-20",
|
|
23042
|
+
source: "harness/adapters/copilot-sdk/evidence/capture-2026-09-20.json",
|
|
23043
|
+
source_artifact_sha256: "c46fba989dc0e772012c5fdb1291cf099326d24da2b551788be2e1e85113e49a",
|
|
23044
|
+
binding: {
|
|
23045
|
+
runtime_id: "github-copilot-sdk-hosted",
|
|
23046
|
+
runtime_version: "copilot-runtime@1.0.85",
|
|
23047
|
+
component_versions: ["@github/copilot-sdk@1.0.14", "copilot-protocol@3"],
|
|
23048
|
+
// The capture's own provenance: tamperward@2.31.0+3671a5e6 (capture-2026-09-20.json).
|
|
23049
|
+
tamperward_version: "2.31.0+3671a5e6",
|
|
23050
|
+
// NOT authentically recoverable from the committed capture: the sanitized artifact records
|
|
23051
|
+
// no adapter capability-declaration hash. We do NOT invent a historical hash. `null` makes
|
|
23052
|
+
// the applicability match fail CLOSED (a null recorded hash can never equal a concrete
|
|
23053
|
+
// current hash), so this record can never promote — the honest, safe outcome.
|
|
23054
|
+
adapter_capability_hash: null,
|
|
23055
|
+
// The capability ids the capture actually observed (the `observations` below).
|
|
23056
|
+
tested_capabilities: ["pre-deny:shell", "hook-not-invoked", "transport:timeout"],
|
|
23057
|
+
model: "gpt-5.4",
|
|
23058
|
+
platform: "darwin-arm64",
|
|
23059
|
+
execution_mode: "headless",
|
|
23060
|
+
hook_config_hash: null
|
|
23061
|
+
},
|
|
23062
|
+
observations: [
|
|
23063
|
+
{
|
|
23064
|
+
id: "pre-deny:shell",
|
|
23065
|
+
result: "proven",
|
|
23066
|
+
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)"
|
|
23067
|
+
},
|
|
23068
|
+
{
|
|
23069
|
+
id: "hook-not-invoked",
|
|
23070
|
+
result: "proven",
|
|
23071
|
+
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"
|
|
23072
|
+
},
|
|
23073
|
+
{
|
|
23074
|
+
id: "transport:timeout",
|
|
23075
|
+
result: "inconclusive",
|
|
23076
|
+
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"
|
|
23077
|
+
}
|
|
23078
|
+
]
|
|
23079
|
+
}
|
|
23080
|
+
];
|
|
23081
|
+
}
|
|
23082
|
+
});
|
|
23083
|
+
|
|
23084
|
+
// src/runtime-qualification.ts
|
|
23085
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
23086
|
+
function includesKind(set, kind) {
|
|
23087
|
+
return set.includes(kind);
|
|
23088
|
+
}
|
|
23089
|
+
function find(unsupported, re) {
|
|
23090
|
+
return unsupported.find((u) => re.test(u));
|
|
23091
|
+
}
|
|
23092
|
+
function indexEvidence(evidence) {
|
|
23093
|
+
const m = /* @__PURE__ */ new Map();
|
|
23094
|
+
if (!evidence) return m;
|
|
23095
|
+
for (const o of evidence.observations) m.set(o.id, o);
|
|
23096
|
+
return m;
|
|
23097
|
+
}
|
|
23098
|
+
function assessFromEvidence(id, index, evidence) {
|
|
23099
|
+
const obs = index.get(id);
|
|
23100
|
+
if (!obs || !evidence) return null;
|
|
23101
|
+
const state = obs.result === "proven" ? "PROVEN" : obs.result === "fail-open" ? "FAIL-OPEN" : "INCONCLUSIVE";
|
|
23102
|
+
return {
|
|
23103
|
+
id,
|
|
23104
|
+
state,
|
|
23105
|
+
evidence: {
|
|
23106
|
+
source: "committed-evidence",
|
|
23107
|
+
detail: `${obs.detail} [retained evidence ${evidence.ref} \xB7 ${evidence.source} \xB7 sha256 ${evidence.source_artifact_sha256.slice(0, 12)}]`
|
|
23108
|
+
}
|
|
23109
|
+
};
|
|
23110
|
+
}
|
|
23111
|
+
function assessPreDeny(caps, id, kind, kindNote, index, evidence) {
|
|
23112
|
+
const proven = assessFromEvidence(id, index, evidence);
|
|
23113
|
+
if (proven) return proven;
|
|
23114
|
+
if (includesKind(caps.preDeny, kind)) {
|
|
23115
|
+
return {
|
|
23116
|
+
id,
|
|
23117
|
+
state: "PARTIAL",
|
|
23118
|
+
evidence: {
|
|
23119
|
+
source: "adapter-declaration",
|
|
23120
|
+
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`
|
|
23121
|
+
}
|
|
23122
|
+
};
|
|
23123
|
+
}
|
|
23124
|
+
const note = find(caps.unsupported, PRE_DENY_UNPROVEN_RE);
|
|
23125
|
+
if (note) return { id, state: "UNPROVEN", evidence: { source: "adapter-unsupported", detail: note } };
|
|
23126
|
+
return {
|
|
23127
|
+
id,
|
|
23128
|
+
state: "UNPROVEN",
|
|
23129
|
+
evidence: {
|
|
23130
|
+
source: "not-declared",
|
|
23131
|
+
detail: `operation kind '${kind}' (${kindNote}) is absent from the declared preDeny set and unnamed in unsupported`
|
|
23132
|
+
}
|
|
23133
|
+
};
|
|
23134
|
+
}
|
|
23135
|
+
function assessTransport(caps, id, index, evidence) {
|
|
23136
|
+
const observed = assessFromEvidence(id, index, evidence);
|
|
23137
|
+
if (observed) return observed;
|
|
23138
|
+
const kw = TRANSPORT_KEYWORDS[id];
|
|
23139
|
+
const failOpen = caps.unsupported.find((u) => FAIL_OPEN_RE.test(u) && kw.test(u));
|
|
23140
|
+
if (failOpen) return { id, state: "FAIL-OPEN", evidence: { source: "adapter-unsupported", detail: failOpen } };
|
|
23141
|
+
const unproven = find(caps.unsupported, TRANSPORT_UNPROVEN_RE);
|
|
23142
|
+
if (unproven) return { id, state: "UNPROVEN", evidence: { source: "adapter-unsupported", detail: unproven } };
|
|
23143
|
+
return {
|
|
23144
|
+
id,
|
|
23145
|
+
state: "PARTIAL",
|
|
23146
|
+
evidence: {
|
|
23147
|
+
source: "contract",
|
|
23148
|
+
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"
|
|
23149
|
+
}
|
|
23150
|
+
};
|
|
23151
|
+
}
|
|
23152
|
+
function assessCapabilities(caps, evidence) {
|
|
23153
|
+
const out3 = [];
|
|
23154
|
+
const ev = indexEvidence(evidence);
|
|
23155
|
+
const fromEvidence = (id) => assessFromEvidence(id, ev, evidence);
|
|
23156
|
+
out3.push(assessPreDeny(caps, "pre-deny:shell", "shell", "a shell/exec tool call", ev, evidence));
|
|
23157
|
+
out3.push(assessPreDeny(caps, "pre-deny:native-edit", "file-edit", "a native file-edit tool", ev, evidence));
|
|
23158
|
+
out3.push(assessPreDeny(caps, "pre-deny:delete", "file-edit", "a delete proposed as a file-edit/native tool", ev, evidence));
|
|
23159
|
+
out3.push(assessPreDeny(caps, "pre-deny:rename", "file-edit", "a rename proposed as a file-edit/native tool", ev, evidence));
|
|
23160
|
+
out3.push(assessPreDeny(caps, "pre-deny:git-mutation", "shell", "a git mutation proposed as a shell command", ev, evidence));
|
|
23161
|
+
out3.push(assessPreDeny(caps, "pre-deny:mcp", "mcp", "an MCP tool call", ev, evidence));
|
|
23162
|
+
const postObserveEvidence = fromEvidence("post-observe");
|
|
23163
|
+
if (postObserveEvidence) {
|
|
23164
|
+
out3.push(postObserveEvidence);
|
|
23165
|
+
} else if (caps.postObserve.length > 0) {
|
|
23166
|
+
out3.push({
|
|
23167
|
+
id: "post-observe",
|
|
23168
|
+
state: "PARTIAL",
|
|
23169
|
+
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` }
|
|
23170
|
+
});
|
|
23171
|
+
} else {
|
|
23172
|
+
const note = find(caps.unsupported, POST_ACTION_RE);
|
|
23173
|
+
out3.push(
|
|
23174
|
+
note ? { id: "post-observe", state: "UNSUPPORTED", evidence: { source: "adapter-unsupported", detail: note } } : {
|
|
23175
|
+
id: "post-observe",
|
|
23176
|
+
state: "UNPROVEN",
|
|
23177
|
+
evidence: { source: "not-declared", detail: "no post-action observation kind is declared and none is named unsupported" }
|
|
23178
|
+
}
|
|
23179
|
+
);
|
|
23180
|
+
}
|
|
23181
|
+
const endOfTurnEvidence = fromEvidence("end-of-turn");
|
|
23182
|
+
if (endOfTurnEvidence) {
|
|
23183
|
+
out3.push(endOfTurnEvidence);
|
|
23184
|
+
} else if (!caps.endOfTurn) {
|
|
23185
|
+
out3.push({ id: "end-of-turn", state: "UNSUPPORTED", evidence: { source: "adapter-declaration", detail: "endOfTurn is declared false" } });
|
|
23186
|
+
} else {
|
|
23187
|
+
const caveat = find(caps.unsupported, LIFECYCLE_ONLY_RE);
|
|
23188
|
+
out3.push(
|
|
23189
|
+
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" } }
|
|
23190
|
+
);
|
|
23191
|
+
}
|
|
23192
|
+
const hasDenyPath = caps.endOfTurn || caps.preDeny.length > 0;
|
|
23193
|
+
out3.push(
|
|
23194
|
+
hasDenyPath ? {
|
|
23195
|
+
id: "denial-reason-delivery",
|
|
23196
|
+
state: "PARTIAL",
|
|
23197
|
+
evidence: {
|
|
23198
|
+
source: "contract",
|
|
23199
|
+
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"
|
|
23200
|
+
}
|
|
23201
|
+
} : {
|
|
23202
|
+
id: "denial-reason-delivery",
|
|
23203
|
+
state: "UNPROVEN",
|
|
23204
|
+
evidence: { source: "not-declared", detail: "the adapter declares no pre-deny or end-of-turn deny path, so no reason is delivered" }
|
|
23205
|
+
}
|
|
23206
|
+
);
|
|
23207
|
+
out3.push({
|
|
23208
|
+
id: "continue-after-denial",
|
|
23209
|
+
state: "UNPROVEN",
|
|
23210
|
+
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" }
|
|
23211
|
+
});
|
|
23212
|
+
for (const id of ["transport:missing-executable", "transport:non-zero", "transport:timeout", "transport:malformed", "transport:empty"]) {
|
|
23213
|
+
out3.push(assessTransport(caps, id, ev, evidence));
|
|
23214
|
+
}
|
|
23215
|
+
out3.push(
|
|
23216
|
+
fromEvidence("hook-not-invoked") ?? {
|
|
23217
|
+
id: "hook-not-invoked",
|
|
23218
|
+
state: "PARTIAL",
|
|
23219
|
+
evidence: {
|
|
23220
|
+
source: "contract",
|
|
23221
|
+
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"
|
|
23222
|
+
}
|
|
23223
|
+
}
|
|
23224
|
+
);
|
|
23225
|
+
out3.push(
|
|
23226
|
+
caps.endOfTurn ? {
|
|
23227
|
+
id: "detached/quiescence",
|
|
23228
|
+
state: "PARTIAL",
|
|
23229
|
+
evidence: {
|
|
23230
|
+
source: "contract",
|
|
23231
|
+
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"
|
|
23232
|
+
}
|
|
23233
|
+
} : {
|
|
23234
|
+
id: "detached/quiescence",
|
|
23235
|
+
state: "UNPROVEN",
|
|
23236
|
+
evidence: { source: "not-declared", detail: "no end-of-turn sweep is declared, so a detached/deferred effect is not reconciled in-loop" }
|
|
23237
|
+
}
|
|
23238
|
+
);
|
|
23239
|
+
return out3;
|
|
23240
|
+
}
|
|
23241
|
+
function aggregateInLoop(assessments) {
|
|
23242
|
+
const byId = new Map(assessments.map((a) => [a.id, a]));
|
|
23243
|
+
const required = REQUIRED_IN_LOOP_IDS.map((id) => byId.get(id)).filter((a) => a !== void 0);
|
|
23244
|
+
const anyDangerous = required.some((a) => a.state === "FAIL-OPEN" || a.state === "INCONCLUSIVE");
|
|
23245
|
+
const present = (id) => {
|
|
23246
|
+
const s = byId.get(id)?.state;
|
|
23247
|
+
return s === "PROVEN" || s === "PARTIAL";
|
|
23248
|
+
};
|
|
23249
|
+
const anyPreDenyPresent = PRE_DENY_IDS.some(present);
|
|
23250
|
+
const endOfTurnPresent = present("end-of-turn");
|
|
23251
|
+
if (!anyPreDenyPresent && !endOfTurnPresent) return "NONE";
|
|
23252
|
+
if (!anyDangerous && required.every((a) => a.state === "PROVEN")) return "FULL";
|
|
23253
|
+
return "PARTIAL";
|
|
23254
|
+
}
|
|
23255
|
+
function sha16(s) {
|
|
23256
|
+
return createHash17("sha256").update(s).digest("hex").slice(0, 16);
|
|
23257
|
+
}
|
|
23258
|
+
function capabilityHash(caps) {
|
|
23259
|
+
const stable = JSON.stringify({
|
|
23260
|
+
preDeny: [...caps.preDeny].sort(),
|
|
23261
|
+
postObserve: [...caps.postObserve].sort(),
|
|
23262
|
+
endOfTurn: caps.endOfTurn,
|
|
23263
|
+
unsupported: [...caps.unsupported].sort()
|
|
23264
|
+
});
|
|
23265
|
+
return sha16(stable);
|
|
23266
|
+
}
|
|
23267
|
+
function loadBearing(b) {
|
|
23268
|
+
return {
|
|
23269
|
+
"runtime.id": b.runtime.id,
|
|
23270
|
+
"runtime.version": b.runtime.version ?? "\u2205",
|
|
23271
|
+
"tamperward.version": b.tamperward.version,
|
|
23272
|
+
"tamperward.commit": b.tamperward.commit ?? "\u2205",
|
|
23273
|
+
"adapter.capability_hash": b.adapter.capability_hash,
|
|
23274
|
+
"hook_config_hash": b.hook_config_hash ?? "\u2205",
|
|
23275
|
+
"execution_mode": b.execution_mode,
|
|
23276
|
+
"platform": b.platform,
|
|
23277
|
+
"model": b.model ?? "\u2205",
|
|
23278
|
+
"tested_capabilities": [...b.tested_capabilities].sort().join(",")
|
|
23279
|
+
};
|
|
23280
|
+
}
|
|
23281
|
+
function qualificationStaleness(stored, current) {
|
|
23282
|
+
const a = loadBearing(stored);
|
|
23283
|
+
const b = loadBearing(current);
|
|
23284
|
+
const changed = [];
|
|
23285
|
+
for (const key2 of Object.keys(b)) {
|
|
23286
|
+
if (a[key2] !== b[key2]) changed.push(`${key2}: ${a[key2]} \u2192 ${b[key2]}`);
|
|
23287
|
+
}
|
|
23288
|
+
return { stale: changed.length > 0, changed };
|
|
23289
|
+
}
|
|
23290
|
+
function evidenceId(binding, assessments) {
|
|
23291
|
+
const projection = {
|
|
23292
|
+
load_bearing: loadBearing(binding),
|
|
23293
|
+
states: assessments.map((a) => [a.id, a.state])
|
|
23294
|
+
};
|
|
23295
|
+
return sha16(JSON.stringify(projection));
|
|
23296
|
+
}
|
|
23297
|
+
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;
|
|
23298
|
+
var init_runtime_qualification = __esm({
|
|
23299
|
+
"src/runtime-qualification.ts"() {
|
|
23300
|
+
"use strict";
|
|
23301
|
+
CAPABILITY_STATES = [
|
|
23302
|
+
"PROVEN",
|
|
23303
|
+
"PARTIAL",
|
|
23304
|
+
"UNPROVEN",
|
|
23305
|
+
"UNSUPPORTED",
|
|
23306
|
+
"FAIL-OPEN",
|
|
23307
|
+
"INCONCLUSIVE"
|
|
23308
|
+
];
|
|
23309
|
+
RUNTIME_CAPABILITY_IDS = [
|
|
23310
|
+
"pre-deny:shell",
|
|
23311
|
+
"pre-deny:native-edit",
|
|
23312
|
+
"pre-deny:delete",
|
|
23313
|
+
"pre-deny:rename",
|
|
23314
|
+
"pre-deny:git-mutation",
|
|
23315
|
+
"pre-deny:mcp",
|
|
23316
|
+
"post-observe",
|
|
23317
|
+
"end-of-turn",
|
|
23318
|
+
"denial-reason-delivery",
|
|
23319
|
+
"continue-after-denial",
|
|
23320
|
+
"transport:missing-executable",
|
|
23321
|
+
"transport:non-zero",
|
|
23322
|
+
"transport:timeout",
|
|
23323
|
+
"transport:malformed",
|
|
23324
|
+
"transport:empty",
|
|
23325
|
+
"hook-not-invoked",
|
|
23326
|
+
"detached/quiescence"
|
|
23327
|
+
];
|
|
23328
|
+
EVIDENCE_SOURCES = [
|
|
23329
|
+
"adapter-declaration",
|
|
23330
|
+
// structural preDeny / postObserve / endOfTurn membership
|
|
23331
|
+
"adapter-unsupported",
|
|
23332
|
+
// named verbatim in the adapter's `unsupported[]` prose
|
|
23333
|
+
"contract",
|
|
23334
|
+
// the neutral steering contract guarantees this at the adapter boundary
|
|
23335
|
+
"committed-evidence",
|
|
23336
|
+
// a committed real-runtime evidence record proved it
|
|
23337
|
+
"not-declared"
|
|
23338
|
+
// absent from every declaration — no proof either way
|
|
23339
|
+
];
|
|
23340
|
+
FINAL_AUTHORITY = "AVAILABLE";
|
|
23341
|
+
FAIL_OPEN_RE = /fails?\s+open/i;
|
|
23342
|
+
PRE_DENY_UNPROVEN_RE = /pre-action deny|pre-deny|shell pre-deny/i;
|
|
23343
|
+
TRANSPORT_UNPROVEN_RE = /(?:fail-?closed|transport)[^.]*\b(?:not (?:yet )?proven|unproven|unmeasured)/i;
|
|
23344
|
+
LIFECYCLE_ONLY_RE = /lifecycle continuation|not a filesystem veto|only block turn completion|block turn completion and force continuation/i;
|
|
23345
|
+
POST_ACTION_RE = /post-action|post-observe|posttooluse|stop sweep is the post-turn/i;
|
|
23346
|
+
TRANSPORT_KEYWORDS = {
|
|
23347
|
+
"transport:missing-executable": /missing exec|executable|enoent|not found/i,
|
|
23348
|
+
"transport:non-zero": /non-?zero|exit code|exit 2|non-?2xx/i,
|
|
23349
|
+
"transport:timeout": /time-?out|timed[- ]out/i,
|
|
23350
|
+
"transport:malformed": /malformed|parse/i,
|
|
23351
|
+
"transport:empty": /\bempty\b/i
|
|
23352
|
+
};
|
|
23353
|
+
PRE_DENY_IDS = [
|
|
23354
|
+
"pre-deny:shell",
|
|
23355
|
+
"pre-deny:native-edit",
|
|
23356
|
+
"pre-deny:delete",
|
|
23357
|
+
"pre-deny:rename",
|
|
23358
|
+
"pre-deny:git-mutation",
|
|
23359
|
+
"pre-deny:mcp"
|
|
23360
|
+
];
|
|
23361
|
+
REQUIRED_IN_LOOP_IDS = [
|
|
23362
|
+
...PRE_DENY_IDS,
|
|
23363
|
+
"end-of-turn",
|
|
23364
|
+
"transport:missing-executable",
|
|
23365
|
+
"transport:non-zero",
|
|
23366
|
+
"transport:timeout",
|
|
23367
|
+
"transport:malformed",
|
|
23368
|
+
"transport:empty",
|
|
23369
|
+
"hook-not-invoked"
|
|
23370
|
+
];
|
|
23371
|
+
}
|
|
23372
|
+
});
|
|
23373
|
+
|
|
23374
|
+
// src/cli/runtime.ts
|
|
23375
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
23376
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
|
|
23377
|
+
import { join as join30, dirname as dirname15 } from "node:path";
|
|
23378
|
+
function storePath(cwd) {
|
|
23379
|
+
const dir = gitDir(cwd);
|
|
23380
|
+
return dir ? join30(dir, "tamperward", "runtime-qualification.json") : null;
|
|
23381
|
+
}
|
|
23382
|
+
function readStore(cwd) {
|
|
23383
|
+
const path = storePath(cwd);
|
|
23384
|
+
if (!path || !existsSync22(path)) return null;
|
|
23385
|
+
try {
|
|
23386
|
+
const parsed = JSON.parse(readFileSync28(path, "utf8"));
|
|
23387
|
+
if (parsed && typeof parsed === "object" && parsed.records && typeof parsed.records === "object") return parsed;
|
|
23388
|
+
} catch {
|
|
23389
|
+
return null;
|
|
23390
|
+
}
|
|
23391
|
+
return null;
|
|
23392
|
+
}
|
|
23393
|
+
function isRec(v) {
|
|
23394
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23395
|
+
}
|
|
23396
|
+
function asStringOrNull(v) {
|
|
23397
|
+
return v === null || typeof v === "string" ? v : void 0;
|
|
23398
|
+
}
|
|
23399
|
+
function validateStoredReport(value) {
|
|
23400
|
+
if (!isRec(value)) return { reason: "record is not an object" };
|
|
23401
|
+
if (value.schema_version !== 1) return { reason: "unrecognized schema_version" };
|
|
23402
|
+
if (value.command !== "runtime") return { reason: 'command is not "runtime"' };
|
|
23403
|
+
const subcommand = value.subcommand;
|
|
23404
|
+
if (subcommand !== "verify" && subcommand !== "status") return { reason: "invalid subcommand" };
|
|
23405
|
+
const recorded = value.recorded;
|
|
23406
|
+
const stale = value.stale;
|
|
23407
|
+
if (typeof recorded !== "boolean" || typeof stale !== "boolean") return { reason: "invalid recorded/stale flags" };
|
|
23408
|
+
const changedInputs = value.changed_inputs;
|
|
23409
|
+
if (!Array.isArray(changedInputs) || !changedInputs.every((s) => typeof s === "string")) return { reason: "invalid changed_inputs" };
|
|
23410
|
+
const runtime = value.runtime;
|
|
23411
|
+
if (!isRec(runtime)) return { reason: "invalid runtime binding" };
|
|
23412
|
+
const rId = runtime.id;
|
|
23413
|
+
const rLabel = runtime.label;
|
|
23414
|
+
const rVersion = asStringOrNull(runtime.version);
|
|
23415
|
+
if (typeof rId !== "string" || !rId || typeof rLabel !== "string" || !rLabel || rVersion === void 0)
|
|
23416
|
+
return { reason: "invalid runtime binding" };
|
|
23417
|
+
const tw = value.tamperward;
|
|
23418
|
+
if (!isRec(tw)) return { reason: "invalid tamperward binding" };
|
|
23419
|
+
const twVersion = tw.version;
|
|
23420
|
+
const twCommit = asStringOrNull(tw.commit);
|
|
23421
|
+
if (typeof twVersion !== "string" || !twVersion || twCommit === void 0) return { reason: "invalid tamperward binding" };
|
|
23422
|
+
const adapter = value.adapter;
|
|
23423
|
+
if (!isRec(adapter)) return { reason: "invalid adapter binding" };
|
|
23424
|
+
const adName = adapter.name;
|
|
23425
|
+
const adHash = adapter.capability_hash;
|
|
23426
|
+
if (typeof adName !== "string" || !adName || typeof adHash !== "string" || !adHash) return { reason: "invalid adapter binding" };
|
|
23427
|
+
const hookConfigHash = asStringOrNull(value.hook_config_hash);
|
|
23428
|
+
if (hookConfigHash === void 0) return { reason: "invalid hook_config_hash" };
|
|
23429
|
+
const executionMode = value.execution_mode;
|
|
23430
|
+
if (executionMode !== "headless" && executionMode !== "interactive") return { reason: "invalid execution_mode" };
|
|
23431
|
+
const platform = value.platform;
|
|
23432
|
+
if (typeof platform !== "string" || !platform) return { reason: "invalid platform" };
|
|
23433
|
+
const model = asStringOrNull(value.model);
|
|
23434
|
+
if (model === void 0) return { reason: "invalid model" };
|
|
23435
|
+
const testedCapabilities = value.tested_capabilities;
|
|
23436
|
+
if (!Array.isArray(testedCapabilities) || !testedCapabilities.every((c) => typeof c === "string" && CAP_ID_SET.has(c)))
|
|
23437
|
+
return { reason: "invalid tested_capabilities" };
|
|
23438
|
+
const timestamp = value.timestamp;
|
|
23439
|
+
if (typeof timestamp !== "string" || !timestamp) return { reason: "invalid timestamp" };
|
|
23440
|
+
const storedEvidenceId = value.evidence_id;
|
|
23441
|
+
if (typeof storedEvidenceId !== "string" || !storedEvidenceId) return { reason: "invalid evidence_id" };
|
|
23442
|
+
if (!Array.isArray(value.capabilities)) return { reason: "capabilities is not an array" };
|
|
23443
|
+
const capabilities = [];
|
|
23444
|
+
for (const c of value.capabilities) {
|
|
23445
|
+
if (!isRec(c)) return { reason: "invalid capability entry" };
|
|
23446
|
+
const id = RUNTIME_CAPABILITY_IDS.find((x) => x === c.id);
|
|
23447
|
+
const state = CAPABILITY_STATES.find((x) => x === c.state);
|
|
23448
|
+
if (!id || !state) return { reason: "invalid capability entry" };
|
|
23449
|
+
const ev = c.evidence;
|
|
23450
|
+
if (!isRec(ev)) return { reason: "invalid capability evidence" };
|
|
23451
|
+
const source = EVIDENCE_SOURCES.find((x) => x === ev.source);
|
|
23452
|
+
const detail = ev.detail;
|
|
23453
|
+
if (!source || typeof detail !== "string") return { reason: "invalid capability evidence" };
|
|
23454
|
+
capabilities.push({ id, state, evidence: { source, detail } });
|
|
23455
|
+
}
|
|
23456
|
+
const inLoop = value.in_loop_protection;
|
|
23457
|
+
if (inLoop !== "FULL" && inLoop !== "PARTIAL" && inLoop !== "NONE") return { reason: "invalid in_loop_protection" };
|
|
23458
|
+
if (value.final_authority !== FINAL_AUTHORITY) return { reason: "invalid final_authority" };
|
|
23459
|
+
const note = value.note;
|
|
23460
|
+
if (typeof note !== "string") return { reason: "invalid note" };
|
|
23461
|
+
const base = {
|
|
23462
|
+
runtime: { id: rId, label: rLabel, version: rVersion },
|
|
23463
|
+
tamperward: { version: twVersion, commit: twCommit },
|
|
23464
|
+
adapter: { name: adName, capability_hash: adHash },
|
|
23465
|
+
hook_config_hash: hookConfigHash,
|
|
23466
|
+
execution_mode: executionMode,
|
|
23467
|
+
platform,
|
|
23468
|
+
model,
|
|
23469
|
+
tested_capabilities: testedCapabilities
|
|
23470
|
+
};
|
|
23471
|
+
const recomputedId = evidenceId(base, capabilities);
|
|
23472
|
+
if (recomputedId !== storedEvidenceId) return { reason: `evidence_id mismatch (recorded ${storedEvidenceId}, recomputed ${recomputedId})` };
|
|
23473
|
+
const recomputedAgg = aggregateInLoop(capabilities);
|
|
23474
|
+
if (inLoop !== recomputedAgg) return { reason: `in_loop_protection mismatch (recorded ${inLoop}, recomputed ${recomputedAgg})` };
|
|
23475
|
+
const report2 = {
|
|
23476
|
+
schema_version: 1,
|
|
23477
|
+
command: "runtime",
|
|
23478
|
+
subcommand,
|
|
23479
|
+
recorded,
|
|
23480
|
+
stale,
|
|
23481
|
+
changed_inputs: changedInputs,
|
|
23482
|
+
runtime: base.runtime,
|
|
23483
|
+
tamperward: base.tamperward,
|
|
23484
|
+
adapter: base.adapter,
|
|
23485
|
+
hook_config_hash: hookConfigHash,
|
|
23486
|
+
execution_mode: executionMode,
|
|
23487
|
+
platform,
|
|
23488
|
+
model,
|
|
23489
|
+
tested_capabilities: testedCapabilities,
|
|
23490
|
+
timestamp,
|
|
23491
|
+
evidence_id: storedEvidenceId,
|
|
23492
|
+
capabilities,
|
|
23493
|
+
in_loop_protection: inLoop,
|
|
23494
|
+
final_authority: FINAL_AUTHORITY,
|
|
23495
|
+
note
|
|
23496
|
+
};
|
|
23497
|
+
return { report: report2 };
|
|
23498
|
+
}
|
|
23499
|
+
function writeRecord(cwd, id, report2) {
|
|
23500
|
+
const path = storePath(cwd);
|
|
23501
|
+
if (!path) return false;
|
|
23502
|
+
const existing = readStore(cwd);
|
|
23503
|
+
const store = existing ?? { schema_version: report2.schema_version, updated_at: "", records: {} };
|
|
23504
|
+
store.records[id] = report2;
|
|
23505
|
+
store.updated_at = report2.timestamp;
|
|
23506
|
+
try {
|
|
23507
|
+
mkdirSync13(dirname15(path), { recursive: true });
|
|
23508
|
+
writeFileSync16(path, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
23509
|
+
return true;
|
|
23510
|
+
} catch {
|
|
23511
|
+
return false;
|
|
23512
|
+
}
|
|
23513
|
+
}
|
|
23514
|
+
function resolveRuntimeVersion(canonicalId) {
|
|
23515
|
+
const override = process.env.TAMPERWARD_RUNTIME_VERSION;
|
|
23516
|
+
if (override && override.trim()) return override.trim();
|
|
23517
|
+
const bins = {
|
|
23518
|
+
"claude-code": "claude",
|
|
23519
|
+
codex: "codex",
|
|
23520
|
+
"github-copilot-cli": "copilot"
|
|
23521
|
+
};
|
|
23522
|
+
const bin = bins[canonicalId];
|
|
23523
|
+
if (!bin) return null;
|
|
23524
|
+
try {
|
|
23525
|
+
const out3 = execFileSync16(bin, ["--version"], { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "ignore"] });
|
|
23526
|
+
const m = out3.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
23527
|
+
return m ? m[0] : out3.trim() || null;
|
|
23528
|
+
} catch {
|
|
23529
|
+
return null;
|
|
23530
|
+
}
|
|
23531
|
+
}
|
|
23532
|
+
function tamperwardBuildTag(version, commit) {
|
|
23533
|
+
return commit ? `${version}+${commit.slice(0, 8)}` : version;
|
|
23534
|
+
}
|
|
23535
|
+
function resolveComponentVersions(_canonicalId) {
|
|
23536
|
+
return [];
|
|
23537
|
+
}
|
|
23538
|
+
function resolveHookConfigHash(canonicalId, cwd) {
|
|
23539
|
+
if (canonicalId === "claude-code") {
|
|
23540
|
+
return sha16(JSON.stringify(interventionWiring(cwd)));
|
|
23541
|
+
}
|
|
23542
|
+
const files = {
|
|
23543
|
+
"github-copilot-cli": join30(cwd, ".github", "hooks", "tamperward.json")
|
|
23544
|
+
};
|
|
23545
|
+
const file = files[canonicalId];
|
|
23546
|
+
if (!file || !existsSync22(file)) return null;
|
|
23547
|
+
try {
|
|
23548
|
+
const raw = readFileSync28(file, "utf8");
|
|
23549
|
+
let hooks = raw;
|
|
23550
|
+
try {
|
|
23551
|
+
const parsed = JSON.parse(raw);
|
|
23552
|
+
hooks = parsed && typeof parsed === "object" && "hooks" in parsed ? parsed.hooks : parsed;
|
|
23553
|
+
} catch {
|
|
23554
|
+
}
|
|
23555
|
+
return sha16(JSON.stringify(hooks ?? null));
|
|
23556
|
+
} catch {
|
|
23557
|
+
return null;
|
|
23558
|
+
}
|
|
23559
|
+
}
|
|
23560
|
+
function buildQualification(adapter, opts, cwd) {
|
|
23561
|
+
const canonical2 = adapter.name;
|
|
23562
|
+
const capHash = capabilityHash(adapter.capabilities);
|
|
23563
|
+
const mode = opts.mode ?? "headless";
|
|
23564
|
+
const version = resolveRuntimeVersion(canonical2);
|
|
23565
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
23566
|
+
const model = opts.model ?? null;
|
|
23567
|
+
const hookConfigHash = resolveHookConfigHash(canonical2, cwd);
|
|
23568
|
+
const commit = TW_COMMIT;
|
|
23569
|
+
const testedRaw = [...RUNTIME_CAPABILITY_IDS];
|
|
23570
|
+
const matchKey = {
|
|
23571
|
+
runtime_id: canonical2,
|
|
23572
|
+
runtime_version: version,
|
|
23573
|
+
// No live resolver for a runtime's SDK/protocol component versions ships yet, so the
|
|
23574
|
+
// current side is honestly the empty (unresolved) set. An unresolved component set never
|
|
23575
|
+
// matches a probed non-empty one — it fails closed, never promotes. We do not fabricate one.
|
|
23576
|
+
component_versions: resolveComponentVersions(canonical2),
|
|
23577
|
+
tamperward_version: tamperwardBuildTag(TW_VERSION, commit),
|
|
23578
|
+
adapter_capability_hash: capHash,
|
|
23579
|
+
tested_capabilities: testedRaw,
|
|
23580
|
+
model,
|
|
23581
|
+
platform,
|
|
23582
|
+
execution_mode: mode,
|
|
23583
|
+
hook_config_hash: hookConfigHash
|
|
23584
|
+
};
|
|
23585
|
+
const evidence = matchRetainedEvidence(matchKey);
|
|
23586
|
+
const assessments = assessCapabilities(adapter.capabilities, evidence);
|
|
23587
|
+
const base = {
|
|
23588
|
+
runtime: { id: canonical2, label: labelFor(canonical2), version },
|
|
23589
|
+
tamperward: { version: TW_VERSION, commit },
|
|
23590
|
+
adapter: { name: canonical2, capability_hash: capHash },
|
|
23591
|
+
hook_config_hash: hookConfigHash,
|
|
23592
|
+
execution_mode: mode,
|
|
23593
|
+
platform,
|
|
23594
|
+
model,
|
|
23595
|
+
tested_capabilities: testedRaw
|
|
23596
|
+
};
|
|
23597
|
+
const eid = evidenceId(base, assessments);
|
|
23598
|
+
const binding = { ...base, timestamp: (/* @__PURE__ */ new Date()).toISOString(), evidence_id: eid };
|
|
23599
|
+
return { binding, assessments };
|
|
23600
|
+
}
|
|
23601
|
+
function reportFrom(subcommand, binding, assessments) {
|
|
23602
|
+
return machineOutput({
|
|
23603
|
+
command: "runtime",
|
|
23604
|
+
subcommand,
|
|
23605
|
+
recorded: true,
|
|
23606
|
+
stale: false,
|
|
23607
|
+
changed_inputs: [],
|
|
23608
|
+
runtime: binding.runtime,
|
|
23609
|
+
tamperward: binding.tamperward,
|
|
23610
|
+
adapter: binding.adapter,
|
|
23611
|
+
hook_config_hash: binding.hook_config_hash,
|
|
23612
|
+
execution_mode: binding.execution_mode,
|
|
23613
|
+
platform: binding.platform,
|
|
23614
|
+
model: binding.model,
|
|
23615
|
+
tested_capabilities: binding.tested_capabilities,
|
|
23616
|
+
timestamp: binding.timestamp,
|
|
23617
|
+
evidence_id: binding.evidence_id,
|
|
23618
|
+
capabilities: assessments,
|
|
23619
|
+
in_loop_protection: aggregateInLoop(assessments),
|
|
23620
|
+
final_authority: FINAL_AUTHORITY,
|
|
23621
|
+
note: HONESTY_NOTE
|
|
23622
|
+
});
|
|
23623
|
+
}
|
|
23624
|
+
function resolveTarget(opts, cwd) {
|
|
23625
|
+
if (opts.runtime) {
|
|
23626
|
+
const a = adapterFor(opts.runtime);
|
|
23627
|
+
return a ?? { error: `no shipped runtime adapter for "${opts.runtime}"` };
|
|
23628
|
+
}
|
|
23629
|
+
const detected = detectRuntimes(cwd);
|
|
23630
|
+
for (const rt of detected) {
|
|
23631
|
+
const a = adapterFor(rt.id);
|
|
23632
|
+
if (a) return a;
|
|
23633
|
+
}
|
|
23634
|
+
const reason = detected.length > 0 ? "no adapter-backed runtime detected in this repository" : "no runtime detected in this repository";
|
|
23635
|
+
return { error: `${reason}; pass --runtime <id> to qualify a specific runtime` };
|
|
23636
|
+
}
|
|
23637
|
+
function renderText3(report2, cwd) {
|
|
23638
|
+
const colour = colourEnabled(process.env, process.stdout);
|
|
23639
|
+
const w = (s) => process.stdout.write(s + "\n");
|
|
23640
|
+
const dim = (s) => paint(s, DIM, colour);
|
|
23641
|
+
w(`${paint(report2.runtime.label, BOLD, colour)} \u2014 runtime protection qualification`);
|
|
23642
|
+
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}`));
|
|
23643
|
+
w(dim(` evidence ${report2.evidence_id} \xB7 ${report2.timestamp}`));
|
|
23644
|
+
if (!report2.recorded) {
|
|
23645
|
+
w("");
|
|
23646
|
+
w(`${paint("[UNQUALIFIED]", severityColour("bad") + BOLD, colour)} no qualification recorded for this runtime.`);
|
|
23647
|
+
if (report2.note) w(dim(` ${report2.note.replace(/—/g, "-")}`));
|
|
23648
|
+
w(`Run: ${paint("tamperward runtime verify", BOLD, colour)}`);
|
|
23649
|
+
return;
|
|
23650
|
+
}
|
|
23651
|
+
if (report2.stale) {
|
|
23652
|
+
w("");
|
|
23653
|
+
w(`${paint("[STALE]", severityColour("bad") + BOLD, colour)} a load-bearing input changed \u2014 this qualification no longer applies:`);
|
|
23654
|
+
for (const c of report2.changed_inputs) w(` \xB7 ${c}`);
|
|
23655
|
+
w(`Run: ${paint("tamperward runtime verify", BOLD, colour)}`);
|
|
23656
|
+
}
|
|
23657
|
+
w("");
|
|
23658
|
+
const width = Math.max(...report2.capabilities.map((c) => c.id.length));
|
|
23659
|
+
for (const cap of report2.capabilities) {
|
|
23660
|
+
const sev = STATE_SEVERITY[cap.state];
|
|
23661
|
+
const tag = paint(cap.state.padEnd(12), (sev === "bad" ? BOLD : "") + severityColour(sev), colour);
|
|
23662
|
+
w(` ${cap.id.padEnd(width)} ${tag}`);
|
|
23663
|
+
}
|
|
23664
|
+
w("");
|
|
23665
|
+
const agg = report2.in_loop_protection;
|
|
23666
|
+
const aggTag = paint(agg, (AGGREGATE_SEVERITY[agg] === "bad" ? BOLD : "") + severityColour(AGGREGATE_SEVERITY[agg]), colour);
|
|
23667
|
+
w(` ${"In-loop protection".padEnd(width)} ${aggTag}`);
|
|
23668
|
+
w(` ${"Final authority".padEnd(width)} ${paint(report2.final_authority, severityColour("ok"), colour)}`);
|
|
23669
|
+
w("");
|
|
23670
|
+
w(dim(" Final authority (CI / pristine verification) is independent of the runtime hook: a weak in-loop"));
|
|
23671
|
+
w(dim(" capability never weakens adjudication. Steering and authority remain separate."));
|
|
23672
|
+
w("");
|
|
23673
|
+
w(dim(" " + report2.note.replace(/—/g, "-")));
|
|
23674
|
+
w("");
|
|
23675
|
+
w(dim(" Evidence:"));
|
|
23676
|
+
for (const cap of report2.capabilities) {
|
|
23677
|
+
w(dim(` ${cap.id} [${cap.state}] (${cap.evidence.source}): ${cap.evidence.detail}`));
|
|
23678
|
+
}
|
|
23679
|
+
}
|
|
23680
|
+
function emit2(report2, opts, cwd) {
|
|
23681
|
+
if (opts.json) {
|
|
23682
|
+
process.stdout.write(JSON.stringify(report2) + "\n");
|
|
23683
|
+
return;
|
|
23684
|
+
}
|
|
23685
|
+
renderText3(report2, cwd);
|
|
23686
|
+
}
|
|
23687
|
+
function runRuntime(sub, opts) {
|
|
23688
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
23689
|
+
if (sub === "verify") {
|
|
23690
|
+
const target = resolveTarget(opts, cwd);
|
|
23691
|
+
if ("error" in target) {
|
|
23692
|
+
process.stderr.write(`tamperward: ${target.error}
|
|
23693
|
+
`);
|
|
23694
|
+
return 2;
|
|
23695
|
+
}
|
|
23696
|
+
const { binding, assessments } = buildQualification(target, opts, cwd);
|
|
23697
|
+
const report2 = reportFrom("verify", binding, assessments);
|
|
23698
|
+
const wrote = writeRecord(cwd, target.name, report2);
|
|
23699
|
+
if (!wrote) {
|
|
23700
|
+
const dest = storePath(cwd);
|
|
23701
|
+
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`;
|
|
23702
|
+
const failure = {
|
|
23703
|
+
...report2,
|
|
23704
|
+
recorded: false,
|
|
23705
|
+
capabilities: [],
|
|
23706
|
+
in_loop_protection: "NONE",
|
|
23707
|
+
note: reason
|
|
23708
|
+
};
|
|
23709
|
+
emit2(failure, opts, cwd);
|
|
23710
|
+
process.stderr.write(`tamperward: ${reason}
|
|
23711
|
+
`);
|
|
23712
|
+
return 1;
|
|
23713
|
+
}
|
|
23714
|
+
emit2(report2, opts, cwd);
|
|
23715
|
+
return 0;
|
|
23716
|
+
}
|
|
23717
|
+
if (sub === "status") {
|
|
23718
|
+
const target = resolveTarget(opts, cwd);
|
|
23719
|
+
if ("error" in target) {
|
|
23720
|
+
process.stderr.write(`tamperward: ${target.error}
|
|
23721
|
+
`);
|
|
23722
|
+
return 2;
|
|
23723
|
+
}
|
|
23724
|
+
const store = readStore(cwd);
|
|
23725
|
+
const raw = store?.records[target.name];
|
|
23726
|
+
const validated = raw === void 0 ? { reason: "none recorded" } : validateStoredReport(raw);
|
|
23727
|
+
if ("reason" in validated) {
|
|
23728
|
+
const { binding, assessments } = buildQualification(target, opts, cwd);
|
|
23729
|
+
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`;
|
|
23730
|
+
const empty2 = {
|
|
23731
|
+
...reportFrom("status", binding, assessments),
|
|
23732
|
+
recorded: false,
|
|
23733
|
+
capabilities: [],
|
|
23734
|
+
in_loop_protection: "NONE",
|
|
23735
|
+
note
|
|
23736
|
+
};
|
|
23737
|
+
emit2(empty2, opts, cwd);
|
|
23738
|
+
return 0;
|
|
23739
|
+
}
|
|
23740
|
+
const stored = validated.report;
|
|
23741
|
+
const { binding: current } = buildQualification(target, opts, cwd);
|
|
23742
|
+
const storedBinding = {
|
|
23743
|
+
runtime: stored.runtime,
|
|
23744
|
+
tamperward: stored.tamperward,
|
|
23745
|
+
adapter: stored.adapter,
|
|
23746
|
+
hook_config_hash: stored.hook_config_hash,
|
|
23747
|
+
execution_mode: stored.execution_mode,
|
|
23748
|
+
platform: stored.platform,
|
|
23749
|
+
model: stored.model,
|
|
23750
|
+
tested_capabilities: stored.tested_capabilities,
|
|
23751
|
+
timestamp: stored.timestamp,
|
|
23752
|
+
evidence_id: stored.evidence_id
|
|
23753
|
+
};
|
|
23754
|
+
const staleness = qualificationStaleness(storedBinding, current);
|
|
23755
|
+
const report2 = {
|
|
23756
|
+
...stored,
|
|
23757
|
+
subcommand: "status",
|
|
23758
|
+
stale: staleness.stale,
|
|
23759
|
+
changed_inputs: staleness.changed
|
|
23760
|
+
};
|
|
23761
|
+
emit2(report2, opts, cwd);
|
|
23762
|
+
return 0;
|
|
23763
|
+
}
|
|
23764
|
+
process.stderr.write(`tamperward: runtime requires one of verify | status (got "${sub ?? ""}")
|
|
23765
|
+
`);
|
|
23766
|
+
return 2;
|
|
23767
|
+
}
|
|
23768
|
+
function parseRuntime(args) {
|
|
23769
|
+
const [sub, ...rest] = args;
|
|
23770
|
+
const opts = {};
|
|
23771
|
+
for (let i = 0; i < rest.length; i++) {
|
|
23772
|
+
const a = rest[i];
|
|
23773
|
+
if (a === "--cwd") opts.cwd = rest[++i];
|
|
23774
|
+
else if (a === "--runtime") opts.runtime = rest[++i];
|
|
23775
|
+
else if (a === "--model") opts.model = rest[++i];
|
|
23776
|
+
else if (a === "--json") opts.json = true;
|
|
23777
|
+
else if (a === "--mode") {
|
|
23778
|
+
const v = rest[++i];
|
|
23779
|
+
if (v === "headless" || v === "interactive") opts.mode = v;
|
|
23780
|
+
}
|
|
23781
|
+
}
|
|
23782
|
+
return { sub, opts };
|
|
23783
|
+
}
|
|
23784
|
+
var HONESTY_NOTE, CAP_ID_SET, STATE_SEVERITY, AGGREGATE_SEVERITY, RUNTIME_SUBCOMMANDS;
|
|
23785
|
+
var init_runtime = __esm({
|
|
23786
|
+
"src/cli/runtime.ts"() {
|
|
23787
|
+
"use strict";
|
|
23788
|
+
init_build();
|
|
23789
|
+
init_wiring();
|
|
23790
|
+
init_machine_output();
|
|
23791
|
+
init_text();
|
|
23792
|
+
init_status();
|
|
23793
|
+
init_registry();
|
|
23794
|
+
init_runtimes();
|
|
23795
|
+
init_evidence();
|
|
23796
|
+
init_verification_state();
|
|
23797
|
+
init_runtime_qualification();
|
|
23798
|
+
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.";
|
|
23799
|
+
CAP_ID_SET = new Set(RUNTIME_CAPABILITY_IDS);
|
|
23800
|
+
STATE_SEVERITY = {
|
|
23801
|
+
PROVEN: "ok",
|
|
23802
|
+
PARTIAL: "warn",
|
|
23803
|
+
UNPROVEN: "warn",
|
|
23804
|
+
UNSUPPORTED: "info",
|
|
23805
|
+
"FAIL-OPEN": "bad",
|
|
23806
|
+
INCONCLUSIVE: "bad"
|
|
23807
|
+
};
|
|
23808
|
+
AGGREGATE_SEVERITY = { FULL: "ok", PARTIAL: "warn", NONE: "bad" };
|
|
23809
|
+
RUNTIME_SUBCOMMANDS = ["verify", "status"];
|
|
23810
|
+
}
|
|
23811
|
+
});
|
|
23812
|
+
|
|
23813
|
+
// src/cli/signoff-label.ts
|
|
23814
|
+
function runSignoffLabel(opts) {
|
|
23815
|
+
if (!opts.rule) {
|
|
23816
|
+
process.stderr.write("tamperward signoff-label --rule <rule> --head <full-head-sha> [--file <path>]\n");
|
|
23817
|
+
return 2;
|
|
23818
|
+
}
|
|
23819
|
+
if (!opts.head) {
|
|
23820
|
+
process.stderr.write("tamperward: --head is required and must be a full 40- or 64-character object id.\n");
|
|
23821
|
+
return 2;
|
|
23822
|
+
}
|
|
23823
|
+
const want = opts.file ? `${opts.rule}:${opts.file}` : opts.rule;
|
|
23824
|
+
const token = compactOobToken(want, opts.head);
|
|
23825
|
+
if (!token) {
|
|
23826
|
+
process.stderr.write("tamperward: --head must be a full 40- or 64-character hexadecimal object id.\n");
|
|
23827
|
+
return 2;
|
|
23828
|
+
}
|
|
23829
|
+
process.stdout.write(`${token}
|
|
23830
|
+
`);
|
|
23831
|
+
return 0;
|
|
23832
|
+
}
|
|
23833
|
+
var init_signoff_label = __esm({
|
|
23834
|
+
"src/cli/signoff-label.ts"() {
|
|
23835
|
+
"use strict";
|
|
23836
|
+
init_signoff();
|
|
23837
|
+
}
|
|
23838
|
+
});
|
|
23839
|
+
|
|
23840
|
+
// src/cli/main.ts
|
|
23841
|
+
var main_exports = {};
|
|
23842
|
+
__export(main_exports, {
|
|
23843
|
+
guardedMain: () => guardedMain,
|
|
23844
|
+
main: () => main,
|
|
23845
|
+
runHookFromRaw: () => runHookFromRaw,
|
|
23846
|
+
validateCliArgs: () => validateCliArgs
|
|
23847
|
+
});
|
|
23848
|
+
function parseAllow(args) {
|
|
23849
|
+
const o = {};
|
|
23850
|
+
for (let i = 0; i < args.length; i++) {
|
|
23851
|
+
const a = args[i];
|
|
23852
|
+
if (a === "--file") o.file = args[++i];
|
|
23853
|
+
else if (a === "--reason") o.reason = args[++i];
|
|
23854
|
+
else if (a === "--cwd") o.cwd = args[++i];
|
|
23855
|
+
else if (!a.startsWith("--") && !o.rule) o.rule = a;
|
|
23856
|
+
}
|
|
23857
|
+
return o;
|
|
23858
|
+
}
|
|
23859
|
+
function runAgentCommand(kind, args) {
|
|
23860
|
+
const agent = args[0];
|
|
23861
|
+
if (agent !== "claude") {
|
|
23862
|
+
process.stderr.write(`tamperward: unsupported ${kind} agent "${agent ?? ""}" (only "claude" so far)
|
|
23863
|
+
`);
|
|
23864
|
+
return 2;
|
|
23865
|
+
}
|
|
23866
|
+
return kind === "hook" ? runHookClaude() : runSweepClaude();
|
|
23867
|
+
}
|
|
23868
|
+
function parseInit(args) {
|
|
23869
|
+
const o = {};
|
|
23870
|
+
for (let i = 0; i < args.length; i++) {
|
|
23871
|
+
const a = args[i];
|
|
23872
|
+
if (a === "--cwd") o.cwd = args[++i];
|
|
23873
|
+
else if (a === "--dry-run") o.dryRun = true;
|
|
23874
|
+
else if (a === "--force-workflow") o.forceWorkflow = true;
|
|
23875
|
+
}
|
|
23876
|
+
return o;
|
|
23877
|
+
}
|
|
23878
|
+
function parseOnboard(args) {
|
|
23879
|
+
const o = {};
|
|
23880
|
+
for (let i = 0; i < args.length; i++) {
|
|
23881
|
+
const a = args[i];
|
|
23882
|
+
if (a === "--cwd") o.cwd = args[++i];
|
|
23883
|
+
else if (a === "--base") o.base = args[++i];
|
|
23884
|
+
else if (a === "--repo") o.repo = args[++i];
|
|
23885
|
+
else if (a === "--branch") o.branch = args[++i];
|
|
23886
|
+
else if (a === "--verify-command") o.verifyCommand = args[++i];
|
|
23887
|
+
else if (a === "--skip-demo") o.skipDemo = true;
|
|
23888
|
+
else if (a === "--demo") o.demo = true;
|
|
23889
|
+
else if (a === "--no-github") o.noGithub = true;
|
|
23890
|
+
else if (a === "--yes") o.yes = true;
|
|
23891
|
+
}
|
|
23892
|
+
return o;
|
|
23893
|
+
}
|
|
23894
|
+
function parseDoctor(args) {
|
|
23895
|
+
const o = {};
|
|
23896
|
+
for (let i = 0; i < args.length; i++) {
|
|
23897
|
+
const a = args[i];
|
|
23898
|
+
if (a === "--cwd") o.cwd = args[++i];
|
|
23899
|
+
else if (a === "--base") o.base = args[++i];
|
|
23900
|
+
else if (a === "--workflow") o.workflow = args[++i];
|
|
23901
|
+
else if (a === "--github") o.github = true;
|
|
23902
|
+
else if (a === "--json") o.json = true;
|
|
23903
|
+
else if (a === "--repo") o.repo = args[++i];
|
|
23904
|
+
else if (a === "--branch") o.branch = args[++i];
|
|
23905
|
+
}
|
|
23906
|
+
return o;
|
|
23907
|
+
}
|
|
23908
|
+
function parseStatus(args) {
|
|
23909
|
+
const o = {};
|
|
23910
|
+
for (let i = 0; i < args.length; i++) {
|
|
23911
|
+
const a = args[i];
|
|
23912
|
+
if (a === "--json") o.json = true;
|
|
23913
|
+
else if (a === "--cwd") o.cwd = args[++i];
|
|
23914
|
+
}
|
|
23915
|
+
return o;
|
|
23916
|
+
}
|
|
23917
|
+
function parseStats(args) {
|
|
23918
|
+
const o = {};
|
|
23919
|
+
for (let i = 0; i < args.length; i++) {
|
|
23920
|
+
const a = args[i];
|
|
23921
|
+
if (a === "--cwd") o.cwd = args[++i];
|
|
23922
|
+
else if (a === "--file") o.file = args[++i];
|
|
23923
|
+
else if (a === "--since") o.since = args[++i];
|
|
23924
|
+
else if (a === "--json") o.json = true;
|
|
23925
|
+
}
|
|
23926
|
+
return o;
|
|
23927
|
+
}
|
|
23928
|
+
function parseSignoffLabel(args) {
|
|
23929
|
+
const o = {};
|
|
23930
|
+
for (let i = 0; i < args.length; i++) {
|
|
23931
|
+
const a = args[i];
|
|
23932
|
+
if (a === "--rule") o.rule = args[++i];
|
|
23933
|
+
else if (a === "--file") o.file = args[++i];
|
|
23934
|
+
else if (a === "--head") o.head = args[++i];
|
|
23935
|
+
}
|
|
23936
|
+
return o;
|
|
23937
|
+
}
|
|
23938
|
+
function parseCheck(args) {
|
|
23939
|
+
const o = {};
|
|
23940
|
+
for (let i = 0; i < args.length; i++) {
|
|
23941
|
+
const a = args[i];
|
|
23942
|
+
if (a === "--staged") o.staged = true;
|
|
23943
|
+
else if (a === "--worktree") o.worktree = true;
|
|
23944
|
+
else if (a === "--json") o.json = true;
|
|
23945
|
+
else if (a === "--format") {
|
|
23946
|
+
const v = args[++i];
|
|
23947
|
+
if (v !== void 0 && isFormat(v)) o.format = v;
|
|
23948
|
+
} else if (a === "--diff") o.diff = args[++i];
|
|
23949
|
+
else if (a === "--cwd") o.cwd = args[++i];
|
|
23950
|
+
}
|
|
23951
|
+
return o;
|
|
23952
|
+
}
|
|
23953
|
+
function splitLongOptionEquals(args) {
|
|
23954
|
+
const out3 = [];
|
|
23955
|
+
for (let i = 0; i < args.length; i++) {
|
|
23956
|
+
const arg = args[i];
|
|
21723
23957
|
if (arg === "--") {
|
|
21724
23958
|
out3.push(...args.slice(i));
|
|
21725
23959
|
break;
|
|
@@ -21791,6 +24025,12 @@ function validateFlatArgs(args, grammar) {
|
|
|
21791
24025
|
seen,
|
|
21792
24026
|
positionals
|
|
21793
24027
|
};
|
|
24028
|
+
} else if (rule === "mode" && v !== "headless" && v !== "interactive") {
|
|
24029
|
+
return {
|
|
24030
|
+
error: `${a} needs one of headless | interactive (got "${v}")`,
|
|
24031
|
+
seen,
|
|
24032
|
+
positionals
|
|
24033
|
+
};
|
|
21794
24034
|
}
|
|
21795
24035
|
continue;
|
|
21796
24036
|
}
|
|
@@ -21942,6 +24182,19 @@ function validateCliArgs(cmd, args) {
|
|
|
21942
24182
|
values: { "--dir": "string", "--log": "string", "--base": "string" }
|
|
21943
24183
|
}).error;
|
|
21944
24184
|
}
|
|
24185
|
+
if (cmd === "runtime") {
|
|
24186
|
+
const [sub, ...rest] = args;
|
|
24187
|
+
if (sub === void 0) return `runtime requires a subcommand (${RUNTIME_SUBCOMMANDS.join(" | ")})`;
|
|
24188
|
+
if (sub !== "verify" && sub !== "status") {
|
|
24189
|
+
return `unknown runtime subcommand "${sub}" (${RUNTIME_SUBCOMMANDS.join(" | ")})`;
|
|
24190
|
+
}
|
|
24191
|
+
const parsed = validateFlatArgs(rest, {
|
|
24192
|
+
flags: ["--json"],
|
|
24193
|
+
values: { "--cwd": "string", "--runtime": "string", "--mode": "mode", "--model": "string" }
|
|
24194
|
+
});
|
|
24195
|
+
if (parsed.error) return parsed.error;
|
|
24196
|
+
return void 0;
|
|
24197
|
+
}
|
|
21945
24198
|
if (cmd === "research") {
|
|
21946
24199
|
const [sub, ...rest] = args;
|
|
21947
24200
|
if (sub === void 0) return `research requires a subcommand (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
|
|
@@ -22149,6 +24402,26 @@ Formats:
|
|
|
22149
24402
|
emits the versioned status document for
|
|
22150
24403
|
editors, CI and dashboards. Local CURRENT
|
|
22151
24404
|
is posture, never repository/CI authority.
|
|
24405
|
+
tamperward runtime verify [--runtime ID] qualify the in-loop runtime: an operation-
|
|
24406
|
+
[--mode headless|interactive] specific capability matrix (pre-deny:shell,
|
|
24407
|
+
[--model M] [--json] [--cwd D] native-edit, mcp, end-of-turn, transport:*,
|
|
24408
|
+
\u2026) with an explicit state per capability
|
|
24409
|
+
(PROVEN | PARTIAL | UNPROVEN | UNSUPPORTED |
|
|
24410
|
+
FAIL-OPEN | INCONCLUSIVE; never a score),
|
|
24411
|
+
derived from the shipped adapter's declared
|
|
24412
|
+
capabilities and committed evidence. Bound to
|
|
24413
|
+
the runtime version, TamperWard version/commit,
|
|
24414
|
+
adapter hash, hook-config hash, execution mode,
|
|
24415
|
+
platform and model; persisted git-locally.
|
|
24416
|
+
Reports EXISTING facts only \u2014 no promotion, no
|
|
24417
|
+
Round 4.1 claim. Final authority (CI / pristine
|
|
24418
|
+
verify) stays separate from in-loop steering.
|
|
24419
|
+
tamperward runtime status [--runtime ID] render the latest recorded qualification
|
|
24420
|
+
[--mode headless|interactive] WITHOUT rerunning it, and mark it STALE when a
|
|
24421
|
+
[--model M] [--json] [--cwd D] load-bearing input (version/config/adapter/mode/
|
|
24422
|
+
model/\u2026) changed since it was recorded. --mode and
|
|
24423
|
+
--model select the binding staleness is compared
|
|
24424
|
+
against, as for verify.
|
|
22152
24425
|
tamperward doctor [--base R] report installation + authority posture
|
|
22153
24426
|
[--workflow F] [--cwd D] and validate the CI verifier's outer-time
|
|
22154
24427
|
[--json] envelope against the trusted policy.
|
|
@@ -22215,6 +24488,10 @@ function main(argv) {
|
|
|
22215
24488
|
return runTraceVerify(parseTraceVerify(rest));
|
|
22216
24489
|
case "research":
|
|
22217
24490
|
return runResearchCommand(rest);
|
|
24491
|
+
case "runtime": {
|
|
24492
|
+
const { sub, opts } = parseRuntime(rest);
|
|
24493
|
+
return runRuntime(sub, opts);
|
|
24494
|
+
}
|
|
22218
24495
|
case "run":
|
|
22219
24496
|
return runEnvelope({
|
|
22220
24497
|
...parseRun(rest),
|
|
@@ -22265,6 +24542,7 @@ var init_main = __esm({
|
|
|
22265
24542
|
init_watch();
|
|
22266
24543
|
init_onboard();
|
|
22267
24544
|
init_research();
|
|
24545
|
+
init_runtime();
|
|
22268
24546
|
init_audit();
|
|
22269
24547
|
init_signoff_label();
|
|
22270
24548
|
}
|
|
@@ -22273,7 +24551,7 @@ var init_main = __esm({
|
|
|
22273
24551
|
// src/cli/index.ts
|
|
22274
24552
|
init_hook_client();
|
|
22275
24553
|
init_exit();
|
|
22276
|
-
import { readFileSync as
|
|
24554
|
+
import { readFileSync as readFileSync29 } from "node:fs";
|
|
22277
24555
|
function loadMain() {
|
|
22278
24556
|
return Promise.resolve().then(() => (init_main(), main_exports));
|
|
22279
24557
|
}
|
|
@@ -22286,7 +24564,7 @@ async function launch(argv) {
|
|
|
22286
24564
|
if (kind && hookServiceEnabled()) {
|
|
22287
24565
|
let raw = null;
|
|
22288
24566
|
try {
|
|
22289
|
-
raw =
|
|
24567
|
+
raw = readFileSync29(0, "utf8");
|
|
22290
24568
|
} catch {
|
|
22291
24569
|
raw = null;
|
|
22292
24570
|
}
|