tamperward 2.39.22 → 2.39.23
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 +81 -4
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -153,7 +153,16 @@ function resolvesToClaudeSettings(path, env = process.env) {
|
|
|
153
153
|
if (isClaudeSettings(path, env)) return true;
|
|
154
154
|
return isAbsolute(path) && isClaudeSettings(canonicalPath(path), env);
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
function runtimeWiringOf(path) {
|
|
157
|
+
const p = path.replace(/\\/g, "/");
|
|
158
|
+
if (CODEX_HOOK_CONFIG.test(p)) return "codex";
|
|
159
|
+
if (COPILOT_HOOK_CONFIG.test(p)) return "copilot";
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function resolvesToRuntimeWiring(path) {
|
|
163
|
+
return runtimeWiringOf(path) ?? (isAbsolute(path) ? runtimeWiringOf(canonicalPath(path)) : null);
|
|
164
|
+
}
|
|
165
|
+
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;
|
|
157
166
|
var init_wiring = __esm({
|
|
158
167
|
"src/wiring.ts"() {
|
|
159
168
|
"use strict";
|
|
@@ -174,6 +183,8 @@ var init_wiring = __esm({
|
|
|
174
183
|
OURS = /^\s*npx --yes --global --userconfig=\/dev\/null --globalconfig=\/dev\/null\/npmrc-global --registry=https:\/\/registry\.npmjs\.org\/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward(?:@(\S+))? (?:(hook claude|sweep claude) \|\| \(echo tamperward: authority failed to start >&2 && exit 2\)|(check --staged))\s*$/;
|
|
175
184
|
INIT_SCRIPT = /^#!\/bin\/sh\n# tamperward: block agent shortcuts before they land\nnpx --yes --global --userconfig=\/dev\/null --globalconfig=\/dev\/null\/npmrc-global --registry=https:\/\/registry\.npmjs\.org\/ --node-options=' ' --script-shell= --ignore-scripts --offline=false --prefer-online tamperward(?:@(\S+))? check --staged\n*$/;
|
|
176
185
|
CLAUDE_SETTINGS = /(?:^|\/)(?:\.claude\/settings(?:\.local)?\.json|managed-settings\.json)$/;
|
|
186
|
+
CODEX_HOOK_CONFIG = /(?:^|\/)\.codex\/config\.toml$/;
|
|
187
|
+
COPILOT_HOOK_CONFIG = /(?:^|\/)\.github\/hooks\/tamperward\.json$/;
|
|
177
188
|
}
|
|
178
189
|
});
|
|
179
190
|
|
|
@@ -588,6 +599,12 @@ function defaultPolicy(version = 1) {
|
|
|
588
599
|
"**/.tamperward.yml",
|
|
589
600
|
".claude/settings.json",
|
|
590
601
|
".claude/settings.local.json",
|
|
602
|
+
// The project hook wiring of the experimental runtimes: where Codex reads
|
|
603
|
+
// project hooks and where the Copilot CLI reads repository hooks. Each is
|
|
604
|
+
// the interception for its runtime; removing or weakening it before a
|
|
605
|
+
// protected edit is the tamper, and the gate must see the wiring change.
|
|
606
|
+
".codex/config.toml",
|
|
607
|
+
".github/hooks/tamperward.json",
|
|
591
608
|
"**/.pre-commit-config.yaml",
|
|
592
609
|
// CODEOWNERS is enforcement wiring, not documentation: it is the only
|
|
593
610
|
// thing that can require a human on a change to the workflow that
|
|
@@ -2736,6 +2753,45 @@ function codeownersWeakening(before, after, extra = []) {
|
|
|
2736
2753
|
}
|
|
2737
2754
|
return out3;
|
|
2738
2755
|
}
|
|
2756
|
+
function codexHooksTables(toml) {
|
|
2757
|
+
const out3 = [];
|
|
2758
|
+
let inHooks = false;
|
|
2759
|
+
for (const raw of toml.split("\n")) {
|
|
2760
|
+
const line = raw.trim();
|
|
2761
|
+
if (!line || line.startsWith("#")) continue;
|
|
2762
|
+
const table = line.match(TOML_TABLE);
|
|
2763
|
+
if (table) inHooks = isHooksTable(table[1].replace(/["']/g, ""));
|
|
2764
|
+
if (inHooks) out3.push(line);
|
|
2765
|
+
}
|
|
2766
|
+
return out3.join("\n");
|
|
2767
|
+
}
|
|
2768
|
+
function codexHooksWeakening(before, after) {
|
|
2769
|
+
const was = codexHooksTables(before);
|
|
2770
|
+
if (!was) return [];
|
|
2771
|
+
const now = codexHooksTables(after);
|
|
2772
|
+
if (now === was) return [];
|
|
2773
|
+
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
|
+
return ["the [hooks] tables changed \u2014 the PreToolUse and Stop wiring Codex reads is no longer what it was"];
|
|
2775
|
+
}
|
|
2776
|
+
function hooksMember(src) {
|
|
2777
|
+
let doc;
|
|
2778
|
+
try {
|
|
2779
|
+
doc = JSON.parse(src);
|
|
2780
|
+
} catch {
|
|
2781
|
+
return null;
|
|
2782
|
+
}
|
|
2783
|
+
if (!isDoc(doc)) return { hooks: null };
|
|
2784
|
+
return { hooks: doc.hooks === void 0 ? null : JSON.stringify(doc.hooks) };
|
|
2785
|
+
}
|
|
2786
|
+
function copilotHooksWeakening(before, after) {
|
|
2787
|
+
const was = hooksMember(before);
|
|
2788
|
+
if (was === null || was.hooks === null) return [];
|
|
2789
|
+
const now = hooksMember(after);
|
|
2790
|
+
if (now === null) return ["the file no longer parses as JSON \u2014 the Copilot CLI loads no hook from it"];
|
|
2791
|
+
if (now.hooks === was.hooks) return [];
|
|
2792
|
+
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
|
+
return ['the "hooks" entries changed \u2014 the preToolUse and agentStop wiring the Copilot CLI reads is no longer what it was'];
|
|
2794
|
+
}
|
|
2739
2795
|
function parseDoc(src) {
|
|
2740
2796
|
try {
|
|
2741
2797
|
const v = yaml.parse(src);
|
|
@@ -3184,7 +3240,7 @@ function xargsWritesHook(toks, fed, policy, ctx) {
|
|
|
3184
3240
|
if ((cmd === "sed" || cmd === "perl") && args.some((a) => /^-[A-Za-z]*i|^--in-place/.test(a))) return `xargs ${cmd} -i rewrites a protected hook named earlier in the line`;
|
|
3185
3241
|
return null;
|
|
3186
3242
|
}
|
|
3187
|
-
var isComment, RUNNER, TOOL, SCRIPT, SUBCOMMAND_TOOLS, PREFIX, MODE, CWD, unq, alwaysTrue, EXIT_ZERO, EXIT_ANY, FN_DEF, SENSITIVE_ENV, assignedNames, WAIT_PID, TRANSPARENT, strip, baseIdentity, isCodeowners, GATE_CRITICAL, isDoc, isLefthook, isLefthookLocal, isPreCommitConfig, isPackageJson, GATE, str, list, redirectingEnv, off, STAGE_ALIAS, GATE_STAGES, ALL_STAGES, normStage, INSTALL_SCRIPTS, GIT_HOOKS, ownerExec, SYMBOLIC, WRAPPERS, INTERPRETERS, INLINE_FLAG, EDITORS, SCRIPT_WORD, HOOK_BASENAMES, REDIRECT_TARGET;
|
|
3243
|
+
var isComment, RUNNER, TOOL, SCRIPT, SUBCOMMAND_TOOLS, PREFIX, MODE, CWD, unq, alwaysTrue, EXIT_ZERO, EXIT_ANY, FN_DEF, SENSITIVE_ENV, assignedNames, WAIT_PID, TRANSPARENT, strip, baseIdentity, isCodeowners, GATE_CRITICAL, TOML_TABLE, isHooksTable, isDoc, isLefthook, isLefthookLocal, isPreCommitConfig, isPackageJson, GATE, str, list, redirectingEnv, off, STAGE_ALIAS, GATE_STAGES, ALL_STAGES, normStage, INSTALL_SCRIPTS, GIT_HOOKS, ownerExec, SYMBOLIC, WRAPPERS, INTERPRETERS, INLINE_FLAG, EDITORS, SCRIPT_WORD, HOOK_BASENAMES, REDIRECT_TARGET;
|
|
3188
3244
|
var init_hook_wiring = __esm({
|
|
3189
3245
|
"src/detectors/hook-wiring.ts"() {
|
|
3190
3246
|
"use strict";
|
|
@@ -3244,6 +3300,8 @@ var init_hook_wiring = __esm({
|
|
|
3244
3300
|
"/lefthook.yml",
|
|
3245
3301
|
"/lefthook.yaml"
|
|
3246
3302
|
];
|
|
3303
|
+
TOML_TABLE = /^\s*\[\[?\s*([^\]]*?)\s*\]\]?/;
|
|
3304
|
+
isHooksTable = (name) => name === "hooks" || name.startsWith("hooks.");
|
|
3247
3305
|
isDoc = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
3248
3306
|
isLefthook = (path) => /(?:^|\/)\.?lefthook(?:-local)?\.(?:ya?ml|toml|json)$/.test(path);
|
|
3249
3307
|
isLefthookLocal = (path) => /(?:^|\/)\.?lefthook-local\.(?:ya?ml|toml|json)$/.test(path);
|
|
@@ -8879,7 +8937,22 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
8879
8937
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
8880
8938
|
);
|
|
8881
8939
|
}
|
|
8882
|
-
|
|
8940
|
+
function runtimeWiringFindings(c, wiring, policy) {
|
|
8941
|
+
const finding = (reason) => makeFinding(RULE9, policy, {
|
|
8942
|
+
file: c.path,
|
|
8943
|
+
message: `The ${RUNTIME_WIRING_LABEL[wiring]} hook wiring was weakened: ${reason}.`,
|
|
8944
|
+
evidence: reason,
|
|
8945
|
+
remediation: "Leave the hook entries that run tamperward as they were. Editing the wiring is editing the gate: restore it, or sign off."
|
|
8946
|
+
});
|
|
8947
|
+
if (c.after != null) {
|
|
8948
|
+
if (c.before == null) return [];
|
|
8949
|
+
const weaken = wiring === "codex" ? codexHooksWeakening : copilotHooksWeakening;
|
|
8950
|
+
return weaken(c.before, c.after).map(finding);
|
|
8951
|
+
}
|
|
8952
|
+
const lost = removedLines(c).find((l) => /\btamperward\b/.test(l.content));
|
|
8953
|
+
return lost ? [finding(`a line running tamperward was removed (\`${lost.content.trim()}\`)`)] : [];
|
|
8954
|
+
}
|
|
8955
|
+
var RULE9, ownerExec2, EVENTS, isObj, declares, GATE_KEYS, HOOK_TYPES, SIBLING_KEYS, MENTIONS_GATE, FORGERY, ENV_MECHANISM, envMechanism, mentionsGate, carriesGate, CANONICAL_GATE, COMMIT_STAGES, namesGate, liveOrCanonical, HUSKY_SHIM, HUSKY_SHIM_NAMES, HUSKY_H_KNOWN, HUSKY_DEPRECATION, stripTrailingNl, isHuskyScript, isHookNote, RUNTIME_WIRING_LABEL, hookTampering;
|
|
8883
8956
|
var init_hook_tampering = __esm({
|
|
8884
8957
|
"src/detectors/hook-tampering.ts"() {
|
|
8885
8958
|
"use strict";
|
|
@@ -8959,6 +9032,7 @@ They WILL FAIL in v10.0.0
|
|
|
8959
9032
|
stripTrailingNl = (s) => s.endsWith("\n") ? s.slice(0, -1) : s;
|
|
8960
9033
|
isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
8961
9034
|
isHookNote = (path) => !/(?:^|\/)\.husky\/_\//.test(path) && /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
9035
|
+
RUNTIME_WIRING_LABEL = { codex: "Codex", copilot: "Copilot CLI" };
|
|
8962
9036
|
hookTampering = {
|
|
8963
9037
|
id: RULE9,
|
|
8964
9038
|
surface: ["file", "command"],
|
|
@@ -8967,8 +9041,9 @@ They WILL FAIL in v10.0.0
|
|
|
8967
9041
|
const out3 = [];
|
|
8968
9042
|
for (const c of changes) {
|
|
8969
9043
|
let semantic;
|
|
9044
|
+
let wiring;
|
|
8970
9045
|
if (c.kind === "file") {
|
|
8971
|
-
const targetsHook = isProtected(c.path, policy, "hooks") || (c.oldPath ? isProtected(c.oldPath, policy, "hooks") : false) || resolvesToClaudeSettings(c.path);
|
|
9046
|
+
const targetsHook = isProtected(c.path, policy, "hooks") || (c.oldPath ? isProtected(c.oldPath, policy, "hooks") : false) || resolvesToClaudeSettings(c.path) || resolvesToRuntimeWiring(c.path) !== null;
|
|
8972
9047
|
if (!targetsHook) {
|
|
8973
9048
|
if (isPackageJson(c.path) && c.before != null && c.after != null) {
|
|
8974
9049
|
for (const reason of packageJsonWeakening(c.before, c.after)) {
|
|
@@ -9015,6 +9090,8 @@ They WILL FAIL in v10.0.0
|
|
|
9015
9090
|
} else if ((c.path.endsWith(POLICY_FILE) || resolvesToClaudeSettings(c.path)) && (semantic = c.path.endsWith(POLICY_FILE) ? policyFindings(c, policy, ctx) : claudeSettingsFindings(c, policy)) !== null) {
|
|
9016
9091
|
out3.push(...semantic);
|
|
9017
9092
|
} else if (c.op === "rename") {
|
|
9093
|
+
} else if ((wiring = resolvesToRuntimeWiring(c.path)) !== null) {
|
|
9094
|
+
out3.push(...runtimeWiringFindings(c, wiring, policy));
|
|
9018
9095
|
} else if (isCodeowners(c.path)) {
|
|
9019
9096
|
if (c.before != null && c.after != null) {
|
|
9020
9097
|
for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.39.
|
|
3
|
+
"version": "2.39.23",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|