{"version":3,"file":"loader.mjs","names":[],"sources":["../src/loader.ts"],"sourcesContent":["/**\n * Permission policy loader — walk-up discovery and merge.\n *\n * Walks up from `cwd`, collecting canonical and native agent config files.\n * Merge semantics:\n *   - with/without: unioned from all discovered canonical files\n *   - up: outermost canonical file wins (team controls walk-up depth)\n *   - defaultMode: last-defined wins (innermost/cwd overrides)\n *   - rules: collected from all sources, deduplicated deny-first\n *\n * Load order at each directory (innermost processed last):\n *   1. `.agents/permissions.json` (team, committed)\n *   2. `.agents/permissions.local.json` (personal, gitignored)\n *   3. Native agent configs filtered by with/without\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\nimport { type AgentPermissionPolicy, type Rule } from \"./schema.ts\";\nimport {\n  AGENT_FILES,\n  type AgentFileDef,\n  parseJson,\n  validatePolicy,\n  decodeNative,\n} from \"./agent-files.ts\";\nimport {\n  collectRules,\n  mapMode,\n  deduplicateRules,\n  type PermissionPolicy,\n} from \"./evaluate.ts\";\nimport { type AgentId, CODECS } from \"./compat/codecs.ts\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PolicyLoadOptions {\n  /** Starting directory for walk-up discovery. */\n  cwd: string;\n}\n\ninterface DiscoveredFile {\n  /** Agent identifier (e.g. \"canonical\", \"claude-code\"). */\n  agent: AgentId | \"canonical\";\n  /** Absolute path to the config file. */\n  path: string;\n  /** Whether this is a local override (read-only in merge). */\n  local: boolean;\n}\n\ninterface DecodedLayer {\n  file: DiscoveredFile;\n  policy: AgentPermissionPolicy;\n}\n\n// ---------------------------------------------------------------------------\n// Walk-up discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve effective `up` and agent filter from all discovered canonical files.\n *\n * - `up`: outermost canonical file's value wins (team controls depth).\n *   Falls back to `\"all\"` if no canonical file specifies it.\n * - `with`/`without`: unioned across all canonical files.\n */\nfunction resolveDiscoveryConfig(canonicalLayers: DecodedLayer[]): {\n  up: number;\n  agentFilter: Set<string> | undefined;\n} {\n  let up = Infinity;\n  const withAgents = new Set<AgentId>();\n  const withoutAgents = new Set<AgentId>();\n\n  // canonicalLayers are outermost-first after reverse.\n  for (const layer of canonicalLayers) {\n    const { with: w, without: wo, up: u } = layer.policy;\n    if (u !== undefined) {\n      const resolved = u === \"all\" ? Infinity : u;\n      // Outermost wins — take the first up value we see (outermost-first)\n      if (up === Infinity) {\n        up = resolved;\n      }\n    }\n    if (w !== undefined) {\n      for (const a of w) withAgents.add(a);\n    }\n    if (wo !== undefined) {\n      for (const a of wo) withoutAgents.add(a);\n    }\n  }\n\n  // Build agent filter\n  let agentFilter: Set<string> | undefined;\n  if (withAgents.size > 0 && withoutAgents.size > 0) {\n    // Invalid — a single file can't have both, and cross-file we\n    // treat the combination as: with takes precedence (most restrictive)\n    const allAgents = [...Object.keys(CODECS), \"canonical\"];\n    agentFilter = new Set([...withAgents, \"canonical\"]);\n    // Also include any agent NOT in withoutAgents\n    for (const a of allAgents) {\n      if (!withoutAgents.has(a as AgentId)) {\n        agentFilter.add(a);\n      }\n    }\n  } else if (withAgents.size > 0) {\n    agentFilter = new Set([...withAgents, \"canonical\"]);\n  } else if (withoutAgents.size > 0) {\n    const allAgents = [...Object.keys(CODECS), \"canonical\"];\n    const excluded = new Set(withoutAgents);\n    agentFilter = new Set(allAgents.filter((a) => !excluded.has(a as AgentId)));\n  }\n\n  // No with/without = canonical only (safe default)\n  // withAgents and withoutAgents are both empty, agentFilter stays undefined\n  // but discoverFiles still needs to know not to read native configs.\n  // We achieve this by passing a filter that only includes \"canonical\".\n  if (withAgents.size === 0 && withoutAgents.size === 0) {\n    agentFilter = new Set([\"canonical\"]);\n  }\n\n  return { up, agentFilter };\n}\n\n/**\n * Walk up from cwd, collecting agent config files.\n * Returns files ordered outermost-first (outermost layers first,\n * innermost/cwd layers last) so that last-defined-wins merge\n * gives cwd the highest priority.\n *\n * Within each directory, committed files come before local files,\n * so local overrides committed at the same level.\n */\nfunction discoverFiles(\n  cwd: string,\n  up: number,\n  agentFilter: Set<string> | undefined,\n): DiscoveredFile[] {\n  // Collect per-directory buckets, cwd-first\n  const dirBuckets: DiscoveredFile[][] = [];\n  let current = resolve(cwd);\n  let remaining = up === Infinity ? Number.MAX_SAFE_INTEGER : up + 1;\n\n  while (remaining > 0) {\n    const bucket: DiscoveredFile[] = [];\n    for (const [agent, def] of Object.entries(AGENT_FILES) as [\n      AgentId | \"canonical\",\n      AgentFileDef,\n    ][]) {\n      // Apply agent filter\n      if (agentFilter && !agentFilter.has(agent)) continue;\n\n      // Skip agents without extract (codex TOML, crush no file)\n      if (agent !== \"canonical\" && def.extract === undefined) continue;\n\n      const main = join(current, def.name);\n      if (existsSync(main)) {\n        bucket.push({ agent, path: main, local: false });\n      }\n\n      if (def.localName) {\n        const local = join(current, def.localName);\n        if (existsSync(local)) {\n          bucket.push({ agent, path: local, local: true });\n        }\n      }\n    }\n    dirBuckets.push(bucket);\n\n    remaining--;\n    const parent = dirname(current);\n    if (parent === current) break; // reached root\n    current = parent;\n  }\n\n  // Reverse directory order so outermost is first.\n  // Within each directory, committed comes before local.\n  dirBuckets.reverse();\n  return dirBuckets.flat();\n}\n\n// ---------------------------------------------------------------------------\n// Reading and decoding\n// ---------------------------------------------------------------------------\n\nasync function readFileContent(filePath: string): Promise<string | undefined> {\n  try {\n    return await readFile(filePath, \"utf-8\");\n  } catch {\n    return undefined;\n  }\n}\n\nfunction decodeFile(\n  file: DiscoveredFile,\n  raw: unknown,\n): AgentPermissionPolicy | undefined {\n  if (file.agent === \"canonical\") {\n    const result = validatePolicy(raw);\n    return result.ok ? result.value : undefined;\n  }\n\n  const result = decodeNative(file.agent, raw);\n  return result.ok ? result.value : undefined;\n}\n\nasync function readAndDecode(\n  file: DiscoveredFile,\n): Promise<DecodedLayer | undefined> {\n  const content = await readFileContent(file.path);\n  if (content === undefined) return undefined;\n\n  const parsed = parseJson(content, file.path);\n  if (!parsed.ok) return undefined;\n\n  const policy = decodeFile(file, parsed.value);\n  if (policy === undefined) return undefined;\n\n  return { file, policy };\n}\n\n// ---------------------------------------------------------------------------\n// Merging\n// ---------------------------------------------------------------------------\n\nfunction mergeLayers(layers: DecodedLayer[]): PermissionPolicy {\n  if (layers.length === 0) {\n    return { defaultMode: \"standard\" };\n  }\n\n  let mode: PermissionPolicy[\"defaultMode\"] = \"standard\";\n  const allRules: Rule[] = [];\n\n  // Layers are outermost-first. Last-defined wins for defaultMode.\n  for (const layer of layers) {\n    if (layer.policy.defaultMode) {\n      mode = mapMode(layer.policy.defaultMode);\n    }\n    allRules.push(...collectRules(layer.policy));\n  }\n\n  const rules = deduplicateRules(allRules);\n\n  return {\n    defaultMode: mode,\n    ...(rules.length > 0 ? { rules } : {}),\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Load and merge permission policy from all sources.\n *\n * Two-pass process:\n *   1. Discover canonical files, resolve `up`/`with`/`without` from them.\n *   2. Re-discover all files (canonical + native) using resolved config,\n *      decode, and merge.\n */\nexport async function loadPolicy(\n  options: PolicyLoadOptions,\n): Promise<PermissionPolicy> {\n  const { cwd } = options;\n\n  // Pass 1: discover canonical files with max walk-up to find all of them\n  const canonicalFiles = discoverFiles(cwd, Infinity, new Set([\"canonical\"]));\n  const canonicalLayers: DecodedLayer[] = [];\n  for (const file of canonicalFiles) {\n    const layer = await readAndDecode(file);\n    if (layer) canonicalLayers.push(layer);\n  }\n\n  if (canonicalLayers.length === 0) {\n    return { defaultMode: \"standard\" };\n  }\n\n  // Resolve discovery config from canonical files\n  const { up, agentFilter } = resolveDiscoveryConfig(canonicalLayers);\n\n  // Pass 2: discover all files using resolved config\n  const allFiles = discoverFiles(cwd, up, agentFilter);\n\n  const allLayers: DecodedLayer[] = [];\n  for (const file of allFiles) {\n    const layer = await readAndDecode(file);\n    if (layer) allLayers.push(layer);\n  }\n\n  return mergeLayers(allLayers);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsEA,SAAS,uBAAuB,iBAG9B;CACA,IAAI,KAAK;CACT,MAAM,6BAAa,IAAI,KAAc;CACrC,MAAM,gCAAgB,IAAI,KAAc;CAGxC,KAAK,MAAM,SAAS,iBAAiB;EACnC,MAAM,EAAE,MAAM,GAAG,SAAS,IAAI,IAAI,MAAM,MAAM;EAC9C,IAAI,MAAM,QAAW;GACnB,MAAM,WAAW,MAAM,QAAQ,WAAW;GAE1C,IAAI,OAAO,UACT,KAAK;;EAGT,IAAI,MAAM,QACR,KAAK,MAAM,KAAK,GAAG,WAAW,IAAI,EAAE;EAEtC,IAAI,OAAO,QACT,KAAK,MAAM,KAAK,IAAI,cAAc,IAAI,EAAE;;CAK5C,IAAI;CACJ,IAAI,WAAW,OAAO,KAAK,cAAc,OAAO,GAAG;EAGjD,MAAM,YAAY,CAAC,GAAG,OAAO,KAAK,OAAO,EAAE,YAAY;EACvD,cAAc,IAAI,IAAI,CAAC,GAAG,YAAY,YAAY,CAAC;EAEnD,KAAK,MAAM,KAAK,WACd,IAAI,CAAC,cAAc,IAAI,EAAa,EAClC,YAAY,IAAI,EAAE;QAGjB,IAAI,WAAW,OAAO,GAC3B,cAAc,IAAI,IAAI,CAAC,GAAG,YAAY,YAAY,CAAC;MAC9C,IAAI,cAAc,OAAO,GAAG;EACjC,MAAM,YAAY,CAAC,GAAG,OAAO,KAAK,OAAO,EAAE,YAAY;EACvD,MAAM,WAAW,IAAI,IAAI,cAAc;EACvC,cAAc,IAAI,IAAI,UAAU,QAAQ,MAAM,CAAC,SAAS,IAAI,EAAa,CAAC,CAAC;;CAO7E,IAAI,WAAW,SAAS,KAAK,cAAc,SAAS,GAClD,cAAc,IAAI,IAAI,CAAC,YAAY,CAAC;CAGtC,OAAO;EAAE;EAAI;EAAa;;;;;;;;;;;AAY5B,SAAS,cACP,KACA,IACA,aACkB;CAElB,MAAM,aAAiC,EAAE;CACzC,IAAI,UAAU,QAAQ,IAAI;CAC1B,IAAI,YAAY,OAAO,WAAW,OAAO,mBAAmB,KAAK;CAEjE,OAAO,YAAY,GAAG;EACpB,MAAM,SAA2B,EAAE;EACnC,KAAK,MAAM,CAAC,OAAO,QAAQ,OAAO,QAAQ,YAAY,EAGjD;GAEH,IAAI,eAAe,CAAC,YAAY,IAAI,MAAM,EAAE;GAG5C,IAAI,UAAU,eAAe,IAAI,YAAY,QAAW;GAExD,MAAM,OAAO,KAAK,SAAS,IAAI,KAAK;GACpC,IAAI,WAAW,KAAK,EAClB,OAAO,KAAK;IAAE;IAAO,MAAM;IAAM,OAAO;IAAO,CAAC;GAGlD,IAAI,IAAI,WAAW;IACjB,MAAM,QAAQ,KAAK,SAAS,IAAI,UAAU;IAC1C,IAAI,WAAW,MAAM,EACnB,OAAO,KAAK;KAAE;KAAO,MAAM;KAAO,OAAO;KAAM,CAAC;;;EAItD,WAAW,KAAK,OAAO;EAEvB;EACA,MAAM,SAAS,QAAQ,QAAQ;EAC/B,IAAI,WAAW,SAAS;EACxB,UAAU;;CAKZ,WAAW,SAAS;CACpB,OAAO,WAAW,MAAM;;AAO1B,eAAe,gBAAgB,UAA+C;CAC5E,IAAI;EACF,OAAO,MAAM,SAAS,UAAU,QAAQ;SAClC;EACN;;;AAIJ,SAAS,WACP,MACA,KACmC;CACnC,IAAI,KAAK,UAAU,aAAa;EAC9B,MAAM,SAAS,eAAe,IAAI;EAClC,OAAO,OAAO,KAAK,OAAO,QAAQ;;CAGpC,MAAM,SAAS,aAAa,KAAK,OAAO,IAAI;CAC5C,OAAO,OAAO,KAAK,OAAO,QAAQ;;AAGpC,eAAe,cACb,MACmC;CACnC,MAAM,UAAU,MAAM,gBAAgB,KAAK,KAAK;CAChD,IAAI,YAAY,QAAW,OAAO;CAElC,MAAM,SAAS,UAAU,SAAS,KAAK,KAAK;CAC5C,IAAI,CAAC,OAAO,IAAI,OAAO;CAEvB,MAAM,SAAS,WAAW,MAAM,OAAO,MAAM;CAC7C,IAAI,WAAW,QAAW,OAAO;CAEjC,OAAO;EAAE;EAAM;EAAQ;;AAOzB,SAAS,YAAY,QAA0C;CAC7D,IAAI,OAAO,WAAW,GACpB,OAAO,EAAE,aAAa,YAAY;CAGpC,IAAI,OAAwC;CAC5C,MAAM,WAAmB,EAAE;CAG3B,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,OAAO,aACf,OAAO,QAAQ,MAAM,OAAO,YAAY;EAE1C,SAAS,KAAK,GAAG,aAAa,MAAM,OAAO,CAAC;;CAG9C,MAAM,QAAQ,iBAAiB,SAAS;CAExC,OAAO;EACL,aAAa;EACb,GAAI,MAAM,SAAS,IAAI,EAAE,OAAO,GAAG,EAAE;EACtC;;;;;;;;;;AAeH,eAAsB,WACpB,SAC2B;CAC3B,MAAM,EAAE,QAAQ;CAGhB,MAAM,iBAAiB,cAAc,KAAK,UAAU,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC;CAC3E,MAAM,kBAAkC,EAAE;CAC1C,KAAK,MAAM,QAAQ,gBAAgB;EACjC,MAAM,QAAQ,MAAM,cAAc,KAAK;EACvC,IAAI,OAAO,gBAAgB,KAAK,MAAM;;CAGxC,IAAI,gBAAgB,WAAW,GAC7B,OAAO,EAAE,aAAa,YAAY;CAIpC,MAAM,EAAE,IAAI,gBAAgB,uBAAuB,gBAAgB;CAGnE,MAAM,WAAW,cAAc,KAAK,IAAI,YAAY;CAEpD,MAAM,YAA4B,EAAE;CACpC,KAAK,MAAM,QAAQ,UAAU;EAC3B,MAAM,QAAQ,MAAM,cAAc,KAAK;EACvC,IAAI,OAAO,UAAU,KAAK,MAAM;;CAGlC,OAAO,YAAY,UAAU"}