@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.
@@ -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 = 1;
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/pre-delete-check.ts
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-check.ts
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/write-hook-message.ts
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
- async function main() {
4370
- try {
4371
- const input = await readStdin();
4372
- const hookInput = JSON.parse(input);
4373
- const messages = runPreDeleteChecks(hookInput);
4374
- for (const msg of messages) writeHookContext(HOOK_EVENT, msg);
4375
- } catch (err) {
4376
- recordHookFailure("pre-delete-check", err);
4377
- process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
4378
- process.exit(2);
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.exit(0);
4610
+ process.stdout.write(
4611
+ JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4612
+ );
4381
4613
  }
4382
- function readStdin() {
4383
- return new Promise((resolve5) => {
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
- setTimeout(() => resolve5(data), 400);
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
  }