@massu/core 2.5.4 → 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 +66 -0
- package/dist/cli.js +505 -407
- package/dist/hooks/auto-learning-pipeline.js +26 -2
- package/dist/hooks/classify-failure.js +53 -656
- package/dist/hooks/cost-tracker.js +26 -2
- package/dist/hooks/fix-detector.js +30 -4
- package/dist/hooks/incident-pipeline.js +26 -2
- package/dist/hooks/intent-suggester.js +26 -2
- package/dist/hooks/memory-recall.js +26 -2
- package/dist/hooks/post-edit-context.js +26 -2
- package/dist/hooks/post-tool-use.js +26 -2
- package/dist/hooks/pre-compact.js +26 -2
- package/dist/hooks/pre-delete-check.js +405 -71
- package/dist/hooks/pre-tool-use-gate.js +196 -223
- package/dist/hooks/quality-event.js +26 -2
- package/dist/hooks/rule-enforcement-pipeline.js +26 -2
- package/dist/hooks/security-gate.js +570 -181
- package/dist/hooks/session-end.js +1081 -354
- package/dist/hooks/session-start.js +26 -2
- package/dist/hooks/user-prompt.js +26 -2
- package/package.json +1 -1
|
@@ -951,7 +951,8 @@ var init_config = __esm({
|
|
|
951
951
|
input_per_million: z2.number(),
|
|
952
952
|
output_per_million: z2.number(),
|
|
953
953
|
cache_read_per_million: z2.number().optional(),
|
|
954
|
-
cache_write_per_million: z2.number().optional()
|
|
954
|
+
cache_write_per_million: z2.number().optional(),
|
|
955
|
+
cache_write_1h_per_million: z2.number().optional()
|
|
955
956
|
});
|
|
956
957
|
AnalyticsConfigSchema = z2.object({
|
|
957
958
|
quality: z2.object({
|
|
@@ -2208,6 +2209,7 @@ __export(memory_db_exports, {
|
|
|
2208
2209
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
2209
2210
|
recordRecallHits: () => recordRecallHits,
|
|
2210
2211
|
recordTelemetry: () => recordTelemetry,
|
|
2212
|
+
releaseMetaLease: () => releaseMetaLease,
|
|
2211
2213
|
removePendingSync: () => removePendingSync,
|
|
2212
2214
|
sanitizeFts5Query: () => sanitizeFts5Query,
|
|
2213
2215
|
sanitizeFts5QueryOr: () => sanitizeFts5QueryOr,
|
|
@@ -2216,6 +2218,7 @@ __export(memory_db_exports, {
|
|
|
2216
2218
|
searchObservations: () => searchObservations,
|
|
2217
2219
|
setLastProcessedLine: () => setLastProcessedLine,
|
|
2218
2220
|
setMemoryMeta: () => setMemoryMeta,
|
|
2221
|
+
tryAcquireMetaLease: () => tryAcquireMetaLease,
|
|
2219
2222
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
2220
2223
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
2221
2224
|
});
|
|
@@ -2721,6 +2724,15 @@ function initMemorySchema(db) {
|
|
|
2721
2724
|
);
|
|
2722
2725
|
CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
|
|
2723
2726
|
`);
|
|
2727
|
+
const costCols = db.prepare(`PRAGMA table_info(session_costs)`).all();
|
|
2728
|
+
const addCostCol = (name, ddl) => {
|
|
2729
|
+
if (!costCols.some((c) => c.name === name)) db.exec(`ALTER TABLE session_costs ADD COLUMN ${ddl}`);
|
|
2730
|
+
};
|
|
2731
|
+
addCostCol("cost_status", `cost_status TEXT NOT NULL DEFAULT 'unknown'`);
|
|
2732
|
+
addCostCol("unpriced_models", `unpriced_models TEXT NOT NULL DEFAULT ''`);
|
|
2733
|
+
addCostCol("priced_responses", `priced_responses INTEGER NOT NULL DEFAULT 0`);
|
|
2734
|
+
addCostCol("unpriced_responses", `unpriced_responses INTEGER NOT NULL DEFAULT 0`);
|
|
2735
|
+
addCostCol("pricing_version", `pricing_version INTEGER NOT NULL DEFAULT 0`);
|
|
2724
2736
|
db.exec(`
|
|
2725
2737
|
CREATE TABLE IF NOT EXISTS feature_costs (
|
|
2726
2738
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -3150,6 +3162,18 @@ function getMemoryMeta(db, key) {
|
|
|
3150
3162
|
function setMemoryMeta(db, key, value) {
|
|
3151
3163
|
db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
|
|
3152
3164
|
}
|
|
3165
|
+
function tryAcquireMetaLease(db, key, ttlSec, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
3166
|
+
const r = db.prepare(
|
|
3167
|
+
`INSERT INTO memory_meta (key, value) VALUES (?, ?)
|
|
3168
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
3169
|
+
WHERE memory_meta.value = ''
|
|
3170
|
+
OR CAST(substr(memory_meta.value, instr(memory_meta.value, ':') + 1) AS INTEGER) <= ?`
|
|
3171
|
+
).run(key, `${process.pid}:${nowSec + ttlSec}`, nowSec);
|
|
3172
|
+
return Number(r.changes) === 1;
|
|
3173
|
+
}
|
|
3174
|
+
function releaseMetaLease(db, key) {
|
|
3175
|
+
db.prepare(`UPDATE memory_meta SET value = '' WHERE key = ? AND value LIKE ?`).run(key, `${process.pid}:%`);
|
|
3176
|
+
}
|
|
3153
3177
|
function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
|
|
3154
3178
|
db.prepare(
|
|
3155
3179
|
`INSERT OR REPLACE INTO observation_embeddings (observation_id, model_id, dim, vec, created_at)
|
|
@@ -4018,7 +4042,7 @@ var init_memory_db = __esm({
|
|
|
4018
4042
|
init_rule_candidate_store();
|
|
4019
4043
|
init_hook_failure_signal();
|
|
4020
4044
|
init_memory_embed_sweep();
|
|
4021
|
-
MEMORY_SCHEMA_VERSION =
|
|
4045
|
+
MEMORY_SCHEMA_VERSION = 2;
|
|
4022
4046
|
TOOL_COST_EVENTS_RETENTION_DAYS = 90;
|
|
4023
4047
|
MAX_DRAFT_TEXT_LEN = 16384;
|
|
4024
4048
|
FUNNEL_EVENT_OUTBOX_CAP = 2e4;
|
|
@@ -4030,7 +4054,244 @@ var init_memory_db = __esm({
|
|
|
4030
4054
|
}
|
|
4031
4055
|
});
|
|
4032
4056
|
|
|
4033
|
-
// src/hooks/
|
|
4057
|
+
// src/hooks/lib/security-gate-checks.ts
|
|
4058
|
+
var DANGEROUS_BASH_PATTERNS = [
|
|
4059
|
+
// BLOCK — destructive and irreversible, or arbitrary remote code execution.
|
|
4060
|
+
{ pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
|
|
4061
|
+
{ pattern: /rm\s+-[a-z]*f[a-z]*r[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
|
|
4062
|
+
{ pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/, label: "curl | bash (remote code execution)", severity: "block" },
|
|
4063
|
+
{ pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/, label: "wget | bash (remote code execution)", severity: "block" },
|
|
4064
|
+
{ pattern: />\s*\/etc\/passwd/, label: "write to /etc/passwd", severity: "block" },
|
|
4065
|
+
{ pattern: />\s*\/etc\/shadow/, label: "write to /etc/shadow", severity: "block" },
|
|
4066
|
+
{ pattern: />\s*\/etc\/sudoers/, label: "write to /etc/sudoers", severity: "block" },
|
|
4067
|
+
{ pattern: /dd\s+if=.*of=\/dev\/(?:sda|sdb|hda|hdb|nvme)/, label: "dd to raw device", severity: "block" },
|
|
4068
|
+
{ pattern: /mkfs\s+\/dev\//, label: "format disk device", severity: "block" },
|
|
4069
|
+
{ pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}/, label: "fork bomb", severity: "block" },
|
|
4070
|
+
{ pattern: /eval\s+.*\$\(.*curl/, label: "eval with remote curl", severity: "block" },
|
|
4071
|
+
{ pattern: /base64\s+-d\s+.*\|\s*(?:bash|sh|zsh)/, label: "base64 decoded shell exec", severity: "block" },
|
|
4072
|
+
// WARN — bad practice, but has legitimate uses (fixing a broken permission bit,
|
|
4073
|
+
// a throwaway container). Blocking these would make the product hostile.
|
|
4074
|
+
{ pattern: /chmod\s+777/, label: "chmod 777 (world-writable permissions)", severity: "warn" },
|
|
4075
|
+
{ pattern: /chmod\s+-R\s+777/, label: "chmod -R 777 (world-writable permissions)", severity: "warn" }
|
|
4076
|
+
];
|
|
4077
|
+
var PROTECTED_FILE_PATTERNS = [
|
|
4078
|
+
{ pattern: /\.env$/, label: ".env file" },
|
|
4079
|
+
{ pattern: /\.env\./, label: ".env.* file" },
|
|
4080
|
+
{ pattern: /credentials(?:\.json)?$/, label: "credentials file" },
|
|
4081
|
+
{ pattern: /\.pem$/, label: ".pem certificate/key file" },
|
|
4082
|
+
{ pattern: /\.key$/, label: ".key file" },
|
|
4083
|
+
{ pattern: /\.p12$/, label: ".p12 keystore file" },
|
|
4084
|
+
{ pattern: /\.pfx$/, label: ".pfx keystore file" },
|
|
4085
|
+
{ pattern: /id_rsa$/, label: "RSA private key" },
|
|
4086
|
+
{ pattern: /id_ed25519$/, label: "Ed25519 private key" },
|
|
4087
|
+
{ pattern: /id_ecdsa$/, label: "ECDSA private key" },
|
|
4088
|
+
{ pattern: /\.ssh\/config$/, label: "SSH config file" },
|
|
4089
|
+
{ pattern: /secrets\.yaml$/, label: "secrets.yaml file" },
|
|
4090
|
+
{ pattern: /secrets\.yml$/, label: "secrets.yml file" },
|
|
4091
|
+
{ pattern: /\.netrc$/, label: ".netrc credentials file" },
|
|
4092
|
+
{ pattern: /aws\/credentials$/, label: "AWS credentials file" },
|
|
4093
|
+
{ pattern: /kubeconfig$/, label: "Kubernetes config file" }
|
|
4094
|
+
];
|
|
4095
|
+
var SECRET_BASENAME_EXTENSIONS = ["pem", "key", "p12", "pfx", "secret"];
|
|
4096
|
+
var DOTENV_TEMPLATE_SUFFIXES = ["example", "template", "sample", "defaults", "dist"];
|
|
4097
|
+
var SECRET_EXACT_BASENAMES = [
|
|
4098
|
+
"id_rsa",
|
|
4099
|
+
"id_ed25519",
|
|
4100
|
+
"id_ecdsa",
|
|
4101
|
+
".netrc",
|
|
4102
|
+
"kubeconfig",
|
|
4103
|
+
"credentials.json",
|
|
4104
|
+
"secrets.yaml",
|
|
4105
|
+
"secrets.yml"
|
|
4106
|
+
];
|
|
4107
|
+
var CREDENTIALS_PATH_SUFFIX = "/credentials";
|
|
4108
|
+
var DOTENV = ".env";
|
|
4109
|
+
var SECRET_DATA_EXTENSIONS = ["yaml", "yml", "json", "toml", "ini", "env", "conf", "properties", "txt"];
|
|
4110
|
+
function looksLikeCode(token) {
|
|
4111
|
+
return /[()[\]{};=`$<>|&*?!"']/.test(token) || /\s/.test(token);
|
|
4112
|
+
}
|
|
4113
|
+
function namesSecretFile(token) {
|
|
4114
|
+
if (!token || looksLikeCode(token)) return null;
|
|
4115
|
+
const clean = token.replace(/[,:.]+$/, "");
|
|
4116
|
+
if (!clean) return null;
|
|
4117
|
+
const base = clean.split("/").pop() ?? "";
|
|
4118
|
+
if (!base) return null;
|
|
4119
|
+
if (SECRET_EXACT_BASENAMES.includes(base)) return base;
|
|
4120
|
+
if (clean.endsWith(".ssh/config")) return "SSH config file";
|
|
4121
|
+
if (clean.endsWith(CREDENTIALS_PATH_SUFFIX)) return "credentials file";
|
|
4122
|
+
if (base === DOTENV) return `${DOTENV} file`;
|
|
4123
|
+
if (base.startsWith(`${DOTENV}.`)) {
|
|
4124
|
+
const suffix = base.slice(DOTENV.length + 1);
|
|
4125
|
+
return DOTENV_TEMPLATE_SUFFIXES.includes(suffix) ? null : `${base} file`;
|
|
4126
|
+
}
|
|
4127
|
+
if (base.endsWith(DOTENV) && clean.includes("/")) return `${base} file`;
|
|
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`;
|
|
4135
|
+
}
|
|
4136
|
+
return null;
|
|
4137
|
+
}
|
|
4138
|
+
function shellTokens(command) {
|
|
4139
|
+
const out = [];
|
|
4140
|
+
let cur = "";
|
|
4141
|
+
let quote = null;
|
|
4142
|
+
for (const ch of command) {
|
|
4143
|
+
if (quote) {
|
|
4144
|
+
if (ch === quote) quote = null;
|
|
4145
|
+
else cur += ch;
|
|
4146
|
+
continue;
|
|
4147
|
+
}
|
|
4148
|
+
if (ch === "'" || ch === '"') {
|
|
4149
|
+
quote = ch;
|
|
4150
|
+
cur += ch;
|
|
4151
|
+
continue;
|
|
4152
|
+
}
|
|
4153
|
+
if (/\s/.test(ch)) {
|
|
4154
|
+
if (cur) out.push(cur);
|
|
4155
|
+
cur = "";
|
|
4156
|
+
continue;
|
|
4157
|
+
}
|
|
4158
|
+
cur += ch;
|
|
4159
|
+
}
|
|
4160
|
+
if (cur) out.push(cur);
|
|
4161
|
+
return out;
|
|
4162
|
+
}
|
|
4163
|
+
function secretFilesInCommand(command) {
|
|
4164
|
+
const hits = [];
|
|
4165
|
+
for (const token of shellTokens(command)) {
|
|
4166
|
+
const label = namesSecretFile(token);
|
|
4167
|
+
if (label) hits.push(label);
|
|
4168
|
+
}
|
|
4169
|
+
return hits;
|
|
4170
|
+
}
|
|
4171
|
+
function checkBashCommand(command) {
|
|
4172
|
+
for (const { pattern, label, severity } of DANGEROUS_BASH_PATTERNS) {
|
|
4173
|
+
if (pattern.test(command)) {
|
|
4174
|
+
return { label, severity };
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
return null;
|
|
4178
|
+
}
|
|
4179
|
+
function checkFilePath(filePath) {
|
|
4180
|
+
for (const { pattern, label } of PROTECTED_FILE_PATTERNS) {
|
|
4181
|
+
if (pattern.test(filePath)) {
|
|
4182
|
+
return label;
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
return null;
|
|
4186
|
+
}
|
|
4187
|
+
var DANGEROUS_PYTHON_PATTERNS = [
|
|
4188
|
+
{ pattern: /\beval\s*\(/, label: "Python eval() \u2014 arbitrary code execution", scope: "code" },
|
|
4189
|
+
{ pattern: /\bexec\s*\(/, label: "Python exec() \u2014 arbitrary code execution", scope: "code" },
|
|
4190
|
+
{ pattern: /\b__import__\s*\(/, label: "Python __import__() \u2014 dynamic import (potential code injection)", scope: "code" },
|
|
4191
|
+
{ pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: "subprocess.call(shell=True) \u2014 shell injection risk", scope: "code" },
|
|
4192
|
+
{ pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: "subprocess.Popen(shell=True) \u2014 shell injection risk", scope: "code" },
|
|
4193
|
+
{ pattern: /os\.system\s*\(/, label: "os.system() \u2014 shell injection risk", scope: "code" },
|
|
4194
|
+
{ pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: "f-string in SQL \u2014 SQL injection risk", scope: "raw" },
|
|
4195
|
+
{ pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: "String formatting in SQL \u2014 SQL injection risk", scope: "raw" }
|
|
4196
|
+
];
|
|
4197
|
+
function stripPythonCommentsAndStringBodies(src) {
|
|
4198
|
+
let out = "";
|
|
4199
|
+
let i = 0;
|
|
4200
|
+
while (i < src.length) {
|
|
4201
|
+
const ch = src[i];
|
|
4202
|
+
if (ch === "#") {
|
|
4203
|
+
while (i < src.length && src[i] !== "\n") i++;
|
|
4204
|
+
continue;
|
|
4205
|
+
}
|
|
4206
|
+
if (ch === '"' || ch === "'") {
|
|
4207
|
+
const triple = src.startsWith(ch.repeat(3), i);
|
|
4208
|
+
const delim = triple ? ch.repeat(3) : ch;
|
|
4209
|
+
out += delim;
|
|
4210
|
+
i += delim.length;
|
|
4211
|
+
let closed = false;
|
|
4212
|
+
while (i < src.length) {
|
|
4213
|
+
if (src[i] === "\\") {
|
|
4214
|
+
i += 2;
|
|
4215
|
+
continue;
|
|
4216
|
+
}
|
|
4217
|
+
if (src.startsWith(delim, i)) {
|
|
4218
|
+
out += delim;
|
|
4219
|
+
i += delim.length;
|
|
4220
|
+
closed = true;
|
|
4221
|
+
break;
|
|
4222
|
+
}
|
|
4223
|
+
if (!triple && src[i] === "\n") break;
|
|
4224
|
+
i++;
|
|
4225
|
+
}
|
|
4226
|
+
if (!closed) return null;
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
out += ch;
|
|
4230
|
+
i++;
|
|
4231
|
+
}
|
|
4232
|
+
return out;
|
|
4233
|
+
}
|
|
4234
|
+
function checkPythonContent(content) {
|
|
4235
|
+
const stripped = stripPythonCommentsAndStringBodies(content);
|
|
4236
|
+
for (const { pattern, label, scope } of DANGEROUS_PYTHON_PATTERNS) {
|
|
4237
|
+
const subject = scope === "raw" || stripped === null ? content : stripped;
|
|
4238
|
+
if (pattern.test(subject)) {
|
|
4239
|
+
return label;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
return null;
|
|
4243
|
+
}
|
|
4244
|
+
function runSecurityGateFindings(hookInput) {
|
|
4245
|
+
const findings = [];
|
|
4246
|
+
const { tool_name, tool_input } = hookInput;
|
|
4247
|
+
if (tool_name === "Bash" && tool_input.command) {
|
|
4248
|
+
const secretFiles = secretFilesInCommand(tool_input.command);
|
|
4249
|
+
if (secretFiles.length > 0) {
|
|
4250
|
+
findings.push({
|
|
4251
|
+
severity: "block",
|
|
4252
|
+
message: `SECURITY GATE: command names a secret file: ${secretFiles.join(", ")}
|
|
4253
|
+
Command: ${tool_input.command.slice(0, 200)}
|
|
4254
|
+
BLOCKED. If this file is a template or fixture, name it with a template suffix (.example/.template/.sample) rather than weakening this check.`
|
|
4255
|
+
});
|
|
4256
|
+
}
|
|
4257
|
+
const violation = checkBashCommand(tool_input.command);
|
|
4258
|
+
if (violation) {
|
|
4259
|
+
findings.push({
|
|
4260
|
+
severity: violation.severity,
|
|
4261
|
+
message: `SECURITY GATE: Dangerous command pattern detected: ${violation.label}
|
|
4262
|
+
Command: ${tool_input.command.slice(0, 200)}` + (violation.severity === "block" ? `
|
|
4263
|
+
BLOCKED. This pattern has no legitimate use in an agent session.` : `
|
|
4264
|
+
Review carefully before proceeding.`)
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path) {
|
|
4269
|
+
const violation = checkFilePath(tool_input.file_path);
|
|
4270
|
+
if (violation) {
|
|
4271
|
+
findings.push({
|
|
4272
|
+
severity: "warn",
|
|
4273
|
+
message: `SECURITY GATE: Attempt to write to protected file: ${violation}
|
|
4274
|
+
Path: ${tool_input.file_path}
|
|
4275
|
+
Ensure this is intentional and no secrets will be exposed.`
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
const pyContent = tool_input.content || tool_input.new_string;
|
|
4280
|
+
if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path?.endsWith(".py") && pyContent) {
|
|
4281
|
+
const pyViolation = checkPythonContent(pyContent);
|
|
4282
|
+
if (pyViolation) {
|
|
4283
|
+
findings.push({
|
|
4284
|
+
severity: "warn",
|
|
4285
|
+
message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}
|
|
4286
|
+
File: ${tool_input.file_path}
|
|
4287
|
+
Review carefully before proceeding.`
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
return findings;
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4034
4295
|
init_db_driver();
|
|
4035
4296
|
import { existsSync as existsSync9 } from "fs";
|
|
4036
4297
|
|
|
@@ -4126,7 +4387,7 @@ function getFeatureImpact(db, filePaths) {
|
|
|
4126
4387
|
};
|
|
4127
4388
|
}
|
|
4128
4389
|
|
|
4129
|
-
// src/hooks/pre-delete-
|
|
4390
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4130
4391
|
init_config();
|
|
4131
4392
|
|
|
4132
4393
|
// src/lib/source-layout.ts
|
|
@@ -4176,58 +4437,7 @@ function isUnderSourceDir(path, layout = getSourceLayout()) {
|
|
|
4176
4437
|
return layout.sourceDirs.some((d) => path.startsWith(`${d}/`));
|
|
4177
4438
|
}
|
|
4178
4439
|
|
|
4179
|
-
// src/hooks/lib/
|
|
4180
|
-
var HOOK_EVENTS = [
|
|
4181
|
-
"UserPromptSubmit",
|
|
4182
|
-
"UserPromptExpansion",
|
|
4183
|
-
"SessionStart",
|
|
4184
|
-
"PreToolUse",
|
|
4185
|
-
"PostToolUse",
|
|
4186
|
-
"Stop",
|
|
4187
|
-
"SubagentStop",
|
|
4188
|
-
"PreCompact"
|
|
4189
|
-
];
|
|
4190
|
-
var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
|
|
4191
|
-
function isHookEvent(value) {
|
|
4192
|
-
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
4193
|
-
}
|
|
4194
|
-
function writeHookContext(hookEventName, message) {
|
|
4195
|
-
if (!isHookEvent(hookEventName)) {
|
|
4196
|
-
process.stderr.write(
|
|
4197
|
-
`[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
|
|
4198
|
-
`
|
|
4199
|
-
);
|
|
4200
|
-
return;
|
|
4201
|
-
}
|
|
4202
|
-
if (typeof message !== "string" || message.length === 0) return;
|
|
4203
|
-
let additionalContext = message;
|
|
4204
|
-
if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
|
|
4205
|
-
const marker = `
|
|
4206
|
-
[massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
|
|
4207
|
-
additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
|
|
4208
|
-
}
|
|
4209
|
-
process.stdout.write(
|
|
4210
|
-
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4211
|
-
);
|
|
4212
|
-
}
|
|
4213
|
-
|
|
4214
|
-
// src/hooks/pre-delete-check.ts
|
|
4215
|
-
init_hook_failure_signal();
|
|
4216
|
-
|
|
4217
|
-
// src/hooks/lib/is-direct-invocation.ts
|
|
4218
|
-
import { pathToFileURL } from "url";
|
|
4219
|
-
function isDirectInvocation(moduleUrl) {
|
|
4220
|
-
const entry = process.argv[1];
|
|
4221
|
-
if (!entry) return false;
|
|
4222
|
-
try {
|
|
4223
|
-
return moduleUrl === pathToFileURL(entry).href;
|
|
4224
|
-
} catch {
|
|
4225
|
-
return false;
|
|
4226
|
-
}
|
|
4227
|
-
}
|
|
4228
|
-
|
|
4229
|
-
// src/hooks/pre-delete-check.ts
|
|
4230
|
-
var HOOK_EVENT = "PreToolUse";
|
|
4440
|
+
// src/hooks/lib/pre-delete-checks.ts
|
|
4231
4441
|
var PROJECT_ROOT2 = getProjectRoot();
|
|
4232
4442
|
var KNOWLEDGE_PROTECTED_FILES = [
|
|
4233
4443
|
"knowledge-db.ts",
|
|
@@ -4366,30 +4576,154 @@ function runPreDeleteChecks(hookInput) {
|
|
|
4366
4576
|
}
|
|
4367
4577
|
return messages;
|
|
4368
4578
|
}
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
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);
|
|
4594
|
+
}
|
|
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;
|
|
4379
4609
|
}
|
|
4380
|
-
process.
|
|
4610
|
+
process.stdout.write(
|
|
4611
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4612
|
+
);
|
|
4381
4613
|
}
|
|
4382
|
-
|
|
4383
|
-
|
|
4614
|
+
|
|
4615
|
+
// src/hooks/lib/read-stdin.ts
|
|
4616
|
+
var StdinTimeoutError = class extends Error {
|
|
4617
|
+
bytesRead;
|
|
4618
|
+
constructor(ms, bytesRead) {
|
|
4619
|
+
super(
|
|
4620
|
+
`stdin did not reach EOF within ${ms}ms (${bytesRead} bytes read). The payload is INCOMPLETE and must not be treated as valid input.`
|
|
4621
|
+
);
|
|
4622
|
+
this.name = "StdinTimeoutError";
|
|
4623
|
+
this.bytesRead = bytesRead;
|
|
4624
|
+
}
|
|
4625
|
+
};
|
|
4626
|
+
function readStdinToEof(deadlineMs = 4e3) {
|
|
4627
|
+
return new Promise((resolve5, reject) => {
|
|
4384
4628
|
let data = "";
|
|
4629
|
+
let settled = false;
|
|
4630
|
+
const timer = setTimeout(() => {
|
|
4631
|
+
if (settled) return;
|
|
4632
|
+
settled = true;
|
|
4633
|
+
reject(new StdinTimeoutError(deadlineMs, data.length));
|
|
4634
|
+
}, deadlineMs);
|
|
4635
|
+
const finish = (fn) => {
|
|
4636
|
+
if (settled) return;
|
|
4637
|
+
settled = true;
|
|
4638
|
+
clearTimeout(timer);
|
|
4639
|
+
fn();
|
|
4640
|
+
};
|
|
4385
4641
|
process.stdin.setEncoding("utf-8");
|
|
4386
4642
|
process.stdin.on("data", (chunk) => {
|
|
4387
4643
|
data += chunk;
|
|
4388
4644
|
});
|
|
4389
|
-
process.stdin.on("end", () => resolve5(data));
|
|
4390
|
-
|
|
4645
|
+
process.stdin.on("end", () => finish(() => resolve5(data)));
|
|
4646
|
+
process.stdin.on("error", (err) => finish(() => reject(err)));
|
|
4391
4647
|
});
|
|
4392
4648
|
}
|
|
4649
|
+
|
|
4650
|
+
// src/hooks/lib/pre-tool-use-decision.ts
|
|
4651
|
+
init_hook_failure_signal();
|
|
4652
|
+
var EXIT_DENY = 2;
|
|
4653
|
+
var EXIT_ALLOW = 0;
|
|
4654
|
+
function deny(reason, detail) {
|
|
4655
|
+
process.stderr.write(
|
|
4656
|
+
`MASSU SECURITY GATE \u2014 BLOCKED
|
|
4657
|
+
|
|
4658
|
+
${reason}
|
|
4659
|
+
` + (detail ? `
|
|
4660
|
+
${detail}
|
|
4661
|
+
` : "") + `
|
|
4662
|
+
The gate denies by default when it cannot verify a command is safe.
|
|
4663
|
+
`
|
|
4664
|
+
);
|
|
4665
|
+
process.exit(EXIT_DENY);
|
|
4666
|
+
}
|
|
4667
|
+
async function runPreToolUseDecision(hookName, event) {
|
|
4668
|
+
let hookInput;
|
|
4669
|
+
let input = "";
|
|
4670
|
+
try {
|
|
4671
|
+
input = await readStdinToEof();
|
|
4672
|
+
if (!input.trim()) {
|
|
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.");
|
|
4675
|
+
}
|
|
4676
|
+
hookInput = JSON.parse(input);
|
|
4677
|
+
} catch (err) {
|
|
4678
|
+
recordHookFailure(hookName, err, {
|
|
4679
|
+
phase: "read-parse",
|
|
4680
|
+
bytesRead: input.length,
|
|
4681
|
+
endsWithBrace: input.trimEnd().endsWith("}")
|
|
4682
|
+
});
|
|
4683
|
+
deny(
|
|
4684
|
+
"The gate could not READ or PARSE the tool-call payload, so it cannot know what it was being asked to approve.",
|
|
4685
|
+
err instanceof Error ? `Cause: ${err.message}` : void 0
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
let findings;
|
|
4689
|
+
try {
|
|
4690
|
+
const security = runSecurityGateFindings(hookInput);
|
|
4691
|
+
const deletes = runPreDeleteChecks(hookInput).map(
|
|
4692
|
+
(message) => ({ severity: "warn", message })
|
|
4693
|
+
);
|
|
4694
|
+
findings = [...security, ...deletes];
|
|
4695
|
+
} catch (err) {
|
|
4696
|
+
recordHookFailure(hookName, err, { phase: "evaluate" });
|
|
4697
|
+
deny(
|
|
4698
|
+
"A security check CRASHED while evaluating this tool call. A check that did not run has not passed.",
|
|
4699
|
+
err instanceof Error ? `Cause: ${err.message}` : void 0
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
for (const f of findings.filter((x) => x.severity === "warn")) {
|
|
4703
|
+
writeHookContext(event, f.message);
|
|
4704
|
+
}
|
|
4705
|
+
const blocking = findings.filter((f) => f.severity === "block");
|
|
4706
|
+
if (blocking.length > 0) deny(blocking.map((f) => f.message).join("\n\n"));
|
|
4707
|
+
process.exit(EXIT_ALLOW);
|
|
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-delete-check.ts
|
|
4723
|
+
var HOOK_EVENT = "PreToolUse";
|
|
4724
|
+
async function main() {
|
|
4725
|
+
await runPreToolUseDecision("pre-delete-check", HOOK_EVENT);
|
|
4726
|
+
}
|
|
4393
4727
|
if (isDirectInvocation(import.meta.url)) {
|
|
4394
4728
|
main();
|
|
4395
4729
|
}
|