@sema-agent/core
Version:
Stateless, task-oriented AI agent core
499 lines (498 loc) • 37.8 kB
TypeScript
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;