{"version":3,"file":"replay.cjs","names":["isRecord","compile","splitShellCommand","explain"],"sources":["../src/replay.ts"],"sourcesContent":["/**\n * Replaying a policy over a recorded agent session, and proposing allow rules from one.\n *\n * Import: import { parseTranscript, replayCalls, suggestRules } from \"agent-perms/replay\";\n *\n * The transcript format is Claude Code's session log: JSON Lines, one entry per line. The parser relies on these fields only:\n *\n * - `message.content[]` blocks of `type: \"tool_use\"`, with `id`, `name` and `input`, for each call.\n * - `message.content[]` blocks of `type: \"tool_result\"`, with `tool_use_id`, `is_error` and `content` (a string, or a list of `{ type: \"text\", text }` blocks), for how each call ended.\n * - `cwd` and `gitBranch` on the entry holding the `tool_use` block, as the call's context. The log records no environment variables or git remote, so conditions on those stay unknown.\n *\n * The transcript does not record which rule decided a call, or whether a call that ran was allowed outright or approved when asked. It records only the outcome, read from the result: an error starting `Permission to use ` and containing ` has been denied` means a rule denied it, an error starting `The user doesn't want to proceed with this tool use.` means the user declined when asked, and any other result means it ran. A call with no result has no recorded outcome.\n */\n\nimport {\n  compile,\n  explain,\n  type CompiledPolicy,\n  type DecisionStep,\n  type EvaluationContext,\n  type PermissionDecision,\n  type PermissionPolicy,\n} from \"./evaluate.ts\";\nimport { isRecord } from \"./guards.ts\";\nimport type { Rule } from \"./schema.ts\";\nimport { splitShellCommand } from \"./shell.ts\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** How a recorded call ended: it ran, a rule denied it, or the user declined it when asked. */\nexport type RecordedOutcome = \"ran\" | \"denied\" | \"rejected\";\n\n/** One tool call read from a transcript. */\nexport interface ToolCall {\n  /** The tool name as the agent called it. */\n  tool: string;\n  /**\n   * What rule patterns match against: the command, file path or URL. Absent for a tool with no such input, which only a rule without a pattern can match.\n   */\n  subject?: string;\n  /** The working directory and branch recorded with the call, where the transcript has them. */\n  context: EvaluationContext;\n  /** How the call ended, when the transcript records its result. */\n  outcome?: RecordedOutcome;\n}\n\n/** A call judged against a policy. */\nexport interface ReplayedCall {\n  call: ToolCall;\n  decision: PermissionDecision;\n  steps: DecisionStep[];\n}\n\n/** The result of replaying a policy over recorded calls. */\nexport interface ReplayReport {\n  /** How many calls the policy would allow, ask about and deny. */\n  counts: Record<PermissionDecision, number>;\n  /** The calls whose recorded outcome the policy's decision could not have produced, in order. */\n  differing: ReplayedCall[];\n}\n\n/** A proposed allow rule and how many approved calls it covers. */\nexport interface Suggestion {\n  rule: Rule;\n  count: number;\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\n/**\n * The input field each tool's rule patterns match against. A tool not listed has no such field, so only a rule without a pattern applies to it.\n */\nconst SUBJECT_FIELDS: Readonly<Record<string, string>> = {\n  bash: \"command\",\n  read: \"file_path\",\n  write: \"file_path\",\n  edit: \"file_path\",\n  multiedit: \"file_path\",\n  notebookedit: \"notebook_path\",\n  webfetch: \"url\",\n};\n\nconst DENIED_PREFIX = \"Permission to use \";\nconst DENIED_MARKER = \" has been denied\";\nconst REJECTED_PREFIX = \"The user doesn't want to proceed with this tool use.\";\n\n/**\n * Read the tool calls from a JSON Lines session transcript, in the order they were made.\n *\n * @throws {SyntaxError} When a non-blank line is not JSON, naming the line.\n */\nexport function parseTranscript(text: string): ToolCall[] {\n  const calls = new Map<string, ToolCall>();\n  const outcomes = new Map<string, RecordedOutcome>();\n\n  text.split(\"\\n\").forEach((line, index) => {\n    if (line.trim() === \"\") return;\n    let entry: unknown;\n    try {\n      entry = JSON.parse(line);\n    } catch (e) {\n      const reason = e instanceof Error ? e.message : String(e);\n      throw new SyntaxError(\n        `transcript line ${String(index + 1)} is not JSON: ${reason}`,\n        { cause: e },\n      );\n    }\n    if (!isRecord(entry) || !isRecord(entry.message)) return;\n    const { content } = entry.message;\n    if (!Array.isArray(content)) return;\n\n    for (const block of content) {\n      if (!isRecord(block)) continue;\n      if (\n        block.type === \"tool_use\" &&\n        typeof block.id === \"string\" &&\n        typeof block.name === \"string\" &&\n        !calls.has(block.id)\n      ) {\n        calls.set(block.id, readCall(block.name, block.input, entry));\n      } else if (\n        block.type === \"tool_result\" &&\n        typeof block.tool_use_id === \"string\"\n      ) {\n        outcomes.set(block.tool_use_id, readOutcome(block));\n      }\n    }\n  });\n\n  return [...calls].map(([id, call]) => {\n    const outcome = outcomes.get(id);\n    return outcome === undefined ? call : { ...call, outcome };\n  });\n}\n\nfunction readCall(\n  tool: string,\n  input: unknown,\n  entry: Record<string, unknown>,\n): ToolCall {\n  const context: EvaluationContext = {};\n  if (typeof entry.cwd === \"string\") context.cwd = entry.cwd;\n  if (typeof entry.gitBranch === \"string\") context.branch = entry.gitBranch;\n\n  const field = SUBJECT_FIELDS[tool.toLowerCase()];\n  const subject =\n    field !== undefined && isRecord(input) ? input[field] : undefined;\n  return typeof subject === \"string\"\n    ? { tool, subject, context }\n    : { tool, context };\n}\n\nfunction readOutcome(block: Record<string, unknown>): RecordedOutcome {\n  if (block.is_error !== true) return \"ran\";\n  const text = resultText(block.content);\n  if (text.startsWith(DENIED_PREFIX) && text.includes(DENIED_MARKER)) {\n    return \"denied\";\n  }\n  if (text.startsWith(REJECTED_PREFIX)) return \"rejected\";\n  return \"ran\";\n}\n\n/** The text of a result's content: the string itself, or its text blocks joined. */\nfunction resultText(content: unknown): string {\n  if (typeof content === \"string\") return content;\n  if (!Array.isArray(content)) return \"\";\n  return content\n    .map((part) =>\n      isRecord(part) && typeof part.text === \"string\" ? part.text : \"\",\n    )\n    .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// Replay\n// ---------------------------------------------------------------------------\n\n/**\n * The decisions that can produce each recorded outcome. A call that ran was allowed or approved when asked; a denied call was denied; a call the user declined was asked about.\n */\nconst PRODUCED_BY: Readonly<\n  Record<RecordedOutcome, readonly PermissionDecision[]>\n> = {\n  ran: [\"allow\", \"ask\"],\n  denied: [\"deny\"],\n  rejected: [\"ask\"],\n};\n\n/** Judge a call against a compiled policy, as {@link explain} judges one. */\nfunction judge(policy: CompiledPolicy, call: ToolCall): ReplayedCall {\n  const { decision, steps } = policy.explain(\n    call.tool,\n    call.subject ?? \"\",\n    call.context,\n  );\n  return { call, decision, steps };\n}\n\n/**\n * Judge every call against a policy: count the decisions, and list the calls whose recorded outcome that decision could not have produced. A call with no recorded outcome is counted but never listed.\n */\nexport function replayCalls(\n  policy: PermissionPolicy,\n  calls: readonly ToolCall[],\n): ReplayReport {\n  const counts: Record<PermissionDecision, number> = {\n    allow: 0,\n    ask: 0,\n    deny: 0,\n  };\n  const differing: ReplayedCall[] = [];\n  const compiled = compile(policy);\n  for (const call of calls) {\n    const replayed = judge(compiled, call);\n    counts[replayed.decision]++;\n    if (\n      call.outcome !== undefined &&\n      !PRODUCED_BY[call.outcome].includes(replayed.decision)\n    ) {\n      differing.push(replayed);\n    }\n  }\n  return { counts, differing };\n}\n\n// ---------------------------------------------------------------------------\n// Suggest\n// ---------------------------------------------------------------------------\n\n/** One command or subject that can be allowed on its own. */\ninterface Unit {\n  tool: string;\n  subject: string | undefined;\n}\n\n/** A key identifying a unit, with tool names compared case-insensitively as rules compare them. */\nfunction unitKey(unit: Unit): string {\n  return JSON.stringify([unit.tool.toLowerCase(), unit.subject ?? null]);\n}\n\n/**\n * The units a call is made of: each command of a Bash line, or the call's subject. `undefined` for a shell line the splitter does not model, which no rule may allow command by command.\n */\nfunction unitsOf(call: ToolCall): Unit[] | undefined {\n  if (call.tool.toLowerCase() !== \"bash\" || call.subject === undefined) {\n    return [{ tool: call.tool, subject: call.subject }];\n  }\n  return splitShellCommand(call.subject)?.map((subject) => ({\n    tool: call.tool,\n    subject,\n  }));\n}\n\n/** Escape pattern syntax so a pattern matches `text` literally. */\nfunction literal(text: string): string {\n  return text.replace(/[\\\\()*]/g, \"\\\\$&\");\n}\n\n/** Whether `rule` on its own matches `unit`. */\nfunction covers(rule: Rule, unit: Unit): boolean {\n  return explain(\n    { defaultMode: \"readonly\", rules: [rule] },\n    unit.tool,\n    unit.subject ?? \"\",\n  ).steps.some((step) => step.rule === rule);\n}\n\n/** The words of a command, split on whitespace. */\nfunction words(command: string): string[] {\n  return command.trim().split(/\\s+/);\n}\n\n/**\n * The words every command starts with, stopping before the first word that differs or carries quoting or pattern syntax, where a word boundary in the text is not a word boundary to the shell.\n */\nfunction sharedWords(commands: readonly string[]): string[] {\n  const [first, ...rest] = commands.map(words);\n  if (first === undefined) return [];\n  const shared: string[] = [];\n  for (const [i, word] of first.entries()) {\n    if (/[\"'\\\\()*]/.test(word)) break;\n    if (!rest.every((other) => other[i] === word)) break;\n    shared.push(word);\n  }\n  return shared;\n}\n\ninterface Tally {\n  unit: Unit;\n  count: number;\n}\n\n/**\n * The narrowest allow rules for one group of approved units: an exact rule when they are all the same, otherwise a prefix rule on the words they share. A prefix that would also match an excluded unit is dropped for exact rules on the units approved at least `minCount` times.\n */\nfunction rulesForGroup(\n  tallies: readonly Tally[],\n  excluded: readonly Unit[],\n  minCount: number,\n): Suggestion[] {\n  const exact = (tally: Tally): Suggestion => ({\n    rule:\n      tally.unit.subject === undefined\n        ? { tool: tally.unit.tool, tier: \"allow\" }\n        : {\n            tool: tally.unit.tool,\n            pattern: literal(tally.unit.subject),\n            tier: \"allow\",\n          },\n    count: tally.count,\n  });\n  const exacts = (): Suggestion[] =>\n    tallies.filter((t) => t.count >= minCount).map(exact);\n\n  const [only] = tallies;\n  if (only !== undefined && tallies.length === 1) return exacts();\n\n  const shared = sharedWords(tallies.map((t) => t.unit.subject ?? \"\"));\n  const tool = only?.unit.tool;\n  if (tool === undefined || shared.length === 0) return exacts();\n\n  const prefix: Rule = {\n    tool,\n    pattern: `${literal(shared.join(\" \"))}:*`,\n    tier: \"allow\",\n  };\n  const count = tallies.reduce((sum, t) => sum + t.count, 0);\n  if (count < minCount || excluded.some((unit) => covers(prefix, unit))) {\n    return exacts();\n  }\n  return [{ rule: prefix, count }];\n}\n\n/**\n * Propose allow rules for what the transcript shows was approved at least `minCount` times.\n *\n * Only calls that ran count as approved. A Bash line is taken command by command, and a line the shell splitter does not model is skipped. A command is never proposed when it appeared in a line a rule of `policy` denies, or in a line the transcript records as denied or declined, and a command `policy` already allows, or a rule of it asks about, is left out. Bash commands sharing a first word become one prefix rule on the words they all start with, unless that prefix would also match such an excluded command; everything else gets an exact rule, or a rule without a pattern for a tool with no subject. Rules are ordered by how many calls they cover, then by first appearance.\n */\nexport function suggestRules(\n  calls: readonly ToolCall[],\n  policy: PermissionPolicy,\n  minCount: number,\n): Suggestion[] {\n  const approved = new Map<string, Tally>();\n  const excluded = new Map<string, Unit>();\n  const compiled = compile(policy);\n\n  for (const call of calls) {\n    const units = unitsOf(call);\n    const { steps } = judge(compiled, call);\n    const refused =\n      call.outcome === \"denied\" ||\n      call.outcome === \"rejected\" ||\n      steps.some((step) => step.reason === \"rule\" && step.decision === \"deny\");\n    if (refused) {\n      for (const unit of units ?? [{ tool: call.tool, subject: call.subject }])\n        excluded.set(unitKey(unit), unit);\n      continue;\n    }\n    if (call.outcome !== \"ran\" || units === undefined) continue;\n    for (const unit of units) {\n      // An allow rule changes nothing for a unit the policy already allows or a rule asks about.\n      const settled = steps.some(\n        (step) =>\n          step.command === (unit.subject ?? \"\") &&\n          (step.decision === \"allow\" || step.reason === \"rule\"),\n      );\n      if (settled) continue;\n      const key = unitKey(unit);\n      const tally = approved.get(key);\n      if (tally === undefined) approved.set(key, { unit, count: 1 });\n      else tally.count++;\n    }\n  }\n\n  const excludedUnits = [...excluded.values()];\n  const groups = new Map<string, Tally[]>();\n  for (const [key, tally] of approved) {\n    if (excluded.has(key)) continue;\n    const { tool, subject } = tally.unit;\n    const groupKey =\n      tool.toLowerCase() === \"bash\" && subject !== undefined\n        ? JSON.stringify([\"bash\", words(subject)[0]])\n        : key;\n    const group = groups.get(groupKey);\n    if (group === undefined) groups.set(groupKey, [tally]);\n    else group.push(tally);\n  }\n\n  const suggestions = [...groups.values()].flatMap((group) =>\n    rulesForGroup(group, excludedUnits, minCount),\n  );\n  // A stable sort keeps first appearance among equal counts.\n  return suggestions.sort((a, b) => b.count - a.count);\n}\n"],"mappings":"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"}