@massu/core 2.5.5 → 2.5.6
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/CHANGELOG.md +33 -0
- package/dist/cli.js +69 -42
- package/dist/hooks/classify-failure.js +27 -654
- package/dist/hooks/fix-detector.js +4 -2
- package/dist/hooks/pre-delete-check.js +379 -69
- package/dist/hooks/pre-tool-use-gate.js +170 -221
- package/dist/hooks/security-gate.js +544 -179
- package/dist/hooks/session-end.js +470 -63
- package/package.json +1 -1
|
@@ -1619,6 +1619,77 @@ var init_rule_candidate_store = __esm({
|
|
|
1619
1619
|
}
|
|
1620
1620
|
});
|
|
1621
1621
|
|
|
1622
|
+
// src/hooks/lib/hook-failure-signal.ts
|
|
1623
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
|
|
1624
|
+
import { join as join4, dirname as dirname5 } from "path";
|
|
1625
|
+
function resolveFailureLogPath() {
|
|
1626
|
+
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1627
|
+
if (explicit) return explicit;
|
|
1628
|
+
let dir = process.cwd();
|
|
1629
|
+
for (let i = 0; i < 12; i++) {
|
|
1630
|
+
if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
|
|
1631
|
+
return join4(dir, ".massu", "hook-failures.jsonl");
|
|
1632
|
+
}
|
|
1633
|
+
const parent = dirname5(dir);
|
|
1634
|
+
if (parent === dir) break;
|
|
1635
|
+
dir = parent;
|
|
1636
|
+
}
|
|
1637
|
+
return join4(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1638
|
+
}
|
|
1639
|
+
function recordHookFailure(hook, error, context) {
|
|
1640
|
+
const record = {
|
|
1641
|
+
hook,
|
|
1642
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1643
|
+
stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
|
|
1644
|
+
context,
|
|
1645
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1646
|
+
};
|
|
1647
|
+
let wroteSomething = false;
|
|
1648
|
+
try {
|
|
1649
|
+
const path = resolveFailureLogPath();
|
|
1650
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1651
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
1652
|
+
wroteSomething = true;
|
|
1653
|
+
} catch {
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
process.stderr.write(
|
|
1657
|
+
`[massu] HOOK FAILURE in ${hook}: ${record.error}
|
|
1658
|
+
[massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
|
|
1659
|
+
`
|
|
1660
|
+
);
|
|
1661
|
+
wroteSomething = true;
|
|
1662
|
+
} catch {
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
recordHookHealthRow(record);
|
|
1666
|
+
} catch {
|
|
1667
|
+
}
|
|
1668
|
+
return wroteSomething;
|
|
1669
|
+
}
|
|
1670
|
+
function recordHookHealthRow(record) {
|
|
1671
|
+
const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
|
|
1672
|
+
const db = getMemoryDb2();
|
|
1673
|
+
try {
|
|
1674
|
+
db.prepare(
|
|
1675
|
+
`INSERT INTO hook_health (hook, error, context_json, occurred_at)
|
|
1676
|
+
VALUES (?, ?, ?, ?)`
|
|
1677
|
+
).run(
|
|
1678
|
+
record.hook,
|
|
1679
|
+
record.error,
|
|
1680
|
+
record.context ? JSON.stringify(record.context) : null,
|
|
1681
|
+
record.timestamp
|
|
1682
|
+
);
|
|
1683
|
+
} finally {
|
|
1684
|
+
db.close();
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
var init_hook_failure_signal = __esm({
|
|
1688
|
+
"src/hooks/lib/hook-failure-signal.ts"() {
|
|
1689
|
+
"use strict";
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1622
1693
|
// src/memory-embedder-tokenizer.ts
|
|
1623
1694
|
import { readFileSync as readFileSync5 } from "fs";
|
|
1624
1695
|
function loadVocab(vocabPath) {
|
|
@@ -1742,8 +1813,8 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1742
1813
|
|
|
1743
1814
|
// src/memory-embedder.ts
|
|
1744
1815
|
import { fileURLToPath } from "url";
|
|
1745
|
-
import { dirname as
|
|
1746
|
-
import { existsSync as
|
|
1816
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
1817
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1747
1818
|
import { createRequire as createRequire3 } from "module";
|
|
1748
1819
|
function getActiveEmbedModel() {
|
|
1749
1820
|
return _activeModel;
|
|
@@ -1765,21 +1836,21 @@ function embeddingsDisabled() {
|
|
|
1765
1836
|
}
|
|
1766
1837
|
function resolveModelDir() {
|
|
1767
1838
|
try {
|
|
1768
|
-
let dir =
|
|
1839
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1769
1840
|
let root = null;
|
|
1770
1841
|
for (let i = 0; i < 8; i++) {
|
|
1771
|
-
if (
|
|
1842
|
+
if (existsSync7(join5(dir, "package.json"))) {
|
|
1772
1843
|
root = dir;
|
|
1773
1844
|
break;
|
|
1774
1845
|
}
|
|
1775
|
-
const parent =
|
|
1846
|
+
const parent = dirname6(dir);
|
|
1776
1847
|
if (parent === dir) break;
|
|
1777
1848
|
dir = parent;
|
|
1778
1849
|
}
|
|
1779
1850
|
if (!root) return null;
|
|
1780
1851
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1781
|
-
const candidate =
|
|
1782
|
-
if (
|
|
1852
|
+
const candidate = join5(root, rel);
|
|
1853
|
+
if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
|
|
1783
1854
|
}
|
|
1784
1855
|
return null;
|
|
1785
1856
|
} catch {
|
|
@@ -1789,8 +1860,8 @@ function resolveModelDir() {
|
|
|
1789
1860
|
function resolveWasmDir() {
|
|
1790
1861
|
try {
|
|
1791
1862
|
const req3 = createRequire3(import.meta.url);
|
|
1792
|
-
const
|
|
1793
|
-
return
|
|
1863
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1864
|
+
return dirname6(main2);
|
|
1794
1865
|
} catch {
|
|
1795
1866
|
return null;
|
|
1796
1867
|
}
|
|
@@ -1810,8 +1881,8 @@ async function getSession() {
|
|
|
1810
1881
|
ort.env.wasm.proxy = false;
|
|
1811
1882
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1812
1883
|
_ortTensor = ort.Tensor;
|
|
1813
|
-
const modelPath =
|
|
1814
|
-
const vocabPath =
|
|
1884
|
+
const modelPath = join5(modelDir, "model_quantized.onnx");
|
|
1885
|
+
const vocabPath = join5(modelDir, "vocab.txt");
|
|
1815
1886
|
_vocab = loadVocab(vocabPath);
|
|
1816
1887
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1817
1888
|
executionProviders: ["wasm"],
|
|
@@ -2151,8 +2222,8 @@ __export(memory_db_exports, {
|
|
|
2151
2222
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
2152
2223
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
2153
2224
|
});
|
|
2154
|
-
import { dirname as
|
|
2155
|
-
import { existsSync as
|
|
2225
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2226
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
2156
2227
|
function sanitizeFts5Query(raw) {
|
|
2157
2228
|
const trimmed = raw.trim();
|
|
2158
2229
|
if (!trimmed) return '""';
|
|
@@ -2168,11 +2239,11 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
2168
2239
|
}
|
|
2169
2240
|
function getMemoryDb() {
|
|
2170
2241
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
2171
|
-
const dir =
|
|
2172
|
-
if (!
|
|
2173
|
-
|
|
2242
|
+
const dir = dirname7(dbPath);
|
|
2243
|
+
if (!existsSync8(dir)) {
|
|
2244
|
+
mkdirSync6(dir, { recursive: true });
|
|
2174
2245
|
}
|
|
2175
|
-
const preExisting =
|
|
2246
|
+
const preExisting = existsSync8(dbPath);
|
|
2176
2247
|
const db = openDatabase2(dbPath);
|
|
2177
2248
|
db.pragma("journal_mode = WAL");
|
|
2178
2249
|
db.pragma("foreign_keys = ON");
|
|
@@ -3983,129 +4054,7 @@ var init_memory_db = __esm({
|
|
|
3983
4054
|
}
|
|
3984
4055
|
});
|
|
3985
4056
|
|
|
3986
|
-
// src/hooks/lib/
|
|
3987
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
|
|
3988
|
-
import { join as join5, dirname as dirname7 } from "path";
|
|
3989
|
-
function resolveFailureLogPath() {
|
|
3990
|
-
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
3991
|
-
if (explicit) return explicit;
|
|
3992
|
-
let dir = process.cwd();
|
|
3993
|
-
for (let i = 0; i < 12; i++) {
|
|
3994
|
-
if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
|
|
3995
|
-
return join5(dir, ".massu", "hook-failures.jsonl");
|
|
3996
|
-
}
|
|
3997
|
-
const parent = dirname7(dir);
|
|
3998
|
-
if (parent === dir) break;
|
|
3999
|
-
dir = parent;
|
|
4000
|
-
}
|
|
4001
|
-
return join5(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
4002
|
-
}
|
|
4003
|
-
function recordHookFailure(hook, error, context) {
|
|
4004
|
-
const record = {
|
|
4005
|
-
hook,
|
|
4006
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4007
|
-
stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
|
|
4008
|
-
context,
|
|
4009
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4010
|
-
};
|
|
4011
|
-
let wroteSomething = false;
|
|
4012
|
-
try {
|
|
4013
|
-
const path = resolveFailureLogPath();
|
|
4014
|
-
mkdirSync6(dirname7(path), { recursive: true });
|
|
4015
|
-
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
4016
|
-
wroteSomething = true;
|
|
4017
|
-
} catch {
|
|
4018
|
-
}
|
|
4019
|
-
try {
|
|
4020
|
-
process.stderr.write(
|
|
4021
|
-
`[massu] HOOK FAILURE in ${hook}: ${record.error}
|
|
4022
|
-
[massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
|
|
4023
|
-
`
|
|
4024
|
-
);
|
|
4025
|
-
wroteSomething = true;
|
|
4026
|
-
} catch {
|
|
4027
|
-
}
|
|
4028
|
-
try {
|
|
4029
|
-
recordHookHealthRow(record);
|
|
4030
|
-
} catch {
|
|
4031
|
-
}
|
|
4032
|
-
return wroteSomething;
|
|
4033
|
-
}
|
|
4034
|
-
function recordHookHealthRow(record) {
|
|
4035
|
-
const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
|
|
4036
|
-
const db = getMemoryDb2();
|
|
4037
|
-
try {
|
|
4038
|
-
db.prepare(
|
|
4039
|
-
`INSERT INTO hook_health (hook, error, context_json, occurred_at)
|
|
4040
|
-
VALUES (?, ?, ?, ?)`
|
|
4041
|
-
).run(
|
|
4042
|
-
record.hook,
|
|
4043
|
-
record.error,
|
|
4044
|
-
record.context ? JSON.stringify(record.context) : null,
|
|
4045
|
-
record.timestamp
|
|
4046
|
-
);
|
|
4047
|
-
} finally {
|
|
4048
|
-
db.close();
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
|
-
var init_hook_failure_signal = __esm({
|
|
4052
|
-
"src/hooks/lib/hook-failure-signal.ts"() {
|
|
4053
|
-
"use strict";
|
|
4054
|
-
}
|
|
4055
|
-
});
|
|
4056
|
-
|
|
4057
|
-
// src/hooks/lib/write-hook-message.ts
|
|
4058
|
-
var HOOK_EVENTS = [
|
|
4059
|
-
"UserPromptSubmit",
|
|
4060
|
-
"UserPromptExpansion",
|
|
4061
|
-
"SessionStart",
|
|
4062
|
-
"PreToolUse",
|
|
4063
|
-
"PostToolUse",
|
|
4064
|
-
"Stop",
|
|
4065
|
-
"SubagentStop",
|
|
4066
|
-
"PreCompact"
|
|
4067
|
-
];
|
|
4068
|
-
var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
|
|
4069
|
-
function isHookEvent(value) {
|
|
4070
|
-
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
4071
|
-
}
|
|
4072
|
-
function writeHookContext(hookEventName, message) {
|
|
4073
|
-
if (!isHookEvent(hookEventName)) {
|
|
4074
|
-
process.stderr.write(
|
|
4075
|
-
`[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
|
|
4076
|
-
`
|
|
4077
|
-
);
|
|
4078
|
-
return;
|
|
4079
|
-
}
|
|
4080
|
-
if (typeof message !== "string" || message.length === 0) return;
|
|
4081
|
-
let additionalContext = message;
|
|
4082
|
-
if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
|
|
4083
|
-
const marker = `
|
|
4084
|
-
[massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
|
|
4085
|
-
additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
|
|
4086
|
-
}
|
|
4087
|
-
process.stdout.write(
|
|
4088
|
-
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4089
|
-
);
|
|
4090
|
-
}
|
|
4091
|
-
|
|
4092
|
-
// src/hooks/security-gate.ts
|
|
4093
|
-
init_hook_failure_signal();
|
|
4094
|
-
|
|
4095
|
-
// src/hooks/lib/is-direct-invocation.ts
|
|
4096
|
-
import { pathToFileURL } from "url";
|
|
4097
|
-
function isDirectInvocation(moduleUrl) {
|
|
4098
|
-
const entry = process.argv[1];
|
|
4099
|
-
if (!entry) return false;
|
|
4100
|
-
try {
|
|
4101
|
-
return moduleUrl === pathToFileURL(entry).href;
|
|
4102
|
-
} catch {
|
|
4103
|
-
return false;
|
|
4104
|
-
}
|
|
4105
|
-
}
|
|
4106
|
-
|
|
4107
|
-
// src/hooks/security-gate.ts
|
|
4108
|
-
var HOOK_EVENT = "PreToolUse";
|
|
4057
|
+
// src/hooks/lib/security-gate-checks.ts
|
|
4109
4058
|
var DANGEROUS_BASH_PATTERNS = [
|
|
4110
4059
|
// BLOCK — destructive and irreversible, or arbitrary remote code execution.
|
|
4111
4060
|
{ pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
|
|
@@ -4157,6 +4106,7 @@ var SECRET_EXACT_BASENAMES = [
|
|
|
4157
4106
|
];
|
|
4158
4107
|
var CREDENTIALS_PATH_SUFFIX = "/credentials";
|
|
4159
4108
|
var DOTENV = ".env";
|
|
4109
|
+
var SECRET_DATA_EXTENSIONS = ["yaml", "yml", "json", "toml", "ini", "env", "conf", "properties", "txt"];
|
|
4160
4110
|
function looksLikeCode(token) {
|
|
4161
4111
|
return /[()[\]{};=`$<>|&*?!"']/.test(token) || /\s/.test(token);
|
|
4162
4112
|
}
|
|
@@ -4175,8 +4125,13 @@ function namesSecretFile(token) {
|
|
|
4175
4125
|
return DOTENV_TEMPLATE_SUFFIXES.includes(suffix) ? null : `${base} file`;
|
|
4176
4126
|
}
|
|
4177
4127
|
if (base.endsWith(DOTENV) && clean.includes("/")) return `${base} file`;
|
|
4178
|
-
for (const
|
|
4179
|
-
if (base.endsWith(`.${
|
|
4128
|
+
for (const ext2 of SECRET_BASENAME_EXTENSIONS) {
|
|
4129
|
+
if (base.endsWith(`.${ext2}`)) return `${base} file`;
|
|
4130
|
+
}
|
|
4131
|
+
const parts = base.toLowerCase().split(".");
|
|
4132
|
+
const ext = parts.length > 1 ? parts[parts.length - 1] : "";
|
|
4133
|
+
if (SECRET_DATA_EXTENSIONS.includes(ext) && parts.slice(0, -1).some((p) => p === "secret" || p === "secrets")) {
|
|
4134
|
+
return `${base} file`;
|
|
4180
4135
|
}
|
|
4181
4136
|
return null;
|
|
4182
4137
|
}
|
|
@@ -4335,38 +4290,8 @@ Review carefully before proceeding.`
|
|
|
4335
4290
|
}
|
|
4336
4291
|
return findings;
|
|
4337
4292
|
}
|
|
4338
|
-
function runSecurityGateChecks(hookInput) {
|
|
4339
|
-
return runSecurityGateFindings(hookInput).map((f) => f.message);
|
|
4340
|
-
}
|
|
4341
|
-
async function main() {
|
|
4342
|
-
try {
|
|
4343
|
-
const input = await readStdin();
|
|
4344
|
-
const hookInput = JSON.parse(input);
|
|
4345
|
-
const messages = runSecurityGateChecks(hookInput);
|
|
4346
|
-
for (const msg of messages) writeHookContext(HOOK_EVENT, msg);
|
|
4347
|
-
} catch (err) {
|
|
4348
|
-
recordHookFailure("security-gate", err);
|
|
4349
|
-
process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
|
|
4350
|
-
process.exit(2);
|
|
4351
|
-
}
|
|
4352
|
-
process.exit(0);
|
|
4353
|
-
}
|
|
4354
|
-
function readStdin() {
|
|
4355
|
-
return new Promise((resolve5) => {
|
|
4356
|
-
let data = "";
|
|
4357
|
-
process.stdin.setEncoding("utf-8");
|
|
4358
|
-
process.stdin.on("data", (chunk) => {
|
|
4359
|
-
data += chunk;
|
|
4360
|
-
});
|
|
4361
|
-
process.stdin.on("end", () => resolve5(data));
|
|
4362
|
-
setTimeout(() => resolve5(data), 400);
|
|
4363
|
-
});
|
|
4364
|
-
}
|
|
4365
|
-
if (isDirectInvocation(import.meta.url)) {
|
|
4366
|
-
main();
|
|
4367
|
-
}
|
|
4368
4293
|
|
|
4369
|
-
// src/hooks/pre-delete-
|
|
4294
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4370
4295
|
init_db_driver();
|
|
4371
4296
|
import { existsSync as existsSync9 } from "fs";
|
|
4372
4297
|
|
|
@@ -4462,7 +4387,7 @@ function getFeatureImpact(db, filePaths) {
|
|
|
4462
4387
|
};
|
|
4463
4388
|
}
|
|
4464
4389
|
|
|
4465
|
-
// src/hooks/pre-delete-
|
|
4390
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4466
4391
|
init_config();
|
|
4467
4392
|
|
|
4468
4393
|
// src/lib/source-layout.ts
|
|
@@ -4512,9 +4437,7 @@ function isUnderSourceDir(path, layout = getSourceLayout()) {
|
|
|
4512
4437
|
return layout.sourceDirs.some((d) => path.startsWith(`${d}/`));
|
|
4513
4438
|
}
|
|
4514
4439
|
|
|
4515
|
-
// src/hooks/pre-delete-
|
|
4516
|
-
init_hook_failure_signal();
|
|
4517
|
-
var HOOK_EVENT2 = "PreToolUse";
|
|
4440
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4518
4441
|
var PROJECT_ROOT2 = getProjectRoot();
|
|
4519
4442
|
var KNOWLEDGE_PROTECTED_FILES = [
|
|
4520
4443
|
"knowledge-db.ts",
|
|
@@ -4653,32 +4576,40 @@ function runPreDeleteChecks(hookInput) {
|
|
|
4653
4576
|
}
|
|
4654
4577
|
return messages;
|
|
4655
4578
|
}
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
function
|
|
4670
|
-
return
|
|
4671
|
-
let data = "";
|
|
4672
|
-
process.stdin.setEncoding("utf-8");
|
|
4673
|
-
process.stdin.on("data", (chunk) => {
|
|
4674
|
-
data += chunk;
|
|
4675
|
-
});
|
|
4676
|
-
process.stdin.on("end", () => resolve5(data));
|
|
4677
|
-
setTimeout(() => resolve5(data), 400);
|
|
4678
|
-
});
|
|
4579
|
+
|
|
4580
|
+
// src/hooks/lib/write-hook-message.ts
|
|
4581
|
+
var HOOK_EVENTS = [
|
|
4582
|
+
"UserPromptSubmit",
|
|
4583
|
+
"UserPromptExpansion",
|
|
4584
|
+
"SessionStart",
|
|
4585
|
+
"PreToolUse",
|
|
4586
|
+
"PostToolUse",
|
|
4587
|
+
"Stop",
|
|
4588
|
+
"SubagentStop",
|
|
4589
|
+
"PreCompact"
|
|
4590
|
+
];
|
|
4591
|
+
var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
|
|
4592
|
+
function isHookEvent(value) {
|
|
4593
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
4679
4594
|
}
|
|
4680
|
-
|
|
4681
|
-
|
|
4595
|
+
function writeHookContext(hookEventName, message) {
|
|
4596
|
+
if (!isHookEvent(hookEventName)) {
|
|
4597
|
+
process.stderr.write(
|
|
4598
|
+
`[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
|
|
4599
|
+
`
|
|
4600
|
+
);
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
if (typeof message !== "string" || message.length === 0) return;
|
|
4604
|
+
let additionalContext = message;
|
|
4605
|
+
if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
|
|
4606
|
+
const marker = `
|
|
4607
|
+
[massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
|
|
4608
|
+
additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
|
|
4609
|
+
}
|
|
4610
|
+
process.stdout.write(
|
|
4611
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4612
|
+
);
|
|
4682
4613
|
}
|
|
4683
4614
|
|
|
4684
4615
|
// src/hooks/lib/read-stdin.ts
|
|
@@ -4716,9 +4647,8 @@ function readStdinToEof(deadlineMs = 4e3) {
|
|
|
4716
4647
|
});
|
|
4717
4648
|
}
|
|
4718
4649
|
|
|
4719
|
-
// src/hooks/pre-tool-use-
|
|
4650
|
+
// src/hooks/lib/pre-tool-use-decision.ts
|
|
4720
4651
|
init_hook_failure_signal();
|
|
4721
|
-
var HOOK_EVENT3 = "PreToolUse";
|
|
4722
4652
|
var EXIT_DENY = 2;
|
|
4723
4653
|
var EXIT_ALLOW = 0;
|
|
4724
4654
|
function deny(reason, detail) {
|
|
@@ -4734,19 +4664,22 @@ The gate denies by default when it cannot verify a command is safe.
|
|
|
4734
4664
|
);
|
|
4735
4665
|
process.exit(EXIT_DENY);
|
|
4736
4666
|
}
|
|
4737
|
-
async function
|
|
4667
|
+
async function runPreToolUseDecision(hookName, event) {
|
|
4738
4668
|
let hookInput;
|
|
4669
|
+
let input = "";
|
|
4739
4670
|
try {
|
|
4740
|
-
|
|
4671
|
+
input = await readStdinToEof();
|
|
4741
4672
|
if (!input.trim()) {
|
|
4742
|
-
recordHookFailure(
|
|
4743
|
-
deny(
|
|
4744
|
-
"The gate received an EMPTY payload and could not determine what tool call was requested."
|
|
4745
|
-
);
|
|
4673
|
+
recordHookFailure(hookName, new Error("empty stdin payload"));
|
|
4674
|
+
deny("The gate received an EMPTY payload and could not determine what tool call was requested.");
|
|
4746
4675
|
}
|
|
4747
4676
|
hookInput = JSON.parse(input);
|
|
4748
4677
|
} catch (err) {
|
|
4749
|
-
recordHookFailure(
|
|
4678
|
+
recordHookFailure(hookName, err, {
|
|
4679
|
+
phase: "read-parse",
|
|
4680
|
+
bytesRead: input.length,
|
|
4681
|
+
endsWithBrace: input.trimEnd().endsWith("}")
|
|
4682
|
+
});
|
|
4750
4683
|
deny(
|
|
4751
4684
|
"The gate could not READ or PARSE the tool-call payload, so it cannot know what it was being asked to approve.",
|
|
4752
4685
|
err instanceof Error ? `Cause: ${err.message}` : void 0
|
|
@@ -4755,26 +4688,42 @@ async function main3() {
|
|
|
4755
4688
|
let findings;
|
|
4756
4689
|
try {
|
|
4757
4690
|
const security = runSecurityGateFindings(hookInput);
|
|
4758
|
-
const deletes = runPreDeleteChecks(
|
|
4759
|
-
|
|
4760
|
-
)
|
|
4691
|
+
const deletes = runPreDeleteChecks(hookInput).map(
|
|
4692
|
+
(message) => ({ severity: "warn", message })
|
|
4693
|
+
);
|
|
4761
4694
|
findings = [...security, ...deletes];
|
|
4762
4695
|
} catch (err) {
|
|
4763
|
-
recordHookFailure(
|
|
4696
|
+
recordHookFailure(hookName, err, { phase: "evaluate" });
|
|
4764
4697
|
deny(
|
|
4765
4698
|
"A security check CRASHED while evaluating this tool call. A check that did not run has not passed.",
|
|
4766
4699
|
err instanceof Error ? `Cause: ${err.message}` : void 0
|
|
4767
4700
|
);
|
|
4768
4701
|
}
|
|
4769
|
-
const blocking = findings.filter((f) => f.severity === "block");
|
|
4770
4702
|
for (const f of findings.filter((x) => x.severity === "warn")) {
|
|
4771
|
-
writeHookContext(
|
|
4772
|
-
}
|
|
4773
|
-
if (blocking.length > 0) {
|
|
4774
|
-
deny(blocking.map((f) => f.message).join("\n\n"));
|
|
4703
|
+
writeHookContext(event, f.message);
|
|
4775
4704
|
}
|
|
4705
|
+
const blocking = findings.filter((f) => f.severity === "block");
|
|
4706
|
+
if (blocking.length > 0) deny(blocking.map((f) => f.message).join("\n\n"));
|
|
4776
4707
|
process.exit(EXIT_ALLOW);
|
|
4777
4708
|
}
|
|
4709
|
+
|
|
4710
|
+
// src/hooks/lib/is-direct-invocation.ts
|
|
4711
|
+
import { pathToFileURL } from "url";
|
|
4712
|
+
function isDirectInvocation(moduleUrl) {
|
|
4713
|
+
const entry = process.argv[1];
|
|
4714
|
+
if (!entry) return false;
|
|
4715
|
+
try {
|
|
4716
|
+
return moduleUrl === pathToFileURL(entry).href;
|
|
4717
|
+
} catch {
|
|
4718
|
+
return false;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
// src/hooks/pre-tool-use-gate.ts
|
|
4723
|
+
var HOOK_EVENT = "PreToolUse";
|
|
4724
|
+
async function main() {
|
|
4725
|
+
await runPreToolUseDecision("pre-tool-use-gate", HOOK_EVENT);
|
|
4726
|
+
}
|
|
4778
4727
|
if (isDirectInvocation(import.meta.url)) {
|
|
4779
|
-
|
|
4728
|
+
main();
|
|
4780
4729
|
}
|