@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.
@@ -2157,6 +2157,8 @@ __export(memory_db_exports, {
2157
2157
  MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
2158
2158
  MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
2159
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,
2160
2162
  TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
2161
2163
  USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
2162
2164
  addConversationTurn: () => addConversationTurn,
@@ -2165,6 +2167,7 @@ __export(memory_db_exports, {
2165
2167
  addSummary: () => addSummary,
2166
2168
  addToolCallDetail: () => addToolCallDetail,
2167
2169
  addUserPrompt: () => addUserPrompt,
2170
+ allPendingSync: () => allPendingSync,
2168
2171
  appendIncidentToFailureClass: () => appendIncidentToFailureClass,
2169
2172
  armUsageCounter: () => armUsageCounter,
2170
2173
  assignImportance: () => assignImportance,
@@ -2172,6 +2175,7 @@ __export(memory_db_exports, {
2172
2175
  createSession: () => createSession,
2173
2176
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
2174
2177
  dequeuePendingSync: () => dequeuePendingSync,
2178
+ discardPendingSync: () => discardPendingSync,
2175
2179
  embedMissingObservations: () => embedMissingObservations,
2176
2180
  endSession: () => endSession,
2177
2181
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -3154,6 +3158,35 @@ function initMemorySchema(db) {
3154
3158
  }
3155
3159
  function enqueueSyncPayload(db, payload) {
3156
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();
3157
3190
  }
3158
3191
  function getMemoryMeta(db, key) {
3159
3192
  const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
@@ -3356,7 +3389,9 @@ function dequeuePendingSync(db, limit = 10) {
3356
3389
  }
3357
3390
  }
3358
3391
  return db.prepare(
3359
- "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
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 ?"
3360
3395
  ).all(limit);
3361
3396
  }
3362
3397
  function removePendingSync(db, id) {
@@ -4029,7 +4064,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
4029
4064
  }
4030
4065
  return bestMatch;
4031
4066
  }
4032
- 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;
4033
4068
  var init_memory_db = __esm({
4034
4069
  "src/memory-db.ts"() {
4035
4070
  "use strict";
@@ -4044,6 +4079,8 @@ var init_memory_db = __esm({
4044
4079
  init_memory_embed_sweep();
4045
4080
  MEMORY_SCHEMA_VERSION = 2;
4046
4081
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
4082
+ PENDING_SYNC_MAX_ITEMS = 500;
4083
+ PENDING_SYNC_MAX_AGE_DAYS = 30;
4047
4084
  MAX_DRAFT_TEXT_LEN = 16384;
4048
4085
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
4049
4086
  _temporalColCache = /* @__PURE__ */ new WeakMap();
@@ -4054,7 +4091,244 @@ var init_memory_db = __esm({
4054
4091
  }
4055
4092
  });
4056
4093
 
4057
- // src/hooks/pre-delete-check.ts
4094
+ // src/hooks/lib/security-gate-checks.ts
4095
+ var DANGEROUS_BASH_PATTERNS = [
4096
+ // BLOCK — destructive and irreversible, or arbitrary remote code execution.
4097
+ { pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
4098
+ { pattern: /rm\s+-[a-z]*f[a-z]*r[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
4099
+ { pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/, label: "curl | bash (remote code execution)", severity: "block" },
4100
+ { pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/, label: "wget | bash (remote code execution)", severity: "block" },
4101
+ { pattern: />\s*\/etc\/passwd/, label: "write to /etc/passwd", severity: "block" },
4102
+ { pattern: />\s*\/etc\/shadow/, label: "write to /etc/shadow", severity: "block" },
4103
+ { pattern: />\s*\/etc\/sudoers/, label: "write to /etc/sudoers", severity: "block" },
4104
+ { pattern: /dd\s+if=.*of=\/dev\/(?:sda|sdb|hda|hdb|nvme)/, label: "dd to raw device", severity: "block" },
4105
+ { pattern: /mkfs\s+\/dev\//, label: "format disk device", severity: "block" },
4106
+ { pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}/, label: "fork bomb", severity: "block" },
4107
+ { pattern: /eval\s+.*\$\(.*curl/, label: "eval with remote curl", severity: "block" },
4108
+ { pattern: /base64\s+-d\s+.*\|\s*(?:bash|sh|zsh)/, label: "base64 decoded shell exec", severity: "block" },
4109
+ // WARN — bad practice, but has legitimate uses (fixing a broken permission bit,
4110
+ // a throwaway container). Blocking these would make the product hostile.
4111
+ { pattern: /chmod\s+777/, label: "chmod 777 (world-writable permissions)", severity: "warn" },
4112
+ { pattern: /chmod\s+-R\s+777/, label: "chmod -R 777 (world-writable permissions)", severity: "warn" }
4113
+ ];
4114
+ var PROTECTED_FILE_PATTERNS = [
4115
+ { pattern: /\.env$/, label: ".env file" },
4116
+ { pattern: /\.env\./, label: ".env.* file" },
4117
+ { pattern: /credentials(?:\.json)?$/, label: "credentials file" },
4118
+ { pattern: /\.pem$/, label: ".pem certificate/key file" },
4119
+ { pattern: /\.key$/, label: ".key file" },
4120
+ { pattern: /\.p12$/, label: ".p12 keystore file" },
4121
+ { pattern: /\.pfx$/, label: ".pfx keystore file" },
4122
+ { pattern: /id_rsa$/, label: "RSA private key" },
4123
+ { pattern: /id_ed25519$/, label: "Ed25519 private key" },
4124
+ { pattern: /id_ecdsa$/, label: "ECDSA private key" },
4125
+ { pattern: /\.ssh\/config$/, label: "SSH config file" },
4126
+ { pattern: /secrets\.yaml$/, label: "secrets.yaml file" },
4127
+ { pattern: /secrets\.yml$/, label: "secrets.yml file" },
4128
+ { pattern: /\.netrc$/, label: ".netrc credentials file" },
4129
+ { pattern: /aws\/credentials$/, label: "AWS credentials file" },
4130
+ { pattern: /kubeconfig$/, label: "Kubernetes config file" }
4131
+ ];
4132
+ var SECRET_BASENAME_EXTENSIONS = ["pem", "key", "p12", "pfx", "secret"];
4133
+ var DOTENV_TEMPLATE_SUFFIXES = ["example", "template", "sample", "defaults", "dist"];
4134
+ var SECRET_EXACT_BASENAMES = [
4135
+ "id_rsa",
4136
+ "id_ed25519",
4137
+ "id_ecdsa",
4138
+ ".netrc",
4139
+ "kubeconfig",
4140
+ "credentials.json",
4141
+ "secrets.yaml",
4142
+ "secrets.yml"
4143
+ ];
4144
+ var CREDENTIALS_PATH_SUFFIX = "/credentials";
4145
+ var DOTENV = ".env";
4146
+ var SECRET_DATA_EXTENSIONS = ["yaml", "yml", "json", "toml", "ini", "env", "conf", "properties", "txt"];
4147
+ function looksLikeCode(token) {
4148
+ return /[()[\]{};=`$<>|&*?!"']/.test(token) || /\s/.test(token);
4149
+ }
4150
+ function namesSecretFile(token) {
4151
+ if (!token || looksLikeCode(token)) return null;
4152
+ const clean = token.replace(/[,:.]+$/, "");
4153
+ if (!clean) return null;
4154
+ const base = clean.split("/").pop() ?? "";
4155
+ if (!base) return null;
4156
+ if (SECRET_EXACT_BASENAMES.includes(base)) return base;
4157
+ if (clean.endsWith(".ssh/config")) return "SSH config file";
4158
+ if (clean.endsWith(CREDENTIALS_PATH_SUFFIX)) return "credentials file";
4159
+ if (base === DOTENV) return `${DOTENV} file`;
4160
+ if (base.startsWith(`${DOTENV}.`)) {
4161
+ const suffix = base.slice(DOTENV.length + 1);
4162
+ return DOTENV_TEMPLATE_SUFFIXES.includes(suffix) ? null : `${base} file`;
4163
+ }
4164
+ if (base.endsWith(DOTENV) && clean.includes("/")) return `${base} file`;
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`;
4172
+ }
4173
+ return null;
4174
+ }
4175
+ function shellTokens(command) {
4176
+ const out = [];
4177
+ let cur = "";
4178
+ let quote = null;
4179
+ for (const ch of command) {
4180
+ if (quote) {
4181
+ if (ch === quote) quote = null;
4182
+ else cur += ch;
4183
+ continue;
4184
+ }
4185
+ if (ch === "'" || ch === '"') {
4186
+ quote = ch;
4187
+ cur += ch;
4188
+ continue;
4189
+ }
4190
+ if (/\s/.test(ch)) {
4191
+ if (cur) out.push(cur);
4192
+ cur = "";
4193
+ continue;
4194
+ }
4195
+ cur += ch;
4196
+ }
4197
+ if (cur) out.push(cur);
4198
+ return out;
4199
+ }
4200
+ function secretFilesInCommand(command) {
4201
+ const hits = [];
4202
+ for (const token of shellTokens(command)) {
4203
+ const label = namesSecretFile(token);
4204
+ if (label) hits.push(label);
4205
+ }
4206
+ return hits;
4207
+ }
4208
+ function checkBashCommand(command) {
4209
+ for (const { pattern, label, severity } of DANGEROUS_BASH_PATTERNS) {
4210
+ if (pattern.test(command)) {
4211
+ return { label, severity };
4212
+ }
4213
+ }
4214
+ return null;
4215
+ }
4216
+ function checkFilePath(filePath) {
4217
+ for (const { pattern, label } of PROTECTED_FILE_PATTERNS) {
4218
+ if (pattern.test(filePath)) {
4219
+ return label;
4220
+ }
4221
+ }
4222
+ return null;
4223
+ }
4224
+ var DANGEROUS_PYTHON_PATTERNS = [
4225
+ { pattern: /\beval\s*\(/, label: "Python eval() \u2014 arbitrary code execution", scope: "code" },
4226
+ { pattern: /\bexec\s*\(/, label: "Python exec() \u2014 arbitrary code execution", scope: "code" },
4227
+ { pattern: /\b__import__\s*\(/, label: "Python __import__() \u2014 dynamic import (potential code injection)", scope: "code" },
4228
+ { pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: "subprocess.call(shell=True) \u2014 shell injection risk", scope: "code" },
4229
+ { pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: "subprocess.Popen(shell=True) \u2014 shell injection risk", scope: "code" },
4230
+ { pattern: /os\.system\s*\(/, label: "os.system() \u2014 shell injection risk", scope: "code" },
4231
+ { pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: "f-string in SQL \u2014 SQL injection risk", scope: "raw" },
4232
+ { pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: "String formatting in SQL \u2014 SQL injection risk", scope: "raw" }
4233
+ ];
4234
+ function stripPythonCommentsAndStringBodies(src) {
4235
+ let out = "";
4236
+ let i = 0;
4237
+ while (i < src.length) {
4238
+ const ch = src[i];
4239
+ if (ch === "#") {
4240
+ while (i < src.length && src[i] !== "\n") i++;
4241
+ continue;
4242
+ }
4243
+ if (ch === '"' || ch === "'") {
4244
+ const triple = src.startsWith(ch.repeat(3), i);
4245
+ const delim = triple ? ch.repeat(3) : ch;
4246
+ out += delim;
4247
+ i += delim.length;
4248
+ let closed = false;
4249
+ while (i < src.length) {
4250
+ if (src[i] === "\\") {
4251
+ i += 2;
4252
+ continue;
4253
+ }
4254
+ if (src.startsWith(delim, i)) {
4255
+ out += delim;
4256
+ i += delim.length;
4257
+ closed = true;
4258
+ break;
4259
+ }
4260
+ if (!triple && src[i] === "\n") break;
4261
+ i++;
4262
+ }
4263
+ if (!closed) return null;
4264
+ continue;
4265
+ }
4266
+ out += ch;
4267
+ i++;
4268
+ }
4269
+ return out;
4270
+ }
4271
+ function checkPythonContent(content) {
4272
+ const stripped = stripPythonCommentsAndStringBodies(content);
4273
+ for (const { pattern, label, scope } of DANGEROUS_PYTHON_PATTERNS) {
4274
+ const subject = scope === "raw" || stripped === null ? content : stripped;
4275
+ if (pattern.test(subject)) {
4276
+ return label;
4277
+ }
4278
+ }
4279
+ return null;
4280
+ }
4281
+ function runSecurityGateFindings(hookInput) {
4282
+ const findings = [];
4283
+ const { tool_name, tool_input } = hookInput;
4284
+ if (tool_name === "Bash" && tool_input.command) {
4285
+ const secretFiles = secretFilesInCommand(tool_input.command);
4286
+ if (secretFiles.length > 0) {
4287
+ findings.push({
4288
+ severity: "block",
4289
+ message: `SECURITY GATE: command names a secret file: ${secretFiles.join(", ")}
4290
+ Command: ${tool_input.command.slice(0, 200)}
4291
+ BLOCKED. If this file is a template or fixture, name it with a template suffix (.example/.template/.sample) rather than weakening this check.`
4292
+ });
4293
+ }
4294
+ const violation = checkBashCommand(tool_input.command);
4295
+ if (violation) {
4296
+ findings.push({
4297
+ severity: violation.severity,
4298
+ message: `SECURITY GATE: Dangerous command pattern detected: ${violation.label}
4299
+ Command: ${tool_input.command.slice(0, 200)}` + (violation.severity === "block" ? `
4300
+ BLOCKED. This pattern has no legitimate use in an agent session.` : `
4301
+ Review carefully before proceeding.`)
4302
+ });
4303
+ }
4304
+ }
4305
+ if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path) {
4306
+ const violation = checkFilePath(tool_input.file_path);
4307
+ if (violation) {
4308
+ findings.push({
4309
+ severity: "warn",
4310
+ message: `SECURITY GATE: Attempt to write to protected file: ${violation}
4311
+ Path: ${tool_input.file_path}
4312
+ Ensure this is intentional and no secrets will be exposed.`
4313
+ });
4314
+ }
4315
+ }
4316
+ const pyContent = tool_input.content || tool_input.new_string;
4317
+ if ((tool_name === "Write" || tool_name === "Edit") && tool_input.file_path?.endsWith(".py") && pyContent) {
4318
+ const pyViolation = checkPythonContent(pyContent);
4319
+ if (pyViolation) {
4320
+ findings.push({
4321
+ severity: "warn",
4322
+ message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}
4323
+ File: ${tool_input.file_path}
4324
+ Review carefully before proceeding.`
4325
+ });
4326
+ }
4327
+ }
4328
+ return findings;
4329
+ }
4330
+
4331
+ // src/hooks/lib/pre-delete-checks.ts
4058
4332
  init_db_driver();
4059
4333
  import { existsSync as existsSync9 } from "fs";
4060
4334
 
@@ -4150,7 +4424,7 @@ function getFeatureImpact(db, filePaths) {
4150
4424
  };
4151
4425
  }
4152
4426
 
4153
- // src/hooks/pre-delete-check.ts
4427
+ // src/hooks/lib/pre-delete-checks.ts
4154
4428
  init_config();
4155
4429
 
4156
4430
  // src/lib/source-layout.ts
@@ -4200,58 +4474,7 @@ function isUnderSourceDir(path, layout = getSourceLayout()) {
4200
4474
  return layout.sourceDirs.some((d) => path.startsWith(`${d}/`));
4201
4475
  }
4202
4476
 
4203
- // src/hooks/lib/write-hook-message.ts
4204
- var HOOK_EVENTS = [
4205
- "UserPromptSubmit",
4206
- "UserPromptExpansion",
4207
- "SessionStart",
4208
- "PreToolUse",
4209
- "PostToolUse",
4210
- "Stop",
4211
- "SubagentStop",
4212
- "PreCompact"
4213
- ];
4214
- var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4215
- function isHookEvent(value) {
4216
- return typeof value === "string" && HOOK_EVENTS.includes(value);
4217
- }
4218
- function writeHookContext(hookEventName, message) {
4219
- if (!isHookEvent(hookEventName)) {
4220
- process.stderr.write(
4221
- `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4222
- `
4223
- );
4224
- return;
4225
- }
4226
- if (typeof message !== "string" || message.length === 0) return;
4227
- let additionalContext = message;
4228
- if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4229
- const marker = `
4230
- [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4231
- additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4232
- }
4233
- process.stdout.write(
4234
- JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4235
- );
4236
- }
4237
-
4238
- // src/hooks/pre-delete-check.ts
4239
- init_hook_failure_signal();
4240
-
4241
- // src/hooks/lib/is-direct-invocation.ts
4242
- import { pathToFileURL } from "url";
4243
- function isDirectInvocation(moduleUrl) {
4244
- const entry = process.argv[1];
4245
- if (!entry) return false;
4246
- try {
4247
- return moduleUrl === pathToFileURL(entry).href;
4248
- } catch {
4249
- return false;
4250
- }
4251
- }
4252
-
4253
- // src/hooks/pre-delete-check.ts
4254
- var HOOK_EVENT = "PreToolUse";
4477
+ // src/hooks/lib/pre-delete-checks.ts
4255
4478
  var PROJECT_ROOT2 = getProjectRoot();
4256
4479
  var KNOWLEDGE_PROTECTED_FILES = [
4257
4480
  "knowledge-db.ts",
@@ -4390,30 +4613,182 @@ function runPreDeleteChecks(hookInput) {
4390
4613
  }
4391
4614
  return messages;
4392
4615
  }
4393
- async function main() {
4394
- try {
4395
- const input = await readStdin();
4396
- const hookInput = JSON.parse(input);
4397
- const messages = runPreDeleteChecks(hookInput);
4398
- for (const msg of messages) writeHookContext(HOOK_EVENT, msg);
4399
- } catch (err) {
4400
- recordHookFailure("pre-delete-check", err);
4401
- process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
4402
- process.exit(2);
4616
+
4617
+ // src/hooks/lib/write-hook-message.ts
4618
+ var HOOK_EVENTS = [
4619
+ "UserPromptSubmit",
4620
+ "UserPromptExpansion",
4621
+ "SessionStart",
4622
+ "PreToolUse",
4623
+ "PostToolUse",
4624
+ "Stop",
4625
+ "SubagentStop",
4626
+ "PreCompact"
4627
+ ];
4628
+ var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4629
+ function isHookEvent(value) {
4630
+ return typeof value === "string" && HOOK_EVENTS.includes(value);
4631
+ }
4632
+ function writeHookContext(hookEventName, message) {
4633
+ if (!isHookEvent(hookEventName)) {
4634
+ process.stderr.write(
4635
+ `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4636
+ `
4637
+ );
4638
+ return;
4639
+ }
4640
+ if (typeof message !== "string" || message.length === 0) return;
4641
+ let additionalContext = message;
4642
+ if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4643
+ const marker = `
4644
+ [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4645
+ additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4403
4646
  }
4404
- process.exit(0);
4647
+ process.stdout.write(
4648
+ JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4649
+ );
4405
4650
  }
4406
- function readStdin() {
4407
- return new Promise((resolve5) => {
4651
+
4652
+ // src/hooks/lib/read-stdin.ts
4653
+ var StdinTimeoutError = class extends Error {
4654
+ bytesRead;
4655
+ constructor(ms, bytesRead) {
4656
+ super(
4657
+ `stdin did not reach EOF within ${ms}ms (${bytesRead} bytes read). The payload is INCOMPLETE and must not be treated as valid input.`
4658
+ );
4659
+ this.name = "StdinTimeoutError";
4660
+ this.bytesRead = bytesRead;
4661
+ }
4662
+ };
4663
+ function readStdinToEof(deadlineMs = 4e3) {
4664
+ return new Promise((resolve5, reject) => {
4408
4665
  let data = "";
4666
+ let settled = false;
4667
+ const timer = setTimeout(() => {
4668
+ if (settled) return;
4669
+ settled = true;
4670
+ reject(new StdinTimeoutError(deadlineMs, data.length));
4671
+ }, deadlineMs);
4672
+ const finish = (fn) => {
4673
+ if (settled) return;
4674
+ settled = true;
4675
+ clearTimeout(timer);
4676
+ fn();
4677
+ };
4409
4678
  process.stdin.setEncoding("utf-8");
4410
4679
  process.stdin.on("data", (chunk) => {
4411
4680
  data += chunk;
4412
4681
  });
4413
- process.stdin.on("end", () => resolve5(data));
4414
- setTimeout(() => resolve5(data), 400);
4682
+ process.stdin.on("end", () => finish(() => resolve5(data)));
4683
+ process.stdin.on("error", (err) => finish(() => reject(err)));
4415
4684
  });
4416
4685
  }
4686
+
4687
+ // src/hooks/lib/pre-tool-use-decision.ts
4688
+ init_hook_failure_signal();
4689
+ var EXIT_DENY = 2;
4690
+ var EXIT_ALLOW = 0;
4691
+ function deny(reason, detail) {
4692
+ process.stderr.write(
4693
+ `MASSU SECURITY GATE \u2014 BLOCKED
4694
+
4695
+ ${reason}
4696
+ ` + (detail ? `
4697
+ ${detail}
4698
+ ` : "") + `
4699
+ The gate denies by default when it cannot verify a command is safe.
4700
+ `
4701
+ );
4702
+ process.exit(EXIT_DENY);
4703
+ }
4704
+ async function runPreToolUseDecision(hookName, event) {
4705
+ let hookInput;
4706
+ let input = "";
4707
+ try {
4708
+ input = await readStdinToEof();
4709
+ if (!input.trim()) {
4710
+ recordHookFailure(hookName, new Error("empty stdin payload"));
4711
+ deny("The gate received an EMPTY payload and could not determine what tool call was requested.");
4712
+ }
4713
+ hookInput = JSON.parse(input);
4714
+ } catch (err) {
4715
+ recordHookFailure(hookName, err, {
4716
+ phase: "read-parse",
4717
+ bytesRead: input.length,
4718
+ endsWithBrace: input.trimEnd().endsWith("}")
4719
+ });
4720
+ deny(
4721
+ "The gate could not READ or PARSE the tool-call payload, so it cannot know what it was being asked to approve.",
4722
+ err instanceof Error ? `Cause: ${err.message}` : void 0
4723
+ );
4724
+ }
4725
+ let findings;
4726
+ try {
4727
+ const security = runSecurityGateFindings(hookInput);
4728
+ const deletes = runPreDeleteChecks(hookInput).map(
4729
+ (message) => ({ severity: "warn", message })
4730
+ );
4731
+ findings = [...security, ...deletes];
4732
+ } catch (err) {
4733
+ recordHookFailure(hookName, err, { phase: "evaluate" });
4734
+ deny(
4735
+ "A security check CRASHED while evaluating this tool call. A check that did not run has not passed.",
4736
+ err instanceof Error ? `Cause: ${err.message}` : void 0
4737
+ );
4738
+ }
4739
+ for (const f of findings.filter((x) => x.severity === "warn")) {
4740
+ writeHookContext(event, f.message);
4741
+ }
4742
+ const blocking = findings.filter((f) => f.severity === "block");
4743
+ if (blocking.length > 0) deny(blocking.map((f) => f.message).join("\n\n"));
4744
+ process.exit(EXIT_ALLOW);
4745
+ }
4746
+
4747
+ // src/lib/safe-write.ts
4748
+ import {
4749
+ writeFileSync as writeFileSync3,
4750
+ renameSync,
4751
+ openSync as openSync2,
4752
+ fsyncSync as fsyncSync2,
4753
+ closeSync as closeSync2,
4754
+ existsSync as existsSync10,
4755
+ realpathSync,
4756
+ unlinkSync as unlinkSync2,
4757
+ accessSync as accessSync2,
4758
+ statSync as statSync2,
4759
+ chmodSync as chmodSync3,
4760
+ constants
4761
+ } from "fs";
4762
+ function isSameRealPath(a, b) {
4763
+ try {
4764
+ return realpathSync(a) === realpathSync(b);
4765
+ } catch {
4766
+ return false;
4767
+ }
4768
+ }
4769
+
4770
+ // src/hooks/lib/is-direct-invocation.ts
4771
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4772
+ function isDirectInvocation(moduleUrl) {
4773
+ const entry = process.argv[1];
4774
+ if (!entry) return false;
4775
+ try {
4776
+ if (moduleUrl === pathToFileURL(entry).href) return true;
4777
+ } catch {
4778
+ return false;
4779
+ }
4780
+ try {
4781
+ return isSameRealPath(fileURLToPath2(moduleUrl), entry);
4782
+ } catch {
4783
+ return false;
4784
+ }
4785
+ }
4786
+
4787
+ // src/hooks/pre-delete-check.ts
4788
+ var HOOK_EVENT = "PreToolUse";
4789
+ async function main() {
4790
+ await runPreToolUseDecision("pre-delete-check", HOOK_EVENT);
4791
+ }
4417
4792
  if (isDirectInvocation(import.meta.url)) {
4418
4793
  main();
4419
4794
  }