UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

499 lines (498 loc) • 37.8 kB
import { type PathGuardFold, type PathGuardRow, type PathRowsHit, type TargetViews } from "../../core/path-guard-rows.js"; /** * One deny entry: a `/`-separated run of path SEGMENTS. `*` matches any run of non-separator * characters within one segment and is the ONLY metacharacter (everything else is literal). The run * may match ANYWHERE in the judged path (`.ssh` covers `~/.ssh/…` and any other `.ssh` directory) — * unless the pattern opens with the HOME word ({@link READ_DENY_HOME_WORD}): `~/<run>` matches only where * the run starts right below the execution environment's home directory (`~/.sema/settings.json` is the * home's file, never a project's `.sema/settings.json`). Only a FIRST segment that is exactly `~` is the * word; `~user` and a `~` further along are literal segments. A pattern that opens with `/` (and is not home-anchored) * is ROOTED, the same kind of anchor with the filesystem root as its place: `/app/config-lkg.json` names the file * at exactly that location, never a project's `app/config-lkg.json` — the run must start at the first segment of an * absolute path (a path that is not absolute is not placed, and the row does not judge it). * String shorthand ≡ `{ pattern }`. Matching is ASCII-case-insensitive by default (a case-insensitive * filesystem serves `.SSH` and `.ssh` as the same directory); `caseSensitive: true` opts a single * entry out. Unicode normalization (NFC/NFD) and volume-level case semantics are NOT modeled (v1 * residual, shared with the write guard; the canonical view — realpath returns the on-disk spelling — * covers half of it). */ export type ReadDenyEntry = string | { pattern: string; caseSensitive?: boolean; }; /** The HOME word of the pattern grammar ({@link ReadDenyEntry}): a FIRST segment spelled exactly this anchors the run * right below the execution environment's home directory. */ export declare const READ_DENY_HOME_WORD = "~"; /** A pattern read as its anchor and its run: `~/a/b` ⇒ the run `a/b`, home-anchored; any other pattern ⇒ itself, * unanchored. The one reader of the home word — the compiler, the traversal globs, the classifier's rendering and the * write face's mirror all take the run from here. */ export declare function readDenyPatternRun(pattern: string): { readonly homeAnchored: boolean; readonly run: string; }; /** How many leading segments of a run are LITERAL — the run's head before the first segment carrying `*`. A home-anchored * row's head names one location below the home, which a mount resolves to where it really leads * ({@link compileReadDeny}'s `linked`); the segments after it are matched below that location as the run matches. */ export declare function readDenyRunHead(run: string): number; /** The normalized (validated, defaulted) form of one entry. */ export interface NormalizedReadDenyEntry { pattern: string; caseSensitive: boolean; } /** A deny verdict: which entry's pattern matched. */ export interface ReadDenyHit { pattern: string; /** The view string the pattern actually matched (set by {@link CompiledReadDeny.matchTarget}: the * canonical key, or the lexical view when only the SPELLING matched). Disclosure must cite the * matched view — under a symlink/case alias the two strings differ, and naming the other one * sends the reader chasing a path the pattern does not match. */ matchedView?: string; } /** A recursive read's root relation to the table ({@link ReadDenyMatcher.rowUnderRoot}): the row the root NAMES or contains * (`unplaced` absent), or — when it names none — a home-anchored row compiled under no home (`unplaced: true`), which cannot be * placed and so may lie under any root. */ export type ReadDenyRootHit = ReadDenyHit & { readonly unplaced?: true; }; /** The same relation as the shell read boundary takes it ({@link ReadDenySeat.denyUnder}): the pattern of the row the root * names (a deny), `{ unplaced }` for a row the root may hold that no home places, or null. */ export type ReadDenyRootAnswer = string | { readonly unplaced: string; } | null; /** The two faces of the deny table: what a READ must not reach, and what a WRITE must not reach. */ export declare const DENY_FACES: readonly ["read", "write"]; export type DenyFace = (typeof DENY_FACES)[number]; /** * A deny row in the rule DSL's PATH grammar — a `Read(<pattern>)` / `Edit(<pattern>)` deny rule, compiled by the * rule compiler against its own bases (`createPermissionRulePolicy`). It joins the SAME table as the segment rows: one * judge (`matchPath` / `matchTarget`), one refusal sentence, one classifier rendering — only the row's reach is the * rule grammar's (the path it names and everything under a directory it names), not a segment run. * · `rule` — the rule as written, the row's name wherever a refusal or the classifier names it; * · `tool` / `pattern` — the rule's two parts, for a rendering that states the pattern inertly; * · `faces` — which faces the row guards (`Read` ⇒ read and write: a path whose reads are denied is not written * either; `Edit` ⇒ write); * · `reaches` — the rule compiler's own reach over an ABSOLUTE path. */ export interface PathRuleDenyRow { readonly rule: string; readonly tool: string; readonly pattern: string; readonly faces: readonly DenyFace[]; readonly reaches: (absolutePath: string) => boolean; /** The row's HEAD — the absolute location its reach starts at (the rule model's `pathRuleHeadOf`: the pattern's * literal prefix under its base, a directory rule's directory), spelled so the execution environment can resolve it. * A leg judges the row where that location LEADS too ({@link withRuleRowsWhereTheyLead}). Absent on a hand-built * row, or one whose head cannot be read: it is judged as spelled. */ readonly head?: string; } /** A row's reach over ONE absolute spelling, three-valued: `unreadable` = the row cannot be judged there (a base its anchor * lacks) — a tightening judgment holds on it, an allow grants nothing on it. */ export type PathRowReach = { readonly reach: "reached" | "not_reached"; } | { readonly reach: "unreadable"; readonly reason: string; }; /** A rule row as the one judgment reads it: the deny-table row, and — on a row a rule binding produced — its three-valued * reach (a row built by hand answers through `reaches` alone: reached or not). */ export interface JudgedPathRow extends PathRuleDenyRow { readonly reach?: (absolutePath: string) => PathRowReach; } /** * Rule rows judged by THE one judgment ({@link judgePathRows}): each rule row read as a guarded row whose reach over a view * is the rule's own reach over the view's spelling (never exemptible — a rule says exactly where it reaches). Same rows * outer, views inner, same tighten / allow law as every other row; the hit names the rule row itself. */ export declare function judgeRuleRows<R extends JudgedPathRow>(rows: readonly R[], views: TargetViews, direction: "tighten" | "allow"): PathRowsHit<R> | undefined; /** The locations a row is also judged at (each leading part of its HEAD: the anchor's children down to the head itself, * a share's root for a UNC head) — what a leg resolves once, for every row whose head it holds. */ export declare function ruleHeadParts(rows: readonly Pick<PathRuleDenyRow, "head">[]): string[]; /** * The rule rows as a leg judges them: each row as spelled, and — for every location on the way to the row's HEAD (each * leading part of it) that the environment resolves to another spelling (a link or firmlink: `/tmp` → `/private/tmp`, * `/var` → `/private/var`, a project root, a home entry or a directory inside the rule reached through a symlink) — the * same row at that spelling, right after it: a path at or below the resolved location is judged as its spelling at or * below that part. Every part, not the head alone: a link inside the rule's literal prefix that is re-pointed after the * rows were placed still meets the row through the parts above it. One rule for every row form and every judge (the * gate's lanes and the deny tables): the reference compiles the same "physical twin" of every deny / ask rule's literal * prefix (cli280.js `ei` 9766583, compiled in `mt` 9767083). `leads` is the leg's resolution of each part (absent or * `undefined` = no twin there); a part that only extends its parent's resolution adds no row. A twin is the row's own * kind (it keeps every member but the head), so a judgment reports the rule the twin stands for. */ export declare function rowsWhereTheyLead<R extends JudgedPathRow>(rows: readonly R[], leads: ReadonlyMap<string, string | undefined>): R[]; /** Where the location `path` itself leads in a leg's resolution (`leads`, keyed by each leading part — the parts * {@link ruleHeadParts} names): `undefined` when the leg did not resolve it or it leads to its own spelling. */ export declare function locationLeadsTo(path: string, leads: ReadonlyMap<string, string | undefined>): string | undefined; /** {@link rowsWhereTheyLead} with each part resolved by `leadsTo` (the leg's resolution; each part once). */ export declare function withRuleRowsWhereTheyLead<R extends JudgedPathRow>(rows: readonly R[], leadsTo: (head: string) => Promise<string | undefined>): Promise<R[]>; /** A rule row as a row of the one table: its name is the rule text, its reach the rule's own over a view's spelling. */ export declare function ruleGuardRow<R extends JudgedPathRow>(rule: R): PathGuardRow & { readonly rule: R; }; /** A compiled segment-glob row as a row of the one table (`faces`, `polarity`, `exemptible` and `origin` are the table's). */ export declare function segmentGuardRow(pat: CompiledSegmentPattern, columns: { readonly faces: readonly DenyFace[]; readonly polarity: "deny" | "ask"; readonly exemptible: boolean; readonly fold: PathGuardFold; readonly origin: PathGuardRow["origin"]; readonly tier?: string; }): PathGuardRow; /** * The WRITE face of the deny table: the rule rows whose faces include `write` — the judge the shell station reads for a * command's write targets (a redirection, `tee`, `sed -i`, `cp` / `mv` / `touch` / `mkdir` / `rm` operands). Same row * type, same judgment ({@link judgePathRows}) as the read face's rows; a hit names the row by its rule text. */ export interface WriteDenyMatcher { /** Which face this judge is — the write tools' path resolution reads it to word and code the refusal as a write. */ readonly face: "write"; readonly ruleRows: readonly PathRuleDenyRow[]; matchPath(path: string): ReadDenyHit | null; /** The target judgment the read face makes, on this face's rows: canonical key, lexical view, the leaf chain's hops. */ matchTarget(canonicalKey: string, lexicalView?: string, opts?: { readonly aliasResolved?: boolean; readonly hops?: readonly string[]; }): ReadDenyHit | null; /** The table ONE call judges with ({@link ReadDenyMatcher.forCall}). */ forCall?(toolCallId: string | undefined): WriteDenyMatcher; } /** Compile the write face over the rule rows (the rows not on it are dropped here, once). */ export declare function compileWriteDeny(rules?: readonly PathRuleDenyRow[]): WriteDenyMatcher; /** One compiled ripgrep glob flag for the traversal legs (`--iglob` = case-insensitive entry). */ export interface ReadDenyRgGlob { flag: "--glob" | "--iglob"; glob: string; } /** * The compiled deny judge — ONE compilation, consumed by every judgment point (target judgment in * resolveKey, the JS walker, the rg exclusion/probe legs, the classify operand probe, the ENOENT * sibling filter, the attachment reader), so the legs cannot drift apart. */ export interface ReadDenyMatcher { /** Normalized entries (built-ins first, then additions; exact duplicates folded). The disclosure / * persistence face. */ readonly entries: readonly NormalizedReadDenyEntry[]; /** The rule-DSL rows on the READ face ({@link PathRuleDenyRow}), judged after the segment rows. Not persisted: * they are the caller policy's, which a resume re-supplies. Absent on a matcher compiled without rules. */ readonly ruleRows?: readonly PathRuleDenyRow[]; /** Judge ONE path view (segment-window semantics). Returns the first matching entry, or null. * `aliasResolved` = the key's MINTER reports the real filesystem already collapsed any win32 * component alias in it (canonicalizeTarget's realpath arm); unset ⇒ both readings are judged, * which is the fail-closed default. Every row of the table judges the path — a segment row (built-in tier, * deployment pattern) and a rule row alike: a path is judged by what a command NAMES, never by a tree * enumerated below it. */ matchPath(path: string, opts?: { readonly aliasResolved?: boolean; }): ReadDenyHit | null; /** §3.3 target judgment: canonical view + lexical view + every hop of the leaf symlink chain (`hops`, * each an unresolved spelling some link wrote); ANY hit decides. `aliasResolved` applies to the * CANONICAL arm only — the lexical view and the hops are unresolved spellings. */ matchTarget(canonicalKey: string, lexicalView?: string, opts?: { readonly aliasResolved?: boolean; readonly hops?: readonly string[]; }): ReadDenyHit | null; /** Traversal exclusions for the rg legs: `!`-polarity glob pairs — `!**\/<p>` plus `!**\/<p>/…` * (the entry itself and its descendants). An entry placed at a home carries none (a glob is relative to the * search root): `matchPath` judges it on the rg leg's output and in the walker. */ readonly rgExclusionGlobs: readonly ReadDenyRgGlob[]; /** Positive-polarity twins of the exclusions, for the bounded existence probe (first hit = entries * matching the deny list exist under the scope and were excluded). */ readonly rgProbeGlobs: readonly ReadDenyRgGlob[]; /** Whether an entry that carries NO traversal glob (one placed at a home) can match a path under `scope` (an absolute * directory): the rg leg cannot exclude such an entry, and a search that reads it and prunes afterwards answers * differently by whether the entry's content matched — so a search over such a scope takes the walker, which prunes * by path before reading. A scope that is not rooted answers true. */ unexcludableUnder?(scope: string): boolean; /** The segment row the absolute directory `scope` NAMES or contains, judged on locations alone, never on what exists * there: a placed row (home-anchored or rooted, or where one of those really leads) whose location lies at or below the * scope, or a spelled row whose run — leading wildcards dropped — starts at the scope's own segments (`secrets/**`, with or * without a leading double-star segment, for `…/secrets` and for `…/secrets/sub`; not for `…/sub` that merely holds a `secrets` directory below * it, and a double-star-led `.env` row names no directory at all). A home-anchored row compiled under NO home names a root * where its spelled run does and otherwise answers `unplaced` (a row that cannot be placed holds wider: any root may hold * it), after every row the root names. Rule rows never answer here: a person's `Read(<path>)` rule judges explicit operands * only. `null` for a scope that is not rooted. The one answer a recursive read's root gets about the table * ({@link unexcludableUnder} asks the placed subset for the search faces). */ rowUnderRoot?(scope: string): ReadDenyRootHit | null; /** A row that lies AT or BELOW `path` — the path is inside one of the row's matches, or a match would run on below the * path from its own segments ({@link rowMeetsPath}) — so moving, copying or removing the path changes what the row * covers. A rule row's reach below a path is not enumerable: any rule row answers. */ rowAtOrBelow?(path: string): ReadDenyHit | null; /** The table ONE call judges with — the Runner's tables are one per read of the person's rules, and a call judges with * the read it was pinned to (an unknown or released call: the run's latest). Absent on a table compiled once: it is * every call's ({@link seatForCall}). */ forCall?(toolCallId: string | undefined): ReadDenyMatcher; } /** A seat the run's tables back — a deny table, a read-root set — as ONE call takes it: the call's own where the seat * carries `forCall`, the seat itself otherwise (a table compiled once, a host's own list) and for a reader that is no * call (no id: the seat's own reading of the run's latest, with every refusal it carries). Taken once, where the call's * judgment starts, so every judgment of the call reads one table. */ export declare function seatForCall<T>(seat: T, toolCallId: string | undefined): T; /** Settles once the tables a seat the run's tables back stands on are ready for a reader that is no call (the seat's * `ready`); any other seat is ready. Never rejects. */ export declare function seatReady(seat: unknown, signal?: AbortSignal): Promise<void>; /** The DENY HALF of a shell read boundary, in the shape `BashReadonlyRootBoundary` declares it (structural: * this module is the leaf every face depends on). `denyMatch` judges one LEXICALLY RESOLVED operand, * returning the matched pattern or null; PRESENT-IFF there is something to judge. */ export interface ReadDenySeat { denyMatch?: (resolvedPath: string) => string | null; /** {@link ReadDenyMatcher.rowUnderRoot} for one lexically resolved directory, as {@link ReadDenyRootAnswer}; present exactly * when `denyMatch` is. */ denyUnder?: (resolvedPath: string) => ReadDenyRootAnswer; /** {@link ReadDenyMatcher.rowAtOrBelow} for one lexically resolved path; present exactly when `denyMatch` is. */ denyAtOrBelow?: (resolvedPath: string) => string | null; denyEntries: readonly string[]; } /** The ONE adapter from a compiled matcher to that seat, so every face mounts it the SAME way and the "wire * the judge only when it has something to judge" rule is spelled once. That rule is load-bearing: the * seat's PRESENCE is itself read as a fact (the recursive-read demotion mints on "a deny judge is wired" = * the check a traversal would outflank is in place), so a judge over an EMPTY set must not be wired. Call * ONCE per mount and reuse the result — the returned closure is then the function OBJECT every face mounted * from that matcher shares, which makes "the same judge" checkable rather than argued. */ export declare function readDenySeat(matcher: ReadDenyMatcher | undefined): ReadDenySeat; /** The ONE naming of the deny table and the pattern that matched. Every face composes its own SUBJECT and * VERB in front of it and its own DISPOSITION behind it (the verb is not shared: one face names the * target, another the REQUESTED SPELLING under a symlink alias). What must not fork is the naming — a * reader comparing two surfaces about one finding must not be reading two findings. A LIST of patterns is * the same naming in the plural, for the one finding that names no single row: a traversal that goes PAST * the judge names the table IN FORCE, never a match it did not make. Four composers: the * structured read faces' refusal (`safety.ts`), the shell faces' refusal card (`readDenyRefusal`), the * lexical walk's verdict sentence (`bash-readonly-classifier.ts`), and the full shell's MANDATED ask card, * which is approvable and so states the finding without claiming a refusal. NOT composers, each stating * its own fact: the compaction lane's withheld-attachment HTML attribute, and the traversal notes' * withheld-COUNT lines, which name the table and a pattern LIST. */ export declare function sensitivePathDenyList(pattern: string | readonly string[], face?: DenyFace): string; /** The deny table's REACH: a deny row holds INSIDE the containment roots, so no root grant answers it. */ export declare const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots."; /** The write face's reach, in the same place of the sentence: a write deny row holds under every root and every mode. */ export declare const WRITE_DENY_SCOPE = "A write deny row holds regardless of the containment roots and of the permission mode."; /** * THE deny refusal sentence — five segments, one mint, every read face: the ACTION, the ABSOLUTE target as the * caller spelled it (with its canonical target in parentheses when the two differ), the DISPOSITION (a deny row is * refused, never escalated), the table BY NAME with the PATTERN verbatim ({@link sensitivePathDenyList}), and * one sentence of why ({@link SENSITIVE_PATH_DENY_SCOPE}). `matchedView` names the view the pattern actually hit * when that is the requested SPELLING rather than the target — under a symlink or case alias the two strings * differ, only one matches, and naming the other sends the reader chasing a path the pattern does not match. * A face prefixes its own `Error (<tool>): ` and may append its own closing clause; the finding itself is * never re-spelled. */ export declare function readDenySentence(finding: { readonly action?: string; readonly target: string; readonly canonical?: string; readonly pattern: string; readonly matchedView?: string; /** The table face that refused (default `read`): a write-face hit names the write list and its reach, and writes. */ readonly face?: DenyFace; }): string; /** The per-part budget of the deny sentence — the gate's own card-account cap, not a second number. */ export declare const READ_DENY_SENTENCE_PART_MAX = 200; /** `s` verbatim when it fits `budget`, else elided in its MIDDLE ({@link elideMiddle}). */ export declare function fitMiddle(s: string, budget: number): string; /** `s` shortened to `budget` by removing its MIDDLE, marked — cut through the repo's surrogate-safe * primitive, never a raw `slice` (a raw cut leaves half an astral code point on a card and in a durable * row). Under a dozen characters the HEAD is taken. */ export declare function elideMiddle(s: string, budget: number): string; /** * The closed set of built-in tiers. A tier is the deployment's selection unit * ({@link ReadDenyBuiltinConfig.tiers}); every {@link READ_FACE_BUILTIN_DENY_TABLE} row belongs to * exactly one. Unknown tier names refuse loudly — this array is the validation source and the * admin-face vocabulary, never re-spelled downstream. */ export declare const READ_DENY_BUILTIN_TIERS: readonly ["credentials", "shell-history", "browser", "wallet", "agent-config"]; /** One member of {@link READ_DENY_BUILTIN_TIERS}. */ export type ReadDenyBuiltinTier = (typeof READ_DENY_BUILTIN_TIERS)[number]; /** * One row of the built-in table: the canonical deny pattern (also the row's STABLE NAME — the * `readDenyBuiltinExclude` key; renaming a pattern is a semantic change and deliberately invalidates * configs naming the old spelling, loudly) and the tier it belongs to. */ export interface ReadDenyBuiltinRow { readonly pattern: string; readonly tier: ReadDenyBuiltinTier; } /** * READ_FACE_BUILTIN_DENY_TABLE — the FULL built-in table with tier metadata: the material basis of * the deployment admin face, what an org administrator "sees" before selecting tiers / excluding * rows. Activation is a SEPARATE fact: the tiers of {@link READ_DENY_DEFAULT_TIERS} are active unless the * deployment selects otherwise, the rest are inert until a deployment names their tier — so this is the * MENU, never the active set. Rows stay curated regardless — the menu has to be worth switching on. * The dotenv family is deliberately NOT here — workspace `.env` files are working material for the * tasks this engine runs; an `outside_workspace` tier is likewise deliberately absent. * * Each row states its tradeoff. An unanchored row is matched as a segment run anywhere in the path, so a * repo fixture spelled `fixtures/.ssh/id_rsa` is refused too — deliberate: the refusal names the pattern, * and a false refusal on a fixture is the cheap direction (deny errs strict). A home-anchored row * (`~/…`, {@link READ_DENY_HOME_WORD}) is matched only below the home directory. * * Two COLUMNS, one table: a name that is a credential wherever it is found is spelled unanchored (a key * copied into a repo is as hot as the original); a name that is the PERSON's file in the home but the * project's own configuration anywhere else is spelled home-anchored. `.npmrc` / `.pypirc` are the second * kind: `~/.npmrc` holds the person's registry tokens, while a project's `.npmrc` is read on every install * question and routinely carries an `${NPM_TOKEN}` placeholder — refusing it cost ordinary work in every mode * and bought nothing the home row does not already hold. The same file read through a link from a project * into the home still meets the home row on its canonical view. * * NOT listed, deliberately: `.env`/`.env.*` (ruled out — workspace material; the WRITE guard still * covers them; the ENGINE's own `.env` under its data root is a different file and IS listed, see the * agent-config tier); `*.pem`/`*.key`/`*.p12`/`*.pfx` (repo certificates/test keys are routinely * READ — refusing them breaks ordinary work, the write guard covers the mutation direction); * `.git/hooks`/`.git/config` (reads are harmless; writes are the escalation and stay guarded); the * engine's own data root as a WHOLE (transcript integrity policy owns it with an ask on the write * face, and the memory library the model is told to read and edit lives under it). * * agent-config tier — the agent's OWN user-level configuration files whose `env` / server blocks carry API * keys in cleartext (the shell's `settings.json` env block, an MCP server env, the engine host's data-root * `.env`). Unguarded, a structured read of one of these lands the key verbatim in the tool result and * therefore in the session record. Every row is HOME-ANCHORED: the user's own files under the home * directory are refused, while a PROJECT's file of the same name (`./.sema/settings.json`, `./.mcp.json`, * `./.claude/settings.json`) is working material the person edits with the model and reads normally — its * writes are the write faces' business. Rows name FILES, not the config DIRECTORIES: `.sema`/`.claude` also * hold agents, skills, hooks and (under the shell's layout, `~/.sema/engine-data`) the engine data root * with the memory library — material the model reads and edits as ordinary work, and a directory-wide row * would need a data-root passthrough (one more rule) to keep it readable. Token * STORES of the same tooling (`.credentials.json`, an agent CLI's `auth.json`, …) are credentials rows, not * agent-config rows: a deployment that switches this tier off to debug its agent configuration must * not also expose the OAuth stores beside it. The rows' RUNS are ALSO the opt-in write deny set's * agent-configuration block ({@link import("../../core/sensitive-path-policy.js").agentConfigDenyPatterns}, * which reads them from here; contract sentence there), so a row added here is judged by both faces. */ export declare const READ_FACE_BUILTIN_DENY_TABLE: readonly ReadDenyBuiltinRow[]; /** * The DEFAULT tier selection — what an unconfigured deployment activates: the `credentials` tier (key * material, cloud and registry credential files, token stores) and the `agent-config` tier (the agent's own * user-level configuration files, home-anchored). The `shell-history`, `browser` and `wallet` tiers are a * deployment's opt-in. Activation is a deployment decision with a visible seat, and the leg's wiring * manifest reports which tiers it got. * * The NAMED default rather than an inline literal: `[]` and absence stay separately spellable on the * seat, and a deployment profile wanting a different default changes THIS expression, not call sites. */ export declare const READ_DENY_DEFAULT_TIERS: readonly ReadDenyBuiltinTier[]; /** * The deployment's built-in-table configuration — deployment seat ONLY (RunnerDeps / * HandsToolkitOptions; deliberately NOT a TaskSpec key and NOT in the governed workflow whitelist, * so no task or governed script can widen below its deployment). * - `tiers` — EXACTLY-these tier selection: listed tiers are active, unlisted ones are not * (`[]` = no built-in tiers at all — an explicit, legal posture; absent = {@link READ_DENY_DEFAULT_TIERS}). * - `exclude` — per-row removal by canonical pattern name, applied AFTER tier selection. * Unknown tier names / unknown row names refuse loudly — never silently ignored. */ export interface ReadDenyBuiltinConfig { tiers?: readonly string[]; exclude?: readonly string[]; } /** * Resolve the ACTIVE built-in rows under a deployment configuration (validated, loud — all three * bad-value states throw: garbage shapes, unknown tier names, unknown row names). The one * resolution used by {@link compileReadDeny}; exported so an admin face can preview the effective * table under a candidate configuration with the engine's own rules. */ export declare function resolveReadDenyBuiltins(config?: ReadDenyBuiltinConfig): readonly ReadDenyBuiltinRow[]; /** * The deployment's built-in configuration as ONE validated snapshot, read off the seats' OWN values: each seat is an * array of names or absent — any other value (a string, the empty string included) refuses loudly, naming the seat, * before anything is spread or copied (a spread string reads as its characters, or as no names at all). The names are * then judged by {@link resolveReadDenyBuiltins}. The snapshot holds copies: a caller mutating its arrays afterwards * moves nothing, so every compile of one mount reads the same selection. */ export declare function readDenyBuiltinConfigOf(seats: { readonly tiers?: unknown; readonly exclude?: unknown; }): ReadDenyBuiltinConfig; /** * The tiers a deployment configuration ACTIVATES — the tier names of {@link resolveReadDenyBuiltins}'s * surviving rows, in {@link READ_DENY_BUILTIN_TIERS} order (the closed set is the ordering source, so * two deployments that select the same tiers in different spellings report the same list). * * MATERIAL, not intentional: a tier whose every row was removed by `exclude` activates nothing and * is NOT reported. An EMPTY result means the built-in table contributes nothing on this leg — the * unconfigured default. Same loud validation as the resolution it wraps. */ export declare function activeReadDenyBuiltinTiers(config?: ReadDenyBuiltinConfig): readonly ReadDenyBuiltinTier[]; /** * READ_FACE_DEFAULT_DENY_ENTRIES — the DEFAULT-ACTIVE built-in set: {@link READ_FACE_BUILTIN_DENY_TABLE} * under {@link READ_DENY_DEFAULT_TIERS} — the answer to "what does an unconfigured deployment refuse, from * this table". Derived, so it follows the default selection; the tier-annotated table is where a consumer * renders "what COULD be switched on". */ export declare const READ_FACE_DEFAULT_DENY_ENTRIES: readonly ReadDenyEntry[]; /** A compiled segment-run pattern (shared engine — the write guard delegates here too). */ export interface CompiledSegmentPattern { raw: string; segments: RegExp[]; /** Fold BOTH sides through {@link asciiLower} before testing (compile lowered the pattern side). */ foldAscii: boolean; /** The segment lists of the directories the run is ANCHORED at (folded as the pattern folds): the run must start * right after one of them. Absent = the run matches anywhere. */ anchors?: readonly (readonly string[])[]; /** Judged in a ROOTED view only: an extra location of a home-anchored row (where its literal head leads), or a row that is * itself rooted (anchored at the filesystem root, {@link ReadDenyEntry}). A view that is not rooted cannot be placed: a * home-anchored row's own run is held anywhere in it (the row cannot be placed, so it holds wider); these two judge it not. */ rootedOnly?: true; /** A home-anchored row compiled under NO home: it cannot be placed, so it holds wider on both relations a path has to it — * its run matches anywhere in a path, and any recursive read's root may hold it ({@link rowNamedByRoot}). */ unplacedHome?: true; } /** * Compile one segment-glob pattern (shared engine). `fold`: * · "none" — byte-exact segments; * · "ascii" — ASCII-case-insensitive (deny-set contract); * · "unicode" — RegExp `i` flag (the write guard's historical host-keyed behavior — kept for it * byte-identically; NOT used by the deny set). * Returns null when the pattern reduces to zero segments — the CALLER owns its loud path (the two * consumers refuse with their own surface-specific texts). */ export declare function compileSegmentPattern(raw: string, fold: "none" | "ascii" | "unicode"): CompiledSegmentPattern | null; /** * Does the path contain a compiled pattern as a CONTIGUOUS run of full path segments? (Shared engine: * `.git/hooks` matches `/repo/.git/hooks/pre-commit`; `.ssh` matches any `.ssh` segment.) An ANCHORED pattern * ({@link CompiledSegmentPattern.anchors}) matches only where the path's leading segments are one anchor's and * the run starts right after them. Splits on BOTH separator families — win32 canonical keys are backslash-form. * Returns the matched raw pattern or null. */ export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[], opts?: { readonly aliasResolved?: boolean; }): string | null; /** One entry's refusal under the deny list's entry law ({@link normalizeEntry}, the compiler's own), or `undefined` when the * entry is lawful — the law read without compiling, for a door that judges a list before anything folds it. */ export declare function readDenyEntryProblem(entry: unknown, layer: string): string | undefined; /** Is a canonical pattern ROOTED — placed at the filesystem root ({@link ReadDenyEntry})? Only {@link canonicalReadDenyPattern} * puts a leading `/` on a canonical pattern, so the text is the whole answer. */ export declare function isRootedReadDenyPattern(canonical: string): boolean; /** * the PURE validation/canonicality predicate over a PERSISTED deny entry (checkpoint v9 * face section), for the resume pre-CAS ladder: it must refuse BEFORE the approval CAS everything * {@link compileReadDeny} would throw on AFTER it (a post-CAS compile throw consumes the human's * approval and strands the row), plus non-canonical spellings (normalizeEntry canonicalizes at every * mint, so a persisted non-canonical pattern was not minted by any release = damaged). Returns the * refusal reason, or null when the entry is valid. */ export declare function persistedReadDenyEntryProblem(e: unknown): string | null; /** The home spellings a home-anchored row is placed under, judged as given: an array of absolute path strings, loud * otherwise and naming `readDenyHomes` — a relative or empty spelling places the row where no home is, and a bare * string would be read as its characters. */ export declare function checkedReadDenyHomes(homes: unknown, seat?: string): readonly string[]; /** * Compile the read-face deny judge: ACTIVE built-in rows ∪ additions. Additions are add-only at every * task layer (`[]` additions ≡ absent, deliberately a no-op rather than a refusal: an array API * cannot observe a "replace" intent, and union-with-empty is the identity, not a silent fallback); * the built-in half is selected by the DEPLOYMENT's `builtin` config (absent = the default tier * selection, see {@link resolveReadDenyBuiltins}). A deployment that turned a tier off can still * re-add single patterns through the additions seats — they then ride as ordinary additions. * Bad entry shapes / zero-segment patterns / unknown tier or row names throw loudly, naming the * layer or knob. * * `homes` — the spellings of the execution environment's home directory a home-anchored entry * ({@link READ_DENY_HOME_WORD}) is placed under: the declared home, the account home and, where the environment's * canonical spelling of one differs, that one too (the faces judge canonical keys beside the spelled views). Absolute * spellings only — any other shape refuses loudly, naming `readDenyHomes`. EMPTY = the environment declares no * home: a home-anchored entry then matches its run anywhere and any recursive read's root may hold it * ({@link ReadDenyMatcher.rowUnderRoot}) — a row that cannot be placed holds wider, never narrower, on both relations. * * `linked` — where a home-anchored or a rooted row's LITERAL HEAD ({@link readDenyRunHead}) really leads, keyed by the row's * pattern: the canonical locations a mount resolved (`readDenyLinksOf`, read-deny-home.ts) where * one of the head's segments is a link (`~/.sema` linked into a dotfiles directory; `/app` a link to `/data/app`). The row is placed at each location too — * the same row, the same name in every refusal — with the rest of its run matched below it. The read faces judge * canonical keys, so every spelling that reaches the linked file meets the row there. Absolute locations only. */ export declare function compileReadDeny(additions?: readonly ReadDenyEntry[], layer?: string, builtin?: ReadDenyBuiltinConfig, rules?: readonly PathRuleDenyRow[], homes?: readonly string[], linked?: ReadonlyMap<string, readonly string[]>): ReadDenyMatcher;