#!/usr/bin/env node
/**
 * PreToolUse(Workflow) hook — Workflow gate F2 (router-gate v4 spec §3.6 / v3.8 F2).
 *
 * Closes:
 * - scriptPath must be pre-approved via approve_workflow_script record + sha256 match
 * - scriptContent static scan for dangerous patterns (env keys, eval, child_process, fs writes outside .scratch/tmp)
 * - resumeFromRunId blocked unconditionally (state replay risk)
 * - per-agent gate inheritance handled by subagent-prompt-prefix.mjs (Stream E); this hook focuses on the outer
 *   Workflow tool call. Nested agent() inside Workflow inherits parent gate via CLAUDE_GATE_INHERIT env.
 */
import { readFileSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { homedir } from 'node:os';
import { join } from 'node:path';

const APPROVE_WINDOW_MS = 5 * 60 * 1000;

// NOTE: this hook DETECTS dangerous patterns in user-supplied workflow scripts;
// none of the regexes below are executed via eval/exec/child_process by this hook itself.
// `/\beval\s*\(/i` and `/\b(?:exec|spawn|...)\s*\(/` are pattern-matchers, not invocations.
const DANGEROUS_PATTERNS = [
  { re: /process\.env\.(ROUTER_LLM_KEY|ANTHROPIC_API_KEY|GITHUB_TOKEN|SENTRY_AUTH_TOKEN)/i, name: 'env key access (ROUTER_LLM_KEY)' },
  { re: /\beval\s*\(/i, name: 'eval()' },
  { re: /\b(?:exec|spawn|execSync|spawnSync|execFile|fork)\s*\(/, name: 'child_process' },
  { re: /\bwriteFileSync\s*\(\s*["'`]\/(?!tmp\/|var\/tmp\/)/i, name: 'fs write absolute' },
  { re: /\.\.\/\.\.\/\.\.\//, name: 'path traversal' },
  // 7.4 (Блок 4.4): доступ к OS-keychain — контроллер не должен через workflow читать ключ
  // подписанта/судьи (keytar + методы получения пароля независимо от service-name).
  { re: /\bkeytar\b/i, name: 'keytar (OS-keychain access)' },
  { re: /\b(?:get|set|delete|find)(?:Password|Credentials)(?:Sync)?\s*\(/i, name: 'keychain password/credentials access' },
];

export function decide({ toolInput, approvedWorkflowScripts, scriptContent, scriptSha256, now }) {
  // 1. resumeFromRunId blocked unconditionally
  if (toolInput && toolInput.resumeFromRunId) {
    return { block: true, reason: 'F2: resumeFromRunId disabled (state replay risk)' };
  }

  const scriptPath = toolInput && toolInput.scriptPath;
  const inlineScript = toolInput && typeof toolInput.script === 'string' ? toolInput.script : null;

  // 7.4 (C5): inline `script` гейтится как scriptPath — скан опасных паттернов + одобрение по
  // sha256 контента (раньше inline-ветка возвращала block:false → полный обход охвата).
  if (!scriptPath && inlineScript != null) {
    for (const { re, name } of DANGEROUS_PATTERNS) {
      if (re.test(inlineScript)) {
        return { block: true, reason: `F2/7.4: inline workflow script содержит опасный паттерн — ${name}` };
      }
    }
    const sha = scriptSha256 || sha256Hex(inlineScript);
    const approval = (approvedWorkflowScripts || []).find(
      (a) => a.sha256 && a.sha256 === sha && typeof a.ts === 'number' && now - a.ts <= APPROVE_WINDOW_MS,
    );
    if (!approval) {
      return { block: true, reason: 'F2/7.4: inline workflow script requires approve_workflow_script (sha256, 5-min window)' };
    }
    return { block: false };
  }

  if (!scriptPath) {
    // ни scriptPath, ни inline `script` (напр. saved-workflow по `name`) — этим хуком не гейтится.
    return { block: false };
  }

  // 2. scriptPath must be approved
  const approval = (approvedWorkflowScripts || []).find(
    (a) => a.scriptPath === scriptPath && typeof a.ts === 'number' && now - a.ts <= APPROVE_WINDOW_MS,
  );
  if (!approval) {
    return { block: true, reason: `F2: workflow ${scriptPath} requires approve_workflow_script (5-min window)` };
  }

  // 3. sha256 match (content unchanged since approval)
  if (approval.sha256 && scriptSha256 && approval.sha256 !== scriptSha256) {
    return { block: true, reason: 'F2: scriptPath sha256 mismatch — content modified after approval' };
  }

  // 4. dangerous pattern scan
  for (const { re, name } of DANGEROUS_PATTERNS) {
    if (re.test(scriptContent || '')) {
      return { block: true, reason: `F2: workflow script contains dangerous pattern — ${name}` };
    }
  }

  return { block: false };
}

export function loadApprovedWorkflowScripts(sessionId, now = Date.now()) {
  const path = join(homedir(), '.claude', 'runtime', `askuser-decisions-${sessionId || 'unknown'}.jsonl`);
  if (!existsSync(path)) return [];
  const out = [];
  try {
    const lines = readFileSync(path, 'utf-8').split(/\r?\n/);
    for (const line of lines) {
      if (!line.trim()) continue;
      let rec;
      try { rec = JSON.parse(line); } catch { continue; }
      if (rec && rec.type === 'approve_workflow_script' && typeof rec.scriptPath === 'string') {
        out.push({ scriptPath: rec.scriptPath, sha256: rec.sha256 || null, ts: typeof rec.ts === 'number' ? rec.ts : 0 });
      }
    }
  } catch { return []; }
  return out.filter((op) => now - op.ts <= APPROVE_WINDOW_MS);
}

export function sha256Hex(content) {
  return createHash('sha256').update(content || '', 'utf-8').digest('hex');
}

async function main() {
  let input = '';
  for await (const chunk of process.stdin) input += chunk;
  let payload;
  try { payload = JSON.parse(input); } catch { return; }

  const { tool_input, session_id } = payload || {};
  if (!tool_input) return;

  const scriptPath = tool_input.scriptPath;
  const inlineScript = typeof tool_input.script === 'string' ? tool_input.script : null;
  let scriptContent = '';
  let scriptSha256 = '';
  if (scriptPath && existsSync(scriptPath)) {
    try {
      scriptContent = readFileSync(scriptPath, 'utf-8');
      scriptSha256 = sha256Hex(scriptContent);
    } catch { /* content read errors fall through to decide() which will handle scriptContent='' */ }
  } else if (!scriptPath && inlineScript != null) {
    // 7.4: inline `script` — контент известен напрямую; sha256 для сверки с одобрением.
    scriptContent = inlineScript;
    scriptSha256 = sha256Hex(inlineScript);
  }

  const approved = loadApprovedWorkflowScripts(session_id, Date.now());
  const r = decide({ toolInput: tool_input, approvedWorkflowScripts: approved, scriptContent, scriptSha256, now: Date.now() });

  if (r.block) {
    process.stderr.write(`[workflow-gate] ${r.reason}\n`);
    process.exit(2);
  }
  process.exit(0);
}

if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('enforce-workflow-gate.mjs')) {
  main().catch((e) => { process.stderr.write(`[workflow-gate] internal error: ${e.message}\n`); process.exit(2); });
}
