@massu/core 2.5.5 → 2.5.7
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 +63 -0
- package/dist/cli.js +167 -46
- package/dist/hooks/auto-learning-pipeline.js +81 -16
- package/dist/hooks/classify-failure.js +90 -652
- package/dist/hooks/cost-tracker.js +69 -4
- package/dist/hooks/fix-detector.js +79 -12
- package/dist/hooks/incident-pipeline.js +72 -7
- package/dist/hooks/intent-suggester.js +69 -4
- package/dist/hooks/memory-recall.js +89 -6
- package/dist/hooks/post-edit-context.js +69 -4
- package/dist/hooks/post-tool-use.js +76 -11
- package/dist/hooks/pre-compact.js +69 -4
- package/dist/hooks/pre-delete-check.js +446 -71
- package/dist/hooks/pre-tool-use-gate.js +237 -223
- package/dist/hooks/quality-event.js +69 -4
- package/dist/hooks/rule-enforcement-pipeline.js +72 -7
- package/dist/hooks/security-gate.js +611 -181
- package/dist/hooks/session-end.js +607 -73
- package/dist/hooks/session-start.js +112 -8
- package/dist/hooks/user-prompt.js +83 -18
- 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) {
|
|
@@ -1719,13 +1790,13 @@ function encode(text, vocab, opts = {}) {
|
|
|
1719
1790
|
const maxLen = opts.maxLen ?? 256;
|
|
1720
1791
|
const basic = basicTokenize(text);
|
|
1721
1792
|
const wpTokens = [];
|
|
1722
|
-
for (const
|
|
1723
|
-
for (const sub of wordpieceTokenize(
|
|
1793
|
+
for (const t2 of basic) {
|
|
1794
|
+
for (const sub of wordpieceTokenize(t2, vocab)) wpTokens.push(sub);
|
|
1724
1795
|
}
|
|
1725
1796
|
const truncated = wpTokens.slice(0, maxLen - 2);
|
|
1726
1797
|
const tokens = [CLS, ...truncated, SEP];
|
|
1727
1798
|
const unkId = vocab.get(UNK) ?? 100;
|
|
1728
|
-
const input_ids = tokens.map((
|
|
1799
|
+
const input_ids = tokens.map((t2) => vocab.get(t2) ?? unkId);
|
|
1729
1800
|
const attention_mask = tokens.map(() => 1);
|
|
1730
1801
|
const token_type_ids = tokens.map(() => 0);
|
|
1731
1802
|
return { tokens, input_ids, attention_mask, token_type_ids };
|
|
@@ -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 {
|
|
@@ -1790,7 +1861,7 @@ function resolveWasmDir() {
|
|
|
1790
1861
|
try {
|
|
1791
1862
|
const req3 = createRequire3(import.meta.url);
|
|
1792
1863
|
const main2 = req3.resolve(ORT_PKG);
|
|
1793
|
-
return
|
|
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"],
|
|
@@ -1827,11 +1898,11 @@ async function getSession() {
|
|
|
1827
1898
|
function meanPool(hidden, mask, seqLen, hiddenSize) {
|
|
1828
1899
|
const out = new Float32Array(hiddenSize);
|
|
1829
1900
|
let maskSum = 0;
|
|
1830
|
-
for (let
|
|
1831
|
-
const m = mask[
|
|
1901
|
+
for (let t2 = 0; t2 < seqLen; t2++) {
|
|
1902
|
+
const m = mask[t2];
|
|
1832
1903
|
if (m === 0) continue;
|
|
1833
1904
|
maskSum += m;
|
|
1834
|
-
const base =
|
|
1905
|
+
const base = t2 * hiddenSize;
|
|
1835
1906
|
for (let h = 0; h < hiddenSize; h++) out[h] += hidden[base + h] * m;
|
|
1836
1907
|
}
|
|
1837
1908
|
const denom = Math.max(maskSum, 1e-9);
|
|
@@ -1839,16 +1910,16 @@ function meanPool(hidden, mask, seqLen, hiddenSize) {
|
|
|
1839
1910
|
return out;
|
|
1840
1911
|
}
|
|
1841
1912
|
function chunkForEmbedding(text) {
|
|
1842
|
-
const
|
|
1843
|
-
if (!
|
|
1844
|
-
if (
|
|
1913
|
+
const t2 = text.trim();
|
|
1914
|
+
if (!t2) return [];
|
|
1915
|
+
if (t2.length <= CHUNK_TARGET_CHARS) return [t2];
|
|
1845
1916
|
const chunks = [];
|
|
1846
1917
|
let start = 0;
|
|
1847
|
-
while (start <
|
|
1848
|
-
let end = Math.min(start + CHUNK_TARGET_CHARS,
|
|
1849
|
-
if (end <
|
|
1918
|
+
while (start < t2.length && chunks.length < MAX_CHUNKS_PER_RECORD) {
|
|
1919
|
+
let end = Math.min(start + CHUNK_TARGET_CHARS, t2.length);
|
|
1920
|
+
if (end < t2.length) {
|
|
1850
1921
|
const floor = start + Math.floor(CHUNK_TARGET_CHARS / 2);
|
|
1851
|
-
const slice =
|
|
1922
|
+
const slice = t2.slice(start, end);
|
|
1852
1923
|
const para = slice.lastIndexOf("\n\n");
|
|
1853
1924
|
const sent = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf(".\n"));
|
|
1854
1925
|
const space = slice.lastIndexOf(" ");
|
|
@@ -1860,9 +1931,9 @@ function chunkForEmbedding(text) {
|
|
|
1860
1931
|
}
|
|
1861
1932
|
}
|
|
1862
1933
|
}
|
|
1863
|
-
const chunk =
|
|
1934
|
+
const chunk = t2.slice(start, end).trim();
|
|
1864
1935
|
if (chunk) chunks.push(chunk);
|
|
1865
|
-
if (end >=
|
|
1936
|
+
if (end >= t2.length) break;
|
|
1866
1937
|
start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1);
|
|
1867
1938
|
}
|
|
1868
1939
|
return chunks;
|
|
@@ -1932,17 +2003,17 @@ async function embedBatch(texts) {
|
|
|
1932
2003
|
const settings = loadEmbedSettings();
|
|
1933
2004
|
if (!settings.enabled) return texts.map(() => null);
|
|
1934
2005
|
const results = [];
|
|
1935
|
-
for (const
|
|
1936
|
-
if (!
|
|
2006
|
+
for (const t2 of texts) {
|
|
2007
|
+
if (!t2 || !t2.trim()) {
|
|
1937
2008
|
results.push(null);
|
|
1938
2009
|
continue;
|
|
1939
2010
|
}
|
|
1940
2011
|
try {
|
|
1941
2012
|
let vec = null;
|
|
1942
2013
|
if (settings.endpoint) {
|
|
1943
|
-
vec = await embedTier0(
|
|
2014
|
+
vec = await embedTier0(t2, settings);
|
|
1944
2015
|
}
|
|
1945
|
-
if (!vec) vec = await embedTier1(
|
|
2016
|
+
if (!vec) vec = await embedTier1(t2);
|
|
1946
2017
|
results.push(vec);
|
|
1947
2018
|
} catch {
|
|
1948
2019
|
results.push(null);
|
|
@@ -2086,6 +2157,8 @@ __export(memory_db_exports, {
|
|
|
2086
2157
|
MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
|
|
2087
2158
|
MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
|
|
2088
2159
|
MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
|
|
2160
|
+
PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
|
|
2161
|
+
PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
|
|
2089
2162
|
TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
|
|
2090
2163
|
USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
|
|
2091
2164
|
addConversationTurn: () => addConversationTurn,
|
|
@@ -2094,6 +2167,7 @@ __export(memory_db_exports, {
|
|
|
2094
2167
|
addSummary: () => addSummary,
|
|
2095
2168
|
addToolCallDetail: () => addToolCallDetail,
|
|
2096
2169
|
addUserPrompt: () => addUserPrompt,
|
|
2170
|
+
allPendingSync: () => allPendingSync,
|
|
2097
2171
|
appendIncidentToFailureClass: () => appendIncidentToFailureClass,
|
|
2098
2172
|
armUsageCounter: () => armUsageCounter,
|
|
2099
2173
|
assignImportance: () => assignImportance,
|
|
@@ -2101,6 +2175,7 @@ __export(memory_db_exports, {
|
|
|
2101
2175
|
createSession: () => createSession,
|
|
2102
2176
|
deduplicateFailedAttempt: () => deduplicateFailedAttempt,
|
|
2103
2177
|
dequeuePendingSync: () => dequeuePendingSync,
|
|
2178
|
+
discardPendingSync: () => discardPendingSync,
|
|
2104
2179
|
embedMissingObservations: () => embedMissingObservations,
|
|
2105
2180
|
endSession: () => endSession,
|
|
2106
2181
|
enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
|
|
@@ -2151,28 +2226,28 @@ __export(memory_db_exports, {
|
|
|
2151
2226
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
2152
2227
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
2153
2228
|
});
|
|
2154
|
-
import { dirname as
|
|
2155
|
-
import { existsSync as
|
|
2229
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2230
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
2156
2231
|
function sanitizeFts5Query(raw) {
|
|
2157
2232
|
const trimmed = raw.trim();
|
|
2158
2233
|
if (!trimmed) return '""';
|
|
2159
2234
|
const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter(Boolean);
|
|
2160
|
-
return tokens.map((
|
|
2235
|
+
return tokens.map((t2) => `"${t2}"`).join(" ");
|
|
2161
2236
|
}
|
|
2162
2237
|
function sanitizeFts5QueryOr(raw) {
|
|
2163
2238
|
const trimmed = raw.trim();
|
|
2164
2239
|
if (!trimmed) return '""';
|
|
2165
|
-
const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((
|
|
2240
|
+
const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((t2) => t2.replace(/[^a-zA-Z0-9]/g, "").length >= 3);
|
|
2166
2241
|
if (tokens.length === 0) return '""';
|
|
2167
|
-
return tokens.map((
|
|
2242
|
+
return tokens.map((t2) => `"${t2}"`).join(" OR ");
|
|
2168
2243
|
}
|
|
2169
2244
|
function getMemoryDb() {
|
|
2170
2245
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
2171
|
-
const dir =
|
|
2172
|
-
if (!
|
|
2173
|
-
|
|
2246
|
+
const dir = dirname7(dbPath);
|
|
2247
|
+
if (!existsSync8(dir)) {
|
|
2248
|
+
mkdirSync6(dir, { recursive: true });
|
|
2174
2249
|
}
|
|
2175
|
-
const preExisting =
|
|
2250
|
+
const preExisting = existsSync8(dbPath);
|
|
2176
2251
|
const db = openDatabase2(dbPath);
|
|
2177
2252
|
db.pragma("journal_mode = WAL");
|
|
2178
2253
|
db.pragma("foreign_keys = ON");
|
|
@@ -3083,6 +3158,35 @@ function initMemorySchema(db) {
|
|
|
3083
3158
|
}
|
|
3084
3159
|
function enqueueSyncPayload(db, payload) {
|
|
3085
3160
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
3161
|
+
const overflow = db.prepare(
|
|
3162
|
+
`SELECT id, retry_count, last_error, payload FROM pending_sync
|
|
3163
|
+
WHERE created_at < datetime('now', ?)
|
|
3164
|
+
OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
|
|
3165
|
+
LIMIT 10000`
|
|
3166
|
+
).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
|
|
3167
|
+
if (overflow.length > 0) {
|
|
3168
|
+
discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
function discardPendingSync(db, rows, why) {
|
|
3172
|
+
if (rows.length === 0) return 0;
|
|
3173
|
+
const rescued = rescueLearningFromStalePayloads(db, rows);
|
|
3174
|
+
for (let i = 0; i < rows.length; i += 500) {
|
|
3175
|
+
const ids = rows.slice(i, i + 500).map((r) => r.id);
|
|
3176
|
+
db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
|
|
3177
|
+
}
|
|
3178
|
+
if (rescued > 0) {
|
|
3179
|
+
process.stderr.write(
|
|
3180
|
+
`[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
|
|
3181
|
+
`
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
|
|
3185
|
+
`);
|
|
3186
|
+
return rows.length;
|
|
3187
|
+
}
|
|
3188
|
+
function allPendingSync(db) {
|
|
3189
|
+
return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
|
|
3086
3190
|
}
|
|
3087
3191
|
function getMemoryMeta(db, key) {
|
|
3088
3192
|
const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
|
|
@@ -3110,9 +3214,9 @@ function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
|
|
|
3110
3214
|
).run(observationId, modelId, dim, float32ToBlob(vec));
|
|
3111
3215
|
}
|
|
3112
3216
|
function observationEmbedText(title, detail) {
|
|
3113
|
-
const
|
|
3217
|
+
const t2 = (title ?? "").trim();
|
|
3114
3218
|
const d = (detail ?? "").trim();
|
|
3115
|
-
return d ? `${
|
|
3219
|
+
return d ? `${t2}. ${d}` : t2;
|
|
3116
3220
|
}
|
|
3117
3221
|
async function embedMissingObservations(db, opts = {}) {
|
|
3118
3222
|
return runEmbedSweep(
|
|
@@ -3285,7 +3389,9 @@ function dequeuePendingSync(db, limit = 10) {
|
|
|
3285
3389
|
}
|
|
3286
3390
|
}
|
|
3287
3391
|
return db.prepare(
|
|
3288
|
-
|
|
3392
|
+
// Least-retried first: oldest-first retried the same 10 items until they were discarded
|
|
3393
|
+
// while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
|
|
3394
|
+
"SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
|
|
3289
3395
|
).all(limit);
|
|
3290
3396
|
}
|
|
3291
3397
|
function removePendingSync(db, id) {
|
|
@@ -3958,7 +4064,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
|
|
|
3958
4064
|
}
|
|
3959
4065
|
return bestMatch;
|
|
3960
4066
|
}
|
|
3961
|
-
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
4067
|
+
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
3962
4068
|
var init_memory_db = __esm({
|
|
3963
4069
|
"src/memory-db.ts"() {
|
|
3964
4070
|
"use strict";
|
|
@@ -3973,6 +4079,8 @@ var init_memory_db = __esm({
|
|
|
3973
4079
|
init_memory_embed_sweep();
|
|
3974
4080
|
MEMORY_SCHEMA_VERSION = 2;
|
|
3975
4081
|
TOOL_COST_EVENTS_RETENTION_DAYS = 90;
|
|
4082
|
+
PENDING_SYNC_MAX_ITEMS = 500;
|
|
4083
|
+
PENDING_SYNC_MAX_AGE_DAYS = 30;
|
|
3976
4084
|
MAX_DRAFT_TEXT_LEN = 16384;
|
|
3977
4085
|
FUNNEL_EVENT_OUTBOX_CAP = 2e4;
|
|
3978
4086
|
_temporalColCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -3983,129 +4091,7 @@ var init_memory_db = __esm({
|
|
|
3983
4091
|
}
|
|
3984
4092
|
});
|
|
3985
4093
|
|
|
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";
|
|
4094
|
+
// src/hooks/lib/security-gate-checks.ts
|
|
4109
4095
|
var DANGEROUS_BASH_PATTERNS = [
|
|
4110
4096
|
// BLOCK — destructive and irreversible, or arbitrary remote code execution.
|
|
4111
4097
|
{ pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
|
|
@@ -4157,6 +4143,7 @@ var SECRET_EXACT_BASENAMES = [
|
|
|
4157
4143
|
];
|
|
4158
4144
|
var CREDENTIALS_PATH_SUFFIX = "/credentials";
|
|
4159
4145
|
var DOTENV = ".env";
|
|
4146
|
+
var SECRET_DATA_EXTENSIONS = ["yaml", "yml", "json", "toml", "ini", "env", "conf", "properties", "txt"];
|
|
4160
4147
|
function looksLikeCode(token) {
|
|
4161
4148
|
return /[()[\]{};=`$<>|&*?!"']/.test(token) || /\s/.test(token);
|
|
4162
4149
|
}
|
|
@@ -4175,8 +4162,13 @@ function namesSecretFile(token) {
|
|
|
4175
4162
|
return DOTENV_TEMPLATE_SUFFIXES.includes(suffix) ? null : `${base} file`;
|
|
4176
4163
|
}
|
|
4177
4164
|
if (base.endsWith(DOTENV) && clean.includes("/")) return `${base} file`;
|
|
4178
|
-
for (const
|
|
4179
|
-
if (base.endsWith(`.${
|
|
4165
|
+
for (const ext2 of SECRET_BASENAME_EXTENSIONS) {
|
|
4166
|
+
if (base.endsWith(`.${ext2}`)) return `${base} file`;
|
|
4167
|
+
}
|
|
4168
|
+
const parts = base.toLowerCase().split(".");
|
|
4169
|
+
const ext = parts.length > 1 ? parts[parts.length - 1] : "";
|
|
4170
|
+
if (SECRET_DATA_EXTENSIONS.includes(ext) && parts.slice(0, -1).some((p) => p === "secret" || p === "secrets")) {
|
|
4171
|
+
return `${base} file`;
|
|
4180
4172
|
}
|
|
4181
4173
|
return null;
|
|
4182
4174
|
}
|
|
@@ -4338,30 +4330,468 @@ Review carefully before proceeding.`
|
|
|
4338
4330
|
function runSecurityGateChecks(hookInput) {
|
|
4339
4331
|
return runSecurityGateFindings(hookInput).map((f) => f.message);
|
|
4340
4332
|
}
|
|
4341
|
-
|
|
4333
|
+
|
|
4334
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4335
|
+
init_db_driver();
|
|
4336
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4337
|
+
|
|
4338
|
+
// src/sentinel-db.ts
|
|
4339
|
+
init_config();
|
|
4340
|
+
init_memory_db();
|
|
4341
|
+
|
|
4342
|
+
// src/lib/sql-table-names.ts
|
|
4343
|
+
init_config();
|
|
4344
|
+
function t(suffix) {
|
|
4345
|
+
return `${getConfig().toolPrefix}_${suffix}`;
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
// src/sentinel-db.ts
|
|
4349
|
+
var PROJECT_ROOT = getProjectRoot();
|
|
4350
|
+
function parsePortalScope(raw) {
|
|
4351
|
+
if (!raw) return [];
|
|
4342
4352
|
try {
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4353
|
+
return JSON.parse(raw);
|
|
4354
|
+
} catch {
|
|
4355
|
+
return [];
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
function toFeature(row) {
|
|
4359
|
+
return {
|
|
4360
|
+
id: row.id,
|
|
4361
|
+
feature_key: row.feature_key,
|
|
4362
|
+
domain: row.domain,
|
|
4363
|
+
subdomain: row.subdomain || null,
|
|
4364
|
+
title: row.title,
|
|
4365
|
+
description: row.description || null,
|
|
4366
|
+
status: row.status,
|
|
4367
|
+
priority: row.priority,
|
|
4368
|
+
portal_scope: parsePortalScope(row.portal_scope),
|
|
4369
|
+
created_at: row.created_at,
|
|
4370
|
+
updated_at: row.updated_at,
|
|
4371
|
+
removed_at: row.removed_at || null,
|
|
4372
|
+
removed_reason: row.removed_reason || null
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
function getFeatureById(db, id) {
|
|
4376
|
+
const row = db.prepare(`SELECT * FROM ${t("sentinel")} WHERE id = ?`).get(id);
|
|
4377
|
+
return row ? toFeature(row) : null;
|
|
4378
|
+
}
|
|
4379
|
+
function getFeatureImpact(db, filePaths) {
|
|
4380
|
+
const fileSet = new Set(filePaths);
|
|
4381
|
+
const affectedFeatureIds = /* @__PURE__ */ new Set();
|
|
4382
|
+
for (const filePath of filePaths) {
|
|
4383
|
+
const links = db.prepare(
|
|
4384
|
+
`SELECT feature_id FROM ${t("sentinel_components")} WHERE component_file = ? LIMIT 10000`
|
|
4385
|
+
).all(filePath);
|
|
4386
|
+
for (const link of links) {
|
|
4387
|
+
affectedFeatureIds.add(link.feature_id);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
const orphaned = [];
|
|
4391
|
+
const degraded = [];
|
|
4392
|
+
const unaffected = [];
|
|
4393
|
+
for (const featureId of affectedFeatureIds) {
|
|
4394
|
+
const feature = getFeatureById(db, featureId);
|
|
4395
|
+
if (!feature || feature.status !== "active") continue;
|
|
4396
|
+
const allComponents = db.prepare(
|
|
4397
|
+
`SELECT component_file, is_primary FROM ${t("sentinel_components")} WHERE feature_id = ? LIMIT 10000`
|
|
4398
|
+
).all(featureId);
|
|
4399
|
+
const affected = allComponents.filter((c) => fileSet.has(c.component_file));
|
|
4400
|
+
const remaining = allComponents.filter((c) => !fileSet.has(c.component_file));
|
|
4401
|
+
const primaryAffected = affected.some((c) => c.is_primary);
|
|
4402
|
+
const item = {
|
|
4403
|
+
feature,
|
|
4404
|
+
affected_files: affected.map((c) => c.component_file),
|
|
4405
|
+
remaining_files: remaining.map((c) => c.component_file),
|
|
4406
|
+
status: "unaffected"
|
|
4407
|
+
};
|
|
4408
|
+
if (primaryAffected && remaining.filter((c) => c.is_primary).length === 0) {
|
|
4409
|
+
item.status = "orphaned";
|
|
4410
|
+
orphaned.push(item);
|
|
4411
|
+
} else if (affected.length > 0) {
|
|
4412
|
+
item.status = "degraded";
|
|
4413
|
+
degraded.push(item);
|
|
4414
|
+
} else {
|
|
4415
|
+
unaffected.push(item);
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
const hasCriticalOrphans = orphaned.some((o) => o.feature.priority === "critical");
|
|
4419
|
+
const hasStandardOrphans = orphaned.some((o) => o.feature.priority === "standard");
|
|
4420
|
+
return {
|
|
4421
|
+
files_analyzed: filePaths,
|
|
4422
|
+
orphaned,
|
|
4423
|
+
degraded,
|
|
4424
|
+
unaffected,
|
|
4425
|
+
blocked: hasCriticalOrphans || hasStandardOrphans,
|
|
4426
|
+
block_reason: hasCriticalOrphans ? `BLOCKED: ${orphaned.length} features would be orphaned (includes critical features). Create migration plan first.` : hasStandardOrphans ? `BLOCKED: ${orphaned.length} standard features would be orphaned. Create migration plan first.` : null
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4431
|
+
init_config();
|
|
4432
|
+
|
|
4433
|
+
// src/lib/source-layout.ts
|
|
4434
|
+
init_config();
|
|
4435
|
+
function normalizeDir(raw) {
|
|
4436
|
+
if (typeof raw !== "string") return null;
|
|
4437
|
+
const posix = raw.replace(/\\/g, "/");
|
|
4438
|
+
if (posix.startsWith("/") || /^[A-Za-z]:\//.test(posix)) return null;
|
|
4439
|
+
const parts = posix.split("/").filter((p) => p !== "" && p !== ".");
|
|
4440
|
+
if (parts.some((p) => p === "..")) return null;
|
|
4441
|
+
if (parts.length === 0) return ".";
|
|
4442
|
+
return parts.join("/");
|
|
4443
|
+
}
|
|
4444
|
+
function declaredLanguageSourceDirs() {
|
|
4445
|
+
const languages = getConfig().framework.languages;
|
|
4446
|
+
if (!languages) return [];
|
|
4447
|
+
const out = [];
|
|
4448
|
+
for (const entry of Object.values(languages)) {
|
|
4449
|
+
if (Array.isArray(entry.source_dirs)) out.push(...entry.source_dirs);
|
|
4450
|
+
}
|
|
4451
|
+
return out;
|
|
4452
|
+
}
|
|
4453
|
+
function getSourceLayout() {
|
|
4454
|
+
const config = getConfig();
|
|
4455
|
+
const paths = config.paths;
|
|
4456
|
+
const declared = [
|
|
4457
|
+
paths.source,
|
|
4458
|
+
...paths.monorepo_roots ?? [],
|
|
4459
|
+
...declaredLanguageSourceDirs()
|
|
4460
|
+
];
|
|
4461
|
+
const normalized = declared.map(normalizeDir).filter((d) => d !== null);
|
|
4462
|
+
const includesRoot = normalized.includes(".");
|
|
4463
|
+
const sourceDirs = includesRoot ? [] : [...new Set(normalized)].sort();
|
|
4464
|
+
const source = normalizeDir(paths.source) ?? ".";
|
|
4465
|
+
const under = (child) => source === "." ? child : `${source}/${child}`;
|
|
4466
|
+
return {
|
|
4467
|
+
sourceDirs,
|
|
4468
|
+
includesRoot,
|
|
4469
|
+
extensions: getResolvedPaths().extensions,
|
|
4470
|
+
pagesDir: normalizeDir(paths.pages ?? under("app")) ?? under("app"),
|
|
4471
|
+
componentsDir: normalizeDir(paths.components ?? under("components")) ?? under("components")
|
|
4472
|
+
};
|
|
4473
|
+
}
|
|
4474
|
+
function isUnderSourceDir(path, layout = getSourceLayout()) {
|
|
4475
|
+
if (normalizeDir(path) === null) return false;
|
|
4476
|
+
if (layout.includesRoot) return true;
|
|
4477
|
+
return layout.sourceDirs.some((d) => path.startsWith(`${d}/`));
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4481
|
+
var PROJECT_ROOT2 = getProjectRoot();
|
|
4482
|
+
var KNOWLEDGE_PROTECTED_FILES = [
|
|
4483
|
+
"knowledge-db.ts",
|
|
4484
|
+
"knowledge-indexer.ts",
|
|
4485
|
+
"knowledge-tools.ts"
|
|
4486
|
+
];
|
|
4487
|
+
function getDataDb() {
|
|
4488
|
+
const dbPath = getResolvedPaths().dataDbPath;
|
|
4489
|
+
if (!existsSync9(dbPath)) return null;
|
|
4490
|
+
try {
|
|
4491
|
+
const db = openDatabase2(dbPath, { readonly: true, selfHeal: false });
|
|
4492
|
+
db.pragma("journal_mode = WAL");
|
|
4493
|
+
return db;
|
|
4494
|
+
} catch {
|
|
4495
|
+
return null;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
function checkKnowledgeFileProtection(input) {
|
|
4499
|
+
const candidateFiles = [];
|
|
4500
|
+
if (input.tool_name === "Bash" && input.tool_input.command) {
|
|
4501
|
+
const cmd = input.tool_input.command;
|
|
4502
|
+
const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
|
|
4503
|
+
if (rmMatch) {
|
|
4504
|
+
const parts = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
|
|
4505
|
+
candidateFiles.push(...parts);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
if (input.tool_name === "Write" && input.tool_input.file_path) {
|
|
4509
|
+
const content = input.tool_input.content || "";
|
|
4510
|
+
if (content.trim().length === 0) {
|
|
4511
|
+
candidateFiles.push(input.tool_input.file_path);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
for (const f of candidateFiles) {
|
|
4515
|
+
const basename3 = f.split("/").pop() ?? f;
|
|
4516
|
+
if (KNOWLEDGE_PROTECTED_FILES.includes(basename3)) {
|
|
4517
|
+
return `KNOWLEDGE SYSTEM PROTECTION: "${basename3}" is a core knowledge system file. Deleting it will break knowledge indexing and memory retrieval. Create a replacement before removing.`;
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
return null;
|
|
4521
|
+
}
|
|
4522
|
+
function extractDeletedFiles(input) {
|
|
4523
|
+
const files = [];
|
|
4524
|
+
if (input.tool_name === "Bash" && input.tool_input.command) {
|
|
4525
|
+
const cmd = input.tool_input.command;
|
|
4526
|
+
const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
|
|
4527
|
+
if (rmMatch) {
|
|
4528
|
+
const paths = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
|
|
4529
|
+
for (const p of paths) {
|
|
4530
|
+
const relPath = isUnderSourceDir(p) ? p : p.replace(PROJECT_ROOT2 + "/", "");
|
|
4531
|
+
if (isUnderSourceDir(relPath) || relPath.endsWith(".py")) {
|
|
4532
|
+
files.push(relPath);
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
if (input.tool_name === "Write" && input.tool_input.file_path) {
|
|
4538
|
+
const content = input.tool_input.content || "";
|
|
4539
|
+
if (content.trim().length === 0) {
|
|
4540
|
+
const relPath = input.tool_input.file_path.replace(PROJECT_ROOT2 + "/", "");
|
|
4541
|
+
if (isUnderSourceDir(relPath) || relPath.endsWith(".py")) {
|
|
4542
|
+
files.push(relPath);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
return files;
|
|
4547
|
+
}
|
|
4548
|
+
function runPreDeleteChecks(hookInput) {
|
|
4549
|
+
const messages = [];
|
|
4550
|
+
try {
|
|
4551
|
+
const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
|
|
4552
|
+
if (knowledgeWarning) {
|
|
4553
|
+
messages.push(knowledgeWarning);
|
|
4554
|
+
return messages;
|
|
4555
|
+
}
|
|
4556
|
+
const deletedFiles = extractDeletedFiles(hookInput);
|
|
4557
|
+
if (deletedFiles.length === 0) return messages;
|
|
4558
|
+
const db = getDataDb();
|
|
4559
|
+
if (!db) return messages;
|
|
4560
|
+
const SENTINEL_TABLE = t("sentinel");
|
|
4561
|
+
try {
|
|
4562
|
+
const tableExists2 = db.prepare(
|
|
4563
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
|
|
4564
|
+
).get(SENTINEL_TABLE);
|
|
4565
|
+
if (!tableExists2) return messages;
|
|
4566
|
+
const impact = getFeatureImpact(db, deletedFiles);
|
|
4567
|
+
if (impact.blocked) {
|
|
4568
|
+
const msg = [
|
|
4569
|
+
`SENTINEL IMPACT WARNING: Deleting ${deletedFiles.length} file(s) would affect features:`,
|
|
4570
|
+
""
|
|
4571
|
+
];
|
|
4572
|
+
if (impact.orphaned.length > 0) {
|
|
4573
|
+
msg.push(`ORPHANED (${impact.orphaned.length} features - no primary components left):`);
|
|
4574
|
+
for (const item of impact.orphaned) {
|
|
4575
|
+
msg.push(` - ${item.feature.feature_key} [${item.feature.priority}]: ${item.feature.title}`);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
if (impact.degraded.length > 0) {
|
|
4579
|
+
msg.push(`DEGRADED (${impact.degraded.length} features - some components removed):`);
|
|
4580
|
+
for (const item of impact.degraded) {
|
|
4581
|
+
msg.push(` - ${item.feature.feature_key}: ${item.feature.title}`);
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
msg.push("");
|
|
4585
|
+
msg.push("Create a migration plan before deleting these files.");
|
|
4586
|
+
messages.push(msg.join("\n"));
|
|
4587
|
+
}
|
|
4588
|
+
const pyFiles = deletedFiles.filter((f) => f.endsWith(".py"));
|
|
4589
|
+
if (pyFiles.length > 0) {
|
|
4590
|
+
try {
|
|
4591
|
+
for (const pyFile of pyFiles) {
|
|
4592
|
+
const importers = db.prepare(
|
|
4593
|
+
`SELECT source_file FROM ${t("py_imports")} WHERE target_file = ? LIMIT 10000`
|
|
4594
|
+
).all(pyFile);
|
|
4595
|
+
const routes = db.prepare(
|
|
4596
|
+
`SELECT method, path FROM ${t("py_routes")} WHERE file = ? LIMIT 10000`
|
|
4597
|
+
).all(pyFile);
|
|
4598
|
+
const models = db.prepare(
|
|
4599
|
+
`SELECT class_name FROM ${t("py_models")} WHERE file = ? LIMIT 10000`
|
|
4600
|
+
).all(pyFile);
|
|
4601
|
+
if (importers.length > 0 || routes.length > 0 || models.length > 0) {
|
|
4602
|
+
const parts = [];
|
|
4603
|
+
if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
|
|
4604
|
+
if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
|
|
4605
|
+
if (models.length > 0) parts.push(`defines ${models.length} models`);
|
|
4606
|
+
messages.push(`PYTHON IMPACT: "${pyFile}" ${parts.join(", ")}. Check dependents before deleting.`);
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
} catch {
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
} finally {
|
|
4613
|
+
db.close();
|
|
4614
|
+
}
|
|
4615
|
+
} catch {
|
|
4351
4616
|
}
|
|
4352
|
-
|
|
4617
|
+
return messages;
|
|
4353
4618
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4619
|
+
|
|
4620
|
+
// src/hooks/lib/write-hook-message.ts
|
|
4621
|
+
var HOOK_EVENTS = [
|
|
4622
|
+
"UserPromptSubmit",
|
|
4623
|
+
"UserPromptExpansion",
|
|
4624
|
+
"SessionStart",
|
|
4625
|
+
"PreToolUse",
|
|
4626
|
+
"PostToolUse",
|
|
4627
|
+
"Stop",
|
|
4628
|
+
"SubagentStop",
|
|
4629
|
+
"PreCompact"
|
|
4630
|
+
];
|
|
4631
|
+
var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
|
|
4632
|
+
function isHookEvent(value) {
|
|
4633
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
4634
|
+
}
|
|
4635
|
+
function writeHookContext(hookEventName, message) {
|
|
4636
|
+
if (!isHookEvent(hookEventName)) {
|
|
4637
|
+
process.stderr.write(
|
|
4638
|
+
`[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
|
|
4639
|
+
`
|
|
4640
|
+
);
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
if (typeof message !== "string" || message.length === 0) return;
|
|
4644
|
+
let additionalContext = message;
|
|
4645
|
+
if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
|
|
4646
|
+
const marker = `
|
|
4647
|
+
[massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
|
|
4648
|
+
additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
|
|
4649
|
+
}
|
|
4650
|
+
process.stdout.write(
|
|
4651
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4652
|
+
);
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
// src/hooks/lib/read-stdin.ts
|
|
4656
|
+
var StdinTimeoutError = class extends Error {
|
|
4657
|
+
bytesRead;
|
|
4658
|
+
constructor(ms, bytesRead) {
|
|
4659
|
+
super(
|
|
4660
|
+
`stdin did not reach EOF within ${ms}ms (${bytesRead} bytes read). The payload is INCOMPLETE and must not be treated as valid input.`
|
|
4661
|
+
);
|
|
4662
|
+
this.name = "StdinTimeoutError";
|
|
4663
|
+
this.bytesRead = bytesRead;
|
|
4664
|
+
}
|
|
4665
|
+
};
|
|
4666
|
+
function readStdinToEof(deadlineMs = 4e3) {
|
|
4667
|
+
return new Promise((resolve5, reject) => {
|
|
4356
4668
|
let data = "";
|
|
4669
|
+
let settled = false;
|
|
4670
|
+
const timer = setTimeout(() => {
|
|
4671
|
+
if (settled) return;
|
|
4672
|
+
settled = true;
|
|
4673
|
+
reject(new StdinTimeoutError(deadlineMs, data.length));
|
|
4674
|
+
}, deadlineMs);
|
|
4675
|
+
const finish = (fn) => {
|
|
4676
|
+
if (settled) return;
|
|
4677
|
+
settled = true;
|
|
4678
|
+
clearTimeout(timer);
|
|
4679
|
+
fn();
|
|
4680
|
+
};
|
|
4357
4681
|
process.stdin.setEncoding("utf-8");
|
|
4358
4682
|
process.stdin.on("data", (chunk) => {
|
|
4359
4683
|
data += chunk;
|
|
4360
4684
|
});
|
|
4361
|
-
process.stdin.on("end", () => resolve5(data));
|
|
4362
|
-
|
|
4685
|
+
process.stdin.on("end", () => finish(() => resolve5(data)));
|
|
4686
|
+
process.stdin.on("error", (err) => finish(() => reject(err)));
|
|
4363
4687
|
});
|
|
4364
4688
|
}
|
|
4689
|
+
|
|
4690
|
+
// src/hooks/lib/pre-tool-use-decision.ts
|
|
4691
|
+
init_hook_failure_signal();
|
|
4692
|
+
var EXIT_DENY = 2;
|
|
4693
|
+
var EXIT_ALLOW = 0;
|
|
4694
|
+
function deny(reason, detail) {
|
|
4695
|
+
process.stderr.write(
|
|
4696
|
+
`MASSU SECURITY GATE \u2014 BLOCKED
|
|
4697
|
+
|
|
4698
|
+
${reason}
|
|
4699
|
+
` + (detail ? `
|
|
4700
|
+
${detail}
|
|
4701
|
+
` : "") + `
|
|
4702
|
+
The gate denies by default when it cannot verify a command is safe.
|
|
4703
|
+
`
|
|
4704
|
+
);
|
|
4705
|
+
process.exit(EXIT_DENY);
|
|
4706
|
+
}
|
|
4707
|
+
async function runPreToolUseDecision(hookName, event) {
|
|
4708
|
+
let hookInput;
|
|
4709
|
+
let input = "";
|
|
4710
|
+
try {
|
|
4711
|
+
input = await readStdinToEof();
|
|
4712
|
+
if (!input.trim()) {
|
|
4713
|
+
recordHookFailure(hookName, new Error("empty stdin payload"));
|
|
4714
|
+
deny("The gate received an EMPTY payload and could not determine what tool call was requested.");
|
|
4715
|
+
}
|
|
4716
|
+
hookInput = JSON.parse(input);
|
|
4717
|
+
} catch (err) {
|
|
4718
|
+
recordHookFailure(hookName, err, {
|
|
4719
|
+
phase: "read-parse",
|
|
4720
|
+
bytesRead: input.length,
|
|
4721
|
+
endsWithBrace: input.trimEnd().endsWith("}")
|
|
4722
|
+
});
|
|
4723
|
+
deny(
|
|
4724
|
+
"The gate could not READ or PARSE the tool-call payload, so it cannot know what it was being asked to approve.",
|
|
4725
|
+
err instanceof Error ? `Cause: ${err.message}` : void 0
|
|
4726
|
+
);
|
|
4727
|
+
}
|
|
4728
|
+
let findings;
|
|
4729
|
+
try {
|
|
4730
|
+
const security = runSecurityGateFindings(hookInput);
|
|
4731
|
+
const deletes = runPreDeleteChecks(hookInput).map(
|
|
4732
|
+
(message) => ({ severity: "warn", message })
|
|
4733
|
+
);
|
|
4734
|
+
findings = [...security, ...deletes];
|
|
4735
|
+
} catch (err) {
|
|
4736
|
+
recordHookFailure(hookName, err, { phase: "evaluate" });
|
|
4737
|
+
deny(
|
|
4738
|
+
"A security check CRASHED while evaluating this tool call. A check that did not run has not passed.",
|
|
4739
|
+
err instanceof Error ? `Cause: ${err.message}` : void 0
|
|
4740
|
+
);
|
|
4741
|
+
}
|
|
4742
|
+
for (const f of findings.filter((x) => x.severity === "warn")) {
|
|
4743
|
+
writeHookContext(event, f.message);
|
|
4744
|
+
}
|
|
4745
|
+
const blocking = findings.filter((f) => f.severity === "block");
|
|
4746
|
+
if (blocking.length > 0) deny(blocking.map((f) => f.message).join("\n\n"));
|
|
4747
|
+
process.exit(EXIT_ALLOW);
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
// src/lib/safe-write.ts
|
|
4751
|
+
import {
|
|
4752
|
+
writeFileSync as writeFileSync3,
|
|
4753
|
+
renameSync,
|
|
4754
|
+
openSync as openSync2,
|
|
4755
|
+
fsyncSync as fsyncSync2,
|
|
4756
|
+
closeSync as closeSync2,
|
|
4757
|
+
existsSync as existsSync10,
|
|
4758
|
+
realpathSync,
|
|
4759
|
+
unlinkSync as unlinkSync2,
|
|
4760
|
+
accessSync as accessSync2,
|
|
4761
|
+
statSync as statSync2,
|
|
4762
|
+
chmodSync as chmodSync3,
|
|
4763
|
+
constants
|
|
4764
|
+
} from "fs";
|
|
4765
|
+
function isSameRealPath(a, b) {
|
|
4766
|
+
try {
|
|
4767
|
+
return realpathSync(a) === realpathSync(b);
|
|
4768
|
+
} catch {
|
|
4769
|
+
return false;
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// src/hooks/lib/is-direct-invocation.ts
|
|
4774
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
4775
|
+
function isDirectInvocation(moduleUrl) {
|
|
4776
|
+
const entry = process.argv[1];
|
|
4777
|
+
if (!entry) return false;
|
|
4778
|
+
try {
|
|
4779
|
+
if (moduleUrl === pathToFileURL(entry).href) return true;
|
|
4780
|
+
} catch {
|
|
4781
|
+
return false;
|
|
4782
|
+
}
|
|
4783
|
+
try {
|
|
4784
|
+
return isSameRealPath(fileURLToPath2(moduleUrl), entry);
|
|
4785
|
+
} catch {
|
|
4786
|
+
return false;
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
// src/hooks/security-gate.ts
|
|
4791
|
+
var HOOK_EVENT = "PreToolUse";
|
|
4792
|
+
async function main() {
|
|
4793
|
+
await runPreToolUseDecision("security-gate", HOOK_EVENT);
|
|
4794
|
+
}
|
|
4365
4795
|
if (isDirectInvocation(import.meta.url)) {
|
|
4366
4796
|
main();
|
|
4367
4797
|
}
|