#!/usr/bin/env node
/**
 * enforce-runtime-write-deny — PreToolUse(Edit|Write|MultiEdit|NotebookEdit).
 *
 * Blocks the Write/Edit TOOL from writing under ~/.claude/runtime/** — closing a
 * pre-existing self-write hole on the v4 git-approval anchor
 * (askuser-decisions-<sess>.jsonl) and any future runtime side-channel. The
 * Bash/PowerShell gates already protect this path for shell commands; the Write
 * tool was the remaining ungated channel. Standalone — independent of
 * safe-baseline (router-gate v4 §"Side-channel write-protection", item 1b / C3).
 *
 * Uses the resolving `pathNormalize` (path.resolve + realpath) so `.`/`..`
 * segments cannot evade the match (V2-2) — `~/.claude/./runtime/x` collapses to
 * the real path before the regex test. Legitimate hooks write there via Node
 * `fs`, not the Claude Write tool, so they are unaffected.
 *
 * Posture: blocks ONLY on a confirmed runtime match; if normalization throws
 * (cannot determine the path) it fails-OPEN, never bricking the session. This
 * deliberately trades a tiny residual (a malformed path that throws) for
 * not self-locking the controller out of all edits on an unexpected error.
 */
import { pathNormalize } from './path-normalization.mjs';
import { readStdin, parseEventJson, exitDecision } from './enforce-hook-helpers.mjs';

const RUNTIME_RE = /(^|\/)\.claude\/runtime(\/|$)/i;

// P10-a (router-mentor): любой инструмент, несущий путь записи в одном из этих
// полей, проверяется — не только 4 именованных Write-tool'а (ловит MCP-писатели).
// B4 (2026-06-05): расширено — MCP-писатели несут путь под разными именами полей.
const PATH_FIELDS = ['file_path', 'notebook_path', 'path', 'target_file', 'filename', 'destination', 'dest', 'output_path', 'uri'];

/** Извлечь предполагаемый путь записи из tool_input по известным полям. '' если нет. */
export function extractPath(toolInput) {
  if (!toolInput || typeof toolInput !== 'object') return '';
  for (const f of PATH_FIELDS) {
    if (typeof toolInput[f] === 'string' && toolInput[f]) return toolInput[f];
  }
  return '';
}

/**
 * Pure decision по пути (toolName-агностично, P10-a). Блок ТОЛЬКО при подтверждённом
 * совпадении с ~/.claude/runtime. Нормализация бросила → fail-open.
 * @param {object} p
 * @param {string} p.filePath
 * @param {Function} [p.normalizeImpl] - injectable normalizer (default: resolving pathNormalize)
 * @returns {{block:boolean, reason?:string}}
 */
export function decide({ filePath, normalizeImpl = pathNormalize }) {
  const fp = String(filePath || '');
  if (!fp) return { block: false };
  let norm;
  try { norm = normalizeImpl(fp); } catch { return { block: false }; } // cannot determine → fail-open
  if (RUNTIME_RE.test(String(norm || ''))) {
    return {
      block: true,
      reason: `Write to «${norm}» denied — ~/.claude/runtime is a protected side-channel (receipts / journal head). Hooks write it via Node fs, not file-writing tools.`,
    };
  }
  return { block: false };
}

/** Decision из полного события: извлекает путь из любого инструмента (P10-a). */
export function decideFromEvent(event) {
  return decide({ filePath: extractPath(event && event.tool_input) });
}

async function main() {
  try {
    const event = parseEventJson(await readStdin());
    const r = decideFromEvent(event);
    exitDecision({ block: r.block, message: r.reason });
  } catch {
    exitDecision({ block: false }); // fail-quiet
  }
}

const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-runtime-write-deny.mjs');
if (isCli) main();
