@sema-agent/core
Version:
Stateless, task-oriented AI agent core
624 lines (623 loc) • 30.3 kB
JavaScript
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,
};
}