tamperward 2.39.24 → 2.41.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 +673 -346
- package/package.json +1 -1
- package/schemas/audit-v1.schema.json +33 -1
- package/schemas/stats-v1.schema.json +166 -1
package/dist/cli/index.js
CHANGED
|
@@ -162,7 +162,39 @@ function runtimeWiringOf(path) {
|
|
|
162
162
|
function resolvesToRuntimeWiring(path) {
|
|
163
163
|
return runtimeWiringOf(path) ?? (isAbsolute(path) ? runtimeWiringOf(canonicalPath(path)) : null);
|
|
164
164
|
}
|
|
165
|
-
|
|
165
|
+
function codexWiringTables() {
|
|
166
|
+
const q = (cmd) => JSON.stringify(cmd);
|
|
167
|
+
return [
|
|
168
|
+
`# ${RUNTIME_WIRING_NOTE}`,
|
|
169
|
+
"[hooks]",
|
|
170
|
+
"[[hooks.PreToolUse]]",
|
|
171
|
+
'matcher = "*"',
|
|
172
|
+
"[[hooks.PreToolUse.hooks]]",
|
|
173
|
+
'type = "command"',
|
|
174
|
+
`command = ${q(CODEX_HOOK_CMD)}`,
|
|
175
|
+
"[[hooks.Stop]]",
|
|
176
|
+
"[[hooks.Stop.hooks]]",
|
|
177
|
+
'type = "command"',
|
|
178
|
+
`command = ${q(CODEX_SWEEP_CMD)}`,
|
|
179
|
+
""
|
|
180
|
+
].join("\n");
|
|
181
|
+
}
|
|
182
|
+
function copilotWiringDocument() {
|
|
183
|
+
return {
|
|
184
|
+
version: 1,
|
|
185
|
+
hooks: {
|
|
186
|
+
preToolUse: [{ type: "command", command: COPILOT_HOOK_CMD, timeout: 60 }],
|
|
187
|
+
agentStop: [{ type: "command", command: COPILOT_SWEEP_CMD, timeout: 60 }]
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function runtimeGatePins(text) {
|
|
192
|
+
return [...text.matchAll(RUNTIME_GATE_RE)].map((m) => m[1]);
|
|
193
|
+
}
|
|
194
|
+
function withoutRuntimeGatePins(text) {
|
|
195
|
+
return text.replace(RUNTIME_GATE_RE, "tamperward@<pin> $2 $3");
|
|
196
|
+
}
|
|
197
|
+
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, CODEX_HOOK_CONFIG, COPILOT_HOOK_CONFIG, CODEX_HOOK_CMD, CODEX_SWEEP_CMD, COPILOT_HOOK_CMD, COPILOT_SWEEP_CMD, RUNTIME_GATE_RE, RUNTIME_WIRING_NOTE, COPILOT_WIRING_NOTE;
|
|
166
198
|
var init_wiring = __esm({
|
|
167
199
|
"src/wiring.ts"() {
|
|
168
200
|
"use strict";
|
|
@@ -185,6 +217,13 @@ var init_wiring = __esm({
|
|
|
185
217
|
CLAUDE_SETTINGS = /(?:^|\/)(?:\.claude\/settings(?:\.local)?\.json|managed-settings\.json)$/;
|
|
186
218
|
CODEX_HOOK_CONFIG = /(?:^|\/)\.codex\/config\.toml$/;
|
|
187
219
|
COPILOT_HOOK_CONFIG = /(?:^|\/)\.github\/hooks\/tamperward\.json$/;
|
|
220
|
+
CODEX_HOOK_CMD = `${NPX_AUTHORITY} tamperward@${TW_VERSION} hook codex${AUTHORITY_FAIL_CLOSED}`;
|
|
221
|
+
CODEX_SWEEP_CMD = `${NPX_AUTHORITY} tamperward@${TW_VERSION} sweep codex${AUTHORITY_FAIL_CLOSED}`;
|
|
222
|
+
COPILOT_HOOK_CMD = `${NPX_AUTHORITY} tamperward@${TW_VERSION} hook copilot${AUTHORITY_FAIL_CLOSED}`;
|
|
223
|
+
COPILOT_SWEEP_CMD = `${NPX_AUTHORITY} tamperward@${TW_VERSION} sweep copilot${AUTHORITY_FAIL_CLOSED}`;
|
|
224
|
+
RUNTIME_GATE_RE = /\btamperward@(\S+) (hook|sweep) (codex|copilot)\b/g;
|
|
225
|
+
RUNTIME_WIRING_NOTE = "tamperward: experimental hook wiring. This runtime has not passed a pinned live qualification; run `tamperward runtime verify` to see what is proven. Editing these entries is editing the gate.";
|
|
226
|
+
COPILOT_WIRING_NOTE = RUNTIME_WIRING_NOTE;
|
|
188
227
|
}
|
|
189
228
|
});
|
|
190
229
|
|
|
@@ -2765,11 +2804,20 @@ function codexHooksTables(toml) {
|
|
|
2765
2804
|
}
|
|
2766
2805
|
return out3.join("\n");
|
|
2767
2806
|
}
|
|
2807
|
+
function runtimeGatePinRaise(was, now) {
|
|
2808
|
+
if (now === was) return "same";
|
|
2809
|
+
if (withoutRuntimeGatePins(now) !== withoutRuntimeGatePins(was)) return "changed";
|
|
2810
|
+
const before = runtimeGatePins(was);
|
|
2811
|
+
const after = runtimeGatePins(now);
|
|
2812
|
+
return after.every((pin, i) => pinNotBelow(pin, before[i])) ? "raised" : "lowered";
|
|
2813
|
+
}
|
|
2768
2814
|
function codexHooksWeakening(before, after) {
|
|
2769
2815
|
const was = codexHooksTables(before);
|
|
2770
2816
|
if (!was) return [];
|
|
2771
2817
|
const now = codexHooksTables(after);
|
|
2772
|
-
|
|
2818
|
+
const change = runtimeGatePinRaise(was, now);
|
|
2819
|
+
if (change === "same" || change === "raised") return [];
|
|
2820
|
+
if (change === "lowered") return ["the tamperward pin in the [hooks] tables was lowered or is no longer a plain version \u2014 the gate only ever raises its pin"];
|
|
2773
2821
|
if (GATE.test(was) && !GATE.test(now)) return ["the tamperward hook entries were removed from the [hooks] tables \u2014 Codex no longer runs the gate"];
|
|
2774
2822
|
return ["the [hooks] tables changed \u2014 the PreToolUse and Stop wiring Codex reads is no longer what it was"];
|
|
2775
2823
|
}
|
|
@@ -2788,7 +2836,9 @@ function copilotHooksWeakening(before, after) {
|
|
|
2788
2836
|
if (was === null || was.hooks === null) return [];
|
|
2789
2837
|
const now = hooksMember(after);
|
|
2790
2838
|
if (now === null) return ["the file no longer parses as JSON \u2014 the Copilot CLI loads no hook from it"];
|
|
2791
|
-
|
|
2839
|
+
const change = runtimeGatePinRaise(was.hooks, now.hooks ?? "");
|
|
2840
|
+
if (change === "same" || change === "raised") return [];
|
|
2841
|
+
if (change === "lowered") return ['the tamperward pin in "hooks" was lowered or is no longer a plain version \u2014 the gate only ever raises its pin'];
|
|
2792
2842
|
if (GATE.test(was.hooks) && !GATE.test(now.hooks ?? "")) return ['the tamperward hook entries were removed from "hooks" \u2014 the Copilot CLI no longer runs the gate'];
|
|
2793
2843
|
return ['the "hooks" entries changed \u2014 the preToolUse and agentStop wiring the Copilot CLI reads is no longer what it was'];
|
|
2794
2844
|
}
|
|
@@ -12546,6 +12596,16 @@ function sessionHash(sessionId) {
|
|
|
12546
12596
|
if (!sessionId) return void 0;
|
|
12547
12597
|
return "sha256:" + createHash6("sha256").update(sessionId).digest("hex").slice(0, 24);
|
|
12548
12598
|
}
|
|
12599
|
+
function sanitizedMetrics(metrics) {
|
|
12600
|
+
const out3 = {};
|
|
12601
|
+
if (!metrics) return out3;
|
|
12602
|
+
for (const field2 of AUDIT_DURATION_FIELDS) {
|
|
12603
|
+
const v = metrics[field2];
|
|
12604
|
+
if (typeof v === "number" && Number.isFinite(v)) out3[field2] = Math.max(0, Math.round(v));
|
|
12605
|
+
}
|
|
12606
|
+
if (typeof metrics.oob_signoff === "boolean") out3.oob_signoff = metrics.oob_signoff;
|
|
12607
|
+
return out3;
|
|
12608
|
+
}
|
|
12549
12609
|
function eventId() {
|
|
12550
12610
|
return "sha256:" + createHash6("sha256").update(randomBytes2(32)).digest("hex").slice(0, 32);
|
|
12551
12611
|
}
|
|
@@ -12570,6 +12630,7 @@ function recordAuditFindings(findings, context) {
|
|
|
12570
12630
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
12571
12631
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12572
12632
|
const session = sessionHash(context.sessionId);
|
|
12633
|
+
const metrics = sanitizedMetrics(context.metrics);
|
|
12573
12634
|
const lines = findings.map((finding) => {
|
|
12574
12635
|
const severity = finding.severity === "block" ? "block" : "warn";
|
|
12575
12636
|
const event = {
|
|
@@ -12581,7 +12642,8 @@ function recordAuditFindings(findings, context) {
|
|
|
12581
12642
|
rule: finding.rule,
|
|
12582
12643
|
severity,
|
|
12583
12644
|
decision: severity === "block" ? "deny" : "warn",
|
|
12584
|
-
...session ? { session } : {}
|
|
12645
|
+
...session ? { session } : {},
|
|
12646
|
+
...metrics
|
|
12585
12647
|
};
|
|
12586
12648
|
return JSON.stringify(event);
|
|
12587
12649
|
});
|
|
@@ -12638,6 +12700,15 @@ function parseAuditEvent(value, line = 0) {
|
|
|
12638
12700
|
if (value.session !== void 0 && (typeof value.session !== "string" || !/^sha256:[0-9a-f]{24}$/.test(value.session))) {
|
|
12639
12701
|
throw new Error(`${where} has an invalid session`);
|
|
12640
12702
|
}
|
|
12703
|
+
for (const field2 of AUDIT_DURATION_FIELDS) {
|
|
12704
|
+
const v = value[field2];
|
|
12705
|
+
if (v !== void 0 && (typeof v !== "number" || !Number.isInteger(v) || v < 0)) {
|
|
12706
|
+
throw new Error(`${where} has an invalid ${field2}`);
|
|
12707
|
+
}
|
|
12708
|
+
}
|
|
12709
|
+
if (value.oob_signoff !== void 0 && typeof value.oob_signoff !== "boolean") {
|
|
12710
|
+
throw new Error(`${where} has an invalid oob_signoff`);
|
|
12711
|
+
}
|
|
12641
12712
|
return {
|
|
12642
12713
|
schema_version: AUDIT_SCHEMA_VERSION,
|
|
12643
12714
|
id: value.id,
|
|
@@ -12647,9 +12718,19 @@ function parseAuditEvent(value, line = 0) {
|
|
|
12647
12718
|
rule: value.rule,
|
|
12648
12719
|
severity: value.severity,
|
|
12649
12720
|
decision: value.decision,
|
|
12650
|
-
...typeof value.session === "string" ? { session: value.session } : {}
|
|
12721
|
+
...typeof value.session === "string" ? { session: value.session } : {},
|
|
12722
|
+
...durationsOf(value),
|
|
12723
|
+
...typeof value.oob_signoff === "boolean" ? { oob_signoff: value.oob_signoff } : {}
|
|
12651
12724
|
};
|
|
12652
12725
|
}
|
|
12726
|
+
function durationsOf(value) {
|
|
12727
|
+
const out3 = {};
|
|
12728
|
+
for (const field2 of AUDIT_DURATION_FIELDS) {
|
|
12729
|
+
const v = value[field2];
|
|
12730
|
+
if (typeof v === "number") out3[field2] = v;
|
|
12731
|
+
}
|
|
12732
|
+
return out3;
|
|
12733
|
+
}
|
|
12653
12734
|
function parseAuditLine(line, lineNumber) {
|
|
12654
12735
|
let value;
|
|
12655
12736
|
try {
|
|
@@ -12724,6 +12805,12 @@ function createAuditAggregator() {
|
|
|
12724
12805
|
const rules = /* @__PURE__ */ new Map();
|
|
12725
12806
|
const surfaces = /* @__PURE__ */ new Map();
|
|
12726
12807
|
const sessions = /* @__PURE__ */ new Set();
|
|
12808
|
+
const durations = {
|
|
12809
|
+
hook_latency_ms: /* @__PURE__ */ new Map(),
|
|
12810
|
+
verify_wall_clock_ms: /* @__PURE__ */ new Map(),
|
|
12811
|
+
bare_suite_wall_clock_ms: /* @__PURE__ */ new Map()
|
|
12812
|
+
};
|
|
12813
|
+
let oobSignoffs = 0;
|
|
12727
12814
|
const bounds = { first: null, last: null };
|
|
12728
12815
|
return {
|
|
12729
12816
|
add(event) {
|
|
@@ -12737,6 +12824,11 @@ function createAuditAggregator() {
|
|
|
12737
12824
|
rules.set(event.rule, bucket);
|
|
12738
12825
|
surfaces.set(event.surface, (surfaces.get(event.surface) ?? 0) + 1);
|
|
12739
12826
|
if (event.session) sessions.add(event.session);
|
|
12827
|
+
for (const field2 of AUDIT_DURATION_FIELDS) {
|
|
12828
|
+
const v = event[field2];
|
|
12829
|
+
if (typeof v === "number") durations[field2].set(v, (durations[field2].get(v) ?? 0) + 1);
|
|
12830
|
+
}
|
|
12831
|
+
if (event.oob_signoff === true) oobSignoffs++;
|
|
12740
12832
|
const ms = Date.parse(event.timestamp);
|
|
12741
12833
|
const { first, last } = bounds;
|
|
12742
12834
|
if (first === null || ms < first.ms || ms === first.ms && event.id.localeCompare(first.id) < 0) {
|
|
@@ -12759,11 +12851,32 @@ function createAuditAggregator() {
|
|
|
12759
12851
|
last_event: bounds.last === null ? null : bounds.last.timestamp,
|
|
12760
12852
|
by_rule,
|
|
12761
12853
|
by_surface,
|
|
12854
|
+
latency: {
|
|
12855
|
+
hook_latency_ms: percentiles(durations.hook_latency_ms),
|
|
12856
|
+
verify_wall_clock_ms: percentiles(durations.verify_wall_clock_ms),
|
|
12857
|
+
bare_suite_wall_clock_ms: percentiles(durations.bare_suite_wall_clock_ms)
|
|
12858
|
+
},
|
|
12859
|
+
oob_signoffs: oobSignoffs,
|
|
12762
12860
|
interpretation: "finding-is-not-proof-of-intent"
|
|
12763
12861
|
};
|
|
12764
12862
|
}
|
|
12765
12863
|
};
|
|
12766
12864
|
}
|
|
12865
|
+
function percentiles(counts) {
|
|
12866
|
+
const values = [...counts.keys()].sort((a, b) => a - b);
|
|
12867
|
+
if (values.length === 0) return null;
|
|
12868
|
+
const samples = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
12869
|
+
const at = (q) => {
|
|
12870
|
+
const rank = Math.max(1, Math.ceil(q * samples));
|
|
12871
|
+
let seen = 0;
|
|
12872
|
+
for (const v of values) {
|
|
12873
|
+
seen += counts.get(v) ?? 0;
|
|
12874
|
+
if (seen >= rank) return v;
|
|
12875
|
+
}
|
|
12876
|
+
return values[values.length - 1];
|
|
12877
|
+
};
|
|
12878
|
+
return { samples, p50: at(0.5), p95: at(0.95), max: values[values.length - 1] };
|
|
12879
|
+
}
|
|
12767
12880
|
function renderAuditStats(summary) {
|
|
12768
12881
|
const out3 = [
|
|
12769
12882
|
"TamperWard audit stats",
|
|
@@ -12786,6 +12899,23 @@ function renderAuditStats(summary) {
|
|
|
12786
12899
|
const width = Math.max(...summary.by_surface.map((row) => row.surface.length));
|
|
12787
12900
|
for (const row of summary.by_surface) out3.push(` ${row.surface.padEnd(width)} ${row.events}`);
|
|
12788
12901
|
}
|
|
12902
|
+
const measures = [
|
|
12903
|
+
["hook_latency_ms", "Hook latency"],
|
|
12904
|
+
["verify_wall_clock_ms", "verify wall-clock"],
|
|
12905
|
+
["bare_suite_wall_clock_ms", "Bare-suite wall-clock"]
|
|
12906
|
+
];
|
|
12907
|
+
const measured = [];
|
|
12908
|
+
for (const [field2, label] of measures) {
|
|
12909
|
+
const p = summary.latency[field2];
|
|
12910
|
+
if (p !== null) measured.push([label, p]);
|
|
12911
|
+
}
|
|
12912
|
+
if (measured.length || summary.oob_signoffs > 0) {
|
|
12913
|
+
out3.push("", "Measures");
|
|
12914
|
+
for (const [label, p] of measured) {
|
|
12915
|
+
out3.push(` ${label.padEnd(22)} p50 ${p.p50} ms p95 ${p.p95} ms max ${p.max} ms (${p.samples} samples)`);
|
|
12916
|
+
}
|
|
12917
|
+
if (summary.oob_signoffs > 0) out3.push(` ${"Out-of-band sign-offs".padEnd(22)} ${summary.oob_signoffs}`);
|
|
12918
|
+
}
|
|
12789
12919
|
out3.push("", "Note: an integrity finding is a signal, not proof of agent intent; legitimate refactors can trigger findings.");
|
|
12790
12920
|
return out3.join("\n") + "\n";
|
|
12791
12921
|
}
|
|
@@ -12807,7 +12937,7 @@ function runStats(opts = {}) {
|
|
|
12807
12937
|
process.stdout.write(opts.json ? JSON.stringify(summary) + "\n" : renderAuditStats(summary));
|
|
12808
12938
|
return 0;
|
|
12809
12939
|
}
|
|
12810
|
-
var AUDIT_SCHEMA_VERSION, AUDIT_LOG_ENV, MAX_AUDIT_LINE_BYTES, ALLOWED_KEYS, STATS_READ_REFUSED;
|
|
12940
|
+
var AUDIT_SCHEMA_VERSION, AUDIT_LOG_ENV, MAX_AUDIT_LINE_BYTES, AUDIT_DURATION_FIELDS, ALLOWED_KEYS, STATS_READ_REFUSED;
|
|
12811
12941
|
var init_audit = __esm({
|
|
12812
12942
|
"src/cli/audit.ts"() {
|
|
12813
12943
|
"use strict";
|
|
@@ -12816,6 +12946,7 @@ var init_audit = __esm({
|
|
|
12816
12946
|
AUDIT_SCHEMA_VERSION = 1;
|
|
12817
12947
|
AUDIT_LOG_ENV = "TAMPERWARD_AUDIT_LOG";
|
|
12818
12948
|
MAX_AUDIT_LINE_BYTES = 16 * 1024;
|
|
12949
|
+
AUDIT_DURATION_FIELDS = ["hook_latency_ms", "verify_wall_clock_ms", "bare_suite_wall_clock_ms"];
|
|
12819
12950
|
ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
12820
12951
|
"schema_version",
|
|
12821
12952
|
"id",
|
|
@@ -12825,7 +12956,9 @@ var init_audit = __esm({
|
|
|
12825
12956
|
"rule",
|
|
12826
12957
|
"severity",
|
|
12827
12958
|
"decision",
|
|
12828
|
-
"session"
|
|
12959
|
+
"session",
|
|
12960
|
+
...AUDIT_DURATION_FIELDS,
|
|
12961
|
+
"oob_signoff"
|
|
12829
12962
|
]);
|
|
12830
12963
|
STATS_READ_REFUSED = "; stats reads the audit log only as a regular file \u2014 remove what stands there so the log can be read";
|
|
12831
12964
|
}
|
|
@@ -12903,7 +13036,8 @@ function verdict(blocks2, kind, audit) {
|
|
|
12903
13036
|
recordAuditFindings(blocks2, {
|
|
12904
13037
|
cwd: audit.cwd,
|
|
12905
13038
|
surface: kind === "PreToolUse" ? "pretooluse" : "stop",
|
|
12906
|
-
sessionId: audit.sessionId
|
|
13039
|
+
sessionId: audit.sessionId,
|
|
13040
|
+
...audit.startedAt !== void 0 ? { metrics: { hook_latency_ms: performance.now() - audit.startedAt } } : {}
|
|
12907
13041
|
});
|
|
12908
13042
|
}
|
|
12909
13043
|
const reason = formatDenial(blocks2);
|
|
@@ -13086,6 +13220,7 @@ function sanctionPredictedWrites(cwd, sessionId, policy, changes) {
|
|
|
13086
13220
|
if (dirty) savePtree(cwd, sessionId, tree);
|
|
13087
13221
|
}
|
|
13088
13222
|
function preToolUseVerdict(input, defaultCwd, trustedRoot) {
|
|
13223
|
+
const startedAt = performance.now();
|
|
13089
13224
|
const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
|
|
13090
13225
|
try {
|
|
13091
13226
|
if (trustedRoot !== void 0) {
|
|
@@ -13096,11 +13231,11 @@ function preToolUseVerdict(input, defaultCwd, trustedRoot) {
|
|
|
13096
13231
|
turnBaseline(cwd, input.session_id);
|
|
13097
13232
|
const policy = loadPolicy(cwd);
|
|
13098
13233
|
const driftBlocks = effectDriftBlocks(cwd, input.session_id, policy);
|
|
13099
|
-
if (driftBlocks) return verdict(driftBlocks, "PreToolUse", { cwd, sessionId: input.session_id });
|
|
13234
|
+
if (driftBlocks) return verdict(driftBlocks, "PreToolUse", { cwd, sessionId: input.session_id, startedAt });
|
|
13100
13235
|
const changes = changesFromClaudeHook(input, cwd, sessionCwd);
|
|
13101
13236
|
const blocks2 = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
|
|
13102
13237
|
if (blocks2.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
|
|
13103
|
-
return verdict(blocks2, "PreToolUse", { cwd, sessionId: input.session_id });
|
|
13238
|
+
return verdict(blocks2, "PreToolUse", { cwd, sessionId: input.session_id, startedAt });
|
|
13104
13239
|
} catch (e) {
|
|
13105
13240
|
return failClosed("PreToolUse", errText2(e), { cwd: sessionCwd, sessionId: input.session_id });
|
|
13106
13241
|
}
|
|
@@ -13206,6 +13341,7 @@ function recordObserverHealth(cwd, sessionId, state, reason) {
|
|
|
13206
13341
|
}
|
|
13207
13342
|
function stopVerdict(input, defaultCwd, trustedRoot) {
|
|
13208
13343
|
if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
|
|
13344
|
+
const startedAt = performance.now();
|
|
13209
13345
|
const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
|
|
13210
13346
|
if (trustedRoot !== void 0) {
|
|
13211
13347
|
const v = validateClaimAgainstRoot(input.cwd, trustedRoot, defaultCwd ?? process.cwd());
|
|
@@ -13247,7 +13383,7 @@ function stopVerdict(input, defaultCwd, trustedRoot) {
|
|
|
13247
13383
|
advanceTurnBaseline(cwd, input.session_id);
|
|
13248
13384
|
commitCursor();
|
|
13249
13385
|
}
|
|
13250
|
-
return verdict(blocks2, "Stop", { cwd, sessionId: input.session_id });
|
|
13386
|
+
return verdict(blocks2, "Stop", { cwd, sessionId: input.session_id, startedAt });
|
|
13251
13387
|
}
|
|
13252
13388
|
function emit(r) {
|
|
13253
13389
|
if (r.stdout) process.stdout.write(r.stdout);
|
|
@@ -13926,11 +14062,200 @@ var init_verifier_limits = __esm({
|
|
|
13926
14062
|
}
|
|
13927
14063
|
});
|
|
13928
14064
|
|
|
14065
|
+
// src/immutable.ts
|
|
14066
|
+
function isPlainData(value) {
|
|
14067
|
+
if (value === null || typeof value !== "object") return false;
|
|
14068
|
+
const proto = Object.getPrototypeOf(value);
|
|
14069
|
+
return proto === Object.prototype || proto === Array.prototype || proto === null;
|
|
14070
|
+
}
|
|
14071
|
+
function deepFreeze(value) {
|
|
14072
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
14073
|
+
const walk2 = (node) => {
|
|
14074
|
+
if (!isPlainData(node) || seen.has(node)) return;
|
|
14075
|
+
seen.add(node);
|
|
14076
|
+
Object.freeze(node);
|
|
14077
|
+
for (const key2 of Reflect.ownKeys(node)) walk2(Reflect.get(node, key2));
|
|
14078
|
+
};
|
|
14079
|
+
walk2(value);
|
|
14080
|
+
return value;
|
|
14081
|
+
}
|
|
14082
|
+
var init_immutable = __esm({
|
|
14083
|
+
"src/immutable.ts"() {
|
|
14084
|
+
"use strict";
|
|
14085
|
+
}
|
|
14086
|
+
});
|
|
14087
|
+
|
|
14088
|
+
// src/runtimes.ts
|
|
14089
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
14090
|
+
import { join as join15 } from "node:path";
|
|
14091
|
+
function isRecord4(x) {
|
|
14092
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
14093
|
+
}
|
|
14094
|
+
function hooksBeyondTamperward(hooks) {
|
|
14095
|
+
if (!isRecord4(hooks)) return hooks != null;
|
|
14096
|
+
for (const entries of Object.values(hooks)) {
|
|
14097
|
+
if (!Array.isArray(entries)) {
|
|
14098
|
+
if (entries != null) return true;
|
|
14099
|
+
continue;
|
|
14100
|
+
}
|
|
14101
|
+
for (const entry of entries) {
|
|
14102
|
+
const hs = isRecord4(entry) ? entry.hooks : null;
|
|
14103
|
+
if (!Array.isArray(hs)) {
|
|
14104
|
+
if (hs != null) return true;
|
|
14105
|
+
continue;
|
|
14106
|
+
}
|
|
14107
|
+
for (const h of hs) {
|
|
14108
|
+
const cmd = isRecord4(h) ? String(h.command ?? "") : "";
|
|
14109
|
+
if (!/tamperward/.test(cmd)) return true;
|
|
14110
|
+
}
|
|
14111
|
+
}
|
|
14112
|
+
}
|
|
14113
|
+
return false;
|
|
14114
|
+
}
|
|
14115
|
+
function claudeSettingsBeyondTamperward(cwd) {
|
|
14116
|
+
const path = join15(cwd, ".claude", "settings.json");
|
|
14117
|
+
let raw;
|
|
14118
|
+
try {
|
|
14119
|
+
if (!existsSync7(path)) return false;
|
|
14120
|
+
raw = readFileSync8(path, "utf8");
|
|
14121
|
+
} catch {
|
|
14122
|
+
return false;
|
|
14123
|
+
}
|
|
14124
|
+
let parsed;
|
|
14125
|
+
try {
|
|
14126
|
+
parsed = JSON.parse(raw);
|
|
14127
|
+
} catch {
|
|
14128
|
+
return raw.trim().length > 0;
|
|
14129
|
+
}
|
|
14130
|
+
if (!isRecord4(parsed)) return raw.trim().length > 0;
|
|
14131
|
+
for (const [key2, value] of Object.entries(parsed)) {
|
|
14132
|
+
if (key2 === "disableAllHooks") continue;
|
|
14133
|
+
if (key2 === "hooks") {
|
|
14134
|
+
if (hooksBeyondTamperward(value)) return true;
|
|
14135
|
+
continue;
|
|
14136
|
+
}
|
|
14137
|
+
return true;
|
|
14138
|
+
}
|
|
14139
|
+
return false;
|
|
14140
|
+
}
|
|
14141
|
+
function codexDirBeyondTamperward(cwd) {
|
|
14142
|
+
const dir = join15(cwd, ".codex");
|
|
14143
|
+
let entries;
|
|
14144
|
+
try {
|
|
14145
|
+
if (!existsSync7(dir) || !statSync3(dir).isDirectory()) return false;
|
|
14146
|
+
entries = readdirSync3(dir);
|
|
14147
|
+
} catch {
|
|
14148
|
+
return false;
|
|
14149
|
+
}
|
|
14150
|
+
if (entries.some((e) => e !== "config.toml")) return true;
|
|
14151
|
+
if (!entries.includes("config.toml")) return false;
|
|
14152
|
+
let raw;
|
|
14153
|
+
try {
|
|
14154
|
+
raw = readFileSync8(join15(dir, "config.toml"), "utf8");
|
|
14155
|
+
} catch {
|
|
14156
|
+
return false;
|
|
14157
|
+
}
|
|
14158
|
+
return raw.split("\n").some((line) => {
|
|
14159
|
+
const t = line.trim();
|
|
14160
|
+
return t !== "" && !t.startsWith("#") && !/^\[\[?hooks/.test(t) && !codexWiringLines().has(t);
|
|
14161
|
+
});
|
|
14162
|
+
}
|
|
14163
|
+
function codexWiringLines() {
|
|
14164
|
+
if (!wiringLines) wiringLines = new Set(codexWiringTables().split("\n").map((l) => l.trim()).filter(Boolean));
|
|
14165
|
+
return wiringLines;
|
|
14166
|
+
}
|
|
14167
|
+
function detectRuntimes(cwd) {
|
|
14168
|
+
const found = [];
|
|
14169
|
+
for (const rt of KNOWN_RUNTIMES) {
|
|
14170
|
+
const marker = rt.markers.find((m) => {
|
|
14171
|
+
try {
|
|
14172
|
+
return existsSync7(join15(cwd, m));
|
|
14173
|
+
} catch {
|
|
14174
|
+
return false;
|
|
14175
|
+
}
|
|
14176
|
+
});
|
|
14177
|
+
const matchedMarker = marker ?? rt.detectExtra?.(cwd) ?? void 0;
|
|
14178
|
+
if (matchedMarker !== void 0) found.push({ ...rt, matchedMarker });
|
|
14179
|
+
}
|
|
14180
|
+
return found;
|
|
14181
|
+
}
|
|
14182
|
+
function hasInLoopRuntime(detected) {
|
|
14183
|
+
return detected.some((r) => r.steering === "in-loop");
|
|
14184
|
+
}
|
|
14185
|
+
function detectionHeadline(detected) {
|
|
14186
|
+
if (detected.length === 0) {
|
|
14187
|
+
return "No agent runtime detected. Wiring Claude Code in-loop steering plus the agent-neutral layers (pre-commit + CI) that protect any agent or human.";
|
|
14188
|
+
}
|
|
14189
|
+
if (detected.length === 1) {
|
|
14190
|
+
const r = detected[0];
|
|
14191
|
+
return `Detected ${r.label} \u2014 ${r.note}`;
|
|
14192
|
+
}
|
|
14193
|
+
const parts = detected.map((r) => `${r.label} (${r.steering === "in-loop" ? "in-loop" : "neutral layers"})`);
|
|
14194
|
+
return `Detected ${detected.length} agent runtimes: ${parts.join(", ")}.`;
|
|
14195
|
+
}
|
|
14196
|
+
function neutralOnlyCaveat(detected) {
|
|
14197
|
+
const neutral = detected.filter((r) => r.steering === "neutral");
|
|
14198
|
+
if (neutral.length === 0) return null;
|
|
14199
|
+
const names = neutral.map((r) => r.label).join(", ");
|
|
14200
|
+
const claudeAlso = hasInLoopRuntime(detected);
|
|
14201
|
+
const lead = claudeAlso ? `In-loop steering (deny-before-execute) ships for Claude Code only today: Claude Code here gets it, but on ${names} your live protection is ` : `In-loop steering (deny-before-execute) ships for Claude Code only today, so on ${names} your live protection is `;
|
|
14202
|
+
const tail = claudeAlso ? "" : " The Claude hook wiring is still written and is inert unless Claude Code runs in this repository.";
|
|
14203
|
+
return `${lead}the agent-neutral layers: pre-commit and CI.${tail}`;
|
|
14204
|
+
}
|
|
14205
|
+
var wiringLines, KNOWN_RUNTIMES;
|
|
14206
|
+
var init_runtimes = __esm({
|
|
14207
|
+
"src/runtimes.ts"() {
|
|
14208
|
+
"use strict";
|
|
14209
|
+
init_immutable();
|
|
14210
|
+
init_wiring();
|
|
14211
|
+
wiringLines = null;
|
|
14212
|
+
KNOWN_RUNTIMES = deepFreeze([
|
|
14213
|
+
{
|
|
14214
|
+
id: "claude-code",
|
|
14215
|
+
// Markers are Claude Code files `init` never writes, so detection never mistakes
|
|
14216
|
+
// TamperWard's own generated `.claude/settings.json` for evidence a human runs
|
|
14217
|
+
// Claude here (#526). `detectExtra` looks at that generated file's CONTENT for
|
|
14218
|
+
// real use (configuration beyond the two hooks init merges in).
|
|
14219
|
+
label: "Claude Code",
|
|
14220
|
+
markers: ["CLAUDE.md", ".claude/settings.local.json", ".claude/commands", ".claude/agents"],
|
|
14221
|
+
detectExtra: (cwd) => claudeSettingsBeyondTamperward(cwd) ? ".claude/settings.json" : null,
|
|
14222
|
+
steering: "in-loop",
|
|
14223
|
+
note: "native in-loop steering (PreToolUse deny + Stop sweep) plus the agent-neutral layers."
|
|
14224
|
+
},
|
|
14225
|
+
{
|
|
14226
|
+
id: "cursor",
|
|
14227
|
+
label: "Cursor",
|
|
14228
|
+
markers: [".cursor", ".cursorrules"],
|
|
14229
|
+
steering: "neutral",
|
|
14230
|
+
note: "agent-neutral layers (pre-commit + CI) today; no in-loop adapter ships for Cursor."
|
|
14231
|
+
},
|
|
14232
|
+
{
|
|
14233
|
+
id: "copilot",
|
|
14234
|
+
label: "GitHub Copilot",
|
|
14235
|
+
markers: [".github/copilot-instructions.md", ".github/copilot"],
|
|
14236
|
+
steering: "neutral",
|
|
14237
|
+
note: "agent-neutral layers (pre-commit + CI) today; the experimental Copilot adapters are not qualified for in-loop steering (see `tamperward runtime verify`)."
|
|
14238
|
+
},
|
|
14239
|
+
{
|
|
14240
|
+
id: "codex",
|
|
14241
|
+
label: "an AGENTS.md-aware agent (e.g. Codex)",
|
|
14242
|
+
// `.codex/config.toml` is a file `init` writes (the hook wiring), so the directory
|
|
14243
|
+
// counts only for what else it holds, or for configuration in that file beyond the
|
|
14244
|
+
// hooks tables init appends (#526).
|
|
14245
|
+
markers: ["AGENTS.md"],
|
|
14246
|
+
detectExtra: (cwd) => codexDirBeyondTamperward(cwd) ? ".codex" : null,
|
|
14247
|
+
steering: "neutral",
|
|
14248
|
+
note: "agent-neutral layers (pre-commit + CI) today; the experimental Codex adapter is not qualified for in-loop steering (see `tamperward runtime verify`)."
|
|
14249
|
+
}
|
|
14250
|
+
]);
|
|
14251
|
+
}
|
|
14252
|
+
});
|
|
14253
|
+
|
|
13929
14254
|
// src/cli/init.ts
|
|
13930
14255
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
13931
14256
|
import { createHash as createHash8 } from "node:crypto";
|
|
13932
|
-
import { existsSync as
|
|
13933
|
-
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as
|
|
14257
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync9, realpathSync as realpathSync5 } from "node:fs";
|
|
14258
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join16, relative as relative4, resolve as resolve6 } from "node:path";
|
|
13934
14259
|
function gitConfig(cwd, key2) {
|
|
13935
14260
|
try {
|
|
13936
14261
|
return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -14023,34 +14348,34 @@ function verifierCandidates(cwd) {
|
|
|
14023
14348
|
if (!candidates.includes(command)) candidates.push(command);
|
|
14024
14349
|
};
|
|
14025
14350
|
try {
|
|
14026
|
-
const pkg = JSON.parse(
|
|
14351
|
+
const pkg = JSON.parse(readFileSync9(join16(cwd, "package.json"), "utf8"));
|
|
14027
14352
|
const test = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts.test : void 0;
|
|
14028
14353
|
if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
|
|
14029
14354
|
} catch {
|
|
14030
14355
|
}
|
|
14031
14356
|
try {
|
|
14032
|
-
const pyproject =
|
|
14357
|
+
const pyproject = readFileSync9(join16(cwd, "pyproject.toml"), "utf8");
|
|
14033
14358
|
if (/^\s*\[tool\.pytest(?:\.ini_options)?\]\s*$/m.test(pyproject)) add("pytest");
|
|
14034
14359
|
} catch {
|
|
14035
14360
|
}
|
|
14036
|
-
if (
|
|
14361
|
+
if (existsSync8(join16(cwd, "pytest.ini"))) add("pytest");
|
|
14037
14362
|
try {
|
|
14038
|
-
const setupCfg =
|
|
14363
|
+
const setupCfg = readFileSync9(join16(cwd, "setup.cfg"), "utf8");
|
|
14039
14364
|
if (/^\s*\[tool:pytest\]\s*$/m.test(setupCfg)) add("pytest");
|
|
14040
14365
|
} catch {
|
|
14041
14366
|
}
|
|
14042
14367
|
try {
|
|
14043
|
-
const tox =
|
|
14368
|
+
const tox = readFileSync9(join16(cwd, "tox.ini"), "utf8");
|
|
14044
14369
|
if (/^\s*\[tox\]\s*$/m.test(tox)) add("tox");
|
|
14045
14370
|
} catch {
|
|
14046
14371
|
}
|
|
14047
14372
|
try {
|
|
14048
|
-
const cargo =
|
|
14373
|
+
const cargo = readFileSync9(join16(cwd, "Cargo.toml"), "utf8");
|
|
14049
14374
|
if (/^\s*\[(?:package|workspace)\]\s*(?:#.*)?$/m.test(cargo)) add("cargo test");
|
|
14050
14375
|
} catch {
|
|
14051
14376
|
}
|
|
14052
14377
|
try {
|
|
14053
|
-
const goMod =
|
|
14378
|
+
const goMod = readFileSync9(join16(cwd, "go.mod"), "utf8");
|
|
14054
14379
|
if (/^\s*module\s+\S+\s*(?:\/\/.*)?$/m.test(goMod)) add("go test ./...");
|
|
14055
14380
|
} catch {
|
|
14056
14381
|
}
|
|
@@ -14090,10 +14415,10 @@ function verifierSetupMessage(cwd) {
|
|
|
14090
14415
|
return lines.join("\n");
|
|
14091
14416
|
}
|
|
14092
14417
|
function planPolicy(cwd) {
|
|
14093
|
-
const path =
|
|
14418
|
+
const path = join16(cwd, POLICY_FILE);
|
|
14094
14419
|
const refused = refuseNonRegular(path);
|
|
14095
14420
|
if (refused) return { item: "policy", path: POLICY_FILE, status: "error", detail: refused };
|
|
14096
|
-
if (
|
|
14421
|
+
if (existsSync8(path)) {
|
|
14097
14422
|
try {
|
|
14098
14423
|
loadPolicy(cwd);
|
|
14099
14424
|
} catch (e) {
|
|
@@ -14111,8 +14436,8 @@ function planPolicy(cwd) {
|
|
|
14111
14436
|
}
|
|
14112
14437
|
function planGitignore(cwd) {
|
|
14113
14438
|
const rel = ".gitignore";
|
|
14114
|
-
const path =
|
|
14115
|
-
if (!
|
|
14439
|
+
const path = join16(cwd, rel);
|
|
14440
|
+
if (!existsSync8(join16(cwd, "node_modules"))) return null;
|
|
14116
14441
|
let existing = "";
|
|
14117
14442
|
let kind;
|
|
14118
14443
|
try {
|
|
@@ -14122,7 +14447,7 @@ function planGitignore(cwd) {
|
|
|
14122
14447
|
}
|
|
14123
14448
|
const refused = refuseNonRegular(path);
|
|
14124
14449
|
if (refused) return { item: "gitignore", path: rel, status: "error", detail: refused };
|
|
14125
|
-
if (kind === "file") existing =
|
|
14450
|
+
if (kind === "file") existing = readFileSync9(path, "utf8");
|
|
14126
14451
|
try {
|
|
14127
14452
|
const tracked = execFileSync6("git", ["ls-files", "--cached", "--", "node_modules"], {
|
|
14128
14453
|
cwd,
|
|
@@ -14173,14 +14498,14 @@ function gateVerdicts(event, groups, needle) {
|
|
|
14173
14498
|
}
|
|
14174
14499
|
function planClaudeHooks(cwd) {
|
|
14175
14500
|
const rel = ".claude/settings.json";
|
|
14176
|
-
const path =
|
|
14501
|
+
const path = join16(cwd, rel);
|
|
14177
14502
|
const refused = refuseNonRegular(path);
|
|
14178
14503
|
if (refused) return { item: "agent", path: rel, status: "error", detail: refused };
|
|
14179
14504
|
let settings = {};
|
|
14180
|
-
if (
|
|
14505
|
+
if (existsSync8(path)) {
|
|
14181
14506
|
let parsed;
|
|
14182
14507
|
try {
|
|
14183
|
-
parsed = JSON.parse(
|
|
14508
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
14184
14509
|
} catch {
|
|
14185
14510
|
return {
|
|
14186
14511
|
item: "agent",
|
|
@@ -14229,7 +14554,7 @@ function planClaudeHooks(cwd) {
|
|
|
14229
14554
|
return {
|
|
14230
14555
|
item: "agent",
|
|
14231
14556
|
path: rel,
|
|
14232
|
-
status:
|
|
14557
|
+
status: existsSync8(path) ? "update" : "create",
|
|
14233
14558
|
detail: [
|
|
14234
14559
|
needPre && "wire PreToolUse deny",
|
|
14235
14560
|
needStop && "wire Stop sweep",
|
|
@@ -14273,7 +14598,7 @@ function hooksDir(cwd) {
|
|
|
14273
14598
|
if (out3) return resolve6(cwd, out3);
|
|
14274
14599
|
} catch {
|
|
14275
14600
|
}
|
|
14276
|
-
return
|
|
14601
|
+
return existsSync8(join16(cwd, ".git")) ? join16(cwd, ".git", "hooks") : null;
|
|
14277
14602
|
}
|
|
14278
14603
|
function display(cwd, path) {
|
|
14279
14604
|
const rel = relative4(cwd, path);
|
|
@@ -14300,29 +14625,29 @@ function planPreCommit(cwd) {
|
|
|
14300
14625
|
const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
|
|
14301
14626
|
let path;
|
|
14302
14627
|
let note;
|
|
14303
|
-
if (
|
|
14304
|
-
path =
|
|
14628
|
+
if (existsSync8(join16(cwd, ".husky"))) {
|
|
14629
|
+
path = join16(cwd, ".husky", "pre-commit");
|
|
14305
14630
|
note = "husky";
|
|
14306
14631
|
} else {
|
|
14307
|
-
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) =>
|
|
14632
|
+
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(join16(cwd, c))));
|
|
14308
14633
|
if (managed) {
|
|
14309
14634
|
return skip(`${managed.name} manages this repository's hooks and regenerates them on install, so a line in the hooks directory would not last \u2014 ${managed.hint}`);
|
|
14310
14635
|
}
|
|
14311
14636
|
const dir = hooksDir(cwd);
|
|
14312
14637
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
14313
14638
|
if (basename3(dir) === "_" && basename3(dirname5(dir)) === ".husky") {
|
|
14314
|
-
path =
|
|
14639
|
+
path = join16(dirname5(dir), "pre-commit");
|
|
14315
14640
|
note = "husky";
|
|
14316
14641
|
} else {
|
|
14317
|
-
path =
|
|
14642
|
+
path = join16(dir, "pre-commit");
|
|
14318
14643
|
const rel2 = display(cwd, dir);
|
|
14319
|
-
note = rel2 ===
|
|
14644
|
+
note = rel2 === join16(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
|
|
14320
14645
|
}
|
|
14321
14646
|
}
|
|
14322
14647
|
const rel = display(cwd, path);
|
|
14323
14648
|
const refused = refuseNonRegular(path);
|
|
14324
14649
|
if (refused) return { item: "pre-commit", path: rel, status: "error", detail: refused };
|
|
14325
|
-
const existing =
|
|
14650
|
+
const existing = existsSync8(path) ? readFileSync9(path, "utf8") : null;
|
|
14326
14651
|
const hookMode = () => existingMode(path, 493) | 73;
|
|
14327
14652
|
if (existing === null) {
|
|
14328
14653
|
return {
|
|
@@ -14408,12 +14733,12 @@ ${block.join("\n")}
|
|
|
14408
14733
|
function planCodeowners(cwd) {
|
|
14409
14734
|
const rel = ".github/CODEOWNERS";
|
|
14410
14735
|
const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
|
|
14411
|
-
(r) => writeTargetKind(
|
|
14736
|
+
(r) => writeTargetKind(join16(cwd, r)) !== "absent"
|
|
14412
14737
|
);
|
|
14413
|
-
const path =
|
|
14738
|
+
const path = join16(cwd, existingRel ?? rel);
|
|
14414
14739
|
const refused = refuseNonRegular(path);
|
|
14415
14740
|
if (refused) return { item: "codeowners", path: existingRel ?? rel, status: "error", detail: refused };
|
|
14416
|
-
const existing = existingRel ?
|
|
14741
|
+
const existing = existingRel ? readFileSync9(path, "utf8") : null;
|
|
14417
14742
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
14418
14743
|
if (existingRel !== void 0 && missing.length === 0) {
|
|
14419
14744
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -14446,14 +14771,14 @@ ${CODEOWNERS_MARK}
|
|
|
14446
14771
|
}
|
|
14447
14772
|
function planWorkflow(cwd, force) {
|
|
14448
14773
|
const rel = ".github/workflows/tamperward.yml";
|
|
14449
|
-
const path =
|
|
14774
|
+
const path = join16(cwd, rel);
|
|
14450
14775
|
const write = () => {
|
|
14451
14776
|
mkdirSync7(dirname5(path), { recursive: true });
|
|
14452
14777
|
atomicReplaceFile(path, workflowFile(WORKFLOW_CONTENT), existingMode(path, 420));
|
|
14453
14778
|
};
|
|
14454
14779
|
const refused = refuseNonRegular(path);
|
|
14455
14780
|
if (refused) return { item: "ci", path: rel, status: "error", detail: refused };
|
|
14456
|
-
if (!
|
|
14781
|
+
if (!existsSync8(path)) {
|
|
14457
14782
|
return {
|
|
14458
14783
|
item: "ci",
|
|
14459
14784
|
path: rel,
|
|
@@ -14464,7 +14789,7 @@ function planWorkflow(cwd, force) {
|
|
|
14464
14789
|
}
|
|
14465
14790
|
let src;
|
|
14466
14791
|
try {
|
|
14467
|
-
src =
|
|
14792
|
+
src = readFileSync9(path, "utf8");
|
|
14468
14793
|
} catch {
|
|
14469
14794
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
14470
14795
|
}
|
|
@@ -14512,6 +14837,96 @@ function sameDir(a, b) {
|
|
|
14512
14837
|
return false;
|
|
14513
14838
|
}
|
|
14514
14839
|
}
|
|
14840
|
+
function planCodexHooks(cwd) {
|
|
14841
|
+
const rel = ".codex/config.toml";
|
|
14842
|
+
const path = join16(cwd, rel);
|
|
14843
|
+
const item = "codex-hooks";
|
|
14844
|
+
const refused = refuseNonRegular(path);
|
|
14845
|
+
if (refused) return { item, path: rel, status: "error", detail: refused };
|
|
14846
|
+
const warning = RUNTIME_WARNING("Codex", "codex");
|
|
14847
|
+
const tables = codexWiringTables();
|
|
14848
|
+
if (!existsSync8(path)) {
|
|
14849
|
+
return {
|
|
14850
|
+
item,
|
|
14851
|
+
path: rel,
|
|
14852
|
+
status: "create",
|
|
14853
|
+
detail: "wire the PreToolUse deny and Stop sweep [hooks] tables (experimental)",
|
|
14854
|
+
warning,
|
|
14855
|
+
apply: () => {
|
|
14856
|
+
mkdirSync7(dirname5(path), { recursive: true });
|
|
14857
|
+
atomicReplaceFile(path, tables, 420);
|
|
14858
|
+
}
|
|
14859
|
+
};
|
|
14860
|
+
}
|
|
14861
|
+
const current = readFileSync9(path, "utf8");
|
|
14862
|
+
const existing = codexHooksTables(current);
|
|
14863
|
+
if (!existing) {
|
|
14864
|
+
const next = current + (current.endsWith("\n") || current === "" ? "" : "\n") + "\n" + tables;
|
|
14865
|
+
return {
|
|
14866
|
+
item,
|
|
14867
|
+
path: rel,
|
|
14868
|
+
status: "update",
|
|
14869
|
+
detail: "append the PreToolUse deny and Stop sweep [hooks] tables (experimental)",
|
|
14870
|
+
warning,
|
|
14871
|
+
apply: () => atomicReplaceFile(path, next, existingMode(path, 420))
|
|
14872
|
+
};
|
|
14873
|
+
}
|
|
14874
|
+
if (existing === codexHooksTables(tables)) return { item, path: rel, status: "ok", detail: "PreToolUse + Stop hooks already wired" };
|
|
14875
|
+
if (withoutRuntimeGatePins(existing) === withoutRuntimeGatePins(codexHooksTables(tables))) {
|
|
14876
|
+
const pins = [...new Set(runtimeGatePins(existing))];
|
|
14877
|
+
const next = current.replace(/\btamperward@\S+ (hook|sweep) codex\b/g, `tamperward@${TW_VERSION} $1 codex`);
|
|
14878
|
+
return {
|
|
14879
|
+
item,
|
|
14880
|
+
path: rel,
|
|
14881
|
+
status: "update",
|
|
14882
|
+
detail: `re-pin the hook commands to tamperward@${TW_VERSION} (was ${pins.join(", ")})`,
|
|
14883
|
+
warning,
|
|
14884
|
+
apply: () => atomicReplaceFile(path, next, existingMode(path, 420))
|
|
14885
|
+
};
|
|
14886
|
+
}
|
|
14887
|
+
return {
|
|
14888
|
+
item,
|
|
14889
|
+
path: rel,
|
|
14890
|
+
status: "error",
|
|
14891
|
+
detail: "the [hooks] tables are not the shape init writes \u2014 remove them or add the tamperward entries by hand, then re-run init (refusing to rewrite a hand-written hooks block)"
|
|
14892
|
+
};
|
|
14893
|
+
}
|
|
14894
|
+
function planCopilotHooks(cwd) {
|
|
14895
|
+
const rel = ".github/hooks/tamperward.json";
|
|
14896
|
+
const path = join16(cwd, rel);
|
|
14897
|
+
const item = "copilot-hooks";
|
|
14898
|
+
const refused = refuseNonRegular(path);
|
|
14899
|
+
if (refused) return { item, path: rel, status: "error", detail: refused };
|
|
14900
|
+
const warning = RUNTIME_WARNING("Copilot CLI", "copilot");
|
|
14901
|
+
const document = copilotWiringDocument();
|
|
14902
|
+
const content = JSON.stringify({ $comment: COPILOT_WIRING_NOTE, ...document }, null, 2) + "\n";
|
|
14903
|
+
const write = () => {
|
|
14904
|
+
mkdirSync7(dirname5(path), { recursive: true });
|
|
14905
|
+
atomicReplaceFile(path, content, existingMode(path, 420));
|
|
14906
|
+
};
|
|
14907
|
+
if (!existsSync8(path)) {
|
|
14908
|
+
return { item, path: rel, status: "create", detail: "wire the preToolUse deny and agentStop sweep hooks (experimental)", warning, apply: write };
|
|
14909
|
+
}
|
|
14910
|
+
let parsed;
|
|
14911
|
+
try {
|
|
14912
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
14913
|
+
} catch {
|
|
14914
|
+
return { item, path: rel, status: "error", detail: "exists but is not valid JSON \u2014 fix it, then re-run init (refusing to overwrite)" };
|
|
14915
|
+
}
|
|
14916
|
+
const hooks = isRecord(parsed) ? JSON.stringify(parsed.hooks ?? null) : "null";
|
|
14917
|
+
const wanted = JSON.stringify(document.hooks);
|
|
14918
|
+
if (hooks === wanted) return { item, path: rel, status: "ok", detail: "preToolUse + agentStop hooks already wired" };
|
|
14919
|
+
if (withoutRuntimeGatePins(hooks) === withoutRuntimeGatePins(wanted)) {
|
|
14920
|
+
const pins = [...new Set(runtimeGatePins(hooks))];
|
|
14921
|
+
return { item, path: rel, status: "update", detail: `re-pin the hook commands to tamperward@${TW_VERSION} (was ${pins.join(", ")})`, warning, apply: write };
|
|
14922
|
+
}
|
|
14923
|
+
return {
|
|
14924
|
+
item,
|
|
14925
|
+
path: rel,
|
|
14926
|
+
status: "error",
|
|
14927
|
+
detail: "the hooks are not the shape init writes \u2014 remove the file or restore the tamperward entries by hand, then re-run init (refusing to rewrite a hand-written hooks file)"
|
|
14928
|
+
};
|
|
14929
|
+
}
|
|
14515
14930
|
function planInit(cwd, opts = {}) {
|
|
14516
14931
|
requireShippedVersion();
|
|
14517
14932
|
const gitignore = planGitignore(cwd);
|
|
@@ -14519,11 +14934,24 @@ function planInit(cwd, opts = {}) {
|
|
|
14519
14934
|
planned("policy", POLICY_FILE, () => planPolicy(cwd)),
|
|
14520
14935
|
...gitignore ? [gitignore] : [],
|
|
14521
14936
|
planned("agent", ".claude/settings.json", () => planClaudeHooks(cwd)),
|
|
14937
|
+
...runtimeWiringPlans(cwd),
|
|
14522
14938
|
planned("pre-commit", "(hooks)", () => planPreCommit(cwd)),
|
|
14523
14939
|
planned("ci", ".github/workflows/tamperward.yml", () => planWorkflow(cwd, opts.forceWorkflow ?? false)),
|
|
14524
14940
|
planned("codeowners", ".github/CODEOWNERS", () => planCodeowners(cwd))
|
|
14525
14941
|
];
|
|
14526
14942
|
}
|
|
14943
|
+
function runtimeWiringPlans(cwd) {
|
|
14944
|
+
let detected;
|
|
14945
|
+
try {
|
|
14946
|
+
detected = new Set(detectRuntimes(cwd).map((r) => r.id));
|
|
14947
|
+
} catch {
|
|
14948
|
+
detected = /* @__PURE__ */ new Set();
|
|
14949
|
+
}
|
|
14950
|
+
return [
|
|
14951
|
+
...detected.has("codex") ? [planned("codex-hooks", ".codex/config.toml", () => planCodexHooks(cwd))] : [],
|
|
14952
|
+
...detected.has("copilot") ? [planned("copilot-hooks", ".github/hooks/tamperward.json", () => planCopilotHooks(cwd))] : []
|
|
14953
|
+
];
|
|
14954
|
+
}
|
|
14527
14955
|
function runInit(opts) {
|
|
14528
14956
|
const requested = resolve6(opts.cwd ?? process.cwd());
|
|
14529
14957
|
const cwd = repoRoot(requested);
|
|
@@ -14578,7 +15006,7 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
14578
15006
|
);
|
|
14579
15007
|
return 0;
|
|
14580
15008
|
}
|
|
14581
|
-
var errText3, isPlainObject, HOOK_RE, SWEEP_RE, PRECOMMIT_RE, LEGACY_OURS, PRE_GLOBAL_OURS, CODEOWNERS_MARK, CODEOWNERS_PATHS, POLICY_CONTENT, WORKFLOW_CONTENT, WORKFLOW_MARK, WORKFLOW_MARK_RE, MANAGED_HOOKS;
|
|
15009
|
+
var errText3, isPlainObject, HOOK_RE, SWEEP_RE, PRECOMMIT_RE, LEGACY_OURS, PRE_GLOBAL_OURS, CODEOWNERS_MARK, CODEOWNERS_PATHS, POLICY_CONTENT, WORKFLOW_CONTENT, WORKFLOW_MARK, WORKFLOW_MARK_RE, MANAGED_HOOKS, RUNTIME_WARNING;
|
|
14582
15010
|
var init_init = __esm({
|
|
14583
15011
|
"src/cli/init.ts"() {
|
|
14584
15012
|
"use strict";
|
|
@@ -14586,6 +15014,8 @@ var init_init = __esm({
|
|
|
14586
15014
|
init_policy_load();
|
|
14587
15015
|
init_verifier_limits();
|
|
14588
15016
|
init_wiring();
|
|
15017
|
+
init_hook_wiring();
|
|
15018
|
+
init_runtimes();
|
|
14589
15019
|
init_narrow();
|
|
14590
15020
|
init_hook_tampering();
|
|
14591
15021
|
init_repo_context();
|
|
@@ -14771,6 +15201,7 @@ jobs:
|
|
|
14771
15201
|
always_run: true`
|
|
14772
15202
|
}
|
|
14773
15203
|
];
|
|
15204
|
+
RUNTIME_WARNING = (label, id) => `EXPERIMENTAL: the ${label} hook wiring was written because this repository shows ${label} markers, but the ${label} adapter has not passed a pinned live qualification. \`tamperward runtime verify --runtime ${id}\` grades what is proven; until it reports in-loop steering, the agent-neutral layers (pre-commit + CI) are the live protection.`;
|
|
14774
15205
|
}
|
|
14775
15206
|
});
|
|
14776
15207
|
|
|
@@ -14811,20 +15242,20 @@ var init_fingerprint = __esm({
|
|
|
14811
15242
|
// src/dependency-env.ts
|
|
14812
15243
|
import { createHash as createHash10 } from "node:crypto";
|
|
14813
15244
|
import {
|
|
14814
|
-
existsSync as
|
|
15245
|
+
existsSync as existsSync9,
|
|
14815
15246
|
lstatSync as lstatSync9,
|
|
14816
|
-
readdirSync as
|
|
14817
|
-
readFileSync as
|
|
15247
|
+
readdirSync as readdirSync4,
|
|
15248
|
+
readFileSync as readFileSync10,
|
|
14818
15249
|
readlinkSync as readlinkSync2,
|
|
14819
15250
|
realpathSync as realpathSync6,
|
|
14820
|
-
statSync as
|
|
15251
|
+
statSync as statSync4
|
|
14821
15252
|
} from "node:fs";
|
|
14822
15253
|
import {
|
|
14823
15254
|
basename as basename4,
|
|
14824
15255
|
delimiter,
|
|
14825
15256
|
dirname as dirname6,
|
|
14826
15257
|
isAbsolute as isAbsolute10,
|
|
14827
|
-
join as
|
|
15258
|
+
join as join17,
|
|
14828
15259
|
relative as relative5,
|
|
14829
15260
|
resolve as resolve7,
|
|
14830
15261
|
sep as sep3
|
|
@@ -14859,7 +15290,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
14859
15290
|
root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
|
|
14860
15291
|
);
|
|
14861
15292
|
}
|
|
14862
|
-
if (!
|
|
15293
|
+
if (!statSync4(root.path).isDirectory()) {
|
|
14863
15294
|
throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
|
|
14864
15295
|
}
|
|
14865
15296
|
h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
|
|
@@ -14869,10 +15300,10 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
14869
15300
|
const hashFile = (path, label) => {
|
|
14870
15301
|
let st;
|
|
14871
15302
|
try {
|
|
14872
|
-
st =
|
|
15303
|
+
st = statSync4(path);
|
|
14873
15304
|
if (!st.isFile()) throw new Error("not a regular file");
|
|
14874
15305
|
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
14875
|
-
h.update(
|
|
15306
|
+
h.update(readFileSync10(path));
|
|
14876
15307
|
h.update("\0");
|
|
14877
15308
|
} catch (e) {
|
|
14878
15309
|
throw new Unattestable(
|
|
@@ -14886,14 +15317,14 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
14886
15317
|
visited.add(realDir);
|
|
14887
15318
|
let entries;
|
|
14888
15319
|
try {
|
|
14889
|
-
entries =
|
|
15320
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
14890
15321
|
} catch (e) {
|
|
14891
15322
|
throw new Unattestable(
|
|
14892
15323
|
"dependency directory " + JSON.stringify(dir) + " cannot be read (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
14893
15324
|
);
|
|
14894
15325
|
}
|
|
14895
15326
|
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
14896
|
-
const path =
|
|
15327
|
+
const path = join17(dir, entry.name);
|
|
14897
15328
|
const rel = namespace ? namespace + "/" + entry.name : entry.name;
|
|
14898
15329
|
let ident;
|
|
14899
15330
|
try {
|
|
@@ -14906,7 +15337,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
14906
15337
|
h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
|
|
14907
15338
|
if (ident.kind === "file") {
|
|
14908
15339
|
try {
|
|
14909
|
-
h.update(
|
|
15340
|
+
h.update(readFileSync10(path));
|
|
14910
15341
|
h.update("\0");
|
|
14911
15342
|
} catch (e) {
|
|
14912
15343
|
throw new Unattestable(
|
|
@@ -14935,7 +15366,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
14935
15366
|
} catch {
|
|
14936
15367
|
continue;
|
|
14937
15368
|
}
|
|
14938
|
-
const followed =
|
|
15369
|
+
const followed = statSync4(path);
|
|
14939
15370
|
if (followed.isFile()) {
|
|
14940
15371
|
hashFile(resolved, "@target:" + rel);
|
|
14941
15372
|
continue;
|
|
@@ -14965,7 +15396,7 @@ function snapshot(roots, probes, cwd) {
|
|
|
14965
15396
|
const h = createHash10("sha256");
|
|
14966
15397
|
for (const probe of probes) {
|
|
14967
15398
|
h.update(
|
|
14968
|
-
"probe\0" + probe.kind + "\0" + probe.path + "\0" + (
|
|
15399
|
+
"probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync9(probe.path) ? "present" : "absent") + "\0"
|
|
14969
15400
|
);
|
|
14970
15401
|
}
|
|
14971
15402
|
for (const root of roots) fingerprintRoot(root, cwd, h);
|
|
@@ -14983,7 +15414,7 @@ function measuredSnapshot(roots, probes, cwd, metrics) {
|
|
|
14983
15414
|
}
|
|
14984
15415
|
function freezeRoot(kind, path) {
|
|
14985
15416
|
try {
|
|
14986
|
-
if (!
|
|
15417
|
+
if (!statSync4(path).isDirectory()) {
|
|
14987
15418
|
throw new Error("not a directory");
|
|
14988
15419
|
}
|
|
14989
15420
|
return { kind, path: resolve7(path), realPath: realpathSync6(path) };
|
|
@@ -15024,7 +15455,7 @@ function resolveExecutable(token, cwd, env) {
|
|
|
15024
15455
|
if (token.includes("/") || token.includes("\\")) {
|
|
15025
15456
|
const path = isAbsolute10(token) ? token : resolve7(cwd, token);
|
|
15026
15457
|
try {
|
|
15027
|
-
return
|
|
15458
|
+
return existsSync9(path) ? realpathSync6(path) : null;
|
|
15028
15459
|
} catch {
|
|
15029
15460
|
return null;
|
|
15030
15461
|
}
|
|
@@ -15032,7 +15463,7 @@ function resolveExecutable(token, cwd, env) {
|
|
|
15032
15463
|
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
15033
15464
|
const candidate = resolve7(entry, token);
|
|
15034
15465
|
try {
|
|
15035
|
-
if (
|
|
15466
|
+
if (existsSync9(candidate) && statSync4(candidate).isFile()) return realpathSync6(candidate);
|
|
15036
15467
|
} catch {
|
|
15037
15468
|
}
|
|
15038
15469
|
}
|
|
@@ -15055,7 +15486,7 @@ function inferredVenvFromToken(token, cwd) {
|
|
|
15055
15486
|
const parentName = basename4(parent).toLowerCase();
|
|
15056
15487
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
15057
15488
|
const root = dirname6(parent);
|
|
15058
|
-
return
|
|
15489
|
+
return existsSync9(join17(root, "pyvenv.cfg")) ? root : null;
|
|
15059
15490
|
}
|
|
15060
15491
|
function unsupportedEcosystem(command, token) {
|
|
15061
15492
|
const name = token ? basename4(token).toLowerCase() : "";
|
|
@@ -15071,13 +15502,13 @@ function inferredVenv(executable) {
|
|
|
15071
15502
|
const parentName = basename4(parent).toLowerCase();
|
|
15072
15503
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
15073
15504
|
const root = dirname6(parent);
|
|
15074
|
-
return
|
|
15505
|
+
return existsSync9(join17(root, "pyvenv.cfg")) ? root : null;
|
|
15075
15506
|
}
|
|
15076
15507
|
function venvProblem(root) {
|
|
15077
|
-
const cfg =
|
|
15078
|
-
if (!
|
|
15508
|
+
const cfg = join17(root, "pyvenv.cfg");
|
|
15509
|
+
if (!existsSync9(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
|
|
15079
15510
|
try {
|
|
15080
|
-
const body =
|
|
15511
|
+
const body = readFileSync10(cfg, "utf8");
|
|
15081
15512
|
if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
|
|
15082
15513
|
return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
|
|
15083
15514
|
}
|
|
@@ -15101,8 +15532,8 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
|
15101
15532
|
}
|
|
15102
15533
|
};
|
|
15103
15534
|
try {
|
|
15104
|
-
const nodeModules =
|
|
15105
|
-
if (
|
|
15535
|
+
const nodeModules = join17(cwd, "node_modules");
|
|
15536
|
+
if (existsSync9(nodeModules)) addRoot("node_modules", nodeModules);
|
|
15106
15537
|
else probes.push({ kind: "node_modules", path: nodeModules });
|
|
15107
15538
|
const token = firstExecutable(command);
|
|
15108
15539
|
const executable = resolveExecutable(token, cwd, env);
|
|
@@ -15114,8 +15545,8 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
|
|
|
15114
15545
|
venv = resolve7(env.VIRTUAL_ENV);
|
|
15115
15546
|
} else {
|
|
15116
15547
|
venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
|
|
15117
|
-
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) &&
|
|
15118
|
-
venv =
|
|
15548
|
+
if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync9(join17(cwd, ".venv", "pyvenv.cfg"))) {
|
|
15549
|
+
venv = join17(cwd, ".venv");
|
|
15119
15550
|
}
|
|
15120
15551
|
}
|
|
15121
15552
|
if (venv) {
|
|
@@ -15221,7 +15652,7 @@ var init_dependency_env = __esm({
|
|
|
15221
15652
|
import { spawnSync } from "node:child_process";
|
|
15222
15653
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
15223
15654
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15224
|
-
import { join as
|
|
15655
|
+
import { join as join18 } from "node:path";
|
|
15225
15656
|
function emptySuiteDiagnostics() {
|
|
15226
15657
|
return {
|
|
15227
15658
|
stdout: { ...EMPTY_STREAM },
|
|
@@ -15301,8 +15732,8 @@ function parseCapturedSupervisorResult(stdoutText) {
|
|
|
15301
15732
|
};
|
|
15302
15733
|
}
|
|
15303
15734
|
function runCapturedProcessSync(executable, args, opts) {
|
|
15304
|
-
const stateDir2 = mkdtempSync2(
|
|
15305
|
-
const configFile =
|
|
15735
|
+
const stateDir2 = mkdtempSync2(join18(tmpdir3(), "tw-suite-capture-"));
|
|
15736
|
+
const configFile = join18(stateDir2, "config.json");
|
|
15306
15737
|
const backstop = opts.backstopMs ?? 3e4;
|
|
15307
15738
|
try {
|
|
15308
15739
|
writeFileSync7(
|
|
@@ -15593,8 +16024,8 @@ if (child) {
|
|
|
15593
16024
|
// src/verifier-backend.ts
|
|
15594
16025
|
import { createHash as createHash11, randomUUID } from "node:crypto";
|
|
15595
16026
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
15596
|
-
import { accessSync as accessSync2, constants as constants4, readFileSync as
|
|
15597
|
-
import { delimiter as delimiter2, dirname as dirname7, join as
|
|
16027
|
+
import { accessSync as accessSync2, constants as constants4, readFileSync as readFileSync11, realpathSync as realpathSync7, statSync as statSync5 } from "node:fs";
|
|
16028
|
+
import { delimiter as delimiter2, dirname as dirname7, join as join19, resolve as resolve8 } from "node:path";
|
|
15598
16029
|
function identity() {
|
|
15599
16030
|
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
15600
16031
|
const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
|
|
@@ -15602,7 +16033,7 @@ function identity() {
|
|
|
15602
16033
|
}
|
|
15603
16034
|
function writableByCaller(path) {
|
|
15604
16035
|
try {
|
|
15605
|
-
const st =
|
|
16036
|
+
const st = statSync5(path);
|
|
15606
16037
|
const { uid, groups } = identity();
|
|
15607
16038
|
if (uid === 0) return true;
|
|
15608
16039
|
if ((st.mode & 2) !== 0) return true;
|
|
@@ -15628,7 +16059,7 @@ function replaceableByCaller(path) {
|
|
|
15628
16059
|
function findExecutable(name) {
|
|
15629
16060
|
for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
|
|
15630
16061
|
if (!raw) continue;
|
|
15631
|
-
const candidate =
|
|
16062
|
+
const candidate = join19(raw, name);
|
|
15632
16063
|
try {
|
|
15633
16064
|
accessSync2(candidate, constants4.X_OK);
|
|
15634
16065
|
return realpathSync7(candidate);
|
|
@@ -15638,7 +16069,7 @@ function findExecutable(name) {
|
|
|
15638
16069
|
return null;
|
|
15639
16070
|
}
|
|
15640
16071
|
function fileSha256(path) {
|
|
15641
|
-
return createHash11("sha256").update(
|
|
16072
|
+
return createHash11("sha256").update(readFileSync11(path)).digest("hex");
|
|
15642
16073
|
}
|
|
15643
16074
|
function engineClientEnv() {
|
|
15644
16075
|
return {
|
|
@@ -16072,8 +16503,8 @@ var init_verifier_backend = __esm({
|
|
|
16072
16503
|
// src/verification-state.ts
|
|
16073
16504
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
16074
16505
|
import { createHash as createHash12 } from "node:crypto";
|
|
16075
|
-
import { mkdirSync as mkdirSync8, readFileSync as
|
|
16076
|
-
import { dirname as dirname8, join as
|
|
16506
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync12, rmSync as rmSync7 } from "node:fs";
|
|
16507
|
+
import { dirname as dirname8, join as join20 } from "node:path";
|
|
16077
16508
|
function git2(args, cwd) {
|
|
16078
16509
|
return execFileSync8("git", args, {
|
|
16079
16510
|
cwd,
|
|
@@ -16123,19 +16554,19 @@ function surfacePaths(base, cwd, policy) {
|
|
|
16123
16554
|
function interventionWiring(cwd) {
|
|
16124
16555
|
const dir = claudeConfigDir();
|
|
16125
16556
|
const sources = [
|
|
16126
|
-
["repo",
|
|
16127
|
-
["repo.local",
|
|
16128
|
-
["user",
|
|
16129
|
-
["user.local",
|
|
16557
|
+
["repo", join20(cwd, ".claude", "settings.json")],
|
|
16558
|
+
["repo.local", join20(cwd, ".claude", "settings.local.json")],
|
|
16559
|
+
["user", join20(dir, "settings.json")],
|
|
16560
|
+
["user.local", join20(dir, "settings.local.json")]
|
|
16130
16561
|
];
|
|
16131
16562
|
return sources.map(([source, path]) => {
|
|
16132
16563
|
let parsed;
|
|
16133
16564
|
try {
|
|
16134
|
-
parsed = JSON.parse(
|
|
16565
|
+
parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
16135
16566
|
} catch {
|
|
16136
16567
|
return { source, present: false };
|
|
16137
16568
|
}
|
|
16138
|
-
if (!
|
|
16569
|
+
if (!isRecord5(parsed)) return { source, present: true, shape: "non-object" };
|
|
16139
16570
|
return {
|
|
16140
16571
|
source,
|
|
16141
16572
|
present: true,
|
|
@@ -16193,11 +16624,11 @@ function stateDir(cwd) {
|
|
|
16193
16624
|
}
|
|
16194
16625
|
function verificationRecordPath(cwd) {
|
|
16195
16626
|
const dir = stateDir(cwd);
|
|
16196
|
-
return dir ?
|
|
16627
|
+
return dir ? join20(dir, "verification-state.json") : null;
|
|
16197
16628
|
}
|
|
16198
16629
|
function verifyingMarkerPath(cwd) {
|
|
16199
16630
|
const dir = stateDir(cwd);
|
|
16200
|
-
return dir ?
|
|
16631
|
+
return dir ? join20(dir, "verifying.json") : null;
|
|
16201
16632
|
}
|
|
16202
16633
|
function recordVerification(cwd, inputs, treeFingerprintValue) {
|
|
16203
16634
|
const path = verificationRecordPath(cwd);
|
|
@@ -16244,24 +16675,24 @@ function invalidateVerificationRecordIfCurrent(cwd) {
|
|
|
16244
16675
|
return false;
|
|
16245
16676
|
}
|
|
16246
16677
|
}
|
|
16247
|
-
function
|
|
16678
|
+
function isRecord5(v) {
|
|
16248
16679
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16249
16680
|
}
|
|
16250
16681
|
function isBinding(v) {
|
|
16251
|
-
if (!
|
|
16682
|
+
if (!isRecord5(v)) return false;
|
|
16252
16683
|
return BINDING_INPUTS.every((k) => {
|
|
16253
16684
|
const val = v[k];
|
|
16254
16685
|
return typeof val === "string" && val.length > 0;
|
|
16255
16686
|
});
|
|
16256
16687
|
}
|
|
16257
16688
|
function parseVerificationRecord(value) {
|
|
16258
|
-
if (!
|
|
16689
|
+
if (!isRecord5(value)) return null;
|
|
16259
16690
|
if (value.schema_version !== VERIFICATION_STATE_SCHEMA_VERSION) return null;
|
|
16260
16691
|
if (value.verdict !== "VERIFIED") return null;
|
|
16261
16692
|
if (typeof value.verified_at !== "string") return null;
|
|
16262
16693
|
if (typeof value.tw_version !== "string") return null;
|
|
16263
16694
|
const inputs = value.inputs;
|
|
16264
|
-
if (!
|
|
16695
|
+
if (!isRecord5(inputs)) return null;
|
|
16265
16696
|
if (typeof inputs.base_ref !== "string" || inputs.base_ref.length === 0) return null;
|
|
16266
16697
|
if (typeof inputs.explicit_base !== "boolean") return null;
|
|
16267
16698
|
if (inputs.command_source !== "policy" && inputs.command_source !== "flag") return null;
|
|
@@ -16303,7 +16734,7 @@ function pidAlive3(pid) {
|
|
|
16303
16734
|
process.kill(pid, 0);
|
|
16304
16735
|
return true;
|
|
16305
16736
|
} catch (e) {
|
|
16306
|
-
return
|
|
16737
|
+
return isRecord5(e) && e.code === "EPERM";
|
|
16307
16738
|
}
|
|
16308
16739
|
}
|
|
16309
16740
|
function beginVerifying(cwd) {
|
|
@@ -16341,7 +16772,7 @@ function readVerifyingMarker(cwd) {
|
|
|
16341
16772
|
} catch {
|
|
16342
16773
|
return null;
|
|
16343
16774
|
}
|
|
16344
|
-
if (!
|
|
16775
|
+
if (!isRecord5(parsed)) return null;
|
|
16345
16776
|
if (parsed.schema_version !== VERIFICATION_STATE_SCHEMA_VERSION) return null;
|
|
16346
16777
|
if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
16347
16778
|
if (typeof parsed.started_at !== "string") return null;
|
|
@@ -16486,8 +16917,8 @@ var init_verification_state = __esm({
|
|
|
16486
16917
|
// src/verification-receipt.ts
|
|
16487
16918
|
import { createHash as createHash13 } from "node:crypto";
|
|
16488
16919
|
import { mkdirSync as mkdirSync9, rmSync as rmSync8 } from "node:fs";
|
|
16489
|
-
import { dirname as dirname9, join as
|
|
16490
|
-
function
|
|
16920
|
+
import { dirname as dirname9, join as join21 } from "node:path";
|
|
16921
|
+
function isRecord6(v) {
|
|
16491
16922
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16492
16923
|
}
|
|
16493
16924
|
function canonicalJson2(value) {
|
|
@@ -16525,7 +16956,7 @@ function receiptFromRecord(record) {
|
|
|
16525
16956
|
}
|
|
16526
16957
|
function receiptPath(cwd) {
|
|
16527
16958
|
const ctx = repoContext(cwd);
|
|
16528
|
-
return ctx ?
|
|
16959
|
+
return ctx ? join21(stateDirectory(ctx.gitDir), "verification-receipt.json") : null;
|
|
16529
16960
|
}
|
|
16530
16961
|
function storeReceipt(cwd, receipt) {
|
|
16531
16962
|
try {
|
|
@@ -16550,10 +16981,10 @@ function isOneOf(list3, v) {
|
|
|
16550
16981
|
return typeof v === "string" && list3.includes(v);
|
|
16551
16982
|
}
|
|
16552
16983
|
function isStages(v) {
|
|
16553
|
-
return
|
|
16984
|
+
return isRecord6(v) && isOneOf(RECEIPT_STAGE_RESULTS, v.candidate) && isOneOf(RECEIPT_STAGE_RESULTS, v.pristine) && isOneOf(RECEIPT_INTEGRITY_RESULTS, v.integrity);
|
|
16554
16985
|
}
|
|
16555
16986
|
function isBinding2(v) {
|
|
16556
|
-
if (!
|
|
16987
|
+
if (!isRecord6(v)) return false;
|
|
16557
16988
|
return BINDING_INPUTS.every((k) => {
|
|
16558
16989
|
const val = v[k];
|
|
16559
16990
|
return typeof val === "string" && val.length > 0;
|
|
@@ -16563,7 +16994,7 @@ function classifyReceipt(value) {
|
|
|
16563
16994
|
if (value === null || value === void 0) {
|
|
16564
16995
|
return { disposition: "ABSENT", detail: "no local verification receipt was provided" };
|
|
16565
16996
|
}
|
|
16566
|
-
if (!
|
|
16997
|
+
if (!isRecord6(value)) {
|
|
16567
16998
|
return { disposition: "UNKNOWN_SCHEMA", detail: "receipt is not a JSON object" };
|
|
16568
16999
|
}
|
|
16569
17000
|
if (value.schema_version !== RECEIPT_SCHEMA_VERSION) {
|
|
@@ -16717,9 +17148,9 @@ var init_verification_receipt = __esm({
|
|
|
16717
17148
|
// src/cli/verify.ts
|
|
16718
17149
|
import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
|
|
16719
17150
|
import { createHash as createHash14 } from "node:crypto";
|
|
16720
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync10, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync3, readFileSync as
|
|
17151
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync10, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync3, rmSync as rmSync9, symlinkSync, writeFileSync as writeFileSync8, existsSync as existsSync10 } from "node:fs";
|
|
16721
17152
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
16722
|
-
import { dirname as dirname10, isAbsolute as isAbsolute11, join as
|
|
17153
|
+
import { dirname as dirname10, isAbsolute as isAbsolute11, join as join22, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
|
|
16723
17154
|
function oracleAssuranceReport() {
|
|
16724
17155
|
return {
|
|
16725
17156
|
level: "suite-exit-only",
|
|
@@ -16808,7 +17239,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
|
|
|
16808
17239
|
continue;
|
|
16809
17240
|
}
|
|
16810
17241
|
const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
|
|
16811
|
-
const current =
|
|
17242
|
+
const current = join22(base, ...stack);
|
|
16812
17243
|
let st;
|
|
16813
17244
|
try {
|
|
16814
17245
|
st = lstatSync10(current);
|
|
@@ -16850,7 +17281,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
16850
17281
|
const parts = rel.split("/").slice(0, -1);
|
|
16851
17282
|
let current = cwd;
|
|
16852
17283
|
for (const part of parts) {
|
|
16853
|
-
current =
|
|
17284
|
+
current = join22(current, part);
|
|
16854
17285
|
let st;
|
|
16855
17286
|
try {
|
|
16856
17287
|
st = lstatSync10(current);
|
|
@@ -16875,7 +17306,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
16875
17306
|
}
|
|
16876
17307
|
const links = [];
|
|
16877
17308
|
for (const rel of listed) {
|
|
16878
|
-
const src =
|
|
17309
|
+
const src = join22(cwd, rel);
|
|
16879
17310
|
rejectLinkedParent(cwd, rel);
|
|
16880
17311
|
let st;
|
|
16881
17312
|
try {
|
|
@@ -16883,7 +17314,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
16883
17314
|
} catch {
|
|
16884
17315
|
continue;
|
|
16885
17316
|
}
|
|
16886
|
-
const out3 =
|
|
17317
|
+
const out3 = join22(dest, rel);
|
|
16887
17318
|
mkdirSync10(dirname10(out3), { recursive: true });
|
|
16888
17319
|
dropSymlink(out3);
|
|
16889
17320
|
if (st.isSymbolicLink()) {
|
|
@@ -16903,17 +17334,17 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
16903
17334
|
for (const link of links) {
|
|
16904
17335
|
validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
|
|
16905
17336
|
}
|
|
16906
|
-
if (dependencyRoot) symlinkSync(dependencyRoot,
|
|
17337
|
+
if (dependencyRoot) symlinkSync(dependencyRoot, join22(dest, "node_modules"), "dir");
|
|
16907
17338
|
}
|
|
16908
17339
|
function workingTreePaths(cwd) {
|
|
16909
17340
|
return git3(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
16910
17341
|
}
|
|
16911
17342
|
function probeFilesystemCaseSensitivity(dir) {
|
|
16912
|
-
const upper =
|
|
16913
|
-
const lower =
|
|
17343
|
+
const upper = join22(dir, ".tw-case-probe-A");
|
|
17344
|
+
const lower = join22(dir, ".tw-case-probe-a");
|
|
16914
17345
|
try {
|
|
16915
17346
|
writeFileSync8(upper, "", { flag: "wx" });
|
|
16916
|
-
return !
|
|
17347
|
+
return !existsSync10(lower);
|
|
16917
17348
|
} finally {
|
|
16918
17349
|
rmSync9(upper, { force: true });
|
|
16919
17350
|
}
|
|
@@ -16973,11 +17404,11 @@ function overlayDigest(dest, paths) {
|
|
|
16973
17404
|
for (const rel of [...paths].sort()) {
|
|
16974
17405
|
h.update(rel);
|
|
16975
17406
|
h.update("\0");
|
|
16976
|
-
const p =
|
|
17407
|
+
const p = join22(dest, rel);
|
|
16977
17408
|
try {
|
|
16978
17409
|
const st = lstatSync10(p);
|
|
16979
17410
|
h.update(String(st.mode));
|
|
16980
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
17411
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync13(p));
|
|
16981
17412
|
} catch {
|
|
16982
17413
|
h.update("<gone>");
|
|
16983
17414
|
}
|
|
@@ -17007,7 +17438,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
17007
17438
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
17008
17439
|
}
|
|
17009
17440
|
const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
17010
|
-
const out3 =
|
|
17441
|
+
const out3 = join22(dest, e.path);
|
|
17011
17442
|
mkdirSync10(dirname10(out3), { recursive: true });
|
|
17012
17443
|
rmSync9(out3, { force: true });
|
|
17013
17444
|
if (e.mode === "120000") {
|
|
@@ -17024,7 +17455,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
|
|
|
17024
17455
|
const inCopy = workingTreePaths(cwd);
|
|
17025
17456
|
for (const rel of inCopy) {
|
|
17026
17457
|
if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
|
|
17027
|
-
rmSync9(
|
|
17458
|
+
rmSync9(join22(dest, rel), { force: true });
|
|
17028
17459
|
removed++;
|
|
17029
17460
|
}
|
|
17030
17461
|
for (const link of restoredLinks) {
|
|
@@ -17038,8 +17469,8 @@ function suiteEnv(scratch) {
|
|
|
17038
17469
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
17039
17470
|
env[k] = v;
|
|
17040
17471
|
}
|
|
17041
|
-
const userRc =
|
|
17042
|
-
const globalRc =
|
|
17472
|
+
const userRc = join22(scratch, "npmrc-user");
|
|
17473
|
+
const globalRc = join22(scratch, "npmrc-global");
|
|
17043
17474
|
for (const f of [userRc, globalRc]) writeFileSync8(f, "", { mode: 292 });
|
|
17044
17475
|
env.npm_config_userconfig = userRc;
|
|
17045
17476
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -17071,7 +17502,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
|
|
|
17071
17502
|
reason: `checkpointed-local verifier is unsupported on ${process.platform}; no trusted local shell contract is defined`
|
|
17072
17503
|
};
|
|
17073
17504
|
}
|
|
17074
|
-
const scratch = mkdtempSync3(
|
|
17505
|
+
const scratch = mkdtempSync3(join22(tmpdir4(), "tw-verify-run-"));
|
|
17075
17506
|
try {
|
|
17076
17507
|
const r = runCapturedProcessSync(shell.executable, shell.args, {
|
|
17077
17508
|
cwd: dir,
|
|
@@ -17317,8 +17748,8 @@ function runVerifyImpl(opts) {
|
|
|
17317
17748
|
} catch {
|
|
17318
17749
|
return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
|
|
17319
17750
|
}
|
|
17320
|
-
const visRoot = mkdtempSync3(
|
|
17321
|
-
const visDir =
|
|
17751
|
+
const visRoot = mkdtempSync3(join22(tmpdir4(), "tw-verify-vis-"));
|
|
17752
|
+
const visDir = join22(visRoot, "t");
|
|
17322
17753
|
const cleanup = (dirs) => {
|
|
17323
17754
|
if (opts.keep) return;
|
|
17324
17755
|
for (const d of dirs) rmSync9(d, { recursive: true, force: true });
|
|
@@ -17327,7 +17758,7 @@ function runVerifyImpl(opts) {
|
|
|
17327
17758
|
if (!filesystemCaseSensitive) {
|
|
17328
17759
|
const present = (rel) => {
|
|
17329
17760
|
try {
|
|
17330
|
-
lstatSync10(
|
|
17761
|
+
lstatSync10(join22(cwd, rel));
|
|
17331
17762
|
return true;
|
|
17332
17763
|
} catch {
|
|
17333
17764
|
return false;
|
|
@@ -17460,8 +17891,8 @@ function runVerifyImpl(opts) {
|
|
|
17460
17891
|
{ stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
|
|
17461
17892
|
);
|
|
17462
17893
|
}
|
|
17463
|
-
const priRoot = mkdtempSync3(
|
|
17464
|
-
const priDir =
|
|
17894
|
+
const priRoot = mkdtempSync3(join22(tmpdir4(), "tw-verify-pri-"));
|
|
17895
|
+
const priDir = join22(priRoot, "t");
|
|
17465
17896
|
let restored = [];
|
|
17466
17897
|
let removedAdded = 0;
|
|
17467
17898
|
try {
|
|
@@ -17791,9 +18222,9 @@ var init_verify = __esm({
|
|
|
17791
18222
|
// src/cli/run.ts
|
|
17792
18223
|
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
17793
18224
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
17794
|
-
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as
|
|
18225
|
+
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync5, readFileSync as readFileSync14, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync10, statSync as statSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
17795
18226
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
17796
|
-
import { dirname as dirname11, join as
|
|
18227
|
+
import { dirname as dirname11, join as join23, resolve as resolve10 } from "node:path";
|
|
17797
18228
|
function authoritativeRunLifecyclePlatform(platform = process.platform) {
|
|
17798
18229
|
return platform === "linux";
|
|
17799
18230
|
}
|
|
@@ -17847,7 +18278,7 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
|
|
|
17847
18278
|
for (const candidate of candidates) {
|
|
17848
18279
|
try {
|
|
17849
18280
|
const real = realpathSync8(candidate);
|
|
17850
|
-
const st =
|
|
18281
|
+
const st = statSync6(real);
|
|
17851
18282
|
if (!st.isFile() || writableByCaller2(real)) continue;
|
|
17852
18283
|
const probe = spawnSync4(
|
|
17853
18284
|
real,
|
|
@@ -17869,9 +18300,9 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
|
|
|
17869
18300
|
};
|
|
17870
18301
|
}
|
|
17871
18302
|
function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
|
|
17872
|
-
const stateDir2 = mkdtempSync4(
|
|
17873
|
-
const resultFile =
|
|
17874
|
-
const agentEnvFile =
|
|
18303
|
+
const stateDir2 = mkdtempSync4(join23(tmpdir5(), "tw-agent-supervisor-"));
|
|
18304
|
+
const resultFile = join23(stateDir2, "result.json");
|
|
18305
|
+
const agentEnvFile = join23(stateDir2, "agent-env.json");
|
|
17875
18306
|
try {
|
|
17876
18307
|
const linux = process.platform === "linux";
|
|
17877
18308
|
let executable;
|
|
@@ -17945,7 +18376,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
|
|
|
17945
18376
|
}
|
|
17946
18377
|
let state = {};
|
|
17947
18378
|
try {
|
|
17948
|
-
state = JSON.parse(
|
|
18379
|
+
state = JSON.parse(readFileSync14(resultFile, "utf8"));
|
|
17949
18380
|
} catch {
|
|
17950
18381
|
return {
|
|
17951
18382
|
exit: 1,
|
|
@@ -17989,7 +18420,7 @@ function observerExited(pid) {
|
|
|
17989
18420
|
if (!pidAlive4(pid)) return true;
|
|
17990
18421
|
if (process.platform !== "linux") return false;
|
|
17991
18422
|
try {
|
|
17992
|
-
const stat =
|
|
18423
|
+
const stat = readFileSync14(`/proc/${pid}/stat`, "utf8");
|
|
17993
18424
|
const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0];
|
|
17994
18425
|
return state === "Z" || state === "X";
|
|
17995
18426
|
} catch (e) {
|
|
@@ -17997,8 +18428,8 @@ function observerExited(pid) {
|
|
|
17997
18428
|
}
|
|
17998
18429
|
}
|
|
17999
18430
|
function supervisedObserverLog(cwd) {
|
|
18000
|
-
const gd = gitDir(cwd) ??
|
|
18001
|
-
return
|
|
18431
|
+
const gd = gitDir(cwd) ?? join23(cwd, ".git");
|
|
18432
|
+
return join23(
|
|
18002
18433
|
stateDirectory(gd),
|
|
18003
18434
|
`run-observer-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}.jsonl`
|
|
18004
18435
|
);
|
|
@@ -18110,7 +18541,7 @@ function git4(args, cwd) {
|
|
|
18110
18541
|
}
|
|
18111
18542
|
function nowTicks() {
|
|
18112
18543
|
try {
|
|
18113
|
-
return Math.floor(parseFloat(
|
|
18544
|
+
return Math.floor(parseFloat(readFileSync14("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
18114
18545
|
} catch {
|
|
18115
18546
|
return Number.POSITIVE_INFINITY;
|
|
18116
18547
|
}
|
|
@@ -18125,7 +18556,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
18125
18556
|
}
|
|
18126
18557
|
let pids;
|
|
18127
18558
|
try {
|
|
18128
|
-
pids =
|
|
18559
|
+
pids = readdirSync5("/proc").filter((d) => /^\d+$/.test(d));
|
|
18129
18560
|
} catch {
|
|
18130
18561
|
return out3;
|
|
18131
18562
|
}
|
|
@@ -18133,7 +18564,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
18133
18564
|
const n2 = Number(pid);
|
|
18134
18565
|
if (n2 === process.pid) continue;
|
|
18135
18566
|
try {
|
|
18136
|
-
const stat =
|
|
18567
|
+
const stat = readFileSync14(`/proc/${pid}/stat`, "utf8");
|
|
18137
18568
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
18138
18569
|
if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
|
|
18139
18570
|
} catch {
|
|
@@ -18141,7 +18572,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
18141
18572
|
}
|
|
18142
18573
|
const procLinks = [`/proc/${pid}/cwd`, `/proc/${pid}/exe`];
|
|
18143
18574
|
try {
|
|
18144
|
-
for (const fd of
|
|
18575
|
+
for (const fd of readdirSync5(`/proc/${pid}/fd`)) procLinks.push(`/proc/${pid}/fd/${fd}`);
|
|
18145
18576
|
} catch {
|
|
18146
18577
|
}
|
|
18147
18578
|
let holds = false;
|
|
@@ -18706,8 +19137,8 @@ raise SystemExit(0)
|
|
|
18706
19137
|
|
|
18707
19138
|
// src/cli/doctor.ts
|
|
18708
19139
|
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
18709
|
-
import { existsSync as
|
|
18710
|
-
import { join as
|
|
19140
|
+
import { existsSync as existsSync11, readFileSync as readFileSync15, readdirSync as readdirSync6 } from "node:fs";
|
|
19141
|
+
import { join as join24, resolve as resolve11 } from "node:path";
|
|
18711
19142
|
function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
|
|
18712
19143
|
if (platform === "linux") {
|
|
18713
19144
|
if (linuxPython?.path) {
|
|
@@ -18762,10 +19193,10 @@ function wiringState(status, authority) {
|
|
|
18762
19193
|
return authority ? "BROKEN" : "WARN";
|
|
18763
19194
|
}
|
|
18764
19195
|
function pinsInFile(path) {
|
|
18765
|
-
if (!
|
|
19196
|
+
if (!existsSync11(path)) return [];
|
|
18766
19197
|
let src = "";
|
|
18767
19198
|
try {
|
|
18768
|
-
src =
|
|
19199
|
+
src = readFileSync15(path, "utf8");
|
|
18769
19200
|
} catch {
|
|
18770
19201
|
return [];
|
|
18771
19202
|
}
|
|
@@ -18786,13 +19217,13 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
|
|
|
18786
19217
|
const warnings = [];
|
|
18787
19218
|
for (const rel of workflowRels) {
|
|
18788
19219
|
const path = resolve11(cwd, rel);
|
|
18789
|
-
if (!
|
|
19220
|
+
if (!existsSync11(path)) {
|
|
18790
19221
|
broken.push(`${rel} is missing`);
|
|
18791
19222
|
continue;
|
|
18792
19223
|
}
|
|
18793
19224
|
let doc;
|
|
18794
19225
|
try {
|
|
18795
|
-
doc = yaml.parse(
|
|
19226
|
+
doc = yaml.parse(readFileSync15(path, "utf8"));
|
|
18796
19227
|
} catch (e) {
|
|
18797
19228
|
broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
|
|
18798
19229
|
continue;
|
|
@@ -18905,9 +19336,9 @@ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow =
|
|
|
18905
19336
|
addWiring("codeowners", "codeowners", true);
|
|
18906
19337
|
checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
|
|
18907
19338
|
const pins = [
|
|
18908
|
-
...pinsInFile(
|
|
18909
|
-
...pinsInFile(
|
|
18910
|
-
...pinsInFile(
|
|
19339
|
+
...pinsInFile(join24(cwd, ".claude", "settings.json")),
|
|
19340
|
+
...pinsInFile(join24(cwd, ".git", "hooks", "pre-commit")),
|
|
19341
|
+
...pinsInFile(join24(cwd, ".husky", "pre-commit"))
|
|
18911
19342
|
];
|
|
18912
19343
|
const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
|
|
18913
19344
|
const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
|
|
@@ -19191,19 +19622,19 @@ function diagnose(opts = {}) {
|
|
|
19191
19622
|
} else {
|
|
19192
19623
|
const workflowDirRel = ".github/workflows";
|
|
19193
19624
|
const workflowDir = resolve11(cwd, workflowDirRel);
|
|
19194
|
-
if (!
|
|
19625
|
+
if (!existsSync11(workflowDir)) {
|
|
19195
19626
|
return fail("ci-verifier", `${workflowDirRel}: workflow directory does not exist`);
|
|
19196
19627
|
}
|
|
19197
19628
|
let entries;
|
|
19198
19629
|
try {
|
|
19199
|
-
entries =
|
|
19630
|
+
entries = readdirSync6(workflowDir).filter((name) => /\.ya?ml$/i.test(name)).sort();
|
|
19200
19631
|
} catch (e) {
|
|
19201
19632
|
return fail(
|
|
19202
19633
|
"ci-verifier",
|
|
19203
19634
|
`${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
|
|
19204
19635
|
);
|
|
19205
19636
|
}
|
|
19206
|
-
for (const name of entries) workflowRels.push(
|
|
19637
|
+
for (const name of entries) workflowRels.push(join24(workflowDirRel, name));
|
|
19207
19638
|
}
|
|
19208
19639
|
const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
|
|
19209
19640
|
const requiredMinutes = Math.ceil(requiredSecs / 60);
|
|
@@ -19211,12 +19642,12 @@ function diagnose(opts = {}) {
|
|
|
19211
19642
|
const authorityWorkflows = [];
|
|
19212
19643
|
for (const workflowRel of workflowRels) {
|
|
19213
19644
|
const workflowPath = resolve11(cwd, workflowRel);
|
|
19214
|
-
if (!
|
|
19645
|
+
if (!existsSync11(workflowPath)) {
|
|
19215
19646
|
return fail("ci-verifier", `${workflowRel}: workflow does not exist`);
|
|
19216
19647
|
}
|
|
19217
19648
|
let doc;
|
|
19218
19649
|
try {
|
|
19219
|
-
doc = yaml.parse(
|
|
19650
|
+
doc = yaml.parse(readFileSync15(workflowPath, "utf8"));
|
|
19220
19651
|
} catch (e) {
|
|
19221
19652
|
return fail(
|
|
19222
19653
|
"ci-verifier",
|
|
@@ -19530,16 +19961,16 @@ var init_status2 = __esm({
|
|
|
19530
19961
|
// src/cli/trace-verify.ts
|
|
19531
19962
|
import { execFileSync as execFileSync12, spawnSync as spawnSync5 } from "node:child_process";
|
|
19532
19963
|
import {
|
|
19533
|
-
existsSync as
|
|
19964
|
+
existsSync as existsSync12,
|
|
19534
19965
|
mkdtempSync as mkdtempSync5,
|
|
19535
|
-
readFileSync as
|
|
19536
|
-
readdirSync as
|
|
19966
|
+
readFileSync as readFileSync16,
|
|
19967
|
+
readdirSync as readdirSync7,
|
|
19537
19968
|
realpathSync as realpathSync9,
|
|
19538
19969
|
rmSync as rmSync11,
|
|
19539
19970
|
symlinkSync as symlinkSync2
|
|
19540
19971
|
} from "node:fs";
|
|
19541
19972
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
19542
|
-
import { isAbsolute as isAbsolute12, join as
|
|
19973
|
+
import { isAbsolute as isAbsolute12, join as join25, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
|
|
19543
19974
|
function unescapeStraceString(s) {
|
|
19544
19975
|
return s.replace(/\\([0-7]{1,3})/g, (_m, oct) => String.fromCharCode(parseInt(oct, 8))).replace(/\\x([0-9a-fA-F]{2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
19545
19976
|
}
|
|
@@ -19787,16 +20218,16 @@ function materializeBase(base, cwd, dest) {
|
|
|
19787
20218
|
`tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
|
|
19788
20219
|
);
|
|
19789
20220
|
}
|
|
19790
|
-
const hostModules =
|
|
19791
|
-
const tracedModules =
|
|
19792
|
-
if (
|
|
20221
|
+
const hostModules = join25(cwd, "node_modules");
|
|
20222
|
+
const tracedModules = join25(dest, "node_modules");
|
|
20223
|
+
if (existsSync12(hostModules) && !existsSync12(tracedModules)) {
|
|
19793
20224
|
symlinkSync2(resolve12(hostModules), tracedModules, "dir");
|
|
19794
20225
|
}
|
|
19795
20226
|
}
|
|
19796
20227
|
function traceFiles(prefix) {
|
|
19797
20228
|
const dir = resolve12(prefix, "..");
|
|
19798
20229
|
const base = prefix.split(sep5).at(-1) ?? prefix;
|
|
19799
|
-
return
|
|
20230
|
+
return readdirSync7(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join25(dir, name)).sort();
|
|
19800
20231
|
}
|
|
19801
20232
|
function rewriteTraceRoot(accesses, actualRoot) {
|
|
19802
20233
|
return accesses.map((item) => {
|
|
@@ -19811,13 +20242,13 @@ function rewriteTraceRoot(accesses, actualRoot) {
|
|
|
19811
20242
|
if (!inside3(actualRoot, real)) return { ...item, path: real };
|
|
19812
20243
|
} catch {
|
|
19813
20244
|
}
|
|
19814
|
-
return { ...item, path:
|
|
20245
|
+
return { ...item, path: join25(TRACE_ROOT, ...rel.split("/")) };
|
|
19815
20246
|
});
|
|
19816
20247
|
}
|
|
19817
20248
|
function straceOnce(root, command, budget) {
|
|
19818
|
-
const traceDir = mkdtempSync5(
|
|
20249
|
+
const traceDir = mkdtempSync5(join25(tmpdir6(), "tw-trace-log-"));
|
|
19819
20250
|
try {
|
|
19820
|
-
const prefix =
|
|
20251
|
+
const prefix = join25(traceDir, "trace");
|
|
19821
20252
|
const traced = spawnSync5(
|
|
19822
20253
|
"strace",
|
|
19823
20254
|
[
|
|
@@ -19846,7 +20277,7 @@ function straceOnce(root, command, budget) {
|
|
|
19846
20277
|
}
|
|
19847
20278
|
);
|
|
19848
20279
|
const logs = traced.error ? [] : traceFiles(prefix);
|
|
19849
|
-
const accesses = logs.flatMap((path) => parseStraceFileAccess(
|
|
20280
|
+
const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync16(path, "utf8"), TRACE_ROOT));
|
|
19850
20281
|
const raw = {
|
|
19851
20282
|
spawnError: traced.error ? traced.error.message : null,
|
|
19852
20283
|
status: traced.status,
|
|
@@ -19861,7 +20292,7 @@ function straceOnce(root, command, budget) {
|
|
|
19861
20292
|
}
|
|
19862
20293
|
}
|
|
19863
20294
|
function runOneTrace(cwd, base, command, budget) {
|
|
19864
|
-
const root = mkdtempSync5(
|
|
20295
|
+
const root = mkdtempSync5(join25(tmpdir6(), "tw-trace-base-"));
|
|
19865
20296
|
try {
|
|
19866
20297
|
materializeBase(base, cwd, root);
|
|
19867
20298
|
const { raw, accesses } = straceOnce(root, command, budget);
|
|
@@ -19871,7 +20302,7 @@ function runOneTrace(cwd, base, command, budget) {
|
|
|
19871
20302
|
}
|
|
19872
20303
|
}
|
|
19873
20304
|
function tracerPreflight(budget = 10) {
|
|
19874
|
-
const probeRoot = mkdtempSync5(
|
|
20305
|
+
const probeRoot = mkdtempSync5(join25(tmpdir6(), "tw-trace-probe-"));
|
|
19875
20306
|
try {
|
|
19876
20307
|
const { raw } = straceOnce(probeRoot, "true", Math.max(1, Math.min(budget, 10)));
|
|
19877
20308
|
return classifyTraceRun(raw);
|
|
@@ -20077,163 +20508,6 @@ var init_trace_verify = __esm({
|
|
|
20077
20508
|
}
|
|
20078
20509
|
});
|
|
20079
20510
|
|
|
20080
|
-
// src/immutable.ts
|
|
20081
|
-
function isPlainData(value) {
|
|
20082
|
-
if (value === null || typeof value !== "object") return false;
|
|
20083
|
-
const proto = Object.getPrototypeOf(value);
|
|
20084
|
-
return proto === Object.prototype || proto === Array.prototype || proto === null;
|
|
20085
|
-
}
|
|
20086
|
-
function deepFreeze(value) {
|
|
20087
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
20088
|
-
const walk2 = (node) => {
|
|
20089
|
-
if (!isPlainData(node) || seen.has(node)) return;
|
|
20090
|
-
seen.add(node);
|
|
20091
|
-
Object.freeze(node);
|
|
20092
|
-
for (const key2 of Reflect.ownKeys(node)) walk2(Reflect.get(node, key2));
|
|
20093
|
-
};
|
|
20094
|
-
walk2(value);
|
|
20095
|
-
return value;
|
|
20096
|
-
}
|
|
20097
|
-
var init_immutable = __esm({
|
|
20098
|
-
"src/immutable.ts"() {
|
|
20099
|
-
"use strict";
|
|
20100
|
-
}
|
|
20101
|
-
});
|
|
20102
|
-
|
|
20103
|
-
// src/runtimes.ts
|
|
20104
|
-
import { existsSync as existsSync12, readFileSync as readFileSync16 } from "node:fs";
|
|
20105
|
-
import { join as join25 } from "node:path";
|
|
20106
|
-
function isRecord6(x) {
|
|
20107
|
-
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
20108
|
-
}
|
|
20109
|
-
function hooksBeyondTamperward(hooks) {
|
|
20110
|
-
if (!isRecord6(hooks)) return hooks != null;
|
|
20111
|
-
for (const entries of Object.values(hooks)) {
|
|
20112
|
-
if (!Array.isArray(entries)) {
|
|
20113
|
-
if (entries != null) return true;
|
|
20114
|
-
continue;
|
|
20115
|
-
}
|
|
20116
|
-
for (const entry of entries) {
|
|
20117
|
-
const hs = isRecord6(entry) ? entry.hooks : null;
|
|
20118
|
-
if (!Array.isArray(hs)) {
|
|
20119
|
-
if (hs != null) return true;
|
|
20120
|
-
continue;
|
|
20121
|
-
}
|
|
20122
|
-
for (const h of hs) {
|
|
20123
|
-
const cmd = isRecord6(h) ? String(h.command ?? "") : "";
|
|
20124
|
-
if (!/tamperward/.test(cmd)) return true;
|
|
20125
|
-
}
|
|
20126
|
-
}
|
|
20127
|
-
}
|
|
20128
|
-
return false;
|
|
20129
|
-
}
|
|
20130
|
-
function claudeSettingsBeyondTamperward(cwd) {
|
|
20131
|
-
const path = join25(cwd, ".claude", "settings.json");
|
|
20132
|
-
let raw;
|
|
20133
|
-
try {
|
|
20134
|
-
if (!existsSync12(path)) return false;
|
|
20135
|
-
raw = readFileSync16(path, "utf8");
|
|
20136
|
-
} catch {
|
|
20137
|
-
return false;
|
|
20138
|
-
}
|
|
20139
|
-
let parsed;
|
|
20140
|
-
try {
|
|
20141
|
-
parsed = JSON.parse(raw);
|
|
20142
|
-
} catch {
|
|
20143
|
-
return raw.trim().length > 0;
|
|
20144
|
-
}
|
|
20145
|
-
if (!isRecord6(parsed)) return raw.trim().length > 0;
|
|
20146
|
-
for (const [key2, value] of Object.entries(parsed)) {
|
|
20147
|
-
if (key2 === "disableAllHooks") continue;
|
|
20148
|
-
if (key2 === "hooks") {
|
|
20149
|
-
if (hooksBeyondTamperward(value)) return true;
|
|
20150
|
-
continue;
|
|
20151
|
-
}
|
|
20152
|
-
return true;
|
|
20153
|
-
}
|
|
20154
|
-
return false;
|
|
20155
|
-
}
|
|
20156
|
-
function detectRuntimes(cwd) {
|
|
20157
|
-
const found = [];
|
|
20158
|
-
for (const rt of KNOWN_RUNTIMES) {
|
|
20159
|
-
const marker = rt.markers.find((m) => {
|
|
20160
|
-
try {
|
|
20161
|
-
return existsSync12(join25(cwd, m));
|
|
20162
|
-
} catch {
|
|
20163
|
-
return false;
|
|
20164
|
-
}
|
|
20165
|
-
});
|
|
20166
|
-
const matchedMarker = marker ?? rt.detectExtra?.(cwd) ?? void 0;
|
|
20167
|
-
if (matchedMarker !== void 0) found.push({ ...rt, matchedMarker });
|
|
20168
|
-
}
|
|
20169
|
-
return found;
|
|
20170
|
-
}
|
|
20171
|
-
function hasInLoopRuntime(detected) {
|
|
20172
|
-
return detected.some((r) => r.steering === "in-loop");
|
|
20173
|
-
}
|
|
20174
|
-
function detectionHeadline(detected) {
|
|
20175
|
-
if (detected.length === 0) {
|
|
20176
|
-
return "No agent runtime detected. Wiring Claude Code in-loop steering plus the agent-neutral layers (pre-commit + CI) that protect any agent or human.";
|
|
20177
|
-
}
|
|
20178
|
-
if (detected.length === 1) {
|
|
20179
|
-
const r = detected[0];
|
|
20180
|
-
return `Detected ${r.label} \u2014 ${r.note}`;
|
|
20181
|
-
}
|
|
20182
|
-
const parts = detected.map((r) => `${r.label} (${r.steering === "in-loop" ? "in-loop" : "neutral layers"})`);
|
|
20183
|
-
return `Detected ${detected.length} agent runtimes: ${parts.join(", ")}.`;
|
|
20184
|
-
}
|
|
20185
|
-
function neutralOnlyCaveat(detected) {
|
|
20186
|
-
const neutral = detected.filter((r) => r.steering === "neutral");
|
|
20187
|
-
if (neutral.length === 0) return null;
|
|
20188
|
-
const names = neutral.map((r) => r.label).join(", ");
|
|
20189
|
-
const claudeAlso = hasInLoopRuntime(detected);
|
|
20190
|
-
const lead = claudeAlso ? `In-loop steering (deny-before-execute) ships for Claude Code only today: Claude Code here gets it, but on ${names} your live protection is ` : `In-loop steering (deny-before-execute) ships for Claude Code only today, so on ${names} your live protection is `;
|
|
20191
|
-
const tail = claudeAlso ? "" : " The Claude hook wiring is still written and is inert unless Claude Code runs in this repository.";
|
|
20192
|
-
return `${lead}the agent-neutral layers: pre-commit and CI.${tail}`;
|
|
20193
|
-
}
|
|
20194
|
-
var KNOWN_RUNTIMES;
|
|
20195
|
-
var init_runtimes = __esm({
|
|
20196
|
-
"src/runtimes.ts"() {
|
|
20197
|
-
"use strict";
|
|
20198
|
-
init_immutable();
|
|
20199
|
-
KNOWN_RUNTIMES = deepFreeze([
|
|
20200
|
-
{
|
|
20201
|
-
id: "claude-code",
|
|
20202
|
-
// Markers are Claude Code files `init` never writes, so detection never mistakes
|
|
20203
|
-
// TamperWard's own generated `.claude/settings.json` for evidence a human runs
|
|
20204
|
-
// Claude here (#526). `detectExtra` looks at that generated file's CONTENT for
|
|
20205
|
-
// real use (configuration beyond the two hooks init merges in).
|
|
20206
|
-
label: "Claude Code",
|
|
20207
|
-
markers: ["CLAUDE.md", ".claude/settings.local.json", ".claude/commands", ".claude/agents"],
|
|
20208
|
-
detectExtra: (cwd) => claudeSettingsBeyondTamperward(cwd) ? ".claude/settings.json" : null,
|
|
20209
|
-
steering: "in-loop",
|
|
20210
|
-
note: "native in-loop steering (PreToolUse deny + Stop sweep) plus the agent-neutral layers."
|
|
20211
|
-
},
|
|
20212
|
-
{
|
|
20213
|
-
id: "cursor",
|
|
20214
|
-
label: "Cursor",
|
|
20215
|
-
markers: [".cursor", ".cursorrules"],
|
|
20216
|
-
steering: "neutral",
|
|
20217
|
-
note: "agent-neutral layers (pre-commit + CI) today; no in-loop adapter ships for Cursor."
|
|
20218
|
-
},
|
|
20219
|
-
{
|
|
20220
|
-
id: "copilot",
|
|
20221
|
-
label: "GitHub Copilot",
|
|
20222
|
-
markers: [".github/copilot-instructions.md", ".github/copilot"],
|
|
20223
|
-
steering: "neutral",
|
|
20224
|
-
note: "agent-neutral layers (pre-commit + CI) today; the experimental Copilot adapters are not qualified for in-loop steering (see `tamperward runtime verify`)."
|
|
20225
|
-
},
|
|
20226
|
-
{
|
|
20227
|
-
id: "codex",
|
|
20228
|
-
label: "an AGENTS.md-aware agent (e.g. Codex)",
|
|
20229
|
-
markers: [".codex", "AGENTS.md"],
|
|
20230
|
-
steering: "neutral",
|
|
20231
|
-
note: "agent-neutral layers (pre-commit + CI) today; the experimental Codex adapter is not qualified for in-loop steering (see `tamperward runtime verify`)."
|
|
20232
|
-
}
|
|
20233
|
-
]);
|
|
20234
|
-
}
|
|
20235
|
-
});
|
|
20236
|
-
|
|
20237
20511
|
// src/adapters/contract.ts
|
|
20238
20512
|
function immutableRuntimeCapabilities(caps) {
|
|
20239
20513
|
return Object.freeze({
|
|
@@ -23145,6 +23419,62 @@ var init_onboard = __esm({
|
|
|
23145
23419
|
}
|
|
23146
23420
|
});
|
|
23147
23421
|
|
|
23422
|
+
// src/cli/runtime-hook.ts
|
|
23423
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
23424
|
+
function isRuntimeHookAgent(agent) {
|
|
23425
|
+
return RUNTIME_HOOK_AGENTS.some((known) => known === agent);
|
|
23426
|
+
}
|
|
23427
|
+
function runRuntimeHookFromRaw(kind, agent, raw, cwd = process.cwd()) {
|
|
23428
|
+
if (!isRuntimeHookAgent(agent)) {
|
|
23429
|
+
return { exitCode: 2, stdout: "", stderr: `tamperward: unsupported ${kind === "Stop" ? "sweep" : "hook"} agent "${agent}" (claude, ${RUNTIME_HOOK_AGENTS.join(", ")})
|
|
23430
|
+
` };
|
|
23431
|
+
}
|
|
23432
|
+
const adapter = adapterFor(agent);
|
|
23433
|
+
if (!adapter) {
|
|
23434
|
+
return { exitCode: 2, stdout: "", stderr: `tamperward: no adapter is registered for "${agent}"
|
|
23435
|
+
` };
|
|
23436
|
+
}
|
|
23437
|
+
const phase = kind === "Stop" ? "end-of-turn" : "pre-action";
|
|
23438
|
+
try {
|
|
23439
|
+
const r = adapter.decide(raw, phase, cwd);
|
|
23440
|
+
return { exitCode: 0, stdout: r.wire ?? "", stderr: "" };
|
|
23441
|
+
} catch (e) {
|
|
23442
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
23443
|
+
const wire = adapter.denyPayload([steeringUnavailableFinding(detail)], phase);
|
|
23444
|
+
return { exitCode: 0, stdout: wire, stderr: "" };
|
|
23445
|
+
}
|
|
23446
|
+
}
|
|
23447
|
+
function runRuntimeHook(kind, agent) {
|
|
23448
|
+
let raw;
|
|
23449
|
+
try {
|
|
23450
|
+
raw = readFileSync20(0, "utf8");
|
|
23451
|
+
} catch (e) {
|
|
23452
|
+
raw = "";
|
|
23453
|
+
const r2 = runRuntimeHookFromRaw(kind, agent, "", process.cwd());
|
|
23454
|
+
if (r2.exitCode !== 0) {
|
|
23455
|
+
process.stderr.write(r2.stderr);
|
|
23456
|
+
return r2.exitCode;
|
|
23457
|
+
}
|
|
23458
|
+
const adapter = adapterFor(agent);
|
|
23459
|
+
const detail = `cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`;
|
|
23460
|
+
process.stdout.write(adapter ? adapter.denyPayload([steeringUnavailableFinding(detail)], kind === "Stop" ? "end-of-turn" : "pre-action") : "");
|
|
23461
|
+
return 0;
|
|
23462
|
+
}
|
|
23463
|
+
const r = runRuntimeHookFromRaw(kind, agent, raw, process.cwd());
|
|
23464
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
23465
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
23466
|
+
return r.exitCode;
|
|
23467
|
+
}
|
|
23468
|
+
var RUNTIME_HOOK_AGENTS;
|
|
23469
|
+
var init_runtime_hook = __esm({
|
|
23470
|
+
"src/cli/runtime-hook.ts"() {
|
|
23471
|
+
"use strict";
|
|
23472
|
+
init_registry();
|
|
23473
|
+
init_contract();
|
|
23474
|
+
RUNTIME_HOOK_AGENTS = Object.freeze(["codex", "copilot"]);
|
|
23475
|
+
}
|
|
23476
|
+
});
|
|
23477
|
+
|
|
23148
23478
|
// src/research/adapter.ts
|
|
23149
23479
|
import { isAbsolute as isAbsolute17, resolve as resolve17 } from "node:path";
|
|
23150
23480
|
function taskEnv(task) {
|
|
@@ -23286,7 +23616,7 @@ var init_capture = __esm({
|
|
|
23286
23616
|
|
|
23287
23617
|
// src/research/manifest.ts
|
|
23288
23618
|
import { createHash as createHash16 } from "node:crypto";
|
|
23289
|
-
import { readFileSync as
|
|
23619
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
23290
23620
|
import { dirname as dirname14, resolve as resolve18 } from "node:path";
|
|
23291
23621
|
function taskFrom(raw, index, manifestDir) {
|
|
23292
23622
|
const where = `task #${index + 1}`;
|
|
@@ -23320,7 +23650,7 @@ function readManifest(path) {
|
|
|
23320
23650
|
const abs = resolve18(path);
|
|
23321
23651
|
let bytes;
|
|
23322
23652
|
try {
|
|
23323
|
-
bytes =
|
|
23653
|
+
bytes = readFileSync21(abs);
|
|
23324
23654
|
} catch (e) {
|
|
23325
23655
|
throw new ResearchError(`cannot read task manifest ${abs}: ${e instanceof Error ? e.message : String(e)}`);
|
|
23326
23656
|
}
|
|
@@ -23739,7 +24069,7 @@ var init_record = __esm({
|
|
|
23739
24069
|
// src/research/run.ts
|
|
23740
24070
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
23741
24071
|
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
23742
|
-
import { closeSync as closeSync5, existsSync as existsSync15, linkSync, mkdirSync as mkdirSync12, openSync as openSync4, readFileSync as
|
|
24072
|
+
import { closeSync as closeSync5, existsSync as existsSync15, linkSync, mkdirSync as mkdirSync12, openSync as openSync4, readFileSync as readFileSync22, renameSync as renameSync4, rmSync as rmSync14, unlinkSync, writeFileSync as writeFileSync11, writeSync as writeSync2 } from "node:fs";
|
|
23743
24073
|
import { hostname } from "node:os";
|
|
23744
24074
|
import { join as join29, resolve as resolve19 } from "node:path";
|
|
23745
24075
|
function git7(args, cwd) {
|
|
@@ -23753,7 +24083,7 @@ function researchLockPath(ledger) {
|
|
|
23753
24083
|
}
|
|
23754
24084
|
function readLockOwner(path) {
|
|
23755
24085
|
try {
|
|
23756
|
-
const parsed = JSON.parse(
|
|
24086
|
+
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
23757
24087
|
if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0) return null;
|
|
23758
24088
|
if (typeof parsed.host !== "string" || typeof parsed.started_at !== "string" || typeof parsed.token !== "string") return null;
|
|
23759
24089
|
return { pid: parsed.pid, host: parsed.host, started_at: parsed.started_at, token: parsed.token };
|
|
@@ -23838,7 +24168,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
23838
24168
|
if (released) return;
|
|
23839
24169
|
released = true;
|
|
23840
24170
|
try {
|
|
23841
|
-
if (
|
|
24171
|
+
if (readFileSync22(path, "utf8") === text) unlinkSync(path);
|
|
23842
24172
|
} catch {
|
|
23843
24173
|
}
|
|
23844
24174
|
}
|
|
@@ -23847,7 +24177,7 @@ function acquireResearchLock(ledger, breakStaleLock = false) {
|
|
|
23847
24177
|
function resumableRecord(path, expected) {
|
|
23848
24178
|
let raw;
|
|
23849
24179
|
try {
|
|
23850
|
-
raw = JSON.parse(
|
|
24180
|
+
raw = JSON.parse(readFileSync22(path, "utf8"));
|
|
23851
24181
|
} catch (e) {
|
|
23852
24182
|
throw new ResearchError(`ledger record ${path} is malformed (${errorMessage(e).split("\n")[0]}); remove it to regenerate the pair, or use a new --out`);
|
|
23853
24183
|
}
|
|
@@ -24269,7 +24599,7 @@ var init_run2 = __esm({
|
|
|
24269
24599
|
});
|
|
24270
24600
|
|
|
24271
24601
|
// src/research/ledger-files.ts
|
|
24272
|
-
import { closeSync as closeSync6, constants as constants5, fstatSync as fstatSync3, lstatSync as lstatSync12, openSync as openSync5, readFileSync as
|
|
24602
|
+
import { closeSync as closeSync6, constants as constants5, fstatSync as fstatSync3, lstatSync as lstatSync12, openSync as openSync5, readFileSync as readFileSync23, readdirSync as readdirSync8, realpathSync as realpathSync12 } from "node:fs";
|
|
24273
24603
|
import { join as join30, resolve as resolve20 } from "node:path";
|
|
24274
24604
|
function lstatOrNull(path) {
|
|
24275
24605
|
try {
|
|
@@ -24312,7 +24642,7 @@ function readRegularFile(path, realPairsDir, name) {
|
|
|
24312
24642
|
if (!opened.isFile() || !sameInode || opened.nlink !== 1) {
|
|
24313
24643
|
throw new ResearchError(`ledger record ${path} changed between listing and reading; refusing to read it`);
|
|
24314
24644
|
}
|
|
24315
|
-
return
|
|
24645
|
+
return readFileSync23(fd);
|
|
24316
24646
|
} finally {
|
|
24317
24647
|
closeSync6(fd);
|
|
24318
24648
|
}
|
|
@@ -24328,7 +24658,7 @@ function readPairFiles(ledger) {
|
|
|
24328
24658
|
const realPairsDir = realpathSync12.native(pairsDir);
|
|
24329
24659
|
let names;
|
|
24330
24660
|
try {
|
|
24331
|
-
names =
|
|
24661
|
+
names = readdirSync8(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
|
|
24332
24662
|
} catch (e) {
|
|
24333
24663
|
throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
|
|
24334
24664
|
}
|
|
@@ -24485,7 +24815,7 @@ var init_summarize = __esm({
|
|
|
24485
24815
|
|
|
24486
24816
|
// src/research/init.ts
|
|
24487
24817
|
import { createHash as createHash17 } from "node:crypto";
|
|
24488
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as
|
|
24818
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync24, writeFileSync as writeFileSync12 } from "node:fs";
|
|
24489
24819
|
import { basename as basename6, dirname as dirname15, resolve as resolve21 } from "node:path";
|
|
24490
24820
|
function idFrom(repo) {
|
|
24491
24821
|
const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
|
|
@@ -24537,7 +24867,7 @@ function runResearchInit(opts) {
|
|
|
24537
24867
|
}
|
|
24538
24868
|
}
|
|
24539
24869
|
function requireHash(path) {
|
|
24540
|
-
return createHash17("sha256").update(
|
|
24870
|
+
return createHash17("sha256").update(readFileSync24(path)).digest("hex");
|
|
24541
24871
|
}
|
|
24542
24872
|
var init_init2 = __esm({
|
|
24543
24873
|
"src/research/init.ts"() {
|
|
@@ -24605,7 +24935,7 @@ var init_report2 = __esm({
|
|
|
24605
24935
|
import { createHash as createHash18 } from "node:crypto";
|
|
24606
24936
|
import { isDeepStrictEqual } from "node:util";
|
|
24607
24937
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
24608
|
-
import { mkdirSync as mkdirSync14, readFileSync as
|
|
24938
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
|
|
24609
24939
|
import { dirname as dirname16, resolve as resolve22 } from "node:path";
|
|
24610
24940
|
function octal(value, width) {
|
|
24611
24941
|
return value.toString(8).padStart(width - 1, "0") + "\0";
|
|
@@ -24711,7 +25041,7 @@ function jsonBytes(value) {
|
|
|
24711
25041
|
function validateResearchBundle(path) {
|
|
24712
25042
|
let files;
|
|
24713
25043
|
try {
|
|
24714
|
-
files = parseTar(gunzipSync(
|
|
25044
|
+
files = parseTar(gunzipSync(readFileSync25(resolve22(path))));
|
|
24715
25045
|
} catch (e) {
|
|
24716
25046
|
throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
24717
25047
|
}
|
|
@@ -24781,7 +25111,7 @@ function createResearchBundle(opts) {
|
|
|
24781
25111
|
const { records, entries } = packagedRecords(ledger);
|
|
24782
25112
|
const summary = summarizeRecords(records);
|
|
24783
25113
|
if (opts.manifest) {
|
|
24784
|
-
const manifest =
|
|
25114
|
+
const manifest = readFileSync25(resolve22(opts.manifest));
|
|
24785
25115
|
const sha3 = createHash18("sha256").update(manifest).digest("hex");
|
|
24786
25116
|
if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
|
|
24787
25117
|
entries.push({ name: "manifest.json", bytes: manifest });
|
|
@@ -24952,7 +25282,7 @@ var init_research = __esm({
|
|
|
24952
25282
|
});
|
|
24953
25283
|
|
|
24954
25284
|
// src/cli/receipt.ts
|
|
24955
|
-
import { appendFileSync as appendFileSync3, readFileSync as
|
|
25285
|
+
import { appendFileSync as appendFileSync3, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "node:fs";
|
|
24956
25286
|
function parseReceipt(args) {
|
|
24957
25287
|
const [sub, ...rest] = args;
|
|
24958
25288
|
const exportOpts = {};
|
|
@@ -25079,7 +25409,7 @@ function ciAdjudicate(cwd, opts) {
|
|
|
25079
25409
|
if (opts.ciResult !== void 0) {
|
|
25080
25410
|
let doc;
|
|
25081
25411
|
try {
|
|
25082
|
-
doc = JSON.parse(
|
|
25412
|
+
doc = JSON.parse(readFileSync26(opts.ciResult, "utf8"));
|
|
25083
25413
|
} catch (e) {
|
|
25084
25414
|
return {
|
|
25085
25415
|
ci: { verdict: "CANNOT_VERIFY", binding, binding_error: `unreadable CI verify result: ${e instanceof Error ? e.message : String(e)}` },
|
|
@@ -25225,7 +25555,7 @@ function loadClaimedReceipt(path) {
|
|
|
25225
25555
|
}
|
|
25226
25556
|
let raw;
|
|
25227
25557
|
try {
|
|
25228
|
-
raw =
|
|
25558
|
+
raw = readFileSync26(path, "utf8");
|
|
25229
25559
|
} catch (e) {
|
|
25230
25560
|
return { disposition: "MALFORMED", detail: `receipt file could not be read (${e instanceof Error ? e.message : String(e)})` };
|
|
25231
25561
|
}
|
|
@@ -25349,13 +25679,9 @@ function parseAllow(args) {
|
|
|
25349
25679
|
return o;
|
|
25350
25680
|
}
|
|
25351
25681
|
function runAgentCommand(kind, args) {
|
|
25352
|
-
const agent = args[0];
|
|
25353
|
-
if (agent
|
|
25354
|
-
|
|
25355
|
-
`);
|
|
25356
|
-
return 2;
|
|
25357
|
-
}
|
|
25358
|
-
return kind === "hook" ? runHookClaude() : runSweepClaude();
|
|
25682
|
+
const agent = args[0] ?? "";
|
|
25683
|
+
if (agent === "claude") return kind === "hook" ? runHookClaude() : runSweepClaude();
|
|
25684
|
+
return runRuntimeHook(kind === "hook" ? "PreToolUse" : "Stop", agent);
|
|
25359
25685
|
}
|
|
25360
25686
|
function parseInit(args) {
|
|
25361
25687
|
const o = {};
|
|
@@ -26075,6 +26401,7 @@ var init_main = __esm({
|
|
|
26075
26401
|
init_run();
|
|
26076
26402
|
init_watch();
|
|
26077
26403
|
init_onboard();
|
|
26404
|
+
init_runtime_hook();
|
|
26078
26405
|
init_research();
|
|
26079
26406
|
init_runtime();
|
|
26080
26407
|
init_receipt();
|
|
@@ -26086,7 +26413,7 @@ var init_main = __esm({
|
|
|
26086
26413
|
// src/cli/index.ts
|
|
26087
26414
|
init_hook_client();
|
|
26088
26415
|
init_exit();
|
|
26089
|
-
import { readFileSync as
|
|
26416
|
+
import { readFileSync as readFileSync27 } from "node:fs";
|
|
26090
26417
|
function loadMain() {
|
|
26091
26418
|
return Promise.resolve().then(() => (init_main(), main_exports));
|
|
26092
26419
|
}
|
|
@@ -26099,7 +26426,7 @@ async function launch(argv) {
|
|
|
26099
26426
|
if (kind && hookServiceEnabled()) {
|
|
26100
26427
|
let raw = null;
|
|
26101
26428
|
try {
|
|
26102
|
-
raw =
|
|
26429
|
+
raw = readFileSync27(0, "utf8");
|
|
26103
26430
|
} catch {
|
|
26104
26431
|
raw = null;
|
|
26105
26432
|
}
|