@sema-agent/core
Version:
Stateless, task-oriented AI agent core
352 lines (351 loc) • 28.9 kB
TypeScript
/**
* design/99 §E6 — per-session permission rules: a durable, session-scoped policy narrowing folded into the
* task-time {@link import("./tool-policy.js").ToolPolicy} (see {@link import("./runner/session-rule-policy.js").createSessionRulePolicy}).
*
* Why core-owned (not a parallel service store): the rules must be ENFORCED by the same policy fold that runs
* every tool call, so storage and enforcement can't drift (the capability-token invariant). core defines
* the seam + an InMemory default; a deployment supplies a DURABLE cross-replica backend (its CAS-rev semantics
* must match this InMemory one byte-for-byte — a cross-backend equivalence test is the contract).
*
* Safety model:
* - **tighten-only by default**: a NORMAL write may only ADD denies / NARROW allowlists / NARROW allowDirs. A
* write that LOOSENS (removes a deny, widens an allowlist/allowDirs) is REFUSED unless it is an OPERATOR
* write. core has NO role system — `operator` is a boolean the caller FREEZES from a verified principal
* (mirrors `steer.trusted`); core never adjudicates operator-ness, it only honors the flag.
* - **CAS rev** (optimistic concurrency): a `putRules` with a stale `expectedRev` is rejected, so two replicas
* can't lose an update (mirrors the checkpoint store's `resolve` OCC).
* - **one grammar**: the two rule buckets hold rule ENTRIES — the grammar every entry list reads, through its one reader
* (`readRuleEntry`, allow-entry.ts): a tool name, a protocol namespace, a shell command rule (`Bash(git push:*)`), a
* path rule, a name or host rule. Entries are stored as written; every station that validates, compares, matches or
* names one reads that reader's answer ({@link judgeSessionRulesWrite}, {@link loosenReasons}, the run-time layer).
* - **a row keeps its grammar**: the write judge stamps the grammar it read the entries in ({@link SESSION_RULES_GRAMMAR}), and
* every station reads a row's entries in the grammar the row carries ({@link readSessionRulesShape}) — a row stored before
* the stamp keeps the reading it was written under, so an upgrade never turns a stored entry into a grant.
* - **rules are read as rules or not at all**: rules whose shape cannot be read, or whose toolDeny holds an entry no grammar
* reads ({@link unreadableDenyEntries}), refuse every call until an operator writes them afresh — never read as fewer.
* - **NOT A SANDBOX**: a shell command rule reads the command line the call carries (every program run in its connector
* chain); the real isolation boundary is the executionEnv, never these rules.
*/
import { compileAllowList, type AllowEntryInert, type AllowEntryInertDetail, type AllowEntryInertReason } from "./allow-entry.js";
import type { EngineNotice } from "./engine-notice.js";
/** The per-session rules. All optional; an absent bucket imposes no constraint of that kind. */
export interface SessionPermissionRules {
/** If set, ONLY the calls these entries admit are allowed — a narrowing allowlist: a call no entry admits is refused.
* Rule entries read in the allow direction (`Read`, `mcp__srv`, `Bash(git:*)`, `Edit(src/**)`). */
toolAllow?: string[];
/** Entries whose calls are always refused (deny wins) — rule entries read in the deny direction (`Bash`, `Bash(rm:*)`,
* `mcp__srv__delete_*`, `Read(//etc/**)`). */
toolDeny?: string[];
/** If set, WRITE tools (`Write`/`Edit`) may only target paths within these dirs; a write-capable
* tool that can't be path-confined (e.g. `Bash`) is denied while this is set. Stored RAW; resolved at run time. */
allowDirs?: string[];
}
/** Every bucket a rules object holds, and how its entries are read: a rule-entry list in one direction, or the write
* confinement's directories. A key outside this table is no bucket ({@link readSessionRulesShape} refuses it). */
declare const SESSION_RULE_BUCKETS: {
readonly toolAllow: "allow";
readonly toolDeny: "deny";
readonly allowDirs: "dirs";
};
/** A bucket that holds rule entries. */
export type SessionRuleEntryField = {
[K in keyof typeof SESSION_RULE_BUCKETS]: (typeof SESSION_RULE_BUCKETS)[K] extends "dirs" ? never : K;
}[keyof typeof SESSION_RULE_BUCKETS];
/**
* The rule grammar this engine reads a session's rule entries in: the `grammar` the write judge stamps on every rules value it
* judges ({@link judgeSessionRulesWrite}), so every store that writes through it keeps the stamp on the row. It counts
* revisions of what an entry MEANS and moves only when that changes:
* - 0, the reading before the one entry reader: a bucket's entries were tool names compared as written, so an allow entry the
* reader now reads as speaking for another name (`Bash(rm:*)`, `Bash(*)`) named no tool and admitted nothing — and so did
* `*`, one more name no tool carried, which voided nothing beside it;
* - 1, the one entry reader taking its tool name from the lenient rule parser (trimmed before a `(`, any character but a
* blank, a parenthesis or a control / format character);
* - 2, the rule syntax's tool-name head (`ruleSpellingOf`): a name is read as written, in the characters names are minted in.
* An entry this grammar reads as no rule in a row an earlier grammar wrote reads as `earlier_grammar` ({@link compileSessionRuleBucket}).
*/
export declare const SESSION_RULES_GRAMMAR = 2;
/** {@link SessionPermissionRules} plus the monotonic `rev` the store stamps (the OCC key). */
export interface StoredSessionRules extends SessionPermissionRules {
rev: number;
/** The row's LINEAGE: which generation of this key the row belongs to. `rev` counts within one lineage; an
* erase (`deleteBySession`) ends it, and the next write on the key starts the next generation, so a reader
* can tell a recreated row from a continuation of the one it enforces whatever `rev` the new row wears.
* Absent = generation 0 (a backend that predates the mark, or a row written before it). */
gen?: number;
/** The rule grammar the row's entries are read in ({@link SESSION_RULES_GRAMMAR}), stamped by the write judge. Absent =
* a row stored before the stamp: grammar 0. */
grammar?: number;
}
/** A rules value as the shape reading hands it on ({@link readSessionRulesShape}): its buckets, and the grammar its entries
* are read in — resolved, so a store that serves it on beside its own counters serves the row's grammar with it. */
export type SessionRulesInGrammar = SessionPermissionRules & {
grammar: number;
};
/** Options for {@link SessionPolicyStore.putRules}. */
export interface PutRulesOptions {
/** CAS: reject the write if the stored `rev` is not this (optimistic concurrency). Omit to skip the check. */
expectedRev?: number;
/** When true, this write MAY loosen the rules (remove a deny / widen an allowlist or allowDirs). A NORMAL
* (non-operator) write is TIGHTEN-ONLY. core does NOT verify operator-ness — the caller (service) FREEZES
* this from a verified, role-checked principal (mirrors `steer.trusted`). */
operator?: boolean;
}
/** The {@link SessionPolicyError.detail} of an `invalid_shape` refusal: which key, and what it holds — its TYPE, never its
* value (`a string`, `null`, `an array holding an entry that is not a string`), or `not a rule bucket` for a key outside
* the buckets. `field` is empty when the rules value itself is not an object. */
export interface SessionRuleShapeRefusal {
field: string;
got: string;
}
/** The {@link SessionPolicyError.detail} of an `inert_entry` refusal: which bucket, which entry, and why it reads as nothing —
* the entry reader's own answer (`readRuleEntry`), reason and facts unchanged. */
export interface SessionRuleEntryRefusal extends AllowEntryInertDetail {
field: SessionRuleEntryField;
entry: string;
reason: AllowEntryInertReason;
}
/** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden`
* = a non-operator write tried to relax the rules; `corrupt` = a backend could not read what it stored (a row, or the
* erase-surviving rev mark of {@link SessionPolicyStore.deleteBySession}) and refuses rather than read it as fewer rules or
* stamp an unproven rev; `invalid_shape` = the rules value is not an object of buckets, each a list of strings
* ({@link readSessionRulesShape}); `inert_entry` = a bucket holds an entry the entry reader reads as nothing. `detail` is
* present on the last two codes and only on them ({@link SessionRuleShapeRefusal}, {@link SessionRuleEntryRefusal}). */
export declare class SessionPolicyError extends Error {
readonly code: "conflict" | "loosen_forbidden" | "corrupt" | "invalid_shape" | "inert_entry";
readonly detail?: (SessionRuleShapeRefusal | SessionRuleEntryRefusal) | undefined;
constructor(code: "conflict" | "loosen_forbidden" | "corrupt" | "invalid_shape" | "inert_entry", message: string, detail?: (SessionRuleShapeRefusal | SessionRuleEntryRefusal) | undefined);
}
/** Construction options both bundled stores take. */
export interface SessionPolicyStoreOptions {
/** The names this deployment mounts its OWN tools under — the roster the store reads a rule entry's tool name against
* (`readRuleEntry`): a retired name declared here is an ordinary name to the write and to the loosening check alike.
* Absent = none. Each name keeps the caller tool name contract a deployment tool's name keeps at the mount
* (`callerToolNameProblem`: a name a rule can name, no `__`); one that does not is refused at construction. */
mountedToolNames?: Iterable<string>;
}
/** A store's declared roster from {@link SessionPolicyStoreOptions.mountedToolNames} — the one reader both bundled stores
* (and so `FileStorageBackendOptions.sessionPolicy`) construct through, refusing loudly when it is not a list of plain
* tool names (a bare string would iterate by character). */
export declare function readMountedToolNames(value: unknown): ReadonlySet<string>;
/**
* The seam a deployment implements for DURABLE, cross-replica per-session rules. Unset on `RunnerDeps` ⇒ the
* feature is OFF (no rules read, zero behavior change). Keyed on `(sessionId, principal-owner)`: core passes
* the running task's `principal` as the owner key; ownership/operator AUTHORIZATION is the implementation's job
* (core has no identity system). `getRules` returns the rules to ENFORCE for a task; `putRules` writes them.
*/
/** A (principal, rules) pair for one session — the unit {@link SessionPolicyStore.listBySession} returns. */
export interface SessionRulesRecord {
/** The principal these rules apply to (undefined = the session-wide default). */
principal: string | undefined;
rules: StoredSessionRules;
}
export interface SessionPolicyStore {
getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
/** Write the rules for a key through the one write judge ({@link judgeSessionRulesWrite}): CAS on `expectedRev`, the
* shape, every entry's reading (operator writes included), then, for a non-operator write over an existing row, the
* tighten-only check — each refusal a {@link SessionPolicyError}, and nothing is written. */
putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise<StoredSessionRules>;
/**
* 2c session-sync: every (principal, rules) record for `sessionId` across ALL principals — `getRules`
* is per-(sessionId, principal), but a cross-backend session EXPORT must bundle the WHOLE session's policy.
* The importer replays each via `putRules(sessionId, record.principal, record.rules)`. Optional — a backend
* that supports cross-backend policy export implements it (local backend uses the in-memory/file impl).
*/
listBySession?(sessionId: string): Promise<SessionRulesRecord[]>;
/**
* 2c session-sync, the ERASE half (server C1b, 2026-07-26): remove EVERY (principal, rules) record for
* `sessionId`. An importer that OVERWRITES a session must be able to erase the abandoned branch's policy
* rows — without this seam they survive the overwrite and keep enforcing the old branch's rules against
* the new content. Optional like {@link listBySession} (a sync-capable backend implements both); the two
* bundled impls do. Idempotent: deleting a session with no records is a no-op.
*
* @contract session_policy.rev_survives_delete — a key's `rev` (one key = `(sessionId, principal)`, the unit
* a row is stored under) is a HIGH-WATER MARK that survives this erase: the first `putRules` on that key
* after a `deleteBySession` stamps a `rev` strictly greater than every rev the key ever carried, so no later
* row of that key reads a smaller `rev` than an earlier one (another principal's row on the same session
* counts on its own), and a
* tighten-only (non-operator) write landing after an erase cannot slip under a floor a reader already
* enforced. A backend that reads "no row" as rev 0 must therefore keep the mark past the row (the bundled
* in-memory store and a deployment's SQL twin both do). The erase also ends the row's LINEAGE: the next
* write on the key carries the next `gen`, so a reader that enforces a row treats a later row of another
* generation as not comparable whatever its rev — a recreated row cannot pass for a continuation.
*/
deleteBySession?(sessionId: string): Promise<void>;
}
/**
* LEXICAL path containment for the tighten gate. **Must agree with the runtime's view of what a dir IS** (the
* runtime resolves `..`/`.`/`//` via `canonicalizeTarget`): both args are `posix.normalize`d FIRST so a
* non-operator can't carry across a write-confinement OUTSIDE the prior dir by writing `<prior>/../etc` — which is
* lexically "within `<prior>`" by a naive `startsWith` but canonicalizes to `/etc` at run time (the tighten
* gate must never UNDER-report a loosening).
*
* The `..`-resolving `posix.normalize` pass is THIS function's own job (the tighten gate's `..`-carrying across
* defense above) and is NOT shared with the runtime's `isWithin` (session-rule-policy.ts), which compares
* already-canonicalized keys and never resolves `..` itself — the two functions agree on every OTHER input
* (same win-family fold, same prefix semantics) but diverge on inputs that still carry an unresolved `..`,
* which is exactly the tighten-gate-only case this function exists to catch.
*
* RESIDUAL (documented, bounded): the gate is LEXICAL and cannot follow symlinks at write time, while the
* runtime IS symlink-aware. A stored allowDir whose LAST component is a pre-existing symlink to outside the
* prior set would be seen as "within" by this gate yet resolve elsewhere at run time. This is bounded: while an
* `allowDirs` confinement is active the runtime DENIES `bash` (non-path-confinable), so a confined non-operator
* cannot CREATE such a symlink — it must pre-exist on the deployment's filesystem. Operators / the durable
* backend should keep `allowDirs` to real directories; a future symlink-aware write-time canonicalize closes it.
*/
declare function lexWithin(root: string, p: string): boolean;
/**
* THE ONE SHAPE READING of a rules value — what every station that takes one reads it through: the write (incoming rules),
* a store's read-back of its own rows, the tighten comparison's prior, the run-time layer's served row. A rules value is an
* object whose keys are buckets ({@link SessionPermissionRules}); each bucket present is a list of strings (`allowDirs`: of
* non-blank strings — a directory is no blank). A bucket's TYPE is all this reads: what an entry means is the entry reader's,
* in the grammar the value is written in ({@link grammarOf}), handed on as `grammar`. The store's counters (`rev`, `gen`) are
* passed over, and a key holding `undefined` holds nothing. Anything else is a fault, named by key and by TYPE, never by value —
* a bare string is never one rule and never its characters (`"rm"` spread into `["r","m"]`), and a key outside the buckets is
* never dropped (a dropped deny is a deny nobody enforces).
*/
export declare function readSessionRulesShape(value: unknown): {
rules: SessionRulesInGrammar;
} | {
fault: SessionRuleShapeRefusal;
sentence: string;
};
/** The CANONICAL stored form of a rules value: its shape read ({@link readSessionRulesShape} — a fault refuses with
* `invalid_shape`), every list copied (detached from the caller's arrays), and `allowDirs` lexically `posix.normalize`d so the
* stored form is `..`-free and the tighten diff compares the form the runtime resolves. Both bundled stores and a durable twin
* stamp exactly this, so a structuredClone and a JSON round-trip of it are byte-identical. */
export declare function normalizeRules(rules: unknown): SessionPermissionRules;
/**
* THE ONE ANSWER to "can these rules' denies be read?": the `toolDeny` entries the list reading ({@link compileSessionRuleBucket},
* in the rules' own grammar) reads as no rule at all. Rules whose deny bucket holds one cannot be read, and rules that cannot be
* read are never read as fewer: they refuse every call (the run-time layer) and a non-operator write over them is a loosening
* ({@link loosenReasons}) — until an operator writes them afresh, and the write judge refuses such an entry. Unlike a definition
* file a plugin ships, which its user cannot edit, a session's rules are written by the session's own operator and can be
* rewritten, so a deny nobody can read costs one write instead of quietly guarding nothing. An entry the grammar reads that names
* no mounted tool (or `*`) is not here: it names nothing and refuses nothing, the rest of the bucket refusing as written.
* `roster` as in {@link compileSessionRuleBucket}; it never changes this answer (a spelling is read before a name is judged).
*/
export declare function unreadableDenyEntries(rules: SessionRulesInGrammar, roster?: ReadonlySet<string>): AllowEntryInert[];
/**
* THE LIST READING of one rule bucket of a rules value the shape reading read — what the run-time layer enforces, the tighten
* comparison compares and the write judge refuses on: `compileAllowList` over the one entry reader, in the bucket's direction,
* against `roster` when given, in the grammar the value is written in. An entry its grammar reads as nothing
* ({@link earlierGrammarReads}) is moved to the entries that read as nothing, reason `earlier_grammar`, before the list is
* compiled: it is one entry that reads as nothing, and the list beside it is read as written. The move is an ENTRY's reading and
* never the list's: an allow list a `*` voids (in any spelling the voiding test reads as `*`, `* (*)` included) is voided in
* every grammar from 1 on, so in such a row the list is read whole, as written — only grammar 0, where `*` was one more name, takes
* it out alone.
*/
export declare function compileSessionRuleBucket(rules: SessionRulesInGrammar, field: SessionRuleEntryField, roster?: ReadonlySet<string>): ReturnType<typeof compileAllowList>;
/**
* The advisory for the entries of one bucket a stored row's earlier grammar reads as nothing ({@link compileSessionRuleBucket},
* `earlier_grammar`; `grammar` = the row's): the session, the bucket, the entries as written, what they do and the way out. An
* entry this engine's grammar reads as no rule is named with the reader's word — in `toolAllow` it admits nothing, in `toolDeny`
* the rules cannot be read and every call is refused ({@link unreadableDenyEntries}); the way out is a rewrite leaving it out
* (the write judge refuses an entry it cannot read). A grammar-0 allow entry that named no tool there is told as it always was:
* to make a parenthesized one the rule it now spells, or (`*`, which is no allow rule in any grammar) to leave it out.
*/
export declare function earlierGrammarAdvisory(sessionId: string, entries: readonly string[], field: SessionRuleEntryField, grammar: number): string;
/** The reader's word for one entry an earlier grammar reads as nothing: `no_rule` — this engine's rule grammar reads it as no
* rule; `named_no_tool` — a grammar-0 allow entry the bare-name comparison of its day read as naming no tool. */
export type EarlierGrammarReader = "no_rule" | "named_no_tool";
/**
* The notice for the entries of one bucket a stored row's earlier grammar reads as nothing (`config.session_rules_earlier_grammar`,
* audience `user` — the session's rules are what the person's calls are judged by): the message is {@link earlierGrammarAdvisory}
* verbatim, and `detail` carries the same facts for a machine — `bucket` (the field), `storedGrammar` (the row's), `entries` (every
* entry, each as `noticeRuleText` hands rule text to a notice, with its reader's word), `rewrite` (the way out: an operator write of
* a new value without the stored row's rev, which stamps `stampsGrammar`), `rulesSessionId` (whose rules — an ancestor's on a
* delegated child's inherited layer) and `sessionId` (the run that read them).
*/
export declare function earlierGrammarNotice(input: {
rulesSessionId: string;
sessionId: string;
entries: readonly string[];
field: SessionRuleEntryField;
grammar: number;
}): EngineNotice;
/** The clause a run-time refusal carries when the allowlist it judged by holds entries its earlier grammar reads as nothing
* ({@link compileSessionRuleBucket}; `grammar` = the row's): the grammar the rules are stored in, then which entries name no tool
* there (grammar 0: `*` by its spelling, the parenthesized ones counted) and how many this engine's grammar reads as no rule —
* counted, since a stored entry's own text is data and never reaches a sentence the model reads. Empty when none. */
export declare function earlierGrammarRefusalClause(entries: readonly string[], grammar: number): string;
/** A layer's delivery of the entries its snapshots' earlier grammar reads as nothing ({@link compileSessionRuleBucket},
* `earlier_grammar`), one bucket at a time with the grammar its row is written in, to `sink`: once per distinct set per layer
* (the ledger is this closure's), an empty set is no news, and a throwing sink never changes what the snapshot enforces. */
export declare function earlierGrammarAnnouncer(sink: ((entries: readonly string[], field: SessionRuleEntryField, grammar: number) => void) | undefined): (entries: readonly string[], field: SessionRuleEntryField, grammar: number) => void;
/**
* THE ONE WRITE JUDGE every store applies before it stamps a row (both bundled stores, and a durable backend at the same point):
* the rules to stamp, or a {@link SessionPolicyError}, in this order — `conflict` (a stale `expectedRev` against `prior`'s rev),
* `invalid_shape` ({@link normalizeRules}), `inert_entry` (an entry that reads as nothing in the grammar the rules are written
* in; operator writes included — a rule that names nothing guards nothing whoever writes it), then, for a non-operator write
* over an existing row, `loosen_forbidden` ({@link loosenReasons}) — or `corrupt` when that prior cannot be read as rules,
* which the comparison never reads as fewer. The rules to stamp carry `grammar` = {@link SESSION_RULES_GRAMMAR}: what passed
* the judge means the same in this engine's grammar, and THIS is the one place the stamp is minted, so every store that
* stores what the judge returns keeps it. `mountedToolNames` = the store's declaration ({@link SessionPolicyStoreOptions.mountedToolNames}).
*/
export declare function judgeSessionRulesWrite(prior: StoredSessionRules | null, rules: unknown, opts: PutRulesOptions | undefined, mountedToolNames?: ReadonlySet<string>): SessionPermissionRules & {
grammar: number;
};
/**
* Pure tighten-only diff: the reasons `next` LOOSENS `prior` (empty ⇒ `next` is a pure tightening or no-op), over the EFFECTIVE
* entries of each rule bucket — the list as the run-time layer enforces it ({@link effectiveEntries}). Rules whose toolDeny holds
* an entry no grammar reads ({@link unreadableDenyEntries}) refuse every call: as `next` they loosen nothing, and as `prior`
* every readable `next` loosens them (one reason, naming the entries). Any other entry that reads as
* nothing is in neither side's effect, so removing one relaxes nothing and adding one widens nothing (the way a session's owner
* takes out an entry that names nothing); an allowlist a `*` voids (in this engine's grammar; in grammar 0 a `*` is one such entry beside the rest) admits nothing, so dropping the `*` widens. toolDeny: removing an effective entry loosens. toolAllow: removing the list
* (→ everything allowed) or adding an effective entry outside the prior set loosens; prior-undefined → any defined next only
* narrows. allowDirs: removing confinement, or adding a dir not within some prior dir, loosens (coarse raw containment).
* Entries compare as written, each side's in the grammar that side is written in (a stored row's entry that its grammar reads as
* nothing is in no effect, so writing it in this engine's grammar widens). Both sides are read through
* {@link readSessionRulesShape} first: a side it cannot read (a key that is no bucket — a retired one included) throws
* `invalid_shape` rather than compare without it. `mountedToolNames` = the store's declaration
* ({@link SessionPolicyStoreOptions.mountedToolNames}; empty when it has none) — the write judge's too.
*/
export declare function loosenReasons(priorValue: SessionPermissionRules, nextValue: SessionPermissionRules, mountedToolNames?: ReadonlySet<string>): string[];
/**
* Does row `a` belong to the SAME lineage as row `b` — the one question every reader asks before it compares
* two rows' `rev`, asked in ONE place so a `gen` that is ABSENT cannot mean three different things at three
* stations. It means what {@link StoredSessionRules.gen} says it means: generation ZERO. A row that spells
* `gen: 0` and a row that omits it are therefore the same row's generation, and a row of generation 1 does not
* continue either of them — which is the case the readings used to disagree on, each treating absence as
* "unknown, so do not judge" and letting a recreated row pass for a continuation of the one it enforced.
*
* ABSENCE STILL CARRIES ONE OTHER FACT, and it is a different question: a backend that stamps no lineage at
* ALL gives its reader no recycle evidence, so a reader that has one of its own (it watched the row vanish)
* keeps using it. That test asks whether the FIELD is there, never what it compares to, and it is the caller's
* to make — this predicate answers only "same lineage or not".
*/
export declare function continuesLineage(a: {
gen?: number;
}, b: {
gen?: number;
}): boolean;
/**
* In-process default {@link SessionPolicyStore} (single-replica). A deployment that needs cross-replica
* durability supplies its own backend with the SAME CAS-rev + tighten-only semantics. Single-threaded JS makes
* the read-check-write trivially atomic here; a durable backend folds it into a CAS WHERE clause.
*/
export declare class InMemorySessionPolicyStore implements SessionPolicyStore {
private readonly map;
/** {@link SessionPolicyStoreOptions.mountedToolNames}, read once at construction. */
private readonly mountedToolNames;
constructor(opts?: SessionPolicyStoreOptions);
/**
* The per-key mark {@link SessionPolicyStore.deleteBySession} may not erase: the largest `rev` this key has
* ever been stamped with, kept outside the row so removing the row cannot roll the counter back. Written by
* every stamp, read by the next one, never read by anything else, and never collected — an erase frees the
* row and keeps this, so the cost is one integer per distinct key the process has ever written.
*/
private readonly highWater;
private key;
getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise<StoredSessionRules>;
listBySession(sessionId: string): Promise<SessionRulesRecord[]>;
deleteBySession(sessionId: string): Promise<void>;
}
/** Test-only escape hatch for the module-private tighten-gate containment predicate (mirrors the
* `__testables` convention in file-snapshot-store.ts) — lets a regression pin the fold's win-family
* conditionality directly rather than only through `loosenReasons`'s allowDirs branch. */
export declare const __testables: {
lexWithin: typeof lexWithin;
};
export {};