tamperward 2.36.0 → 2.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js 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((resolve20) => {
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
- resolve20(value);
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, resolve20) {
5179
- const obj = resolve20(expr);
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(resolve20(p.initializer));
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 resolve20 = resolverFor(sf);
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 = resolve20(node.initializer);
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, resolve20);
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, resolve20);
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 resolve20 = resolverFor(sf);
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(resolve20(node.initializer));
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(resolve20(node.initializer));
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 num2 = src.slice(i).match(/^-?\d+(?:\.\d+)?/);
6980
- if (num2) {
6981
- out3.push({ t: "num", v: num2[0] });
6982
- i += num2[0].length;
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 num2 = (v2) => v2 === TRUTHY ? NaN : typeof v2 === "number" ? v2 : typeof v2 === "boolean" ? v2 ? 1 : 0 : v2 === null ? 0 : v2.trim() === "" ? 0 : Number(v2);
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 = num2(a);
7013
- const y2 = num2(b);
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 = num2(l);
7052
- const y2 = num2(r);
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
  }
@@ -12308,10 +12326,10 @@ function parseAuditJsonl(raw) {
12308
12326
  return events;
12309
12327
  }
12310
12328
  function parseSince(value, nowMs = Date.now()) {
12311
- const relative9 = /^(\d+)(m|h|d)$/.exec(value);
12312
- if (relative9) {
12313
- const count = Number(relative9[1]);
12314
- const unit = relative9[2] === "m" ? 6e4 : relative9[2] === "h" ? 36e5 : 864e5;
12329
+ const relative13 = /^(\d+)(m|h|d)$/.exec(value);
12330
+ if (relative13) {
12331
+ const count = Number(relative13[1]);
12332
+ const unit = relative13[2] === "m" ? 6e4 : relative13[2] === "h" ? 36e5 : 864e5;
12315
12333
  if (!Number.isSafeInteger(count) || count <= 0) throw new Error(`invalid --since value "${value}"`);
12316
12334
  return nowMs - count * unit;
12317
12335
  }
@@ -13019,12 +13037,12 @@ function pidAlive2(pid) {
13019
13037
  }
13020
13038
  }
13021
13039
  function listening(socket) {
13022
- return new Promise((resolve20) => {
13040
+ return new Promise((resolve23) => {
13023
13041
  const sock = createConnection2(socket);
13024
13042
  const done = (v) => {
13025
13043
  clearTimeout(timer);
13026
13044
  sock.destroy();
13027
- resolve20(v);
13045
+ resolve23(v);
13028
13046
  };
13029
13047
  const timer = setTimeout(() => done(false), 1e3);
13030
13048
  sock.once("connect", () => done(true));
@@ -13203,11 +13221,11 @@ async function startHookService(opts) {
13203
13221
  });
13204
13222
  const umask = process.umask(63);
13205
13223
  try {
13206
- await new Promise((resolve20, reject) => {
13224
+ await new Promise((resolve23, reject) => {
13207
13225
  server.once("error", reject);
13208
13226
  server.listen(paths.socket, () => {
13209
13227
  server.off("error", reject);
13210
- resolve20();
13228
+ resolve23();
13211
13229
  });
13212
13230
  });
13213
13231
  } finally {
@@ -13224,8 +13242,8 @@ async function startHookService(opts) {
13224
13242
  get served() {
13225
13243
  return served;
13226
13244
  },
13227
- close: () => new Promise((resolve20) => {
13228
- if (closed) return resolve20();
13245
+ close: () => new Promise((resolve23) => {
13246
+ if (closed) return resolve23();
13229
13247
  closed = true;
13230
13248
  setSnapshotCache(null);
13231
13249
  let settled = false;
@@ -13236,7 +13254,7 @@ async function startHookService(opts) {
13236
13254
  clearTimeout(drain);
13237
13255
  removeQuietly(paths.socket);
13238
13256
  removeQuietly(paths.state);
13239
- resolve20();
13257
+ resolve23();
13240
13258
  };
13241
13259
  server.close(finalize);
13242
13260
  for (const sock of sockets) if (sock !== acceptedSocket) sock.destroy();
@@ -19508,9 +19526,9 @@ function readlineAsker(input, output) {
19508
19526
  let pending = null;
19509
19527
  rl.on("close", () => {
19510
19528
  closed = true;
19511
- const resolve20 = pending;
19529
+ const resolve23 = pending;
19512
19530
  pending = null;
19513
- resolve20?.(null);
19531
+ resolve23?.(null);
19514
19532
  });
19515
19533
  rl.on("SIGINT", () => rl.close());
19516
19534
  const ask = (question) => new Promise((resolvePromise) => {
@@ -21576,171 +21594,2326 @@ var init_research = __esm({
21576
21594
  }
21577
21595
  });
21578
21596
 
21579
- // src/cli/signoff-label.ts
21580
- function runSignoffLabel(opts) {
21581
- if (!opts.rule) {
21582
- process.stderr.write("tamperward signoff-label --rule <rule> --head <full-head-sha> [--file <path>]\n");
21583
- return 2;
21584
- }
21585
- if (!opts.head) {
21586
- process.stderr.write("tamperward: --head is required and must be a full 40- or 64-character object id.\n");
21587
- return 2;
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;
21597
+ // src/adapters/contract.ts
21598
+ function steeringUnavailableFinding(detail) {
21599
+ return {
21600
+ rule: "tamperward-unavailable",
21601
+ severity: "block",
21602
+ message: `TamperWard could not evaluate this operation (${detail}), so it is denied rather than allowed.`,
21603
+ evidence: detail,
21604
+ remediation: "Repair the runtime steering path (hook config, transport, or event delivery), then retry. Do not work around the gate while it is down.",
21605
+ signoff: { required: true, command: 'tamperward allow --reason "..."' }
21606
+ };
21598
21607
  }
21599
- var init_signoff_label = __esm({
21600
- "src/cli/signoff-label.ts"() {
21608
+ function failClosedResult(outcome, detail, phase, denyPayload) {
21609
+ const findings = [steeringUnavailableFinding(detail)];
21610
+ const wire = denyPayload(findings, phase);
21611
+ return { outcome, detail, wire, decision: { verdict: "deny", findings, reason: wire } };
21612
+ }
21613
+ var OPERATION_KINDS;
21614
+ var init_contract = __esm({
21615
+ "src/adapters/contract.ts"() {
21601
21616
  "use strict";
21602
- init_signoff();
21617
+ OPERATION_KINDS = ["shell", "file-edit", "file-read", "mcp", "other"];
21603
21618
  }
21604
21619
  });
21605
21620
 
21606
- // src/cli/main.ts
21607
- var main_exports = {};
21608
- __export(main_exports, {
21609
- guardedMain: () => guardedMain,
21610
- main: () => main,
21611
- runHookFromRaw: () => runHookFromRaw,
21612
- validateCliArgs: () => validateCliArgs
21613
- });
21614
- function parseAllow(args) {
21615
- const o = {};
21616
- for (let i = 0; i < args.length; i++) {
21617
- const a = args[i];
21618
- if (a === "--file") o.file = args[++i];
21619
- else if (a === "--reason") o.reason = args[++i];
21620
- else if (a === "--cwd") o.cwd = args[++i];
21621
- else if (!a.startsWith("--") && !o.rule) o.rule = a;
21622
- }
21623
- return o;
21621
+ // src/adapters/claude/adapter.ts
21622
+ function hookKindFor(phase) {
21623
+ if (phase === "end-of-turn") return "Stop";
21624
+ if (phase === "pre-action") return "PreToolUse";
21625
+ throw new Error(`no deny-capable Claude hook for the observation-only phase "${phase}"`);
21626
+ }
21627
+ function operationKind(toolName) {
21628
+ if (!toolName) return "other";
21629
+ if (toolName === "Bash" || toolName === "BashOutput" || toolName === "KillShell") return "shell";
21630
+ if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit" || toolName === "NotebookEdit") return "file-edit";
21631
+ if (toolName === "Read" || toolName === "Glob" || toolName === "Grep" || toolName === "NotebookRead") return "file-read";
21632
+ if (toolName.startsWith("mcp__")) return "mcp";
21633
+ return "other";
21634
+ }
21635
+ function eventFrom(input, phase) {
21636
+ const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: operationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
21637
+ return {
21638
+ phase,
21639
+ operation,
21640
+ identity: { claimedCwd: input.cwd, sessionId: input.session_id }
21641
+ };
21624
21642
  }
21625
- function runAgentCommand(kind, args) {
21626
- const agent = args[0];
21627
- if (agent !== "claude") {
21628
- process.stderr.write(`tamperward: unsupported ${kind} agent "${agent ?? ""}" (only "claude" so far)
21629
- `);
21630
- return 2;
21643
+ var ClaudeRuntimeAdapter, claudeAdapter;
21644
+ var init_adapter2 = __esm({
21645
+ "src/adapters/claude/adapter.ts"() {
21646
+ "use strict";
21647
+ init_deny();
21648
+ init_hook();
21649
+ init_repo_context();
21650
+ init_contract();
21651
+ ClaudeRuntimeAdapter = class {
21652
+ name = "claude-code";
21653
+ /**
21654
+ * Claude Code's live PreToolUse fires for EVERY tool and can synchronously deny it —
21655
+ * shell, file edit, file read, MCP, and anything else — even under
21656
+ * `--dangerously-skip-permissions`. The end-of-turn Stop sweep is live too. There is no
21657
+ * live per-tool PostToolUse veto (Stop is the post-turn reconciliation), so `postObserve`
21658
+ * is empty and `unsupported` names that gap explicitly rather than implying it.
21659
+ */
21660
+ capabilities = {
21661
+ preDeny: OPERATION_KINDS,
21662
+ postObserve: [],
21663
+ endOfTurn: true,
21664
+ unsupported: [
21665
+ "per-operation post-action veto (end-of-turn Stop sweep is the post-turn reconciliation)",
21666
+ "network-egress control",
21667
+ "identity / authentication"
21668
+ ]
21669
+ };
21670
+ parseEvent(raw, phase) {
21671
+ try {
21672
+ return eventFrom(parseInput(raw), phase);
21673
+ } catch (e) {
21674
+ if (e instanceof HookInputError) return { failure: "parse-failure", detail: e.message };
21675
+ throw e;
21676
+ }
21677
+ }
21678
+ /**
21679
+ * REVIEW POINT 5 — the runtime cwd is a CLAIM, not authority.
21680
+ *
21681
+ * The trusted repository root is derived from the RUNNER context (`defaultCwd`, else the
21682
+ * process cwd) — INDEPENDENTLY of the claim — via git (`repoContext` →
21683
+ * `git rev-parse --show-toplevel`, which resolves symlinks to a canonical path). The
21684
+ * claim is then validated against THAT root with the shared `validateClaimAgainstRoot`:
21685
+ * accepted only when it resolves to the same repository (its root, or a path inside it);
21686
+ * rejected for a malformed path, a non-repository, a different repository, or a symlink
21687
+ * that escapes into another repository. It never weakens what `preToolUseVerdict` does via
21688
+ * `repoRoot()` — it is the explicit boundary that `decide` enforces BEFORE evaluating.
21689
+ */
21690
+ validateIdentity(claim, defaultCwd) {
21691
+ const base = defaultCwd ?? process.cwd();
21692
+ const runnerCtx = repoContext(base);
21693
+ if (!runnerCtx) {
21694
+ return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
21695
+ }
21696
+ const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
21697
+ return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
21698
+ }
21699
+ /** The exact deny wire bytes for `phase`, through the SAME `denyWire` the live
21700
+ * `verdict()` uses (src/cli/hook.ts). Reuses `formatDenial`, so the reason text is
21701
+ * identical to the live path. Byte-for-byte equal to what a live deny emits. */
21702
+ denyPayload(findings, phase) {
21703
+ return denyWire(formatDenial(findings), hookKindFor(phase));
21704
+ }
21705
+ /**
21706
+ * The authoritative synchronous path, enforced in order: parse → validate identity →
21707
+ * decide. Each step that fails does so CLOSED (deny), except `post-action`, which is
21708
+ * observation-only and returns `unsupported` (never a deny wire, never a PreToolUse
21709
+ * fallthrough — BLOCKER 2).
21710
+ *
21711
+ * 1. `post-action` → `unsupported` (Claude has no per-tool post-action veto).
21712
+ * 2. parse failure → `parse-failure`, byte-identical to the live fail-closed deny.
21713
+ * 3. identity claim rejected → a fail-closed DENY, BEFORE any content evaluation, so a
21714
+ * runtime-supplied cwd pointing at another repository can never reach the detectors.
21715
+ * 4. otherwise → delegate to the canonical `preToolUseFromRaw` / `stopFromRaw`, PASSING
21716
+ * the validated trusted root so the live path re-runs the same shared identity check.
21717
+ * `wire` is byte-identical to the live CLI — every deny/allow decision, the fail-closed
21718
+ * wrapping, repo-root resolution (#412), the turn baseline and the transport remain
21719
+ * exactly as src/cli/hook.ts computes them.
21720
+ */
21721
+ decide(raw, phase, defaultCwd) {
21722
+ if (phase === "post-action") {
21723
+ return {
21724
+ outcome: "unsupported",
21725
+ detail: "Claude Code has no per-tool post-action veto; the end-of-turn Stop sweep is its post-turn reconciliation."
21726
+ };
21727
+ }
21728
+ const parsed = this.parseEvent(raw, phase);
21729
+ if ("failure" in parsed) {
21730
+ const live2 = phase === "end-of-turn" ? stopFromRaw(raw, defaultCwd) : preToolUseFromRaw(raw, defaultCwd);
21731
+ return {
21732
+ outcome: "parse-failure",
21733
+ detail: parsed.detail,
21734
+ wire: live2.stdout,
21735
+ decision: { verdict: "deny", findings: [], reason: live2.stdout }
21736
+ };
21737
+ }
21738
+ const idv = this.validateIdentity(parsed.identity, defaultCwd);
21739
+ if (!idv.ok) {
21740
+ const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
21741
+ const wire2 = this.denyPayload(findings, phase);
21742
+ return { outcome: "ok", wire: wire2, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire2 } };
21743
+ }
21744
+ const live = phase === "end-of-turn" ? stopFromRaw(raw, defaultCwd, idv.trustedRoot) : preToolUseFromRaw(raw, defaultCwd, idv.trustedRoot);
21745
+ const wire = live.stdout;
21746
+ const denied = wire.length > 0;
21747
+ return {
21748
+ outcome: "ok",
21749
+ wire,
21750
+ decision: { verdict: denied ? "deny" : "allow", findings: [], reason: denied ? wire : void 0 }
21751
+ };
21752
+ }
21753
+ /**
21754
+ * Map a transport failure (or a required hook that did not fire) onto a fail-closed
21755
+ * DENY at the neutral seam. Claude's LIVE client additionally degrades a hook-service
21756
+ * transport failure to the in-process verdict (src/cli/index.ts / hook-client.ts) rather
21757
+ * than surfacing it here, so this method documents the contract guarantee for the seam;
21758
+ * a partial adapter with no in-process fallback denies through exactly this path.
21759
+ */
21760
+ failClosed(outcome, detail, phase) {
21761
+ return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
21762
+ }
21763
+ };
21764
+ claudeAdapter = new ClaudeRuntimeAdapter();
21631
21765
  }
21632
- return kind === "hook" ? runHookClaude() : runSweepClaude();
21766
+ });
21767
+
21768
+ // src/adapters/apply-patch.ts
21769
+ import { isAbsolute as isAbsolute14, relative as relative9 } from "node:path";
21770
+ function readDisk2(path) {
21771
+ return textOf2(inspectResolved(path));
21633
21772
  }
21634
- function parseInit(args) {
21635
- const o = {};
21636
- for (let i = 0; i < args.length; i++) {
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;
21773
+ function relForDisplay2(path, cwd) {
21774
+ const rel = relative9(cwd, path);
21775
+ return rel && !rel.startsWith("..") && !isAbsolute14(rel) ? rel : path;
21643
21776
  }
21644
- function parseOnboard(args) {
21645
- const o = {};
21646
- for (let i = 0; i < args.length; i++) {
21647
- const a = args[i];
21648
- if (a === "--cwd") o.cwd = args[++i];
21649
- else if (a === "--base") o.base = args[++i];
21650
- else if (a === "--repo") o.repo = args[++i];
21651
- else if (a === "--branch") o.branch = args[++i];
21652
- else if (a === "--verify-command") o.verifyCommand = args[++i];
21653
- else if (a === "--skip-demo") o.skipDemo = true;
21654
- else if (a === "--demo") o.demo = true;
21655
- else if (a === "--no-github") o.noGithub = true;
21656
- else if (a === "--yes") o.yes = true;
21777
+ function parsePatchSections(patch) {
21778
+ const lines = patch.replace(/\r\n/g, "\n").split("\n");
21779
+ let i = 0;
21780
+ while (i < lines.length && !/^\*\*\* Begin Patch/.test(lines[i])) i++;
21781
+ if (i >= lines.length) throw new Error('apply_patch payload has no "*** Begin Patch" header');
21782
+ i++;
21783
+ const sections = [];
21784
+ let current = null;
21785
+ const flush = () => {
21786
+ if (current) sections.push(current);
21787
+ current = null;
21788
+ };
21789
+ for (; i < lines.length; i++) {
21790
+ const line = lines[i];
21791
+ if (/^\*\*\* End Patch/.test(line)) {
21792
+ flush();
21793
+ return sections;
21794
+ }
21795
+ const add = /^\*\*\* Add File: (.*)$/.exec(line);
21796
+ const upd = /^\*\*\* Update File: (.*)$/.exec(line);
21797
+ const del = /^\*\*\* Delete File: (.*)$/.exec(line);
21798
+ const move = /^\*\*\* Move to: (.*)$/.exec(line);
21799
+ if (add) {
21800
+ flush();
21801
+ current = { op: "add", path: add[1].trim(), body: [] };
21802
+ } else if (upd) {
21803
+ flush();
21804
+ current = { op: "update", path: upd[1].trim(), body: [] };
21805
+ } else if (del) {
21806
+ flush();
21807
+ current = { op: "delete", path: del[1].trim(), body: [] };
21808
+ } else if (move && current) {
21809
+ current.moveTo = move[1].trim();
21810
+ } else if (current) {
21811
+ current.body.push(line);
21812
+ }
21813
+ }
21814
+ throw new Error('apply_patch payload is missing its "*** End Patch" trailer');
21815
+ }
21816
+ function addedContent(body) {
21817
+ return body.filter((l) => l.startsWith("+")).map((l) => l.slice(1)).join("\n");
21818
+ }
21819
+ function applyUpdate(before, body) {
21820
+ const lines = before.split("\n");
21821
+ const hunks = [];
21822
+ let hunk = [];
21823
+ for (const l of body) {
21824
+ if (l.startsWith("@@")) {
21825
+ if (hunk.length) hunks.push(hunk);
21826
+ hunk = [];
21827
+ continue;
21828
+ }
21829
+ hunk.push(l);
21830
+ }
21831
+ if (hunk.length) hunks.push(hunk);
21832
+ let cursor = 0;
21833
+ for (const h of hunks) {
21834
+ const oldBlock = [];
21835
+ const newBlock = [];
21836
+ for (const l of h) {
21837
+ if (l === "") {
21838
+ oldBlock.push("");
21839
+ newBlock.push("");
21840
+ } else if (l.startsWith(" ")) {
21841
+ oldBlock.push(l.slice(1));
21842
+ newBlock.push(l.slice(1));
21843
+ } else if (l.startsWith("-")) {
21844
+ oldBlock.push(l.slice(1));
21845
+ } else if (l.startsWith("+")) {
21846
+ newBlock.push(l.slice(1));
21847
+ } else {
21848
+ return null;
21849
+ }
21850
+ }
21851
+ if (oldBlock.length === 0) return null;
21852
+ let at = -1;
21853
+ for (let s = cursor; s <= lines.length - oldBlock.length; s++) {
21854
+ let ok = true;
21855
+ for (let k = 0; k < oldBlock.length; k++) {
21856
+ if (lines[s + k] !== oldBlock[k]) {
21857
+ ok = false;
21858
+ break;
21859
+ }
21860
+ }
21861
+ if (ok) {
21862
+ at = s;
21863
+ break;
21864
+ }
21865
+ }
21866
+ if (at < 0) return null;
21867
+ lines.splice(at, oldBlock.length, ...newBlock);
21868
+ cursor = at + newBlock.length;
21657
21869
  }
21658
- return o;
21870
+ return lines.join("\n");
21659
21871
  }
21660
- function parseDoctor(args) {
21661
- const o = {};
21662
- for (let i = 0; i < args.length; i++) {
21663
- const a = args[i];
21664
- if (a === "--cwd") o.cwd = args[++i];
21665
- else if (a === "--base") o.base = args[++i];
21666
- else if (a === "--workflow") o.workflow = args[++i];
21667
- else if (a === "--github") o.github = true;
21668
- else if (a === "--json") o.json = true;
21669
- else if (a === "--repo") o.repo = args[++i];
21670
- else if (a === "--branch") o.branch = args[++i];
21872
+ function applyPatchChanges(patch, abs, cwd) {
21873
+ const out3 = [];
21874
+ for (const section of parsePatchSections(patch)) {
21875
+ const targetRel = relForDisplay2(abs(section.moveTo ?? section.path), cwd);
21876
+ if (section.op === "add") {
21877
+ out3.push(...synthFileChange(targetRel, null, addedContent(section.body)));
21878
+ } else if (section.op === "delete") {
21879
+ out3.push(...synthFileChange(targetRel, readDisk2(abs(section.path)), null));
21880
+ } else {
21881
+ const before = readDisk2(abs(section.path));
21882
+ const after = applyUpdate(before ?? "", section.body);
21883
+ if (after === null) {
21884
+ throw new Error(
21885
+ `cannot reconstruct the apply_patch update to ${section.path}: its hunks did not locate against the file on disk`
21886
+ );
21887
+ }
21888
+ const synth = synthFileChange(targetRel, before, after);
21889
+ if (section.moveTo && section.moveTo !== section.path) {
21890
+ out3.push(...synth.map((c) => ({ ...c, op: "rename", oldPath: relForDisplay2(abs(section.path), cwd) })));
21891
+ } else {
21892
+ out3.push(...synth);
21893
+ }
21894
+ }
21671
21895
  }
21672
- return o;
21896
+ return out3;
21673
21897
  }
21674
- function parseStatus(args) {
21675
- const o = {};
21676
- for (let i = 0; i < args.length; i++) {
21677
- const a = args[i];
21678
- if (a === "--json") o.json = true;
21679
- else if (a === "--cwd") o.cwd = args[++i];
21898
+ var init_apply_patch = __esm({
21899
+ "src/adapters/apply-patch.ts"() {
21900
+ "use strict";
21901
+ init_changes();
21902
+ init_disk();
21680
21903
  }
21681
- return o;
21904
+ });
21905
+
21906
+ // src/adapters/codex/changes.ts
21907
+ import { isAbsolute as isAbsolute15, relative as relative10, resolve as resolve20 } from "node:path";
21908
+ function asStr2(v) {
21909
+ return typeof v === "string" ? v : "";
21682
21910
  }
21683
- function parseStats(args) {
21684
- const o = {};
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;
21911
+ function readDisk3(path) {
21912
+ return textOf2(inspectResolved(path));
21693
21913
  }
21694
- function parseSignoffLabel(args) {
21695
- const o = {};
21696
- for (let i = 0; i < args.length; i++) {
21697
- const a = args[i];
21698
- if (a === "--rule") o.rule = args[++i];
21699
- else if (a === "--file") o.file = args[++i];
21700
- else if (a === "--head") o.head = args[++i];
21914
+ function relForDisplay3(path, cwd) {
21915
+ const rel = relative10(cwd, path);
21916
+ return rel && !rel.startsWith("..") && !isAbsolute15(rel) ? rel : path;
21917
+ }
21918
+ function shellChanges(args) {
21919
+ const cmd = args.command ?? args.argv;
21920
+ if (Array.isArray(cmd)) {
21921
+ const argv = cmd.map(asStr2).filter((s) => s.length > 0);
21922
+ if (!argv.length) throw new Error("shell event carries no command/argv to reconstruct");
21923
+ return [{ kind: "command", raw: argv.join(" "), argv }];
21924
+ }
21925
+ const raw = asStr2(cmd);
21926
+ if (!raw) throw new Error("shell event carries no command to reconstruct");
21927
+ return [{ kind: "command", raw, argv: raw.split(/\s+/) }];
21928
+ }
21929
+ function changesFromCodex(operation, cwd, base = cwd) {
21930
+ const args = operation.args ?? {};
21931
+ const abs = (path) => resolve20(base, path);
21932
+ if (operation.kind === "shell") return shellChanges(args);
21933
+ if (operation.kind !== "file-edit") return [];
21934
+ if (operation.name === "apply_patch" || operation.name === "patch") {
21935
+ const patch = asStr2(args.command) || asStr2(args.patch) || asStr2(args.input);
21936
+ if (!patch) throw new Error("apply_patch event carries no command/patch/input to reconstruct");
21937
+ return applyPatchChanges(patch, abs, cwd);
21938
+ }
21939
+ const fp = asStr2(args.path) || asStr2(args.file_path);
21940
+ if (!fp) throw new Error(`${operation.name || "file-edit"} event carries no path to reconstruct`);
21941
+ const before = readDisk3(abs(fp));
21942
+ const old = args.old_string ?? args.old_str ?? args.old;
21943
+ const next = args.new_string ?? args.new_str ?? args.new;
21944
+ if (typeof old === "string" || typeof next === "string") {
21945
+ const after = applyEdit(before, asStr2(old), asStr2(next), args.replace_all === true);
21946
+ return synthFileChange(relForDisplay3(abs(fp), cwd), before, after);
21947
+ }
21948
+ const content = args.content ?? args.text;
21949
+ if (typeof content === "string") {
21950
+ return synthFileChange(relForDisplay3(abs(fp), cwd), before, content);
21951
+ }
21952
+ throw new Error(`${operation.name || "file-edit"} event for ${fp} carries no old_string/new_string/content to reconstruct`);
21953
+ }
21954
+ var init_changes2 = __esm({
21955
+ "src/adapters/codex/changes.ts"() {
21956
+ "use strict";
21957
+ init_changes();
21958
+ init_apply_patch();
21959
+ init_disk();
21701
21960
  }
21702
- return o;
21961
+ });
21962
+
21963
+ // src/adapters/codex/deny.ts
21964
+ function assertCodexWirePhase(phase) {
21965
+ if (phase !== "pre-action" && phase !== "end-of-turn") {
21966
+ throw new Error(`unsupported Codex wire phase: ${phase}`);
21967
+ }
21968
+ }
21969
+ function codexWire(reason, phase) {
21970
+ assertCodexWirePhase(phase);
21971
+ const payload = phase === "end-of-turn" ? { decision: "block", reason } : {
21972
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
21973
+ decision: "block",
21974
+ reason
21975
+ };
21976
+ return JSON.stringify(payload) + "\n";
21703
21977
  }
21704
- function parseCheck(args) {
21705
- const o = {};
21706
- for (let i = 0; i < args.length; i++) {
21707
- const a = args[i];
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;
21978
+ function codexDenyWire(findings, phase) {
21979
+ assertCodexWirePhase(phase);
21980
+ if (findings.length === 0) return "";
21981
+ return codexWire(formatDenial(findings), phase);
21718
21982
  }
21719
- function splitLongOptionEquals(args) {
21720
- const out3 = [];
21721
- for (let i = 0; i < args.length; i++) {
21722
- const arg = args[i];
21723
- if (arg === "--") {
21724
- out3.push(...args.slice(i));
21725
- break;
21726
- }
21727
- if (arg.startsWith("--")) {
21728
- const equals = arg.indexOf("=");
21729
- if (equals > 2) {
21730
- out3.push(arg.slice(0, equals), arg.slice(equals + 1));
21731
- continue;
21732
- }
21733
- }
21734
- out3.push(arg);
21983
+ var init_deny2 = __esm({
21984
+ "src/adapters/codex/deny.ts"() {
21985
+ "use strict";
21986
+ init_deny();
21735
21987
  }
21736
- return out3;
21988
+ });
21989
+
21990
+ // src/adapters/codex/schema.ts
21991
+ function codexOperationKind(toolName) {
21992
+ if (!toolName) return "other";
21993
+ if (SHELL_TOOLS.has(toolName)) return "shell";
21994
+ if (FILE_EDIT_TOOLS.has(toolName)) return "file-edit";
21995
+ if (toolName.startsWith(MCP_PREFIX)) return "mcp";
21996
+ if (FILE_READ_TOOLS.has(toolName)) return "file-read";
21997
+ return "other";
21998
+ }
21999
+ function inputFrom(parsed) {
22000
+ return {
22001
+ ...typeof parsed.hook_event_name === "string" ? { hook_event_name: parsed.hook_event_name } : {},
22002
+ ...typeof parsed.tool_name === "string" ? { tool_name: parsed.tool_name } : {},
22003
+ ...isRecord(parsed.tool_input) ? { tool_input: parsed.tool_input } : {},
22004
+ ...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
22005
+ ...typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {},
22006
+ ...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {}
22007
+ };
21737
22008
  }
21738
- function helpRequested(args) {
21739
- for (const arg of args) {
21740
- if (arg === "--") break;
21741
- if (arg === "-h" || arg === "--help") return true;
22009
+ function eventFrom2(input, phase) {
22010
+ const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: codexOperationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
22011
+ const identity2 = { claimedCwd: input.cwd, sessionId: input.session_id };
22012
+ return { phase, operation, identity: identity2 };
22013
+ }
22014
+ function normalizeCodexEvent(raw, phase) {
22015
+ if (!raw.trim()) return eventFrom2({}, phase);
22016
+ let parsed;
22017
+ try {
22018
+ parsed = JSON.parse(raw);
22019
+ } catch (e) {
22020
+ return { failure: "parse-failure", detail: `the Codex hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
21742
22021
  }
21743
- return false;
22022
+ if (!isRecord(parsed)) {
22023
+ return {
22024
+ failure: "parse-failure",
22025
+ detail: `the Codex hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})`
22026
+ };
22027
+ }
22028
+ return eventFrom2(inputFrom(parsed), phase);
22029
+ }
22030
+ var SHELL_TOOLS, FILE_EDIT_TOOLS, FILE_READ_TOOLS, MCP_PREFIX;
22031
+ var init_schema = __esm({
22032
+ "src/adapters/codex/schema.ts"() {
22033
+ "use strict";
22034
+ init_narrow();
22035
+ SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash"]);
22036
+ FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["apply_patch", "Write", "Edit"]);
22037
+ FILE_READ_TOOLS = /* @__PURE__ */ new Set(["view_image"]);
22038
+ MCP_PREFIX = "mcp__";
22039
+ }
22040
+ });
22041
+
22042
+ // src/adapters/codex/adapter.ts
22043
+ var POST_OBSERVE, CodexRuntimeAdapter, codexAdapter;
22044
+ var init_adapter3 = __esm({
22045
+ "src/adapters/codex/adapter.ts"() {
22046
+ "use strict";
22047
+ init_engine();
22048
+ init_policy_load();
22049
+ init_hook();
22050
+ init_session();
22051
+ init_repo_context();
22052
+ init_contract();
22053
+ init_changes2();
22054
+ init_deny2();
22055
+ init_schema();
22056
+ POST_OBSERVE = ["shell", "file-edit", "file-read", "mcp", "other"];
22057
+ CodexRuntimeAdapter = class {
22058
+ name = "codex";
22059
+ /**
22060
+ * CONSERVATIVE and honest. Pre-action deny ENFORCEMENT is not yet proven on a pinned
22061
+ * Codex build, so `preDeny` is empty — the adapter never claims a synchronous veto it
22062
+ * cannot demonstrate. Codex does surface post-execution tool outcomes and an end-of-turn
22063
+ * stop, so those are declared; every real gap is named in `unsupported`. The
22064
+ * `probe:codex-runtime` harness is what may later justify moving a kind into `preDeny`.
22065
+ */
22066
+ capabilities = {
22067
+ preDeny: [],
22068
+ postObserve: POST_OBSERVE,
22069
+ endOfTurn: true,
22070
+ unsupported: [
22071
+ "pre-action deny enforcement not yet proven on a pinned Codex build (see probe:codex-runtime)",
22072
+ "fail-closed hook transport not yet proven (openai/codex#41979)",
22073
+ "network-egress control",
22074
+ "identity / authentication"
22075
+ ]
22076
+ };
22077
+ parseEvent(raw, phase) {
22078
+ return normalizeCodexEvent(raw, phase);
22079
+ }
22080
+ /** Identity is a CLAIM validated against the runner's independently derived trusted root,
22081
+ * reusing `repoContext` / `validateClaimAgainstRoot` exactly as the Claude adapter does.
22082
+ * It never weakens the shared check. */
22083
+ validateIdentity(claim, defaultCwd) {
22084
+ const base = defaultCwd ?? process.cwd();
22085
+ const runnerCtx = repoContext(base);
22086
+ if (!runnerCtx) {
22087
+ return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
22088
+ }
22089
+ const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
22090
+ return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
22091
+ }
22092
+ denyPayload(findings, phase) {
22093
+ if (phase === "post-action") {
22094
+ throw new Error("Codex post-action is observation-only and cannot produce a deny wire");
22095
+ }
22096
+ return codexDenyWire(findings, phase);
22097
+ }
22098
+ /**
22099
+ * parse → validate identity → decide, every failure CLOSED (deny), in order:
22100
+ *
22101
+ * 1. `post-action` → `unsupported` (observation-only; never a deny wire).
22102
+ * 2. parse failure → a fail-closed deny in Codex's wire.
22103
+ * 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
22104
+ * 4. `end-of-turn` → delegate to the canonical git sweep, re-wrapped in Codex's wire.
22105
+ * 5. `pre-action` → reconstruct Change[] and run the SAME `evaluate` engine; a deny
22106
+ * carries the shared denial reason. A reconstruction that cannot be modelled fails
22107
+ * CLOSED to deny rather than allowing an unseen edit.
22108
+ */
22109
+ decide(raw, phase, defaultCwd) {
22110
+ if (phase === "post-action") {
22111
+ return {
22112
+ outcome: "unsupported",
22113
+ detail: "Codex post-action is observation-only; the end-of-turn Stop sweep is the post-turn reconciliation."
22114
+ };
22115
+ }
22116
+ const parsed = this.parseEvent(raw, phase);
22117
+ if ("failure" in parsed) {
22118
+ const findings = [steeringUnavailableFinding(`unparseable Codex event: ${parsed.detail}`)];
22119
+ const wire = this.denyPayload(findings, phase);
22120
+ return { outcome: "parse-failure", detail: parsed.detail, wire, decision: { verdict: "deny", findings, reason: wire } };
22121
+ }
22122
+ const idv = this.validateIdentity(parsed.identity, defaultCwd);
22123
+ if (!idv.ok) {
22124
+ const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
22125
+ const wire = this.denyPayload(findings, phase);
22126
+ return { outcome: "ok", wire, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire } };
22127
+ }
22128
+ if (phase === "end-of-turn") {
22129
+ const wire = stopFromRaw(raw, defaultCwd, idv.trustedRoot).stdout;
22130
+ return { outcome: "ok", wire, decision: { verdict: wire ? "deny" : "allow", findings: [], reason: wire || void 0 } };
22131
+ }
22132
+ try {
22133
+ const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
22134
+ const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
22135
+ turnBaseline(root, parsed.identity.sessionId);
22136
+ const policy = loadPolicy(root);
22137
+ const changes = changesFromCodex(parsed.operation, root, sessionCwd);
22138
+ const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
22139
+ const wire = this.denyPayload(findings, "pre-action");
22140
+ return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
22141
+ } catch (e) {
22142
+ const detail = e instanceof Error ? e.message : String(e);
22143
+ const findings = [steeringUnavailableFinding(detail)];
22144
+ const wire = this.denyPayload(findings, "pre-action");
22145
+ return { outcome: "ok", wire, detail, decision: { verdict: "deny", findings, reason: wire } };
22146
+ }
22147
+ }
22148
+ /** A transport failure or a required hook that did not fire → a fail-closed deny at the
22149
+ * seam, through the adapter's own `denyPayload` so the wire is Codex's native envelope. */
22150
+ failClosed(outcome, detail, phase) {
22151
+ return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
22152
+ }
22153
+ };
22154
+ codexAdapter = new CodexRuntimeAdapter();
22155
+ }
22156
+ });
22157
+
22158
+ // src/adapters/copilot/changes.ts
22159
+ import { isAbsolute as isAbsolute16, relative as relative11, resolve as resolve21 } from "node:path";
22160
+ function asStr3(v) {
22161
+ return typeof v === "string" ? v : "";
22162
+ }
22163
+ function readDisk4(path) {
22164
+ return textOf2(inspectResolved(path));
22165
+ }
22166
+ function relForDisplay4(path, cwd) {
22167
+ const rel = relative11(cwd, path);
22168
+ return rel && !rel.startsWith("..") && !isAbsolute16(rel) ? rel : path;
22169
+ }
22170
+ function shellChanges2(args) {
22171
+ const cmd = args.command ?? args.input ?? args.argv;
22172
+ if (Array.isArray(cmd)) {
22173
+ const argv = cmd.map(asStr3).filter((s) => s.length > 0);
22174
+ if (!argv.length) throw new Error("shell event carries no command/input/argv to reconstruct");
22175
+ return [{ kind: "command", raw: argv.join(" "), argv }];
22176
+ }
22177
+ const raw = asStr3(cmd);
22178
+ if (!raw) throw new Error("shell event carries no command/input to reconstruct");
22179
+ return [{ kind: "command", raw, argv: raw.split(/\s+/) }];
22180
+ }
22181
+ function changesFromCopilot(operation, cwd, base = cwd) {
22182
+ const args = operation.args ?? {};
22183
+ const abs = (path) => resolve21(base, path);
22184
+ if (operation.kind === "shell") return shellChanges2(args);
22185
+ if (operation.kind !== "file-edit") return [];
22186
+ const name = (operation.name || "").toLowerCase();
22187
+ if (name === "apply_patch") {
22188
+ const patch = asStr3(args.input) || asStr3(args.patch) || asStr3(args.command);
22189
+ if (!patch) throw new Error("apply_patch event carries no input/patch/command to reconstruct");
22190
+ return applyPatchChanges(patch, abs, cwd);
22191
+ }
22192
+ const fp = asStr3(args.path) || asStr3(args.file_path) || asStr3(args.filename);
22193
+ if (!fp) throw new Error(`${operation.name || "file-edit"} event carries no path to reconstruct`);
22194
+ const before = readDisk4(abs(fp));
22195
+ const display2 = relForDisplay4(abs(fp), cwd);
22196
+ if (name === "str_replace_editor") {
22197
+ const old2 = args.old_str ?? args.old_string ?? args.old;
22198
+ const next2 = args.new_str ?? args.new_string ?? args.new;
22199
+ if (typeof old2 === "string" || typeof next2 === "string") {
22200
+ return synthFileChange(display2, before, applyEdit(before, asStr3(old2), asStr3(next2), args.replace_all === true));
22201
+ }
22202
+ const fileText = args.file_text ?? args.content ?? args.text;
22203
+ if (typeof fileText === "string") return synthFileChange(display2, before, fileText);
22204
+ throw new Error(
22205
+ `str_replace_editor sub-op "${asStr3(args.command) || "unknown"}" for ${fp} cannot be reconstructed exactly (only str_replace/create are modelled; others fail closed pending a real payload)`
22206
+ );
22207
+ }
22208
+ if (Array.isArray(args.edits)) {
22209
+ let after = before;
22210
+ for (const raw of args.edits) {
22211
+ const ed = isRecord(raw) ? raw : {};
22212
+ after = applyEdit(after, asStr3(ed.old_string ?? ed.old_str), asStr3(ed.new_string ?? ed.new_str), ed.replace_all === true);
22213
+ }
22214
+ return synthFileChange(display2, before, after);
22215
+ }
22216
+ const old = args.old_string ?? args.old_str ?? args.old;
22217
+ const next = args.new_string ?? args.new_str ?? args.new;
22218
+ if (typeof old === "string" || typeof next === "string") {
22219
+ return synthFileChange(display2, before, applyEdit(before, asStr3(old), asStr3(next), args.replace_all === true));
22220
+ }
22221
+ const content = args.content ?? args.text ?? args.file_text;
22222
+ if (typeof content === "string") return synthFileChange(display2, before, content);
22223
+ throw new Error(
22224
+ `${operation.name || "file-edit"} event for ${fp} carries no old_string/new_string/content to reconstruct`
22225
+ );
22226
+ }
22227
+ var init_changes3 = __esm({
22228
+ "src/adapters/copilot/changes.ts"() {
22229
+ "use strict";
22230
+ init_changes();
22231
+ init_apply_patch();
22232
+ init_disk();
22233
+ init_narrow();
22234
+ }
22235
+ });
22236
+
22237
+ // src/adapters/copilot/deny.ts
22238
+ function assertCopilotWirePhase(phase) {
22239
+ const known = ["pre-action", "end-of-turn"];
22240
+ if (!known.includes(phase)) {
22241
+ throw new Error(`unsupported Copilot wire phase: ${phase}`);
22242
+ }
22243
+ }
22244
+ function copilotWire(reason, phase) {
22245
+ assertCopilotWirePhase(phase);
22246
+ const payload = phase === "end-of-turn" ? { decision: "block", reason } : { permissionDecision: "deny", permissionDecisionReason: reason };
22247
+ return JSON.stringify(payload) + "\n";
22248
+ }
22249
+ function copilotDenyWire(findings, phase) {
22250
+ assertCopilotWirePhase(phase);
22251
+ if (findings.length === 0) return "";
22252
+ return copilotWire(formatDenial(findings), phase);
22253
+ }
22254
+ var init_deny3 = __esm({
22255
+ "src/adapters/copilot/deny.ts"() {
22256
+ "use strict";
22257
+ init_deny();
22258
+ }
22259
+ });
22260
+
22261
+ // src/adapters/copilot/schema.ts
22262
+ function copilotOperationKind(toolName) {
22263
+ if (!toolName) return "other";
22264
+ const name = toolName.toLowerCase();
22265
+ if (SHELL_TOOLS2.has(name)) return "shell";
22266
+ if (FILE_EDIT_TOOLS2.has(name)) return "file-edit";
22267
+ if (name.startsWith(MCP_PREFIX2)) return "mcp";
22268
+ if (FILE_READ_TOOLS2.has(name)) return "file-read";
22269
+ return "other";
22270
+ }
22271
+ function argsFrom(parsed) {
22272
+ if (isRecord(parsed.tool_input)) return parsed.tool_input;
22273
+ const ta = parsed.toolArgs;
22274
+ if (isRecord(ta)) return ta;
22275
+ if (typeof ta === "string" && ta.trim()) {
22276
+ try {
22277
+ const decoded = JSON.parse(ta);
22278
+ return isRecord(decoded) ? decoded : void 0;
22279
+ } catch {
22280
+ return void 0;
22281
+ }
22282
+ }
22283
+ return void 0;
22284
+ }
22285
+ function inputFrom2(parsed) {
22286
+ const toolName = typeof parsed.tool_name === "string" ? parsed.tool_name : typeof parsed.toolName === "string" ? parsed.toolName : void 0;
22287
+ const sessionId = typeof parsed.session_id === "string" ? parsed.session_id : typeof parsed.sessionId === "string" ? parsed.sessionId : void 0;
22288
+ const args = argsFrom(parsed);
22289
+ return {
22290
+ ...toolName !== void 0 ? { tool_name: toolName } : {},
22291
+ ...args !== void 0 ? { tool_input: args } : {},
22292
+ ...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
22293
+ ...sessionId !== void 0 ? { session_id: sessionId } : {},
22294
+ ...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {}
22295
+ };
22296
+ }
22297
+ function eventFrom3(input, phase) {
22298
+ const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: copilotOperationKind(input.tool_name), name: input.tool_name ?? "", args: input.tool_input ?? {} };
22299
+ const identity2 = { claimedCwd: input.cwd, sessionId: input.session_id };
22300
+ return { phase, operation, identity: identity2 };
22301
+ }
22302
+ function parseRaw(raw) {
22303
+ if (!raw.trim()) return { kind: "absent" };
22304
+ let parsed;
22305
+ try {
22306
+ parsed = JSON.parse(raw);
22307
+ } catch (e) {
22308
+ return { kind: "fail", detail: `the Copilot hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
22309
+ }
22310
+ if (!isRecord(parsed)) {
22311
+ return { kind: "fail", detail: `the Copilot hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})` };
22312
+ }
22313
+ return { kind: "ok", record: parsed };
22314
+ }
22315
+ function normalizeCopilotEvent(raw, phase) {
22316
+ const parsed = parseRaw(raw);
22317
+ if (parsed.kind === "absent") return eventFrom3({}, phase);
22318
+ if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
22319
+ return eventFrom3(inputFrom2(parsed.record), phase);
22320
+ }
22321
+ function copilotStopInput(raw) {
22322
+ const parsed = parseRaw(raw);
22323
+ if (parsed.kind === "absent") return "{}";
22324
+ if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
22325
+ const input = inputFrom2(parsed.record);
22326
+ return JSON.stringify({
22327
+ ...input.cwd !== void 0 ? { cwd: input.cwd } : {},
22328
+ ...input.session_id !== void 0 ? { session_id: input.session_id } : {},
22329
+ ...input.stop_hook_active !== void 0 ? { stop_hook_active: input.stop_hook_active } : {}
22330
+ });
22331
+ }
22332
+ var SHELL_TOOLS2, FILE_EDIT_TOOLS2, FILE_READ_TOOLS2, MCP_PREFIX2;
22333
+ var init_schema2 = __esm({
22334
+ "src/adapters/copilot/schema.ts"() {
22335
+ "use strict";
22336
+ init_narrow();
22337
+ SHELL_TOOLS2 = /* @__PURE__ */ new Set(["bash", "powershell", "write_bash", "write_powershell"]);
22338
+ FILE_EDIT_TOOLS2 = /* @__PURE__ */ new Set(["create", "edit", "write", "multiedit", "apply_patch", "str_replace_editor"]);
22339
+ FILE_READ_TOOLS2 = /* @__PURE__ */ new Set(["view", "read"]);
22340
+ MCP_PREFIX2 = "mcp__";
22341
+ }
22342
+ });
22343
+
22344
+ // src/adapters/copilot/adapter.ts
22345
+ var CopilotRuntimeAdapter, copilotAdapter;
22346
+ var init_adapter4 = __esm({
22347
+ "src/adapters/copilot/adapter.ts"() {
22348
+ "use strict";
22349
+ init_engine();
22350
+ init_policy_load();
22351
+ init_hook();
22352
+ init_session();
22353
+ init_repo_context();
22354
+ init_contract();
22355
+ init_changes3();
22356
+ init_deny3();
22357
+ init_schema2();
22358
+ CopilotRuntimeAdapter = class {
22359
+ name = "github-copilot-cli";
22360
+ /**
22361
+ * CONSERVATIVE and honest. Pre-action deny ENFORCEMENT is not yet proven on a pinned
22362
+ * Copilot CLI build, so `preDeny` is empty — the adapter never claims a synchronous veto
22363
+ * it has not demonstrated on the real runtime. `postObserve` is empty for milestone one:
22364
+ * Copilot does expose a `postToolUse` observation surface, but this adapter does not yet
22365
+ * consume it as a supported post-action path (`decide(..., 'post-action')` returns
22366
+ * `unsupported`), so it declares no post-observe capability rather than advertising kinds it
22367
+ * does not report. Every real gap is named in `unsupported`. The `probe:copilot-runtime`
22368
+ * harness is what may later justify moving a kind into `preDeny` or populating `postObserve`.
22369
+ */
22370
+ capabilities = {
22371
+ preDeny: [],
22372
+ postObserve: [],
22373
+ endOfTurn: true,
22374
+ unsupported: [
22375
+ "pre-action deny enforcement not yet proven on a pinned Copilot CLI build (see probe:copilot-runtime)",
22376
+ "COMMAND preToolUse hook timeout fails OPEN on Copilot CLI: a timed-out hook lets the tool call proceed (crash / non-zero exit / exit 2 fail closed); an HTTP preToolUse hook fails OPEN on network error / timeout / non-2xx, so only the local command/exec transport is a qualification candidate",
22377
+ "preToolUse is the only PRE-EXECUTION tool veto; agentStop can only block turn completion and force continuation (a lifecycle control, not a filesystem veto), and Copilot overrides the hook after 8 consecutive blocks (stop_hook_active lifecycle); postToolUse observation is not yet consumed by this adapter",
22378
+ "apply_patch / str_replace_editor exact hook payloads are modelled from the published contract but not yet confirmed against a pinned real-run fixture (str_replace_editor sub-ops other than str_replace/create fail closed); a PascalCase Edit carrying a patch-style payload fails closed unless a fixture proves the Edit-field rewrite",
22379
+ "shell-session write tools (write_bash / write_powershell) are classified as mutation-capable shell ops; their exact hook payload is not yet confirmed against a pinned fixture, so an event carrying no reconstructable command/input fails closed",
22380
+ "network-egress control",
22381
+ "identity / authentication"
22382
+ ]
22383
+ };
22384
+ parseEvent(raw, phase) {
22385
+ return normalizeCopilotEvent(raw, phase);
22386
+ }
22387
+ /** Identity is a CLAIM validated against the runner's independently derived trusted root,
22388
+ * reusing `repoContext` / `validateClaimAgainstRoot` exactly as the Claude and Codex
22389
+ * adapters do. It never weakens the shared check. */
22390
+ validateIdentity(claim, defaultCwd) {
22391
+ const base = defaultCwd ?? process.cwd();
22392
+ const runnerCtx = repoContext(base);
22393
+ if (!runnerCtx) {
22394
+ return {
22395
+ ok: false,
22396
+ rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against`
22397
+ };
22398
+ }
22399
+ const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
22400
+ return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
22401
+ }
22402
+ denyPayload(findings, phase) {
22403
+ if (phase === "post-action") {
22404
+ throw new Error("Copilot post-action is observation-only and cannot produce a deny wire");
22405
+ }
22406
+ return copilotDenyWire(findings, phase);
22407
+ }
22408
+ /**
22409
+ * parse → validate identity → decide, every failure CLOSED (deny), in order:
22410
+ *
22411
+ * 1. `post-action` → `unsupported` (observation-only; never a deny wire).
22412
+ * 2. parse failure → a fail-closed deny in Copilot's wire.
22413
+ * 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
22414
+ * 4. `end-of-turn` → delegate to the canonical git sweep, whose wire is already Copilot's
22415
+ * agentStop `{decision:"block",reason}` shape and passes through unchanged.
22416
+ * 5. `pre-action` → reconstruct Change[] and run the SAME `evaluate` engine; a deny
22417
+ * carries the shared denial reason. A reconstruction that cannot be modelled fails
22418
+ * CLOSED to deny rather than allowing an unseen edit.
22419
+ */
22420
+ decide(raw, phase, defaultCwd) {
22421
+ if (phase === "post-action") {
22422
+ return {
22423
+ outcome: "unsupported",
22424
+ detail: "Copilot post-action is observation-only; the end-of-turn agentStop sweep is the post-turn reconciliation."
22425
+ };
22426
+ }
22427
+ const parsed = this.parseEvent(raw, phase);
22428
+ if ("failure" in parsed) {
22429
+ const findings = [steeringUnavailableFinding(`unparseable Copilot event: ${parsed.detail}`)];
22430
+ const wire = this.denyPayload(findings, phase);
22431
+ return { outcome: "parse-failure", detail: parsed.detail, wire, decision: { verdict: "deny", findings, reason: wire } };
22432
+ }
22433
+ const idv = this.validateIdentity(parsed.identity, defaultCwd);
22434
+ if (!idv.ok) {
22435
+ const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
22436
+ const wire = this.denyPayload(findings, phase);
22437
+ return { outcome: "ok", wire, detail: idv.rejected, decision: { verdict: "deny", findings, reason: wire } };
22438
+ }
22439
+ if (phase === "end-of-turn") {
22440
+ const stopInput = copilotStopInput(raw);
22441
+ if (typeof stopInput !== "string") {
22442
+ const findings = [steeringUnavailableFinding(`unparseable Copilot event: ${stopInput.detail}`)];
22443
+ const wire2 = this.denyPayload(findings, "end-of-turn");
22444
+ return { outcome: "parse-failure", detail: stopInput.detail, wire: wire2, decision: { verdict: "deny", findings, reason: wire2 } };
22445
+ }
22446
+ const wire = stopFromRaw(stopInput, defaultCwd, idv.trustedRoot).stdout;
22447
+ return { outcome: "ok", wire, decision: { verdict: wire ? "deny" : "allow", findings: [], reason: wire || void 0 } };
22448
+ }
22449
+ try {
22450
+ const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
22451
+ const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
22452
+ turnBaseline(root, parsed.identity.sessionId);
22453
+ const policy = loadPolicy(root);
22454
+ const changes = changesFromCopilot(parsed.operation, root, sessionCwd);
22455
+ const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
22456
+ const wire = this.denyPayload(findings, "pre-action");
22457
+ return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
22458
+ } catch (e) {
22459
+ const detail = e instanceof Error ? e.message : String(e);
22460
+ const findings = [steeringUnavailableFinding(detail)];
22461
+ const wire = this.denyPayload(findings, "pre-action");
22462
+ return { outcome: "ok", wire, detail, decision: { verdict: "deny", findings, reason: wire } };
22463
+ }
22464
+ }
22465
+ /** A transport failure or a required hook that did not fire → a fail-closed deny at the
22466
+ * seam, through the adapter's own `denyPayload` so the wire is Copilot's native envelope. */
22467
+ failClosed(outcome, detail, phase) {
22468
+ return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
22469
+ }
22470
+ };
22471
+ copilotAdapter = new CopilotRuntimeAdapter();
22472
+ }
22473
+ });
22474
+
22475
+ // src/adapters/copilot-sdk/changes.ts
22476
+ import { execFileSync as execFileSync15 } from "node:child_process";
22477
+ import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync15, readFileSync as readFileSync28, rmSync as rmSync13, existsSync as existsSync21, realpathSync as realpathSync11 } from "node:fs";
22478
+ import { tmpdir as tmpdir8 } from "node:os";
22479
+ import { basename as basename6, dirname as dirname14, isAbsolute as isAbsolute17, relative as relative12, resolve as resolve22, join as join29 } from "node:path";
22480
+ function asStr4(v) {
22481
+ return typeof v === "string" ? v : "";
22482
+ }
22483
+ function relForDisplay5(path, cwd) {
22484
+ const rel = relative12(cwd, path);
22485
+ return rel && !rel.startsWith("..") && !isAbsolute17(rel) ? rel : path;
22486
+ }
22487
+ function canonicalContainedTarget(abs, root) {
22488
+ let realRoot;
22489
+ try {
22490
+ realRoot = realpathSync11(root);
22491
+ } catch (e) {
22492
+ throw new Error(`trusted repository root ${root} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
22493
+ }
22494
+ let dir = abs;
22495
+ const tail = [];
22496
+ while (!existsSync21(dir)) {
22497
+ const parent = dirname14(dir);
22498
+ if (parent === dir) break;
22499
+ tail.unshift(basename6(dir));
22500
+ dir = parent;
22501
+ }
22502
+ let realDir;
22503
+ try {
22504
+ realDir = realpathSync11(dir);
22505
+ } catch (e) {
22506
+ throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
22507
+ }
22508
+ const real = tail.length ? join29(realDir, ...tail) : realDir;
22509
+ const rel = relative12(realRoot, real);
22510
+ if (rel === "" || rel.startsWith("..") || isAbsolute17(rel)) {
22511
+ throw new Error(`write target ${abs} resolves outside the trusted repository root ${realRoot} \u2014 refusing to judge a write beyond the repository`);
22512
+ }
22513
+ return { real, rel };
22514
+ }
22515
+ function sdkFileEditChanges(args, cwd, base = cwd) {
22516
+ const path = asStr4(args.path) || asStr4(args.fileName) || asStr4(args.file_path);
22517
+ if (!path) throw new Error("write event carries no fileName/path to reconstruct");
22518
+ const abs = resolve22(base, path);
22519
+ const canonical2 = canonicalContainedTarget(abs, cwd);
22520
+ const claimedResolved = asStr4(args.resolvedPath);
22521
+ if (claimedResolved && canonicalContainedTarget(resolve22(base, claimedResolved), cwd).real !== canonical2.real) {
22522
+ throw new Error(`the runtime-claimed resolvedPath (${claimedResolved}) does not match the host-derived target for ${path} \u2014 refusing to judge a different path`);
22523
+ }
22524
+ const display2 = canonical2.rel;
22525
+ const requestRel = relForDisplay5(abs, cwd);
22526
+ const entry = inspectResolved(canonical2.real);
22527
+ const isCreate = entry.kind === "absent";
22528
+ const before = textOf2(entry);
22529
+ if (!isCreate && before === null) {
22530
+ throw new Error(`write target ${display2} exists but cannot be read to judge it (${entry.kind})`);
22531
+ }
22532
+ const newFileContents = typeof args.newFileContents === "string" ? args.newFileContents : void 0;
22533
+ const rawDiff = asStr4(args.diff);
22534
+ const afterFromDiff = rawDiff.trim() ? reconstructAfterViaGit(rawDiff, requestRel, before, isCreate) : void 0;
22535
+ if (newFileContents !== void 0) {
22536
+ if (afterFromDiff !== void 0 && afterFromDiff !== newFileContents) {
22537
+ throw new Error(`write supplies both newFileContents and a diff that DISAGREE for ${display2}; ambiguous request \u2014 refusing to judge only one representation`);
22538
+ }
22539
+ return synthFileChange(display2, before, newFileContents);
22540
+ }
22541
+ if (afterFromDiff !== void 0) return synthFileChange(display2, before, afterFromDiff);
22542
+ return null;
22543
+ }
22544
+ function stripPrefix(p) {
22545
+ return /^[ab]\//.test(p) ? p.slice(2) : p;
22546
+ }
22547
+ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
22548
+ const bytes = Buffer.byteLength(rawDiff);
22549
+ if (bytes > DIFF_MAX_BYTES()) throw new Error(`write diff for ${display2} is ${bytes} bytes (over the ${DIFF_MAX_BYTES()}-byte reconstruction budget)`);
22550
+ const { op, patch } = canonicalPatch(rawDiff, display2, isCreate);
22551
+ const dir = mkdtempSync7(join29(tmpdir8(), "hf-sdk-apply-"));
22552
+ try {
22553
+ const targetPath = join29(dir, TARGET);
22554
+ if (op !== "create") writeFileSync15(targetPath, before ?? "");
22555
+ const patchPath = join29(dir, "change.patch");
22556
+ writeFileSync15(patchPath, patch);
22557
+ const git8 = (extra) => execFileSync15("git", ["apply", ...extra, patchPath], {
22558
+ cwd: dir,
22559
+ encoding: "utf8",
22560
+ timeout: APPLY_TIMEOUT_MS(),
22561
+ maxBuffer: APPLY_MAXBUFFER(),
22562
+ killSignal: "SIGKILL"
22563
+ });
22564
+ const stat = git8(["--numstat"]).trim();
22565
+ const rows = stat ? stat.split("\n") : [];
22566
+ if (rows.length !== 1) throw new Error(`write diff spans ${rows.length} files; a single permission target (${display2}) was expected`);
22567
+ const statPath = rows[0].split(" ")[2];
22568
+ if (statPath !== TARGET) throw new Error(`write diff reconstructs a different file (${statPath}) than the bound target`);
22569
+ git8(["--check"]);
22570
+ git8([]);
22571
+ return op === "delete" ? null : readFileSync28(targetPath, "utf8");
22572
+ } catch (e) {
22573
+ throw new Error(`could not reconstruct the write to ${display2} from its diff via git apply: ${e instanceof Error ? e.message : String(e)}`);
22574
+ } finally {
22575
+ rmSync13(dir, { recursive: true, force: true });
22576
+ }
22577
+ }
22578
+ function canonicalPatch(raw, display2, isCreate) {
22579
+ const lines = raw.replace(/\r\n/g, "\n").split("\n");
22580
+ if (lines.length > DIFF_MAX_LINES()) {
22581
+ throw new Error(`write diff for ${display2} is ${lines.length} lines (over the ${DIFF_MAX_LINES()}-line reconstruction budget)`);
22582
+ }
22583
+ const at = lines.findIndex((l) => l.startsWith("@@ "));
22584
+ if (at < 0) throw new Error(`write diff for ${display2} has no hunk to reconstruct`);
22585
+ const header = lines.slice(0, at);
22586
+ const body = lines.slice(at);
22587
+ let src;
22588
+ let dst;
22589
+ let hasGitLine = false;
22590
+ let sawCreateMode = false;
22591
+ let sawDeleteMode = false;
22592
+ let sawSemanticHeader = false;
22593
+ for (const line of header) {
22594
+ if (line.trim() === "") continue;
22595
+ if (line.startsWith("diff --git ")) {
22596
+ if (hasGitLine) throw new Error(`write diff for ${display2} carries more than one diff --git header (contradictory / multi-file) \u2014 refusing to reconstruct`);
22597
+ hasGitLine = true;
22598
+ sawSemanticHeader = true;
22599
+ for (const p of line.slice("diff --git ".length).trim().split(/\s+/)) assertBound(stripPrefix(p), display2);
22600
+ } else if (line.startsWith("--- ")) {
22601
+ if (src !== void 0) throw new Error(`write diff for ${display2} carries more than one --- endpoint (contradictory header) \u2014 refusing to reconstruct`);
22602
+ src = line.slice(4).split(" ")[0].trim();
22603
+ } else if (line.startsWith("+++ ")) {
22604
+ if (dst !== void 0) throw new Error(`write diff for ${display2} carries more than one +++ endpoint (contradictory header) \u2014 refusing to reconstruct`);
22605
+ dst = line.slice(4).split(" ")[0].trim();
22606
+ } else if (line.startsWith("index ")) {
22607
+ sawSemanticHeader = true;
22608
+ } else if (/^new file mode\b/.test(line)) {
22609
+ if (sawCreateMode) throw new Error(`write diff for ${display2} carries duplicate new file mode metadata \u2014 refusing to reconstruct`);
22610
+ sawCreateMode = true;
22611
+ sawSemanticHeader = true;
22612
+ } else if (/^deleted file mode\b/.test(line)) {
22613
+ if (sawDeleteMode) throw new Error(`write diff for ${display2} carries duplicate deleted file mode metadata \u2014 refusing to reconstruct`);
22614
+ sawDeleteMode = true;
22615
+ sawSemanticHeader = true;
22616
+ } else {
22617
+ throw new Error(`write diff for ${display2} carries an unsupported or unrecognized pre-hunk header line ("${line.trim().slice(0, 60)}") \u2014 refusing to reconstruct`);
22618
+ }
22619
+ }
22620
+ let op;
22621
+ if (src === void 0 && dst === void 0) {
22622
+ if (sawSemanticHeader) throw new Error(`write diff for ${display2} carries Git file metadata but no --- / +++ endpoints (malformed)`);
22623
+ op = isCreate ? "create" : "modify";
22624
+ } else if (src !== void 0 && dst !== void 0) {
22625
+ const srcNull = src === "/dev/null";
22626
+ const dstNull = dst === "/dev/null";
22627
+ if (srcNull && dstNull) throw new Error(`write diff for ${display2} has /dev/null on both sides`);
22628
+ if (!srcNull) assertBound(stripPrefix(src), display2);
22629
+ if (!dstNull) assertBound(stripPrefix(dst), display2);
22630
+ op = srcNull ? "create" : dstNull ? "delete" : "modify";
22631
+ } else {
22632
+ throw new Error(`write diff for ${display2} has a one-sided --- / +++ endpoint pair (malformed) \u2014 refusing to repair it into a valid operation`);
22633
+ }
22634
+ if (sawCreateMode && op !== "create") throw new Error(`write diff for ${display2} carries new file mode metadata but its endpoints describe a ${op} \u2014 contradictory, refusing to reconstruct`);
22635
+ if (sawDeleteMode && op !== "delete") throw new Error(`write diff for ${display2} carries deleted file mode metadata but its endpoints describe a ${op} \u2014 contradictory, refusing to reconstruct`);
22636
+ if (op === "create" && !isCreate) throw new Error(`write diff for ${display2} proposes a create but the target already exists`);
22637
+ if (op !== "create" && isCreate) throw new Error(`write diff for ${display2} proposes a ${op} but the target is absent`);
22638
+ const source = op === "create" ? "/dev/null" : `a/${TARGET}`;
22639
+ const dest = op === "delete" ? "/dev/null" : `b/${TARGET}`;
22640
+ const joined = body.join("\n");
22641
+ const patch = `--- ${source}
22642
+ +++ ${dest}
22643
+ ` + (joined.endsWith("\n") ? joined : joined + "\n");
22644
+ return { op, patch };
22645
+ }
22646
+ function assertBound(declared, display2) {
22647
+ if (declared !== display2) {
22648
+ throw new Error(`write diff path (${declared}) does not match the permission request target (${display2}) \u2014 refusing to judge a different file`);
22649
+ }
22650
+ }
22651
+ var num2, APPLY_TIMEOUT_MS, APPLY_MAXBUFFER, DIFF_MAX_BYTES, DIFF_MAX_LINES, TARGET;
22652
+ var init_changes4 = __esm({
22653
+ "src/adapters/copilot-sdk/changes.ts"() {
22654
+ "use strict";
22655
+ init_changes();
22656
+ init_disk();
22657
+ num2 = (name, fallback) => {
22658
+ const raw = process.env[name];
22659
+ const n2 = raw === void 0 ? NaN : Number(raw);
22660
+ return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
22661
+ };
22662
+ APPLY_TIMEOUT_MS = () => num2("TAMPERWARD_RECONSTRUCT_TIMEOUT_MS", 5e3);
22663
+ APPLY_MAXBUFFER = () => num2("TAMPERWARD_RECONSTRUCT_MAXBUFFER", 32 * 1024 * 1024);
22664
+ DIFF_MAX_BYTES = () => num2("TAMPERWARD_RECONSTRUCT_MAX_BYTES", 384 * 1024);
22665
+ DIFF_MAX_LINES = () => num2("TAMPERWARD_RECONSTRUCT_MAX_LINES", 4e3);
22666
+ TARGET = "target";
22667
+ }
22668
+ });
22669
+
22670
+ // src/adapters/copilot-sdk/deny.ts
22671
+ function assertPhase(phase) {
22672
+ const known = ["pre-action", "end-of-turn"];
22673
+ if (!known.includes(phase)) throw new Error(`unsupported Copilot SDK wire phase: ${phase}`);
22674
+ }
22675
+ function copilotSdkWire(reason, phase) {
22676
+ assertPhase(phase);
22677
+ const payload = phase === "end-of-turn" ? { decision: "block", reason } : { kind: "reject", feedback: reason };
22678
+ return JSON.stringify(payload) + "\n";
22679
+ }
22680
+ function copilotSdkDenyWire(findings, phase) {
22681
+ assertPhase(phase);
22682
+ if (findings.length === 0) return "";
22683
+ return copilotSdkWire(formatDenial(findings), phase);
22684
+ }
22685
+ var init_deny4 = __esm({
22686
+ "src/adapters/copilot-sdk/deny.ts"() {
22687
+ "use strict";
22688
+ init_deny();
22689
+ }
22690
+ });
22691
+
22692
+ // src/adapters/copilot-sdk/schema.ts
22693
+ function copilotSdkOperationKind(kind) {
22694
+ switch ((kind ?? "").toLowerCase()) {
22695
+ case "shell":
22696
+ return "shell";
22697
+ case "write":
22698
+ return "file-edit";
22699
+ case "read":
22700
+ return "file-read";
22701
+ case "mcp":
22702
+ return "mcp";
22703
+ default:
22704
+ return "other";
22705
+ }
22706
+ }
22707
+ function str3(v) {
22708
+ return typeof v === "string" ? v : void 0;
22709
+ }
22710
+ function requestFrom2(parsed) {
22711
+ const s = (k) => str3(parsed[k]);
22712
+ return {
22713
+ ...s("kind") !== void 0 ? { kind: s("kind") } : {},
22714
+ ...s("toolName") !== void 0 ? { toolName: s("toolName") } : {},
22715
+ ...s("fileName") !== void 0 ? { fileName: s("fileName") } : {},
22716
+ ...s("resolvedPath") !== void 0 ? { resolvedPath: s("resolvedPath") } : {},
22717
+ ...s("fullCommandText") !== void 0 ? { fullCommandText: s("fullCommandText") } : {},
22718
+ ...s("diff") !== void 0 ? { diff: s("diff") } : {},
22719
+ ...s("newFileContents") !== void 0 ? { newFileContents: s("newFileContents") } : {},
22720
+ ...s("intention") !== void 0 ? { intention: s("intention") } : {},
22721
+ ...s("cwd") !== void 0 ? { cwd: s("cwd") } : {},
22722
+ ...s("sessionId") !== void 0 ? { sessionId: s("sessionId") } : {},
22723
+ ...typeof parsed.stopHookActive === "boolean" ? { stopHookActive: parsed.stopHookActive } : {}
22724
+ };
22725
+ }
22726
+ function argsFor(req2) {
22727
+ const kind = copilotSdkOperationKind(req2.kind);
22728
+ if (kind === "shell") return req2.fullCommandText !== void 0 ? { command: req2.fullCommandText } : {};
22729
+ if (kind === "file-edit") {
22730
+ return {
22731
+ ...req2.fileName !== void 0 ? { path: req2.fileName } : {},
22732
+ // The SDK's experimental runtime-resolved canonical path. Retained as an UNTRUSTED claim: the
22733
+ // host derives its own canonical target and denies on mismatch — resolvedPath is never authority.
22734
+ ...req2.resolvedPath !== void 0 ? { resolvedPath: req2.resolvedPath } : {},
22735
+ ...req2.diff !== void 0 ? { diff: req2.diff } : {},
22736
+ ...req2.newFileContents !== void 0 ? { newFileContents: req2.newFileContents } : {},
22737
+ ...req2.intention !== void 0 ? { intention: req2.intention } : {}
22738
+ };
22739
+ }
22740
+ return {};
22741
+ }
22742
+ function eventFrom4(req2, phase) {
22743
+ const operation = phase === "end-of-turn" ? { kind: "other", name: "stop", args: {} } : { kind: copilotSdkOperationKind(req2.kind), name: req2.toolName ?? "", args: argsFor(req2) };
22744
+ const identity2 = { claimedCwd: req2.cwd, sessionId: req2.sessionId };
22745
+ return { phase, operation, identity: identity2 };
22746
+ }
22747
+ function parseRaw2(raw) {
22748
+ if (!raw.trim()) return { kind: "absent" };
22749
+ let parsed;
22750
+ try {
22751
+ parsed = JSON.parse(raw);
22752
+ } catch (e) {
22753
+ return { kind: "fail", detail: `the Copilot SDK permission request is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
22754
+ }
22755
+ if (!isRecord(parsed)) {
22756
+ return { kind: "fail", detail: `the Copilot SDK permission request is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})` };
22757
+ }
22758
+ return { kind: "ok", record: parsed };
22759
+ }
22760
+ function normalizeCopilotSdkEvent(raw, phase) {
22761
+ const parsed = parseRaw2(raw);
22762
+ if (parsed.kind === "absent") return eventFrom4({}, phase);
22763
+ if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
22764
+ return eventFrom4(requestFrom2(parsed.record), phase);
22765
+ }
22766
+ function copilotSdkStopInput(raw) {
22767
+ const parsed = parseRaw2(raw);
22768
+ if (parsed.kind === "absent") return "{}";
22769
+ if (parsed.kind === "fail") return { failure: "parse-failure", detail: parsed.detail };
22770
+ const req2 = requestFrom2(parsed.record);
22771
+ return JSON.stringify({
22772
+ ...req2.cwd !== void 0 ? { cwd: req2.cwd } : {},
22773
+ ...req2.sessionId !== void 0 ? { session_id: req2.sessionId } : {},
22774
+ ...req2.stopHookActive !== void 0 ? { stop_hook_active: req2.stopHookActive } : {}
22775
+ });
22776
+ }
22777
+ var init_schema3 = __esm({
22778
+ "src/adapters/copilot-sdk/schema.ts"() {
22779
+ "use strict";
22780
+ init_narrow();
22781
+ }
22782
+ });
22783
+
22784
+ // src/adapters/copilot-sdk/adapter.ts
22785
+ var FILE_EDIT_NO_CONTENT_DETAIL, CopilotSdkHostedAdapter, copilotSdkAdapter;
22786
+ var init_adapter5 = __esm({
22787
+ "src/adapters/copilot-sdk/adapter.ts"() {
22788
+ "use strict";
22789
+ init_engine();
22790
+ init_policy_load();
22791
+ init_hook();
22792
+ init_session();
22793
+ init_repo_context();
22794
+ init_contract();
22795
+ init_changes3();
22796
+ init_changes4();
22797
+ init_deny4();
22798
+ init_schema3();
22799
+ FILE_EDIT_NO_CONTENT_DETAIL = "this Copilot SDK write surfaced no usable content (no diff, no newFileContents), so content-aware pre-denial cannot be established for this measured configuration; the end-of-turn git sweep is the authority for the landed write. This adapter does not blanket-deny writes.";
22800
+ CopilotSdkHostedAdapter = class {
22801
+ name = "github-copilot-sdk-hosted";
22802
+ /**
22803
+ * CONSERVATIVE and honest. `preDeny` is empty: even shell pre-deny is only a CANDIDATE until the
22804
+ * spike proves, on a pinned SDK, that a rejected/thrown/timed-out permission callback does NOT
22805
+ * dispatch the tool (the decisive unknown #611 must measure). `postObserve` is empty (the SDK has
22806
+ * no post-execution veto). `endOfTurn` is true via `onAgentStop`. Every real gap is named in
22807
+ * `unsupported`, at the granularity #611 asks for.
22808
+ */
22809
+ capabilities = {
22810
+ preDeny: [],
22811
+ postObserve: [],
22812
+ endOfTurn: true,
22813
+ unsupported: [
22814
+ "shell pre-deny enforcement is only a CANDIDATE until the spike proves on a pinned @github/copilot-sdk that a rejected permission decision actually blocks tool dispatch (see spike:copilot-sdk)",
22815
+ "content-aware file-edit pre-deny is CONDITIONAL on the pinned runtime surfacing usable write content: the adapter reconstructs and content-judges a write carrying a diff / newFileContents, but a write that surfaces neither is unsupported for that measured configuration (allow-through; the end-of-turn sweep is authority) \u2014 never a blanket path deny",
22816
+ "the SDK permission-handler failure semantics (synchronous throw / rejected Promise / timeout) are UNMEASURED: whether a broken decision path fails open (dispatches the tool) or closed is exactly what the spike must establish; a single observed fail-open makes the path ineligible for #482 / Round 4.1",
22817
+ 'onAgentStop is a lifecycle continuation control ({decision:"block"} forces another turn, guarded by stopHookActive), not a filesystem veto',
22818
+ "network-egress control",
22819
+ "identity / authentication"
22820
+ ]
22821
+ };
22822
+ parseEvent(raw, phase) {
22823
+ return normalizeCopilotSdkEvent(raw, phase);
22824
+ }
22825
+ /** Identity is a CLAIM validated against the runner's independently derived trusted root,
22826
+ * reusing `repoContext` / `validateClaimAgainstRoot` exactly as the other adapters do. */
22827
+ validateIdentity(claim, defaultCwd) {
22828
+ const base = defaultCwd ?? process.cwd();
22829
+ const runnerCtx = repoContext(base);
22830
+ if (!runnerCtx) {
22831
+ return { ok: false, rejected: `runner cwd (${base}) is not in a repository, so there is no trusted root to validate against` };
22832
+ }
22833
+ const v = validateClaimAgainstRoot(claim.claimedCwd, runnerCtx.root, base);
22834
+ return v.ok ? { ok: true, trustedRoot: v.trustedRoot } : { ok: false, rejected: v.rejected };
22835
+ }
22836
+ denyPayload(findings, phase) {
22837
+ if (phase === "post-action") {
22838
+ throw new Error("Copilot SDK post-action is observation-only and cannot produce a deny wire");
22839
+ }
22840
+ return copilotSdkDenyWire(findings, phase);
22841
+ }
22842
+ /**
22843
+ * parse → validate identity → decide, every failure CLOSED (deny), in order:
22844
+ * 1. `post-action` → `unsupported` (the SDK has no post-execution veto).
22845
+ * 2. parse failure → a fail-closed deny in the SDK reject wire.
22846
+ * 3. identity claim rejected → a fail-closed deny BEFORE any content evaluation.
22847
+ * 4. `end-of-turn` → delegate to the canonical git sweep, whose wire is already the SDK
22848
+ * `onAgentStop` `{decision:"block",reason}` shape.
22849
+ * 5. `pre-action` + shell → the SAME `evaluate` engine over the reconstructed command.
22850
+ * 6. `pre-action` + file-edit → `unsupported` (content not surfaced; sweep is authority).
22851
+ * 7. `pre-action` + read/mcp/other → no Change → allow.
22852
+ */
22853
+ decide(raw, phase, defaultCwd) {
22854
+ if (phase === "post-action") {
22855
+ return { outcome: "unsupported", detail: "Copilot SDK has no post-execution veto; the end-of-turn onAgentStop sweep is the post-turn reconciliation." };
22856
+ }
22857
+ const parsed = this.parseEvent(raw, phase);
22858
+ if ("failure" in parsed) {
22859
+ const findings = [steeringUnavailableFinding(`unparseable Copilot SDK permission request: ${parsed.detail}`)];
22860
+ const wire = this.denyPayload(findings, phase);
22861
+ return { outcome: "parse-failure", detail: parsed.detail, wire, unavailableReason: "parse-failure", decision: { verdict: "deny", findings, reason: wire } };
22862
+ }
22863
+ const idv = this.validateIdentity(parsed.identity, defaultCwd);
22864
+ if (!idv.ok) {
22865
+ const findings = [steeringUnavailableFinding(`repository identity claim rejected: ${idv.rejected}`)];
22866
+ const wire = this.denyPayload(findings, phase);
22867
+ return { outcome: "ok", wire, detail: idv.rejected, unavailableReason: "identity-rejected", decision: { verdict: "deny", findings, reason: wire } };
22868
+ }
22869
+ if (phase === "end-of-turn") {
22870
+ const stopInput = copilotSdkStopInput(raw);
22871
+ if (typeof stopInput !== "string") {
22872
+ const findings2 = [steeringUnavailableFinding(`unparseable Copilot SDK permission request: ${stopInput.detail}`)];
22873
+ const wire = this.denyPayload(findings2, "end-of-turn");
22874
+ return { outcome: "parse-failure", detail: stopInput.detail, wire, unavailableReason: "parse-failure", decision: { verdict: "deny", findings: findings2, reason: wire } };
22875
+ }
22876
+ const swept = stopFromRaw(stopInput, defaultCwd, idv.trustedRoot);
22877
+ const findings = swept.findings ? [...swept.findings] : [];
22878
+ return { outcome: "ok", wire: swept.stdout, decision: { verdict: swept.stdout ? "deny" : "allow", findings, reason: swept.stdout || void 0 } };
22879
+ }
22880
+ let stage = "repo-context";
22881
+ try {
22882
+ const root = idv.trustedRoot ?? repoRoot(defaultCwd ?? process.cwd());
22883
+ stage = "baseline";
22884
+ turnBaseline(root, parsed.identity.sessionId);
22885
+ const sessionCwd = parsed.identity.claimedCwd ?? defaultCwd ?? process.cwd();
22886
+ stage = "policy-load";
22887
+ const policy = loadPolicy(root);
22888
+ if (parsed.operation.kind === "file-edit") {
22889
+ stage = "reconstruction";
22890
+ const changes2 = sdkFileEditChanges(parsed.operation.args, root, sessionCwd);
22891
+ if (changes2 === null) return { outcome: "unsupported", detail: FILE_EDIT_NO_CONTENT_DETAIL };
22892
+ stage = "evaluate";
22893
+ const findings2 = evaluate(changes2, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
22894
+ const wire2 = this.denyPayload(findings2, "pre-action");
22895
+ return { outcome: "ok", wire: wire2, decision: { verdict: findings2.length ? "deny" : "allow", findings: findings2, reason: wire2 || void 0 } };
22896
+ }
22897
+ stage = "reconstruction";
22898
+ const changes = changesFromCopilot(parsed.operation, root, sessionCwd);
22899
+ stage = "evaluate";
22900
+ const findings = evaluate(changes, policy, void 0, "tool-call", { cwd: root }).filter((f) => f.severity === "block");
22901
+ const wire = this.denyPayload(findings, "pre-action");
22902
+ return { outcome: "ok", wire, decision: { verdict: findings.length ? "deny" : "allow", findings, reason: wire || void 0 } };
22903
+ } catch (e) {
22904
+ const detail = e instanceof Error ? e.message : String(e);
22905
+ const findings = [steeringUnavailableFinding(detail)];
22906
+ const wire = this.denyPayload(findings, "pre-action");
22907
+ return { outcome: "ok", wire, detail, unavailableReason: stage, decision: { verdict: "deny", findings, reason: wire } };
22908
+ }
22909
+ }
22910
+ /** A transport failure or a required callback that did not fire → a fail-closed deny at the
22911
+ * seam, through the adapter's own `denyPayload` so the wire is the SDK's native envelope. */
22912
+ failClosed(outcome, detail, phase) {
22913
+ return failClosedResult(outcome, detail, phase, (f, p) => this.denyPayload(f, p));
22914
+ }
22915
+ };
22916
+ copilotSdkAdapter = new CopilotSdkHostedAdapter();
22917
+ }
22918
+ });
22919
+
22920
+ // src/adapters/registry.ts
22921
+ function adapterFor(id) {
22922
+ const name = ADAPTER_ALIASES[id] ?? id;
22923
+ return RUNTIME_ADAPTERS.find((a) => a.name === name) ?? null;
22924
+ }
22925
+ function labelFor(name) {
22926
+ return ADAPTER_LABELS[name] ?? name;
22927
+ }
22928
+ var RUNTIME_ADAPTERS, ADAPTER_LABELS, ADAPTER_ALIASES;
22929
+ var init_registry = __esm({
22930
+ "src/adapters/registry.ts"() {
22931
+ "use strict";
22932
+ init_adapter2();
22933
+ init_adapter3();
22934
+ init_adapter4();
22935
+ init_adapter5();
22936
+ RUNTIME_ADAPTERS = [
22937
+ claudeAdapter,
22938
+ codexAdapter,
22939
+ copilotAdapter,
22940
+ copilotSdkAdapter
22941
+ ];
22942
+ ADAPTER_LABELS = {
22943
+ "claude-code": "Claude Code",
22944
+ codex: "Codex",
22945
+ "github-copilot-cli": "GitHub Copilot CLI",
22946
+ "github-copilot-sdk-hosted": "GitHub Copilot SDK (hosted)"
22947
+ };
22948
+ ADAPTER_ALIASES = {
22949
+ claude: "claude-code",
22950
+ copilot: "github-copilot-cli",
22951
+ "copilot-cli": "github-copilot-cli",
22952
+ "copilot-sdk": "github-copilot-sdk-hosted"
22953
+ };
22954
+ }
22955
+ });
22956
+
22957
+ // src/adapters/evidence.ts
22958
+ function sameIdSet(a, b) {
22959
+ const ua = [...new Set(a)].sort();
22960
+ const ub = [...new Set(b)].sort();
22961
+ return ua.length === ub.length && ua.every((x, i) => x === ub[i]);
22962
+ }
22963
+ function evidenceBindingMatches(record, current) {
22964
+ return record.runtime_id === current.runtime_id && record.runtime_version === current.runtime_version && record.model === current.model && record.platform === current.platform && record.execution_mode === current.execution_mode && record.hook_config_hash === current.hook_config_hash && // Pinned SDK/protocol component versions — order-insensitive set comparison.
22965
+ sameIdSet(record.component_versions, current.component_versions) && // The TamperWard build (version+commit): a different TamperWard build is a different binding.
22966
+ record.tamperward_version === current.tamperward_version && // Adapter capability hash — REQUIRED (fail-closed) on both sides. A `null` recorded hash
22967
+ // (unrecoverable, never fabricated) can never equal a concrete current hash.
22968
+ record.adapter_capability_hash !== null && current.adapter_capability_hash !== null && record.adapter_capability_hash === current.adapter_capability_hash && // The capability surface the probe tested — order-insensitive set comparison.
22969
+ sameIdSet(record.tested_capabilities, current.tested_capabilities);
22970
+ }
22971
+ function matchRetainedEvidence(current, catalogue = RETAINED_EVIDENCE) {
22972
+ return catalogue.find((r) => evidenceBindingMatches(r.binding, current)) ?? null;
22973
+ }
22974
+ var RETAINED_EVIDENCE;
22975
+ var init_evidence = __esm({
22976
+ "src/adapters/evidence.ts"() {
22977
+ "use strict";
22978
+ RETAINED_EVIDENCE = [
22979
+ {
22980
+ ref: "copilot-sdk-capture-2026-09-20",
22981
+ source: "harness/adapters/copilot-sdk/evidence/capture-2026-09-20.json",
22982
+ source_artifact_sha256: "c46fba989dc0e772012c5fdb1291cf099326d24da2b551788be2e1e85113e49a",
22983
+ binding: {
22984
+ runtime_id: "github-copilot-sdk-hosted",
22985
+ runtime_version: "copilot-runtime@1.0.85",
22986
+ component_versions: ["@github/copilot-sdk@1.0.14", "copilot-protocol@3"],
22987
+ // The capture's own provenance: tamperward@2.31.0+3671a5e6 (capture-2026-09-20.json).
22988
+ tamperward_version: "2.31.0+3671a5e6",
22989
+ // NOT authentically recoverable from the committed capture: the sanitized artifact records
22990
+ // no adapter capability-declaration hash. We do NOT invent a historical hash. `null` makes
22991
+ // the applicability match fail CLOSED (a null recorded hash can never equal a concrete
22992
+ // current hash), so this record can never promote — the honest, safe outcome.
22993
+ adapter_capability_hash: null,
22994
+ // The capability ids the capture actually observed (the `observations` below).
22995
+ tested_capabilities: ["pre-deny:shell", "hook-not-invoked", "transport:timeout"],
22996
+ model: "gpt-5.4",
22997
+ platform: "darwin-arm64",
22998
+ execution_mode: "headless",
22999
+ hook_config_hash: null
23000
+ },
23001
+ observations: [
23002
+ {
23003
+ id: "pre-deny:shell",
23004
+ result: "proven",
23005
+ detail: "returned-reject path (permission.completed result.kind denied): a rejected permission decision blocked shell tool dispatch \u2014 protected state not mutated (shell-pre-deny observation, boundary seq 2 < completion seq 3)"
23006
+ },
23007
+ {
23008
+ id: "hook-not-invoked",
23009
+ result: "proven",
23010
+ detail: "callback-failure path: a broken permission callback (sync-throw, reject and adapter-throw, independently) failed CLOSED \u2014 the tool did not dispatch and protected state stayed intact"
23011
+ },
23012
+ {
23013
+ id: "transport:timeout",
23014
+ result: "inconclusive",
23015
+ detail: "callback-timeout path: a hung permission handler emits no permission.completed (the pinned SDK has no handler timeout), so non-dispatch is intrinsically unobservable \u2014 timeout is not proven either way"
23016
+ }
23017
+ ]
23018
+ }
23019
+ ];
23020
+ }
23021
+ });
23022
+
23023
+ // src/runtime-qualification.ts
23024
+ import { createHash as createHash17 } from "node:crypto";
23025
+ function includesKind(set, kind) {
23026
+ return set.includes(kind);
23027
+ }
23028
+ function find(unsupported, re) {
23029
+ return unsupported.find((u) => re.test(u));
23030
+ }
23031
+ function indexEvidence(evidence) {
23032
+ const m = /* @__PURE__ */ new Map();
23033
+ if (!evidence) return m;
23034
+ for (const o of evidence.observations) m.set(o.id, o);
23035
+ return m;
23036
+ }
23037
+ function assessFromEvidence(id, index, evidence) {
23038
+ const obs = index.get(id);
23039
+ if (!obs || !evidence) return null;
23040
+ const state = obs.result === "proven" ? "PROVEN" : obs.result === "fail-open" ? "FAIL-OPEN" : "INCONCLUSIVE";
23041
+ return {
23042
+ id,
23043
+ state,
23044
+ evidence: {
23045
+ source: "committed-evidence",
23046
+ detail: `${obs.detail} [retained evidence ${evidence.ref} \xB7 ${evidence.source} \xB7 sha256 ${evidence.source_artifact_sha256.slice(0, 12)}]`
23047
+ }
23048
+ };
23049
+ }
23050
+ function assessPreDeny(caps, id, kind, kindNote, index, evidence) {
23051
+ const proven = assessFromEvidence(id, index, evidence);
23052
+ if (proven) return proven;
23053
+ if (includesKind(caps.preDeny, kind)) {
23054
+ return {
23055
+ id,
23056
+ state: "PARTIAL",
23057
+ evidence: {
23058
+ source: "adapter-declaration",
23059
+ detail: `declared preDeny includes '${kind}' (${kindNote}); the contract denies it at the adapter boundary, but no retained real-runtime probe proves this runtime/version invokes and honors the hook live`
23060
+ }
23061
+ };
23062
+ }
23063
+ const note = find(caps.unsupported, PRE_DENY_UNPROVEN_RE);
23064
+ if (note) return { id, state: "UNPROVEN", evidence: { source: "adapter-unsupported", detail: note } };
23065
+ return {
23066
+ id,
23067
+ state: "UNPROVEN",
23068
+ evidence: {
23069
+ source: "not-declared",
23070
+ detail: `operation kind '${kind}' (${kindNote}) is absent from the declared preDeny set and unnamed in unsupported`
23071
+ }
23072
+ };
23073
+ }
23074
+ function assessTransport(caps, id, index, evidence) {
23075
+ const observed = assessFromEvidence(id, index, evidence);
23076
+ if (observed) return observed;
23077
+ const kw = TRANSPORT_KEYWORDS[id];
23078
+ const failOpen = caps.unsupported.find((u) => FAIL_OPEN_RE.test(u) && kw.test(u));
23079
+ if (failOpen) return { id, state: "FAIL-OPEN", evidence: { source: "adapter-unsupported", detail: failOpen } };
23080
+ const unproven = find(caps.unsupported, TRANSPORT_UNPROVEN_RE);
23081
+ if (unproven) return { id, state: "UNPROVEN", evidence: { source: "adapter-unsupported", detail: unproven } };
23082
+ return {
23083
+ id,
23084
+ state: "PARTIAL",
23085
+ evidence: {
23086
+ source: "contract",
23087
+ detail: "the neutral steering contract maps this transport failure to a fail-closed deny at the adapter boundary (failsClosed); full live-runtime honoring across execution modes is not separately proven by static qualification"
23088
+ }
23089
+ };
23090
+ }
23091
+ function assessCapabilities(caps, evidence) {
23092
+ const out3 = [];
23093
+ const ev = indexEvidence(evidence);
23094
+ const fromEvidence = (id) => assessFromEvidence(id, ev, evidence);
23095
+ out3.push(assessPreDeny(caps, "pre-deny:shell", "shell", "a shell/exec tool call", ev, evidence));
23096
+ out3.push(assessPreDeny(caps, "pre-deny:native-edit", "file-edit", "a native file-edit tool", ev, evidence));
23097
+ out3.push(assessPreDeny(caps, "pre-deny:delete", "file-edit", "a delete proposed as a file-edit/native tool", ev, evidence));
23098
+ out3.push(assessPreDeny(caps, "pre-deny:rename", "file-edit", "a rename proposed as a file-edit/native tool", ev, evidence));
23099
+ out3.push(assessPreDeny(caps, "pre-deny:git-mutation", "shell", "a git mutation proposed as a shell command", ev, evidence));
23100
+ out3.push(assessPreDeny(caps, "pre-deny:mcp", "mcp", "an MCP tool call", ev, evidence));
23101
+ const postObserveEvidence = fromEvidence("post-observe");
23102
+ if (postObserveEvidence) {
23103
+ out3.push(postObserveEvidence);
23104
+ } else if (caps.postObserve.length > 0) {
23105
+ out3.push({
23106
+ id: "post-observe",
23107
+ state: "PARTIAL",
23108
+ evidence: { source: "adapter-declaration", detail: `declared postObserve: [${[...caps.postObserve].sort().join(", ")}]; no retained real-runtime probe proves live post-action observation on this runtime/version` }
23109
+ });
23110
+ } else {
23111
+ const note = find(caps.unsupported, POST_ACTION_RE);
23112
+ out3.push(
23113
+ note ? { id: "post-observe", state: "UNSUPPORTED", evidence: { source: "adapter-unsupported", detail: note } } : {
23114
+ id: "post-observe",
23115
+ state: "UNPROVEN",
23116
+ evidence: { source: "not-declared", detail: "no post-action observation kind is declared and none is named unsupported" }
23117
+ }
23118
+ );
23119
+ }
23120
+ const endOfTurnEvidence = fromEvidence("end-of-turn");
23121
+ if (endOfTurnEvidence) {
23122
+ out3.push(endOfTurnEvidence);
23123
+ } else if (!caps.endOfTurn) {
23124
+ out3.push({ id: "end-of-turn", state: "UNSUPPORTED", evidence: { source: "adapter-declaration", detail: "endOfTurn is declared false" } });
23125
+ } else {
23126
+ const caveat = find(caps.unsupported, LIFECYCLE_ONLY_RE);
23127
+ out3.push(
23128
+ caveat ? { id: "end-of-turn", state: "PARTIAL", evidence: { source: "adapter-unsupported", detail: caveat } } : { id: "end-of-turn", state: "PARTIAL", evidence: { source: "adapter-declaration", detail: "endOfTurn is declared true (a stop event can run the mandatory sweep); no retained real-runtime probe proves this runtime/version delivers and honors the stop sweep live" } }
23129
+ );
23130
+ }
23131
+ const hasDenyPath = caps.endOfTurn || caps.preDeny.length > 0;
23132
+ out3.push(
23133
+ hasDenyPath ? {
23134
+ id: "denial-reason-delivery",
23135
+ state: "PARTIAL",
23136
+ evidence: {
23137
+ source: "contract",
23138
+ detail: "a deny carries a human/agent-facing reason on the wire (SteeringDecision.reason); that the runtime delivers it to the agent is not separately proven by static qualification"
23139
+ }
23140
+ } : {
23141
+ id: "denial-reason-delivery",
23142
+ state: "UNPROVEN",
23143
+ evidence: { source: "not-declared", detail: "the adapter declares no pre-deny or end-of-turn deny path, so no reason is delivered" }
23144
+ }
23145
+ );
23146
+ out3.push({
23147
+ id: "continue-after-denial",
23148
+ state: "UNPROVEN",
23149
+ evidence: { source: "not-declared", detail: "whether the agent can continue after a denial is not encoded in the neutral capability declaration and requires live evidence" }
23150
+ });
23151
+ for (const id of ["transport:missing-executable", "transport:non-zero", "transport:timeout", "transport:malformed", "transport:empty"]) {
23152
+ out3.push(assessTransport(caps, id, ev, evidence));
23153
+ }
23154
+ out3.push(
23155
+ fromEvidence("hook-not-invoked") ?? {
23156
+ id: "hook-not-invoked",
23157
+ state: "PARTIAL",
23158
+ evidence: {
23159
+ source: "contract",
23160
+ detail: "the neutral contract fails a required not-invoked hook CLOSED (failsClosed); that a configured hook is actually invoked/honored on this runtime/version is not proven by static qualification"
23161
+ }
23162
+ }
23163
+ );
23164
+ out3.push(
23165
+ caps.endOfTurn ? {
23166
+ id: "detached/quiescence",
23167
+ state: "PARTIAL",
23168
+ evidence: {
23169
+ source: "contract",
23170
+ detail: "the end-of-turn sweep reconciles the net tree at turn end, catching a detached/deferred effect the in-loop phase could only observe; continuous in-turn quiescence is not an in-loop guarantee"
23171
+ }
23172
+ } : {
23173
+ id: "detached/quiescence",
23174
+ state: "UNPROVEN",
23175
+ evidence: { source: "not-declared", detail: "no end-of-turn sweep is declared, so a detached/deferred effect is not reconciled in-loop" }
23176
+ }
23177
+ );
23178
+ return out3;
23179
+ }
23180
+ function aggregateInLoop(assessments) {
23181
+ const byId = new Map(assessments.map((a) => [a.id, a]));
23182
+ const required = REQUIRED_IN_LOOP_IDS.map((id) => byId.get(id)).filter((a) => a !== void 0);
23183
+ const anyDangerous = required.some((a) => a.state === "FAIL-OPEN" || a.state === "INCONCLUSIVE");
23184
+ const present = (id) => {
23185
+ const s = byId.get(id)?.state;
23186
+ return s === "PROVEN" || s === "PARTIAL";
23187
+ };
23188
+ const anyPreDenyPresent = PRE_DENY_IDS.some(present);
23189
+ const endOfTurnPresent = present("end-of-turn");
23190
+ if (!anyPreDenyPresent && !endOfTurnPresent) return "NONE";
23191
+ if (!anyDangerous && required.every((a) => a.state === "PROVEN")) return "FULL";
23192
+ return "PARTIAL";
23193
+ }
23194
+ function sha16(s) {
23195
+ return createHash17("sha256").update(s).digest("hex").slice(0, 16);
23196
+ }
23197
+ function capabilityHash(caps) {
23198
+ const stable = JSON.stringify({
23199
+ preDeny: [...caps.preDeny].sort(),
23200
+ postObserve: [...caps.postObserve].sort(),
23201
+ endOfTurn: caps.endOfTurn,
23202
+ unsupported: [...caps.unsupported].sort()
23203
+ });
23204
+ return sha16(stable);
23205
+ }
23206
+ function loadBearing(b) {
23207
+ return {
23208
+ "runtime.id": b.runtime.id,
23209
+ "runtime.version": b.runtime.version ?? "\u2205",
23210
+ "tamperward.version": b.tamperward.version,
23211
+ "tamperward.commit": b.tamperward.commit ?? "\u2205",
23212
+ "adapter.capability_hash": b.adapter.capability_hash,
23213
+ "hook_config_hash": b.hook_config_hash ?? "\u2205",
23214
+ "execution_mode": b.execution_mode,
23215
+ "platform": b.platform,
23216
+ "model": b.model ?? "\u2205",
23217
+ "tested_capabilities": [...b.tested_capabilities].sort().join(",")
23218
+ };
23219
+ }
23220
+ function qualificationStaleness(stored, current) {
23221
+ const a = loadBearing(stored);
23222
+ const b = loadBearing(current);
23223
+ const changed = [];
23224
+ for (const key2 of Object.keys(b)) {
23225
+ if (a[key2] !== b[key2]) changed.push(`${key2}: ${a[key2]} \u2192 ${b[key2]}`);
23226
+ }
23227
+ return { stale: changed.length > 0, changed };
23228
+ }
23229
+ function evidenceId(binding, assessments) {
23230
+ const projection = {
23231
+ load_bearing: loadBearing(binding),
23232
+ states: assessments.map((a) => [a.id, a.state])
23233
+ };
23234
+ return sha16(JSON.stringify(projection));
23235
+ }
23236
+ var CAPABILITY_STATES, RUNTIME_CAPABILITY_IDS, EVIDENCE_SOURCES, FINAL_AUTHORITY, FAIL_OPEN_RE, PRE_DENY_UNPROVEN_RE, TRANSPORT_UNPROVEN_RE, LIFECYCLE_ONLY_RE, POST_ACTION_RE, TRANSPORT_KEYWORDS, PRE_DENY_IDS, REQUIRED_IN_LOOP_IDS;
23237
+ var init_runtime_qualification = __esm({
23238
+ "src/runtime-qualification.ts"() {
23239
+ "use strict";
23240
+ CAPABILITY_STATES = [
23241
+ "PROVEN",
23242
+ "PARTIAL",
23243
+ "UNPROVEN",
23244
+ "UNSUPPORTED",
23245
+ "FAIL-OPEN",
23246
+ "INCONCLUSIVE"
23247
+ ];
23248
+ RUNTIME_CAPABILITY_IDS = [
23249
+ "pre-deny:shell",
23250
+ "pre-deny:native-edit",
23251
+ "pre-deny:delete",
23252
+ "pre-deny:rename",
23253
+ "pre-deny:git-mutation",
23254
+ "pre-deny:mcp",
23255
+ "post-observe",
23256
+ "end-of-turn",
23257
+ "denial-reason-delivery",
23258
+ "continue-after-denial",
23259
+ "transport:missing-executable",
23260
+ "transport:non-zero",
23261
+ "transport:timeout",
23262
+ "transport:malformed",
23263
+ "transport:empty",
23264
+ "hook-not-invoked",
23265
+ "detached/quiescence"
23266
+ ];
23267
+ EVIDENCE_SOURCES = [
23268
+ "adapter-declaration",
23269
+ // structural preDeny / postObserve / endOfTurn membership
23270
+ "adapter-unsupported",
23271
+ // named verbatim in the adapter's `unsupported[]` prose
23272
+ "contract",
23273
+ // the neutral steering contract guarantees this at the adapter boundary
23274
+ "committed-evidence",
23275
+ // a committed real-runtime evidence record proved it
23276
+ "not-declared"
23277
+ // absent from every declaration — no proof either way
23278
+ ];
23279
+ FINAL_AUTHORITY = "AVAILABLE";
23280
+ FAIL_OPEN_RE = /fails?\s+open/i;
23281
+ PRE_DENY_UNPROVEN_RE = /pre-action deny|pre-deny|shell pre-deny/i;
23282
+ TRANSPORT_UNPROVEN_RE = /(?:fail-?closed|transport)[^.]*\b(?:not (?:yet )?proven|unproven|unmeasured)/i;
23283
+ LIFECYCLE_ONLY_RE = /lifecycle continuation|not a filesystem veto|only block turn completion|block turn completion and force continuation/i;
23284
+ POST_ACTION_RE = /post-action|post-observe|posttooluse|stop sweep is the post-turn/i;
23285
+ TRANSPORT_KEYWORDS = {
23286
+ "transport:missing-executable": /missing exec|executable|enoent|not found/i,
23287
+ "transport:non-zero": /non-?zero|exit code|exit 2|non-?2xx/i,
23288
+ "transport:timeout": /time-?out|timed[- ]out/i,
23289
+ "transport:malformed": /malformed|parse/i,
23290
+ "transport:empty": /\bempty\b/i
23291
+ };
23292
+ PRE_DENY_IDS = [
23293
+ "pre-deny:shell",
23294
+ "pre-deny:native-edit",
23295
+ "pre-deny:delete",
23296
+ "pre-deny:rename",
23297
+ "pre-deny:git-mutation",
23298
+ "pre-deny:mcp"
23299
+ ];
23300
+ REQUIRED_IN_LOOP_IDS = [
23301
+ ...PRE_DENY_IDS,
23302
+ "end-of-turn",
23303
+ "transport:missing-executable",
23304
+ "transport:non-zero",
23305
+ "transport:timeout",
23306
+ "transport:malformed",
23307
+ "transport:empty",
23308
+ "hook-not-invoked"
23309
+ ];
23310
+ }
23311
+ });
23312
+
23313
+ // src/cli/runtime.ts
23314
+ import { execFileSync as execFileSync16 } from "node:child_process";
23315
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync29, writeFileSync as writeFileSync16 } from "node:fs";
23316
+ import { join as join30, dirname as dirname15 } from "node:path";
23317
+ function storePath(cwd) {
23318
+ const dir = gitDir(cwd);
23319
+ return dir ? join30(dir, "tamperward", "runtime-qualification.json") : null;
23320
+ }
23321
+ function readStore(cwd) {
23322
+ const path = storePath(cwd);
23323
+ if (!path || !existsSync22(path)) return null;
23324
+ try {
23325
+ const parsed = JSON.parse(readFileSync29(path, "utf8"));
23326
+ if (parsed && typeof parsed === "object" && parsed.records && typeof parsed.records === "object") return parsed;
23327
+ } catch {
23328
+ return null;
23329
+ }
23330
+ return null;
23331
+ }
23332
+ function isRec(v) {
23333
+ return typeof v === "object" && v !== null && !Array.isArray(v);
23334
+ }
23335
+ function asStringOrNull(v) {
23336
+ return v === null || typeof v === "string" ? v : void 0;
23337
+ }
23338
+ function validateStoredReport(value) {
23339
+ if (!isRec(value)) return { reason: "record is not an object" };
23340
+ if (value.schema_version !== 1) return { reason: "unrecognized schema_version" };
23341
+ if (value.command !== "runtime") return { reason: 'command is not "runtime"' };
23342
+ const subcommand = value.subcommand;
23343
+ if (subcommand !== "verify" && subcommand !== "status") return { reason: "invalid subcommand" };
23344
+ const recorded = value.recorded;
23345
+ const stale = value.stale;
23346
+ if (typeof recorded !== "boolean" || typeof stale !== "boolean") return { reason: "invalid recorded/stale flags" };
23347
+ const changedInputs = value.changed_inputs;
23348
+ if (!Array.isArray(changedInputs) || !changedInputs.every((s) => typeof s === "string")) return { reason: "invalid changed_inputs" };
23349
+ const runtime = value.runtime;
23350
+ if (!isRec(runtime)) return { reason: "invalid runtime binding" };
23351
+ const rId = runtime.id;
23352
+ const rLabel = runtime.label;
23353
+ const rVersion = asStringOrNull(runtime.version);
23354
+ if (typeof rId !== "string" || !rId || typeof rLabel !== "string" || !rLabel || rVersion === void 0)
23355
+ return { reason: "invalid runtime binding" };
23356
+ const tw = value.tamperward;
23357
+ if (!isRec(tw)) return { reason: "invalid tamperward binding" };
23358
+ const twVersion = tw.version;
23359
+ const twCommit = asStringOrNull(tw.commit);
23360
+ if (typeof twVersion !== "string" || !twVersion || twCommit === void 0) return { reason: "invalid tamperward binding" };
23361
+ const adapter = value.adapter;
23362
+ if (!isRec(adapter)) return { reason: "invalid adapter binding" };
23363
+ const adName = adapter.name;
23364
+ const adHash = adapter.capability_hash;
23365
+ if (typeof adName !== "string" || !adName || typeof adHash !== "string" || !adHash) return { reason: "invalid adapter binding" };
23366
+ const hookConfigHash = asStringOrNull(value.hook_config_hash);
23367
+ if (hookConfigHash === void 0) return { reason: "invalid hook_config_hash" };
23368
+ const executionMode = value.execution_mode;
23369
+ if (executionMode !== "headless" && executionMode !== "interactive") return { reason: "invalid execution_mode" };
23370
+ const platform = value.platform;
23371
+ if (typeof platform !== "string" || !platform) return { reason: "invalid platform" };
23372
+ const model = asStringOrNull(value.model);
23373
+ if (model === void 0) return { reason: "invalid model" };
23374
+ const testedCapabilities = value.tested_capabilities;
23375
+ if (!Array.isArray(testedCapabilities) || !testedCapabilities.every((c) => typeof c === "string" && CAP_ID_SET.has(c)))
23376
+ return { reason: "invalid tested_capabilities" };
23377
+ const timestamp = value.timestamp;
23378
+ if (typeof timestamp !== "string" || !timestamp) return { reason: "invalid timestamp" };
23379
+ const storedEvidenceId = value.evidence_id;
23380
+ if (typeof storedEvidenceId !== "string" || !storedEvidenceId) return { reason: "invalid evidence_id" };
23381
+ if (!Array.isArray(value.capabilities)) return { reason: "capabilities is not an array" };
23382
+ const capabilities = [];
23383
+ for (const c of value.capabilities) {
23384
+ if (!isRec(c)) return { reason: "invalid capability entry" };
23385
+ const id = RUNTIME_CAPABILITY_IDS.find((x) => x === c.id);
23386
+ const state = CAPABILITY_STATES.find((x) => x === c.state);
23387
+ if (!id || !state) return { reason: "invalid capability entry" };
23388
+ const ev = c.evidence;
23389
+ if (!isRec(ev)) return { reason: "invalid capability evidence" };
23390
+ const source = EVIDENCE_SOURCES.find((x) => x === ev.source);
23391
+ const detail = ev.detail;
23392
+ if (!source || typeof detail !== "string") return { reason: "invalid capability evidence" };
23393
+ capabilities.push({ id, state, evidence: { source, detail } });
23394
+ }
23395
+ const inLoop = value.in_loop_protection;
23396
+ if (inLoop !== "FULL" && inLoop !== "PARTIAL" && inLoop !== "NONE") return { reason: "invalid in_loop_protection" };
23397
+ if (value.final_authority !== FINAL_AUTHORITY) return { reason: "invalid final_authority" };
23398
+ const note = value.note;
23399
+ if (typeof note !== "string") return { reason: "invalid note" };
23400
+ const base = {
23401
+ runtime: { id: rId, label: rLabel, version: rVersion },
23402
+ tamperward: { version: twVersion, commit: twCommit },
23403
+ adapter: { name: adName, capability_hash: adHash },
23404
+ hook_config_hash: hookConfigHash,
23405
+ execution_mode: executionMode,
23406
+ platform,
23407
+ model,
23408
+ tested_capabilities: testedCapabilities
23409
+ };
23410
+ const recomputedId = evidenceId(base, capabilities);
23411
+ if (recomputedId !== storedEvidenceId) return { reason: `evidence_id mismatch (recorded ${storedEvidenceId}, recomputed ${recomputedId})` };
23412
+ const recomputedAgg = aggregateInLoop(capabilities);
23413
+ if (inLoop !== recomputedAgg) return { reason: `in_loop_protection mismatch (recorded ${inLoop}, recomputed ${recomputedAgg})` };
23414
+ const report2 = {
23415
+ schema_version: 1,
23416
+ command: "runtime",
23417
+ subcommand,
23418
+ recorded,
23419
+ stale,
23420
+ changed_inputs: changedInputs,
23421
+ runtime: base.runtime,
23422
+ tamperward: base.tamperward,
23423
+ adapter: base.adapter,
23424
+ hook_config_hash: hookConfigHash,
23425
+ execution_mode: executionMode,
23426
+ platform,
23427
+ model,
23428
+ tested_capabilities: testedCapabilities,
23429
+ timestamp,
23430
+ evidence_id: storedEvidenceId,
23431
+ capabilities,
23432
+ in_loop_protection: inLoop,
23433
+ final_authority: FINAL_AUTHORITY,
23434
+ note
23435
+ };
23436
+ return { report: report2 };
23437
+ }
23438
+ function writeRecord(cwd, id, report2) {
23439
+ const path = storePath(cwd);
23440
+ if (!path) return false;
23441
+ const existing = readStore(cwd);
23442
+ const store = existing ?? { schema_version: report2.schema_version, updated_at: "", records: {} };
23443
+ store.records[id] = report2;
23444
+ store.updated_at = report2.timestamp;
23445
+ try {
23446
+ mkdirSync13(dirname15(path), { recursive: true });
23447
+ writeFileSync16(path, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
23448
+ return true;
23449
+ } catch {
23450
+ return false;
23451
+ }
23452
+ }
23453
+ function resolveRuntimeVersion(canonicalId) {
23454
+ const override = process.env.TAMPERWARD_RUNTIME_VERSION;
23455
+ if (override && override.trim()) return override.trim();
23456
+ const bins = {
23457
+ "claude-code": "claude",
23458
+ codex: "codex",
23459
+ "github-copilot-cli": "copilot"
23460
+ };
23461
+ const bin = bins[canonicalId];
23462
+ if (!bin) return null;
23463
+ try {
23464
+ const out3 = execFileSync16(bin, ["--version"], { encoding: "utf8", timeout: 3e3, stdio: ["ignore", "pipe", "ignore"] });
23465
+ const m = out3.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
23466
+ return m ? m[0] : out3.trim() || null;
23467
+ } catch {
23468
+ return null;
23469
+ }
23470
+ }
23471
+ function tamperwardBuildTag(version, commit) {
23472
+ return commit ? `${version}+${commit.slice(0, 8)}` : version;
23473
+ }
23474
+ function resolveComponentVersions(_canonicalId) {
23475
+ return [];
23476
+ }
23477
+ function resolveHookConfigHash(canonicalId, cwd) {
23478
+ if (canonicalId === "claude-code") {
23479
+ return sha16(JSON.stringify(interventionWiring(cwd)));
23480
+ }
23481
+ const files = {
23482
+ "github-copilot-cli": join30(cwd, ".github", "hooks", "tamperward.json")
23483
+ };
23484
+ const file = files[canonicalId];
23485
+ if (!file || !existsSync22(file)) return null;
23486
+ try {
23487
+ const raw = readFileSync29(file, "utf8");
23488
+ let hooks = raw;
23489
+ try {
23490
+ const parsed = JSON.parse(raw);
23491
+ hooks = parsed && typeof parsed === "object" && "hooks" in parsed ? parsed.hooks : parsed;
23492
+ } catch {
23493
+ }
23494
+ return sha16(JSON.stringify(hooks ?? null));
23495
+ } catch {
23496
+ return null;
23497
+ }
23498
+ }
23499
+ function buildQualification(adapter, opts, cwd) {
23500
+ const canonical2 = adapter.name;
23501
+ const capHash = capabilityHash(adapter.capabilities);
23502
+ const mode = opts.mode ?? "headless";
23503
+ const version = resolveRuntimeVersion(canonical2);
23504
+ const platform = `${process.platform}-${process.arch}`;
23505
+ const model = opts.model ?? null;
23506
+ const hookConfigHash = resolveHookConfigHash(canonical2, cwd);
23507
+ const commit = TW_COMMIT;
23508
+ const testedRaw = [...RUNTIME_CAPABILITY_IDS];
23509
+ const matchKey = {
23510
+ runtime_id: canonical2,
23511
+ runtime_version: version,
23512
+ // No live resolver for a runtime's SDK/protocol component versions ships yet, so the
23513
+ // current side is honestly the empty (unresolved) set. An unresolved component set never
23514
+ // matches a probed non-empty one — it fails closed, never promotes. We do not fabricate one.
23515
+ component_versions: resolveComponentVersions(canonical2),
23516
+ tamperward_version: tamperwardBuildTag(TW_VERSION, commit),
23517
+ adapter_capability_hash: capHash,
23518
+ tested_capabilities: testedRaw,
23519
+ model,
23520
+ platform,
23521
+ execution_mode: mode,
23522
+ hook_config_hash: hookConfigHash
23523
+ };
23524
+ const evidence = matchRetainedEvidence(matchKey);
23525
+ const assessments = assessCapabilities(adapter.capabilities, evidence);
23526
+ const base = {
23527
+ runtime: { id: canonical2, label: labelFor(canonical2), version },
23528
+ tamperward: { version: TW_VERSION, commit },
23529
+ adapter: { name: canonical2, capability_hash: capHash },
23530
+ hook_config_hash: hookConfigHash,
23531
+ execution_mode: mode,
23532
+ platform,
23533
+ model,
23534
+ tested_capabilities: testedRaw
23535
+ };
23536
+ const eid = evidenceId(base, assessments);
23537
+ const binding = { ...base, timestamp: (/* @__PURE__ */ new Date()).toISOString(), evidence_id: eid };
23538
+ return { binding, assessments };
23539
+ }
23540
+ function reportFrom(subcommand, binding, assessments) {
23541
+ return machineOutput({
23542
+ command: "runtime",
23543
+ subcommand,
23544
+ recorded: true,
23545
+ stale: false,
23546
+ changed_inputs: [],
23547
+ runtime: binding.runtime,
23548
+ tamperward: binding.tamperward,
23549
+ adapter: binding.adapter,
23550
+ hook_config_hash: binding.hook_config_hash,
23551
+ execution_mode: binding.execution_mode,
23552
+ platform: binding.platform,
23553
+ model: binding.model,
23554
+ tested_capabilities: binding.tested_capabilities,
23555
+ timestamp: binding.timestamp,
23556
+ evidence_id: binding.evidence_id,
23557
+ capabilities: assessments,
23558
+ in_loop_protection: aggregateInLoop(assessments),
23559
+ final_authority: FINAL_AUTHORITY,
23560
+ note: HONESTY_NOTE
23561
+ });
23562
+ }
23563
+ function resolveTarget(opts, cwd) {
23564
+ if (opts.runtime) {
23565
+ const a = adapterFor(opts.runtime);
23566
+ return a ?? { error: `no shipped runtime adapter for "${opts.runtime}"` };
23567
+ }
23568
+ const detected = detectRuntimes(cwd);
23569
+ for (const rt of detected) {
23570
+ const a = adapterFor(rt.id);
23571
+ if (a) return a;
23572
+ }
23573
+ const reason = detected.length > 0 ? "no adapter-backed runtime detected in this repository" : "no runtime detected in this repository";
23574
+ return { error: `${reason}; pass --runtime <id> to qualify a specific runtime` };
23575
+ }
23576
+ function renderText3(report2, cwd) {
23577
+ const colour = colourEnabled(process.env, process.stdout);
23578
+ const w = (s) => process.stdout.write(s + "\n");
23579
+ const dim = (s) => paint(s, DIM, colour);
23580
+ w(`${paint(report2.runtime.label, BOLD, colour)} \u2014 runtime protection qualification`);
23581
+ w(dim(` runtime ${report2.runtime.version ?? "(version unresolved)"} \xB7 tamperward ${report2.tamperward.version} \xB7 adapter ${report2.adapter.capability_hash} \xB7 ${report2.execution_mode} \xB7 ${report2.platform}`));
23582
+ w(dim(` evidence ${report2.evidence_id} \xB7 ${report2.timestamp}`));
23583
+ if (!report2.recorded) {
23584
+ w("");
23585
+ w(`${paint("[UNQUALIFIED]", severityColour("bad") + BOLD, colour)} no qualification recorded for this runtime.`);
23586
+ if (report2.note) w(dim(` ${report2.note.replace(/—/g, "-")}`));
23587
+ w(`Run: ${paint("tamperward runtime verify", BOLD, colour)}`);
23588
+ return;
23589
+ }
23590
+ if (report2.stale) {
23591
+ w("");
23592
+ w(`${paint("[STALE]", severityColour("bad") + BOLD, colour)} a load-bearing input changed \u2014 this qualification no longer applies:`);
23593
+ for (const c of report2.changed_inputs) w(` \xB7 ${c}`);
23594
+ w(`Run: ${paint("tamperward runtime verify", BOLD, colour)}`);
23595
+ }
23596
+ w("");
23597
+ const width = Math.max(...report2.capabilities.map((c) => c.id.length));
23598
+ for (const cap of report2.capabilities) {
23599
+ const sev = STATE_SEVERITY[cap.state];
23600
+ const tag = paint(cap.state.padEnd(12), (sev === "bad" ? BOLD : "") + severityColour(sev), colour);
23601
+ w(` ${cap.id.padEnd(width)} ${tag}`);
23602
+ }
23603
+ w("");
23604
+ const agg = report2.in_loop_protection;
23605
+ const aggTag = paint(agg, (AGGREGATE_SEVERITY[agg] === "bad" ? BOLD : "") + severityColour(AGGREGATE_SEVERITY[agg]), colour);
23606
+ w(` ${"In-loop protection".padEnd(width)} ${aggTag}`);
23607
+ w(` ${"Final authority".padEnd(width)} ${paint(report2.final_authority, severityColour("ok"), colour)}`);
23608
+ w("");
23609
+ w(dim(" Final authority (CI / pristine verification) is independent of the runtime hook: a weak in-loop"));
23610
+ w(dim(" capability never weakens adjudication. Steering and authority remain separate."));
23611
+ w("");
23612
+ w(dim(" " + report2.note.replace(/—/g, "-")));
23613
+ w("");
23614
+ w(dim(" Evidence:"));
23615
+ for (const cap of report2.capabilities) {
23616
+ w(dim(` ${cap.id} [${cap.state}] (${cap.evidence.source}): ${cap.evidence.detail}`));
23617
+ }
23618
+ }
23619
+ function emit2(report2, opts, cwd) {
23620
+ if (opts.json) {
23621
+ process.stdout.write(JSON.stringify(report2) + "\n");
23622
+ return;
23623
+ }
23624
+ renderText3(report2, cwd);
23625
+ }
23626
+ function runRuntime(sub, opts) {
23627
+ const cwd = opts.cwd ?? process.cwd();
23628
+ if (sub === "verify") {
23629
+ const target = resolveTarget(opts, cwd);
23630
+ if ("error" in target) {
23631
+ process.stderr.write(`tamperward: ${target.error}
23632
+ `);
23633
+ return 2;
23634
+ }
23635
+ const { binding, assessments } = buildQualification(target, opts, cwd);
23636
+ const report2 = reportFrom("verify", binding, assessments);
23637
+ const wrote = writeRecord(cwd, target.name, report2);
23638
+ if (!wrote) {
23639
+ const dest = storePath(cwd);
23640
+ const reason = `could not persist the qualification to ${dest ?? "the git-local store (no repository found)"}; nothing was recorded, so \`tamperward runtime status\` will report UNQUALIFIED`;
23641
+ const failure = {
23642
+ ...report2,
23643
+ recorded: false,
23644
+ capabilities: [],
23645
+ in_loop_protection: "NONE",
23646
+ note: reason
23647
+ };
23648
+ emit2(failure, opts, cwd);
23649
+ process.stderr.write(`tamperward: ${reason}
23650
+ `);
23651
+ return 1;
23652
+ }
23653
+ emit2(report2, opts, cwd);
23654
+ return 0;
23655
+ }
23656
+ if (sub === "status") {
23657
+ const target = resolveTarget(opts, cwd);
23658
+ if ("error" in target) {
23659
+ process.stderr.write(`tamperward: ${target.error}
23660
+ `);
23661
+ return 2;
23662
+ }
23663
+ const store = readStore(cwd);
23664
+ const raw = store?.records[target.name];
23665
+ const validated = raw === void 0 ? { reason: "none recorded" } : validateStoredReport(raw);
23666
+ if ("reason" in validated) {
23667
+ const { binding, assessments } = buildQualification(target, opts, cwd);
23668
+ const note = raw === void 0 ? `No qualification recorded for ${binding.runtime.label}. Run: tamperward runtime verify` : `Stored qualification for ${binding.runtime.label} was rejected (${validated.reason}) and treated as unrecorded. Run: tamperward runtime verify`;
23669
+ const empty2 = {
23670
+ ...reportFrom("status", binding, assessments),
23671
+ recorded: false,
23672
+ capabilities: [],
23673
+ in_loop_protection: "NONE",
23674
+ note
23675
+ };
23676
+ emit2(empty2, opts, cwd);
23677
+ return 0;
23678
+ }
23679
+ const stored = validated.report;
23680
+ const { binding: current } = buildQualification(target, opts, cwd);
23681
+ const storedBinding = {
23682
+ runtime: stored.runtime,
23683
+ tamperward: stored.tamperward,
23684
+ adapter: stored.adapter,
23685
+ hook_config_hash: stored.hook_config_hash,
23686
+ execution_mode: stored.execution_mode,
23687
+ platform: stored.platform,
23688
+ model: stored.model,
23689
+ tested_capabilities: stored.tested_capabilities,
23690
+ timestamp: stored.timestamp,
23691
+ evidence_id: stored.evidence_id
23692
+ };
23693
+ const staleness = qualificationStaleness(storedBinding, current);
23694
+ const report2 = {
23695
+ ...stored,
23696
+ subcommand: "status",
23697
+ stale: staleness.stale,
23698
+ changed_inputs: staleness.changed
23699
+ };
23700
+ emit2(report2, opts, cwd);
23701
+ return 0;
23702
+ }
23703
+ process.stderr.write(`tamperward: runtime requires one of verify | status (got "${sub ?? ""}")
23704
+ `);
23705
+ return 2;
23706
+ }
23707
+ function parseRuntime(args) {
23708
+ const [sub, ...rest] = args;
23709
+ const opts = {};
23710
+ for (let i = 0; i < rest.length; i++) {
23711
+ const a = rest[i];
23712
+ if (a === "--cwd") opts.cwd = rest[++i];
23713
+ else if (a === "--runtime") opts.runtime = rest[++i];
23714
+ else if (a === "--model") opts.model = rest[++i];
23715
+ else if (a === "--json") opts.json = true;
23716
+ else if (a === "--mode") {
23717
+ const v = rest[++i];
23718
+ if (v === "headless" || v === "interactive") opts.mode = v;
23719
+ }
23720
+ }
23721
+ return { sub, opts };
23722
+ }
23723
+ var HONESTY_NOTE, CAP_ID_SET, STATE_SEVERITY, AGGREGATE_SEVERITY, RUNTIME_SUBCOMMANDS;
23724
+ var init_runtime = __esm({
23725
+ "src/cli/runtime.ts"() {
23726
+ "use strict";
23727
+ init_build();
23728
+ init_wiring();
23729
+ init_machine_output();
23730
+ init_text();
23731
+ init_status();
23732
+ init_registry();
23733
+ init_runtimes();
23734
+ init_evidence();
23735
+ init_verification_state();
23736
+ init_runtime_qualification();
23737
+ HONESTY_NOTE = "PROVEN is reserved for a capability a retained real-runtime probe observed holding under THIS exact runtime/version/config binding; a declaration alone grades PARTIAL (declared at the contract boundary, unproven live) and an absent capability UNPROVEN/UNSUPPORTED. No live in-process probe runs here \u2014 grading is against committed, sanitized real-runtime evidence, and absent a matching record nothing is PROVEN. This reports existing facts only \u2014 no promotion, no Round 4.1 eligibility claim.";
23738
+ CAP_ID_SET = new Set(RUNTIME_CAPABILITY_IDS);
23739
+ STATE_SEVERITY = {
23740
+ PROVEN: "ok",
23741
+ PARTIAL: "warn",
23742
+ UNPROVEN: "warn",
23743
+ UNSUPPORTED: "info",
23744
+ "FAIL-OPEN": "bad",
23745
+ INCONCLUSIVE: "bad"
23746
+ };
23747
+ AGGREGATE_SEVERITY = { FULL: "ok", PARTIAL: "warn", NONE: "bad" };
23748
+ RUNTIME_SUBCOMMANDS = ["verify", "status"];
23749
+ }
23750
+ });
23751
+
23752
+ // src/cli/signoff-label.ts
23753
+ function runSignoffLabel(opts) {
23754
+ if (!opts.rule) {
23755
+ process.stderr.write("tamperward signoff-label --rule <rule> --head <full-head-sha> [--file <path>]\n");
23756
+ return 2;
23757
+ }
23758
+ if (!opts.head) {
23759
+ process.stderr.write("tamperward: --head is required and must be a full 40- or 64-character object id.\n");
23760
+ return 2;
23761
+ }
23762
+ const want = opts.file ? `${opts.rule}:${opts.file}` : opts.rule;
23763
+ const token = compactOobToken(want, opts.head);
23764
+ if (!token) {
23765
+ process.stderr.write("tamperward: --head must be a full 40- or 64-character hexadecimal object id.\n");
23766
+ return 2;
23767
+ }
23768
+ process.stdout.write(`${token}
23769
+ `);
23770
+ return 0;
23771
+ }
23772
+ var init_signoff_label = __esm({
23773
+ "src/cli/signoff-label.ts"() {
23774
+ "use strict";
23775
+ init_signoff();
23776
+ }
23777
+ });
23778
+
23779
+ // src/cli/main.ts
23780
+ var main_exports = {};
23781
+ __export(main_exports, {
23782
+ guardedMain: () => guardedMain,
23783
+ main: () => main,
23784
+ runHookFromRaw: () => runHookFromRaw,
23785
+ validateCliArgs: () => validateCliArgs
23786
+ });
23787
+ function parseAllow(args) {
23788
+ const o = {};
23789
+ for (let i = 0; i < args.length; i++) {
23790
+ const a = args[i];
23791
+ if (a === "--file") o.file = args[++i];
23792
+ else if (a === "--reason") o.reason = args[++i];
23793
+ else if (a === "--cwd") o.cwd = args[++i];
23794
+ else if (!a.startsWith("--") && !o.rule) o.rule = a;
23795
+ }
23796
+ return o;
23797
+ }
23798
+ function runAgentCommand(kind, args) {
23799
+ const agent = args[0];
23800
+ if (agent !== "claude") {
23801
+ process.stderr.write(`tamperward: unsupported ${kind} agent "${agent ?? ""}" (only "claude" so far)
23802
+ `);
23803
+ return 2;
23804
+ }
23805
+ return kind === "hook" ? runHookClaude() : runSweepClaude();
23806
+ }
23807
+ function parseInit(args) {
23808
+ const o = {};
23809
+ for (let i = 0; i < args.length; i++) {
23810
+ const a = args[i];
23811
+ if (a === "--cwd") o.cwd = args[++i];
23812
+ else if (a === "--dry-run") o.dryRun = true;
23813
+ else if (a === "--force-workflow") o.forceWorkflow = true;
23814
+ }
23815
+ return o;
23816
+ }
23817
+ function parseOnboard(args) {
23818
+ const o = {};
23819
+ for (let i = 0; i < args.length; i++) {
23820
+ const a = args[i];
23821
+ if (a === "--cwd") o.cwd = args[++i];
23822
+ else if (a === "--base") o.base = args[++i];
23823
+ else if (a === "--repo") o.repo = args[++i];
23824
+ else if (a === "--branch") o.branch = args[++i];
23825
+ else if (a === "--verify-command") o.verifyCommand = args[++i];
23826
+ else if (a === "--skip-demo") o.skipDemo = true;
23827
+ else if (a === "--demo") o.demo = true;
23828
+ else if (a === "--no-github") o.noGithub = true;
23829
+ else if (a === "--yes") o.yes = true;
23830
+ }
23831
+ return o;
23832
+ }
23833
+ function parseDoctor(args) {
23834
+ const o = {};
23835
+ for (let i = 0; i < args.length; i++) {
23836
+ const a = args[i];
23837
+ if (a === "--cwd") o.cwd = args[++i];
23838
+ else if (a === "--base") o.base = args[++i];
23839
+ else if (a === "--workflow") o.workflow = args[++i];
23840
+ else if (a === "--github") o.github = true;
23841
+ else if (a === "--json") o.json = true;
23842
+ else if (a === "--repo") o.repo = args[++i];
23843
+ else if (a === "--branch") o.branch = args[++i];
23844
+ }
23845
+ return o;
23846
+ }
23847
+ function parseStatus(args) {
23848
+ const o = {};
23849
+ for (let i = 0; i < args.length; i++) {
23850
+ const a = args[i];
23851
+ if (a === "--json") o.json = true;
23852
+ else if (a === "--cwd") o.cwd = args[++i];
23853
+ }
23854
+ return o;
23855
+ }
23856
+ function parseStats(args) {
23857
+ const o = {};
23858
+ for (let i = 0; i < args.length; i++) {
23859
+ const a = args[i];
23860
+ if (a === "--cwd") o.cwd = args[++i];
23861
+ else if (a === "--file") o.file = args[++i];
23862
+ else if (a === "--since") o.since = args[++i];
23863
+ else if (a === "--json") o.json = true;
23864
+ }
23865
+ return o;
23866
+ }
23867
+ function parseSignoffLabel(args) {
23868
+ const o = {};
23869
+ for (let i = 0; i < args.length; i++) {
23870
+ const a = args[i];
23871
+ if (a === "--rule") o.rule = args[++i];
23872
+ else if (a === "--file") o.file = args[++i];
23873
+ else if (a === "--head") o.head = args[++i];
23874
+ }
23875
+ return o;
23876
+ }
23877
+ function parseCheck(args) {
23878
+ const o = {};
23879
+ for (let i = 0; i < args.length; i++) {
23880
+ const a = args[i];
23881
+ if (a === "--staged") o.staged = true;
23882
+ else if (a === "--worktree") o.worktree = true;
23883
+ else if (a === "--json") o.json = true;
23884
+ else if (a === "--format") {
23885
+ const v = args[++i];
23886
+ if (v !== void 0 && isFormat(v)) o.format = v;
23887
+ } else if (a === "--diff") o.diff = args[++i];
23888
+ else if (a === "--cwd") o.cwd = args[++i];
23889
+ }
23890
+ return o;
23891
+ }
23892
+ function splitLongOptionEquals(args) {
23893
+ const out3 = [];
23894
+ for (let i = 0; i < args.length; i++) {
23895
+ const arg = args[i];
23896
+ if (arg === "--") {
23897
+ out3.push(...args.slice(i));
23898
+ break;
23899
+ }
23900
+ if (arg.startsWith("--")) {
23901
+ const equals = arg.indexOf("=");
23902
+ if (equals > 2) {
23903
+ out3.push(arg.slice(0, equals), arg.slice(equals + 1));
23904
+ continue;
23905
+ }
23906
+ }
23907
+ out3.push(arg);
23908
+ }
23909
+ return out3;
23910
+ }
23911
+ function helpRequested(args) {
23912
+ for (const arg of args) {
23913
+ if (arg === "--") break;
23914
+ if (arg === "-h" || arg === "--help") return true;
23915
+ }
23916
+ return false;
21744
23917
  }
21745
23918
  function validateFlatArgs(args, grammar) {
21746
23919
  const flags = new Set(grammar.flags ?? []);
@@ -21791,6 +23964,12 @@ function validateFlatArgs(args, grammar) {
21791
23964
  seen,
21792
23965
  positionals
21793
23966
  };
23967
+ } else if (rule === "mode" && v !== "headless" && v !== "interactive") {
23968
+ return {
23969
+ error: `${a} needs one of headless | interactive (got "${v}")`,
23970
+ seen,
23971
+ positionals
23972
+ };
21794
23973
  }
21795
23974
  continue;
21796
23975
  }
@@ -21942,6 +24121,19 @@ function validateCliArgs(cmd, args) {
21942
24121
  values: { "--dir": "string", "--log": "string", "--base": "string" }
21943
24122
  }).error;
21944
24123
  }
24124
+ if (cmd === "runtime") {
24125
+ const [sub, ...rest] = args;
24126
+ if (sub === void 0) return `runtime requires a subcommand (${RUNTIME_SUBCOMMANDS.join(" | ")})`;
24127
+ if (sub !== "verify" && sub !== "status") {
24128
+ return `unknown runtime subcommand "${sub}" (${RUNTIME_SUBCOMMANDS.join(" | ")})`;
24129
+ }
24130
+ const parsed = validateFlatArgs(rest, {
24131
+ flags: ["--json"],
24132
+ values: { "--cwd": "string", "--runtime": "string", "--mode": "mode", "--model": "string" }
24133
+ });
24134
+ if (parsed.error) return parsed.error;
24135
+ return void 0;
24136
+ }
21945
24137
  if (cmd === "research") {
21946
24138
  const [sub, ...rest] = args;
21947
24139
  if (sub === void 0) return `research requires a subcommand (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
@@ -22149,6 +24341,26 @@ Formats:
22149
24341
  emits the versioned status document for
22150
24342
  editors, CI and dashboards. Local CURRENT
22151
24343
  is posture, never repository/CI authority.
24344
+ tamperward runtime verify [--runtime ID] qualify the in-loop runtime: an operation-
24345
+ [--mode headless|interactive] specific capability matrix (pre-deny:shell,
24346
+ [--model M] [--json] [--cwd D] native-edit, mcp, end-of-turn, transport:*,
24347
+ \u2026) with an explicit state per capability
24348
+ (PROVEN | PARTIAL | UNPROVEN | UNSUPPORTED |
24349
+ FAIL-OPEN | INCONCLUSIVE; never a score),
24350
+ derived from the shipped adapter's declared
24351
+ capabilities and committed evidence. Bound to
24352
+ the runtime version, TamperWard version/commit,
24353
+ adapter hash, hook-config hash, execution mode,
24354
+ platform and model; persisted git-locally.
24355
+ Reports EXISTING facts only \u2014 no promotion, no
24356
+ Round 4.1 claim. Final authority (CI / pristine
24357
+ verify) stays separate from in-loop steering.
24358
+ tamperward runtime status [--runtime ID] render the latest recorded qualification
24359
+ [--mode headless|interactive] WITHOUT rerunning it, and mark it STALE when a
24360
+ [--model M] [--json] [--cwd D] load-bearing input (version/config/adapter/mode/
24361
+ model/\u2026) changed since it was recorded. --mode and
24362
+ --model select the binding staleness is compared
24363
+ against, as for verify.
22152
24364
  tamperward doctor [--base R] report installation + authority posture
22153
24365
  [--workflow F] [--cwd D] and validate the CI verifier's outer-time
22154
24366
  [--json] envelope against the trusted policy.
@@ -22215,6 +24427,10 @@ function main(argv) {
22215
24427
  return runTraceVerify(parseTraceVerify(rest));
22216
24428
  case "research":
22217
24429
  return runResearchCommand(rest);
24430
+ case "runtime": {
24431
+ const { sub, opts } = parseRuntime(rest);
24432
+ return runRuntime(sub, opts);
24433
+ }
22218
24434
  case "run":
22219
24435
  return runEnvelope({
22220
24436
  ...parseRun(rest),
@@ -22265,6 +24481,7 @@ var init_main = __esm({
22265
24481
  init_watch();
22266
24482
  init_onboard();
22267
24483
  init_research();
24484
+ init_runtime();
22268
24485
  init_audit();
22269
24486
  init_signoff_label();
22270
24487
  }
@@ -22273,7 +24490,7 @@ var init_main = __esm({
22273
24490
  // src/cli/index.ts
22274
24491
  init_hook_client();
22275
24492
  init_exit();
22276
- import { readFileSync as readFileSync28 } from "node:fs";
24493
+ import { readFileSync as readFileSync30 } from "node:fs";
22277
24494
  function loadMain() {
22278
24495
  return Promise.resolve().then(() => (init_main(), main_exports));
22279
24496
  }
@@ -22286,7 +24503,7 @@ async function launch(argv) {
22286
24503
  if (kind && hookServiceEnabled()) {
22287
24504
  let raw = null;
22288
24505
  try {
22289
- raw = readFileSync28(0, "utf8");
24506
+ raw = readFileSync30(0, "utf8");
22290
24507
  } catch {
22291
24508
  raw = null;
22292
24509
  }