UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

624 lines (623 loc) • 30.3 kB
import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js"; import { isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "./safety.js"; import { PROBE_REASON_MAX } from "../../core/untrusted-text.js"; import { validatePath } from "../../core/effective-path-target.js"; import { judgePathRows } from "../../core/path-guard-rows.js"; export const READ_DENY_HOME_WORD = "~"; export function readDenyPatternRun(pattern) { const segments = pattern.split("/").filter(Boolean); return segments[0] === READ_DENY_HOME_WORD ? { homeAnchored: true, run: segments.slice(1).join("/") } : { homeAnchored: false, run: pattern }; } export function readDenyRunHead(run) { const segments = run.split("/").filter(Boolean); const wild = segments.findIndex((seg) => seg.includes("*")); return wild === -1 ? segments.length : wild; } export const DENY_FACES = Object.freeze(["read", "write"]); const REACHED = { reach: "reached" }; const NOT_REACHED = { reach: "not_reached" }; const reachOf = (row, spelled) => row.reach?.(spelled) ?? (row.reaches(spelled) ? REACHED : NOT_REACHED); export function judgeRuleRows(rows, views, direction) { const hit = judgePathRows(rows.map(ruleGuard), views, direction); return hit === undefined ? undefined : { ...hit, row: hit.row.rule }; } function ruleGuard(rule) { return { rule, reach: (view) => reachOf(rule, view.path) }; } export function ruleHeadParts(rows) { const out = new Set(); for (const row of rows) if (row.head !== undefined) for (const part of partsOf(row.head)) out.add(part); return [...out]; } function partsOf(head) { const family = pathFamilyOf({ root: head }); const id = pathIdentityOf(head, family, head); if (!id.ok) return [head]; const unc = id.identity.startsWith("//"); const segments = id.identity.split("/").filter((seg) => seg !== ""); const parts = []; for (let k = unc ? 2 : 1; k <= segments.length; k++) parts.push(spellingOfIdentity(`${unc ? "//" : "/"}${segments.slice(0, k).join("/")}`, family)); return parts.length > 0 ? parts : [head]; } export function rowsWhereTheyLead(rows, leads) { const out = []; for (const row of rows) { out.push(row); if (row.head === undefined || leads.size === 0) continue; let parentLed; let parentPart; for (const part of partsOf(row.head)) { const led = leads.get(part); const extendsParent = led !== undefined && parentLed !== undefined && parentPart !== undefined && sameLocation(led, `${parentLed.replace(/[\\/]+$/, "")}/${part.slice(parentPart.replace(/[\\/]+$/, "").length).replace(/^[\\/]+/, "")}`); const twin = extendsParent ? undefined : rowLedTo(row, part, led); if (twin !== undefined) out.push(twin); parentLed = led; parentPart = part; } } return out; } export function locationLeadsTo(path, leads) { const parts = partsOf(path); return leads.get(parts[parts.length - 1]); } export async function withRuleRowsWhereTheyLead(rows, leadsTo) { const parts = ruleHeadParts(rows); const led = await Promise.all(parts.map((part) => leadsTo(part).catch(() => undefined))); return rowsWhereTheyLead(rows, new Map(parts.map((part, i) => [part, led[i]]))); } function sameLocation(a, b) { const family = pathFamilyOf({ root: a }); const x = pathIdentityOf(a, family, a); const y = pathIdentityOf(b, family, b); return x.ok && y.ok && x.identity === y.identity; } function rowLedTo(row, head, at) { if (at === undefined) return undefined; const family = pathFamilyOf({ root: head }); const from = pathIdentityOf(at, family, head); const to = pathIdentityOf(head, family, head); if (!from.ok || !to.ok || from.identity === to.identity) return undefined; const base = from.identity; const under = base.endsWith("/") ? base : `${base}/`; const onto = to.identity.endsWith("/") ? to.identity.slice(0, -1) : to.identity; const mapped = (absolutePath) => { const id = pathIdentityOf(absolutePath, family, head); if (!id.ok) return undefined; if (id.identity === base) return to.identity; return id.identity.startsWith(under) ? `${onto}/${id.identity.slice(under.length)}` : undefined; }; const { head: _head, ...rest } = row; const reach = row.reach; return { ...rest, reaches: (absolutePath) => { const m = mapped(absolutePath); return m !== undefined && row.reaches(m); }, ...(reach !== undefined ? { reach: (absolutePath) => { const m = mapped(absolutePath); return m === undefined ? NOT_REACHED : reach(m); }, } : {}), }; } export function ruleGuardRow(rule) { return { rule, name: rule.rule, grammar: { kind: "rule", rule: rule.rule }, polarity: "deny", faces: rule.faces, placement: "anywhere", fold: "exact", exemptible: false, origin: "rule", reach: (view) => reachOf(rule, view.path) }; } export function segmentGuardRow(pat, columns) { const placement = pat.anchors === undefined ? "anywhere" : pat.rootedOnly !== true ? "home" : pat.anchors.length === 1 && pat.anchors[0].length === 0 ? "rooted" : { below: `/${(pat.anchors[0] ?? []).join("/")}` }; return { name: pat.raw, grammar: { kind: "segment-glob", run: pat.raw }, placement, ...columns, reach: (view) => (segmentRowMeets(pat, segmentReadingsOf(view.path, view.aliasResolved === true)) ? REACHED : NOT_REACHED), }; } function denyTableHit(rows, views) { const hit = judgePathRows(rows, views, "tighten"); if (hit === undefined) return null; const view = hit.view ?? views.views[0]?.path; return { pattern: hit.row.name, ...(view !== undefined ? { matchedView: view } : {}) }; } function denyTargetViews(canonicalKey, lexicalView, opts) { const views = [{ path: canonicalKey, kind: "canonical", ...(opts?.aliasResolved === true ? { aliasResolved: true } : {}) }]; if (lexicalView !== undefined) views.push({ path: lexicalView, kind: "lexical" }); for (const hop of opts?.hops ?? []) views.push({ path: hop, kind: "hop" }); return { views }; } export function compileWriteDeny(rules = []) { const ruleRows = rules.filter((r) => r.faces.includes("write")); const rows = ruleRows.map(ruleGuardRow); const matchPath = (path) => { const hit = denyTableHit(rows, { views: [{ path, kind: "lexical" }] }); return hit === null ? null : { pattern: hit.pattern }; }; return { face: "write", ruleRows, matchPath, matchTarget: (canonicalKey, lexicalView, opts) => denyTableHit(rows, denyTargetViews(canonicalKey, lexicalView, opts)) }; } export function seatForCall(seat, toolCallId) { const forCall = seat?.forCall; return typeof forCall === "function" && toolCallId !== undefined ? forCall.call(seat, toolCallId) : seat; } export function seatReady(seat, signal) { const ready = seat?.ready; return typeof ready === "function" ? Promise.resolve(ready.call(seat, signal)) : Promise.resolve(); } export function readDenySeat(matcher) { const entries = [...(matcher?.entries ?? []).map((e) => e.pattern), ...(matcher?.ruleRows ?? []).map((r) => r.rule)]; if (matcher === undefined || entries.length === 0) return { denyEntries: entries }; return { denyMatch: (p) => matcher.matchPath(p)?.pattern ?? null, denyUnder: (p) => { const hit = matcher.rowUnderRoot?.(p) ?? null; return hit === null ? null : hit.unplaced === true ? { unplaced: hit.pattern } : hit.pattern; }, denyAtOrBelow: (p) => (matcher.rowAtOrBelow !== undefined ? (matcher.rowAtOrBelow(p)?.pattern ?? null) : entries[0]), denyEntries: entries, }; } export function sensitivePathDenyList(pattern, face = "read") { const rows = typeof pattern === "string" ? [pattern] : pattern; return `the ${face === "write" ? "write" : "sensitive-path read"} deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`; } export const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots."; export const WRITE_DENY_SCOPE = "A write deny row holds regardless of the containment roots and of the permission mode."; export function readDenySentence(finding) { const write = finding.face === "write"; const canonicalNote = finding.canonical !== undefined && finding.canonical !== finding.target ? `(canonical target: "${fitMiddle(finding.canonical, READ_DENY_SENTENCE_PART_MAX)}") ` : ""; const lexicalHit = finding.matchedView !== undefined && finding.matchedView !== (finding.canonical ?? finding.target); const judged = lexicalHit ? `the requested path's spelling ("${fitMiddle(finding.matchedView, READ_DENY_SENTENCE_PART_MAX)}") matches` : "the target matches"; return `${finding.action ?? (write ? "writing" : "reading")} "${fitMiddle(finding.target, READ_DENY_SENTENCE_PART_MAX)}" ${canonicalNote}is refused: ${judged} ${sensitivePathDenyList(fitMiddle(finding.pattern, READ_DENY_SENTENCE_PART_MAX), write ? "write" : "read")}. ${write ? WRITE_DENY_SCOPE : SENSITIVE_PATH_DENY_SCOPE}`; } export const READ_DENY_SENTENCE_PART_MAX = PROBE_REASON_MAX; export function fitMiddle(s, budget) { return s.length <= budget ? s : elideMiddle(s, budget); } export function elideMiddle(s, budget) { if (budget < 12) return sliceHeadSafe(s, Math.max(0, budget)); const keep = budget - 1; const head = Math.ceil(keep / 2); return `${sliceHeadSafe(s, head)}\u2026${sliceTailSafe(s, keep - head)}`; } export const READ_DENY_BUILTIN_TIERS = Object.freeze(["credentials", "shell-history", "browser", "wallet", "agent-config"]); export const READ_FACE_BUILTIN_DENY_TABLE = [ { pattern: ".ssh", tier: "credentials" }, { pattern: "id_rsa*", tier: "credentials" }, { pattern: "id_ed25519*", tier: "credentials" }, { pattern: "id_ecdsa*", tier: "credentials" }, { pattern: ".gnupg", tier: "credentials" }, { pattern: ".aws", tier: "credentials" }, { pattern: ".config/gcloud", tier: "credentials" }, { pattern: ".azure", tier: "credentials" }, { pattern: ".kube", tier: "credentials" }, { pattern: ".netrc", tier: "credentials" }, { pattern: "_netrc", tier: "credentials" }, { pattern: ".git-credentials", tier: "credentials" }, { pattern: ".docker/config.json", tier: "credentials" }, { pattern: ".config/gh", tier: "credentials" }, { pattern: "~/.npmrc", tier: "credentials" }, { pattern: "~/.pypirc", tier: "credentials" }, { pattern: ".local/share/keyrings", tier: "credentials" }, { pattern: "Library/Keychains", tier: "credentials" }, { pattern: ".credentials.json", tier: "credentials" }, { pattern: "credentials.env", tier: "credentials" }, { pattern: ".codex/auth.json", tier: "credentials" }, { pattern: ".config/github-copilot", tier: "credentials" }, { pattern: ".gemini/oauth_creds.json", tier: "credentials" }, { pattern: ".bash_history", tier: "shell-history" }, { pattern: ".zsh_history", tier: "shell-history" }, { pattern: "Library/Application Support/Google/Chrome", tier: "browser" }, { pattern: "Library/Application Support/Firefox", tier: "browser" }, { pattern: "Library/Safari", tier: "browser" }, { pattern: ".config/google-chrome", tier: "browser" }, { pattern: ".config/chromium", tier: "browser" }, { pattern: ".mozilla/firefox", tier: "browser" }, { pattern: "AppData/Local/Google/Chrome/User Data", tier: "browser" }, { pattern: "AppData/Local/Microsoft/Edge/User Data", tier: "browser" }, { pattern: "AppData/Roaming/Mozilla/Firefox", tier: "browser" }, { pattern: ".bitcoin", tier: "wallet" }, { pattern: ".ethereum", tier: "wallet" }, { pattern: ".electrum", tier: "wallet" }, { pattern: "Library/Application Support/Exodus", tier: "wallet" }, { pattern: "Library/Application Support/Ledger Live", tier: "wallet" }, { pattern: "wallet.dat", tier: "wallet" }, { pattern: "~/.sema/settings.json", tier: "agent-config" }, { pattern: "~/.sema/settings.local.json", tier: "agent-config" }, { pattern: "~/.sema.*", tier: "agent-config" }, { pattern: "~/.claude/settings.json", tier: "agent-config" }, { pattern: "~/.claude/settings.local.json", tier: "agent-config" }, { pattern: "~/.claude.*", tier: "agent-config" }, { pattern: "~/.mcp.json", tier: "agent-config" }, { pattern: "~/.ai-agent/.env", tier: "agent-config" }, { pattern: "~/.sema/engine-data/.env", tier: "agent-config" }, { pattern: "~/.sema/config-lkg-*.json", tier: "agent-config" }, { pattern: "~/.codex/config.toml", tier: "agent-config" }, { pattern: "~/.cursor/mcp.json", tier: "agent-config" }, { pattern: "~/.continue/config.json", tier: "agent-config" }, { pattern: "~/.continue/config.yaml", tier: "agent-config" }, { pattern: "~/.aider.conf.yml", tier: "agent-config" }, { pattern: "~/.gemini/settings.json", tier: "agent-config" }, ]; export const READ_DENY_DEFAULT_TIERS = ["credentials", "agent-config"]; export function resolveReadDenyBuiltins(config) { let activeTiers; if (config?.tiers === undefined) { activeTiers = new Set(READ_DENY_DEFAULT_TIERS); } else { if (!Array.isArray(config.tiers)) { throw new Error(`readDenyBuiltinTiers: expected an array of tier names, got ${JSON.stringify(config.tiers)}.`); } for (const t of config.tiers) { if (typeof t !== "string" || !READ_DENY_BUILTIN_TIERS.includes(t)) { throw new Error(`readDenyBuiltinTiers: unknown tier ${JSON.stringify(t)} — known tiers: ${READ_DENY_BUILTIN_TIERS.join(", ")}.`); } } activeTiers = new Set(config.tiers); } let excluded; if (config?.exclude === undefined) { excluded = new Set(); } else { if (!Array.isArray(config.exclude)) { throw new Error(`readDenyBuiltinExclude: expected an array of built-in row names (canonical pattern texts), got ${JSON.stringify(config.exclude)}.`); } for (const name of config.exclude) { if (typeof name !== "string" || !READ_FACE_BUILTIN_DENY_TABLE.some((r) => r.pattern === name)) { const anchored = typeof name === "string" ? READ_FACE_BUILTIN_DENY_TABLE.find((r) => r.pattern === `${READ_DENY_HOME_WORD}/${name}`) : undefined; throw new Error(`readDenyBuiltinExclude: ${JSON.stringify(name)} names no built-in row — ${anchored !== undefined ? `the row is anchored at the home directory and is named ${JSON.stringify(anchored.pattern)}; ` : ""}row names are the canonical pattern texts of READ_FACE_BUILTIN_DENY_TABLE (e.g. ".ssh", ".config/gcloud", "~/.sema/settings.json").`); } } excluded = new Set(config.exclude); } return READ_FACE_BUILTIN_DENY_TABLE.filter((r) => activeTiers.has(r.tier) && !excluded.has(r.pattern)); } export function readDenyBuiltinConfigOf(seats) { resolveReadDenyBuiltins(seats); return { ...(seats.tiers !== undefined ? { tiers: [...seats.tiers] } : {}), ...(seats.exclude !== undefined ? { exclude: [...seats.exclude] } : {}), }; } export function activeReadDenyBuiltinTiers(config) { const rows = resolveReadDenyBuiltins(config); return READ_DENY_BUILTIN_TIERS.filter((t) => rows.some((r) => r.tier === t)); } export const READ_FACE_DEFAULT_DENY_ENTRIES = resolveReadDenyBuiltins().map((r) => r.pattern); function asciiLower(s) { let out = ""; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); out += c >= 0x41 && c <= 0x5a ? String.fromCharCode(c + 32) : s[i]; } return out; } export function compileSegmentPattern(raw, fold) { const source = fold === "ascii" ? asciiLower(raw) : raw; const parts = source.split("/").filter(Boolean); if (parts.length === 0) return null; const flags = fold === "unicode" ? "i" : ""; const segments = parts.map((segment) => { const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*"); return new RegExp(`^${escaped}$`, flags); }); return { raw, segments, foldAscii: fold === "ascii" }; } export function matchSegmentPatterns(path, compiled, opts) { const readings = segmentReadingsOf(path, opts?.aliasResolved === true); for (const pat of compiled) if (segmentRowMeets(pat, readings)) return pat.raw; return null; } const SEGMENT_READINGS = new Map(); function segmentReadingsOf(path, aliasResolved) { const key = `${aliasResolved ? "1" : "0"}${path}`; const known = SEGMENT_READINGS.get(key); if (known !== undefined) return known; const views = segmentViewsOf(path, aliasResolved ? { aliasResolved } : undefined); const readings = { views, lowered: views.map((v) => v.map(asciiLower)), rooted: isAbsoluteForFamily("own", path) }; if (SEGMENT_READINGS.size >= 512) SEGMENT_READINGS.clear(); SEGMENT_READINGS.set(key, readings); return readings; } function segmentRowMeets(pat, readings) { const n = pat.segments.length; const views = pat.foldAscii ? readings.lowered : readings.views; for (const segs of views) { const runAt = (i) => i + n <= segs.length && pat.segments.every((re, j) => re.test(segs[i + j] ?? "")); if (pat.anchors !== undefined && readings.rooted) { for (const anchor of pat.anchors) if (anchor.every((a, j) => segs[j] === a) && runAt(anchor.length)) return true; continue; } if (pat.rootedOnly === true) continue; for (let i = 0; i + n <= segs.length; i++) if (runAt(i)) return true; } return false; } function segmentViewsOf(path, opts) { const literalSegs = path.split(/[\\/]/).filter(Boolean); const win32Segs = []; for (const raw of literalSegs) { const dot = /^\.{1,2}$/.test(raw) ? raw : raw.replace(/ +$/, ""); if (dot === ".") continue; if (dot === "..") { const last = win32Segs[win32Segs.length - 1]; if (win32Segs.length > 0 && last !== "..") win32Segs.pop(); else win32Segs.push(".."); continue; } const stripped = raw.replace(/[. ]+$/, ""); win32Segs.push(stripped.length > 0 ? stripped : raw); } const sameView = opts?.aliasResolved === true || (win32Segs.length === literalSegs.length && win32Segs.every((s, i) => s === literalSegs[i])); return sameView ? [literalSegs] : [literalSegs, win32Segs]; } function rowMeetsPath(pat, segs, rooted) { const n = pat.segments.length; if (pat.anchors !== undefined && rooted) { return pat.anchors.some((a) => { const k = Math.min(segs.length, a.length + n); for (let i = 0; i < k; i++) if (i < a.length ? segs[i] !== a[i] : !pat.segments[i - a.length].test(segs[i])) return false; return true; }); } if (pat.rootedOnly === true) return false; for (let i = 0; i < segs.length; i++) { const m = Math.min(n, segs.length - i); if (pat.segments.slice(0, m).every((re, j) => re.test(segs[i + j]))) return true; } return false; } function isAnySegment(re) { return /^\^(\[\^\/\]\*)+\$$/.test(re.source); } function rowNamedByRoot(pat, segs, rooted) { if (pat.anchors !== undefined) return rooted && rowMeetsPath(pat, segs, rooted) ? "named" : "none"; if (pat.rootedOnly === true) return "none"; let k = 0; while (k < pat.segments.length && isAnySegment(pat.segments[k])) k++; const run = pat.segments.slice(k); for (let i = 0; run.length > 0 && i < segs.length; i++) { const m = Math.min(run.length, segs.length - i); if (run.slice(0, m).every((re, j) => re.test(segs[i + j]))) return "named"; } return pat.unplacedHome === true && rooted ? "unplaced" : "none"; } function rowNamedByRootOf(rows, path) { const rooted = isAbsoluteForFamily("own", path); const views = segmentViewsOf(path); let unplaced; for (const pat of rows) { for (const view of views) { const relation = rowNamedByRoot(pat, pat.foldAscii ? view.map(asciiLower) : view, rooted); if (relation === "named") return { pattern: pat.raw }; if (relation === "unplaced") unplaced ??= pat.raw; } } return unplaced === undefined ? null : { pattern: unplaced, unplaced: true }; } function rowAtOrBelowOf(rows, path) { const rooted = isAbsoluteForFamily("own", path); const views = segmentViewsOf(path); for (const pat of rows) { for (const view of views) if (rowMeetsPath(pat, pat.foldAscii ? view.map(asciiLower) : view, rooted)) return pat.raw; } return null; } function homeAnchorSegments(home) { return normalizeAbsPathLexically(home).split(/[\\/]/).filter(Boolean); } function rgGlobEscape(pattern) { return pattern.replace(/[?[\]{}]/g, (c) => `[${c}]`); } export function readDenyEntryProblem(entry, layer) { try { normalizeEntry(entry, layer); return undefined; } catch (e) { return e.message; } } function normalizeEntry(entry, layer) { const shape = typeof entry === "string" ? { pattern: entry, caseSensitive: false } : entry; if (typeof shape !== "object" || shape === null || typeof shape.pattern !== "string") { throw new Error(`readDenyPatterns (${layer}): entry ${JSON.stringify(entry)} is not a pattern string or { pattern, caseSensitive? } object.`); } if (shape.caseSensitive !== undefined && typeof shape.caseSensitive !== "boolean") { throw new Error(`readDenyPatterns (${layer}): entry ${JSON.stringify(entry)} has a non-boolean caseSensitive.`); } if (shape.pattern.includes("\\")) { throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains a backslash — patterns are "/"-separated segment runs (both path families are matched); spell the segments with "/".`); } const segments = shape.pattern.split("/").filter(Boolean); const { homeAnchored, run } = readDenyPatternRun(segments.join("/")); if (run.length === 0) { throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains no path segments${homeAnchored ? " below the home word" : ""} and would ${homeAnchored ? "name the whole home directory" : "deny nothing"}. ` + `Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".config/gcloud", "id_rsa*", "~/.sema/settings.json"); remove the entry or spell the segments.`); } return { pattern: canonicalReadDenyPattern(shape.pattern), caseSensitive: shape.caseSensitive === true }; } function canonicalReadDenyPattern(pattern) { const joined = pattern.split("/").filter(Boolean).join("/"); return pattern.startsWith("/") && !readDenyPatternRun(joined).homeAnchored ? `/${joined}` : joined; } export function isRootedReadDenyPattern(canonical) { return canonical.startsWith("/"); } export function persistedReadDenyEntryProblem(e) { if (typeof e !== "object" || e === null) return "entry is not an object"; const pattern = e.pattern; const caseSensitive = e.caseSensitive; if (typeof pattern !== "string") return "pattern is not a string"; if (typeof caseSensitive !== "boolean") return "caseSensitive is not a boolean"; if (pattern.includes("\\")) return "pattern contains a backslash"; const segments = pattern.split("/").filter(Boolean); if (readDenyPatternRun(segments.join("/")).run.length === 0) return "pattern has no path segments"; if (canonicalReadDenyPattern(pattern) !== pattern) return "pattern is not in canonical a/b form"; return null; } export function checkedReadDenyHomes(homes, seat = "readDenyHomes") { if (!Array.isArray(homes) || !homes.every((h) => validatePath(h, { intent: "validate-coordinate" }) !== undefined)) { let shown; try { shown = JSON.stringify(homes) ?? String(homes); } catch { shown = String(homes); } throw new Error(`${seat}: expected an array of absolute ${seat === "readDenyHomes" ? "home paths" : "locations"}, got ${shown} — a home-anchored row ("~/…") is placed ${seat === "readDenyHomes" ? "under each" : "at each"}, so a relative or empty spelling would guard a place that is not the home's.`); } return homes; } export function compileReadDeny(additions = [], layer = "additions", builtin, rules = [], homes = [], linked = new Map()) { const ruleRows = rules.filter((r) => r.faces.includes("read")); const normalized = []; const seen = new Set(); const push = (e) => { const key = `${e.caseSensitive ? "s" : "i"}:${e.pattern}`; if (seen.has(key)) return; seen.add(key); normalized.push(e); }; const columns = new Map(); for (const row of resolveReadDenyBuiltins(builtin)) { const e = normalizeEntry(row.pattern, "built-in"); push(e); if (!columns.has(e.pattern)) columns.set(e.pattern, { origin: "builtin", tier: row.tier }); } for (const entry of additions) { const e = normalizeEntry(entry, layer); push(e); if (!columns.has(e.pattern)) columns.set(e.pattern, { origin: "addition" }); } const homeSegments = [...new Set(checkedReadDenyHomes(homes))].map(homeAnchorSegments); const compiled = normalized.map((e) => { const { homeAnchored, run } = readDenyPatternRun(e.pattern); const fold = e.caseSensitive ? "none" : "ascii"; const c = compileSegmentPattern(run, fold); if (c === null) throw new Error(`readDenyPatterns: pattern ${JSON.stringify(e.pattern)} compiled to zero segments.`); if (isRootedReadDenyPattern(e.pattern)) return { ...c, raw: e.pattern, anchors: [[]], rootedOnly: true }; if (!homeAnchored) return { ...c, raw: e.pattern }; if (homeSegments.length === 0) return { ...c, raw: e.pattern, unplacedHome: true }; return { ...c, raw: e.pattern, anchors: homeSegments.map((segs) => (fold === "ascii" ? segs.map(asciiLower) : segs)) }; }); if (!(linked instanceof Map)) throw new Error(`readDenyHomeLinks: expected a Map from a row's pattern to its locations, got ${typeof linked}.`); const linkedPlacements = []; normalized.forEach((e, i) => { const locations = linked.get(e.pattern); const { homeAnchored, run } = readDenyPatternRun(e.pattern); if (locations === undefined || !(homeAnchored || isRootedReadDenyPattern(e.pattern))) return; const c = compiled[i]; const rest = c.segments.slice(readDenyRunHead(run)); for (const location of new Set(checkedReadDenyHomes(locations, "readDenyHomeLinks"))) { const segs = homeAnchorSegments(location); linkedPlacements.push({ raw: e.pattern, segments: rest, foldAscii: c.foldAscii, anchors: [c.foldAscii ? segs.map(asciiLower) : segs], rootedOnly: true }); } }); const judged = linkedPlacements.length === 0 ? compiled : [...compiled, ...linkedPlacements]; const segmentRows = judged.map((c) => { const col = columns.get(c.raw); return segmentGuardRow(c, { faces: ["read"], polarity: "deny", exemptible: false, fold: c.foldAscii ? "ascii" : "exact", origin: col?.origin ?? "addition", ...(col?.tier !== undefined ? { tier: col.tier } : {}) }); }); const tableRows = [...segmentRows, ...ruleRows.map(ruleGuardRow)]; const rgExclusionGlobs = []; const rgProbeGlobs = []; for (const row of segmentRows) { if (row.placement !== "anywhere" || !row.faces.includes("read") || row.grammar.kind !== "segment-glob") continue; const flag = row.fold === "exact" ? "--glob" : "--iglob"; const g = rgGlobEscape(readDenyPatternRun(row.grammar.run).run); rgExclusionGlobs.push({ flag, glob: `!**/${g}` }, { flag, glob: `!**/${g}/**` }); rgProbeGlobs.push({ flag, glob: `**/${g}` }, { flag, glob: `**/${g}/**` }); } const placed = judged.filter((c) => c.anchors !== undefined); const unexcludableUnder = (scope) => placed.length > 0 && (!isAbsoluteForFamily("own", scope) || rowAtOrBelowOf(placed, normalizeAbsPathLexically(scope)) !== null); const rowAtOrBelow = (path) => { const hit = rowAtOrBelowOf(judged, path); if (hit !== null) return { pattern: hit }; return ruleRows.length === 0 ? null : { pattern: ruleRows[0].rule }; }; const rowUnderRoot = (scope) => { if (judged.length === 0 || !isAbsoluteForFamily("own", scope)) return null; return rowNamedByRootOf(judged, normalizeAbsPathLexically(scope)); }; const matchPath = (path, opts) => { const hit = denyTableHit(tableRows, { views: [{ path, kind: "lexical", ...(opts?.aliasResolved === true ? { aliasResolved: true } : {}) }] }); return hit === null ? null : { pattern: hit.pattern }; }; return { entries: normalized, ...(ruleRows.length > 0 ? { ruleRows } : {}), matchPath, matchTarget: (canonicalKey, lexicalView, opts) => denyTableHit(tableRows, denyTargetViews(canonicalKey, lexicalView, opts)), rgExclusionGlobs, rgProbeGlobs, unexcludableUnder, rowUnderRoot, rowAtOrBelow, }; }