UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

242 lines (241 loc) • 15 kB
import { type ToolCallRequest } from "./tool-policy.js"; import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js"; import { type PersistedRule } from "./permission-rule-model.js"; import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js"; import { type EngineNotice } from "./engine-notice.js"; /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. */ export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"]; export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number]; /** One entry the grammar reads (kind closed): a whole non-shell tool; a protocol namespace; the whole shell tool; a shell * command rule; the Read directory form; a deny-side path pattern (a skill's `disallowed-tools`). */ export type CompiledAllowEntry = { readonly kind: "tool"; readonly name: string; } | { readonly kind: "namespace"; readonly pattern: string; } | { readonly kind: "shell"; readonly name: string; } | { readonly kind: "command"; readonly rule: PersistedRule; } | { readonly kind: "read_dir"; readonly rule: PersistedRule; } | { readonly kind: "path"; readonly rule: PersistedRule; }; /** Read one entry under one direction. */ export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny"): CompiledAllowEntry | AllowEntryInertReason; /** * Compile one list WHOLE: the entries it reads and the ones it reads as nothing. An allow list holding `*` voids the list * (every other entry on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list reads * as nothing by itself and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside it. */ export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny"): { rows: Array<{ entry: string; compiled: CompiledAllowEntry; }>; inert: Array<{ entry: string; reason: AllowEntryInertReason; }>; }; /** The programs whose open allow rule the reference implementation drops in auto mode — its interpreter / launcher table * plus the shells and the run-anything builtins (2.1.280). A program that only reaches the network is not here. */ export declare const AUTO_STRIPPED_PROGRAMS: readonly ["python", "python3", "python2", "node", "deno", "tsx", "ruby", "perl", "php", "lua", "npx", "bunx", "npm run", "yarn run", "pnpm run", "bun run", "bash", "sh", "ssh", "zsh", "fish", "eval", "exec", "env", "xargs", "sudo"]; /** * Does auto mode strip this allow entry for this call (see the module doc)? Read for every rule-shaped member — a list * entry, and a person's stored row through {@link autoStrippedRule}; a hook's allow is not an entry and is never stripped. * A stripped entry takes no part in the match (the reference removes it before matching), so it neither clears nor is * disclosed. */ export declare function autoStripped(compiled: CompiledAllowEntry, call: ToolCallRequest | undefined): boolean; /** {@link autoStripped} over a person's stored ALLOW row (a deny / ask row is never stripped; a path row never is). */ export declare function autoStrippedRule(rule: PersistedRule, call: ToolCallRequest): boolean; /** One row a list matcher reads. */ export interface AllowEntryRow { readonly behavior: "allow" | "deny"; readonly entry: string; readonly compiled: CompiledAllowEntry; } /** The call facts a content row is judged against — the persisted lane's own bases. */ export interface AllowEntryJudgeContext { readonly root: string | undefined; readonly sessionId: string | undefined; readonly liveCwd: () => string | undefined; readonly home: string | undefined; } /** * The ONE list matcher, both directions — the rows that speak for the call, or undefined. ALLOW: the first name row that * reaches the call, else the command rows' coverage (the persisted allow matcher — the whole command, or each segment * covered by a row or read-only by itself), else the Read directory rows. DENY: a name row, a command row whose program run * the call performs (the wide tightening reading; a call that cannot be read against the row is refused too), or a path row * the call's target reaches. */ export declare function judgeAllowEntries<R extends AllowEntryRow>(rows: readonly R[], req: ToolCallRequest, behavior: "allow" | "deny", ctx: AllowEntryJudgeContext): readonly R[] | undefined; /** The allow rows of a list, as the allow layer reads them in `mode`: the strip applied BEFORE the match (in `auto`). */ export declare function allowingRows<R extends AllowEntryRow>(rows: readonly R[], req: ToolCallRequest, mode: PermissionMode | undefined): readonly R[]; /** The two sources {@link AllowEntryTable} holds (a skill's list is the skill table's). */ export type AllowEntrySource = "request" | "deployment"; /** What spoke for a call: the entries it stands on, as the list spells them. */ export interface AllowEntryHit { readonly entries: readonly string[]; } /** The face the gate reads one entry list through. */ export interface AllowEntryLane { /** The allow layer's member: what grants this call in this mode, or undefined. */ allowing(req: ToolCallRequest, mode: PermissionMode | undefined): AllowEntryHit | undefined; } /** The gate inputs the two entry lists ride (a member group of `ToolGateInput`). Each is the allow layer's member for its * source — read under the one consumption predicate beside a person's stored rows and a skill's list, clearing only what an * allow rule may clear — with its attribution sink (`onAllowed` observes a call it cleared; never affects the outcome). * Absent ⇒ that source has no rows. */ export interface AllowEntryGateSeats { deploymentAllows?: AllowEntryLane & { onAllowed?: (info: { toolName: string; toolCallId: string; entries: readonly string[]; }) => void; }; requestAllows?: AllowEntryLane & { onAllowed?: (info: { toolName: string; toolCallId: string; entries: readonly string[]; }) => void; }; } /** * One source's allow rows, compiled once: the request's (prepare seeds it from the spec's snapshot and, in a delegated * child, from the parent's capture) or the deployment's (compiled from the Runner's snapshot). Allow rows only — neither * list has a deny side. Identity is the contract: prepare seeds it, the delegation capture and the gate's lane read it. */ export declare class AllowEntryTable { private held; /** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. */ seed(entries: readonly string[]): ReadonlyArray<{ entry: string; reason: AllowEntryInertReason; }>; /** The entries held, as written (a copy) — what a delegated child is handed. */ entries(): readonly string[]; /** The lane over this table, bound to the leg's call facts. */ lane(ctx: AllowEntryJudgeContext): AllowEntryLane; } /** * Seed one leg's request and deployment tables — a delegated child's captured request entries first (an entry that reads as * nothing was announced where it was written), then the leg's own list; the deployment's (announced once, at construction) — * and return the dead entries of the leg's OWN lists, the skills' beside the request's, for its one start-of-run notice. */ export declare function seedAllowEntryTables(tables: { readonly request: AllowEntryTable; readonly deployment: AllowEntryTable; }, lists: { skillInert: ReadonlyArray<{ skill: string; list: "allowed-tools" | "disallowed-tools"; entry: string; reason: AllowEntryInertReason; }>; inherited: readonly string[] | undefined; request: readonly string[] | undefined; deployment: readonly string[] | undefined; }): AllowEntryInertNote[]; /** The deployment's allow rules, read ONCE at construction: screened (a value that is not a list of strings refuses), frozen, * and its dead entries announced once (no session). Returns the deps with the snapshot in place of the caller's list. */ export declare function withDeploymentAllowedTools<D extends { readonly allowedTools?: readonly string[]; readonly onNotice?: ((n: EngineNotice) => void) | undefined; }>(deps: D): D; /** The code a list value that is not a list of strings refuses with — the policy compiler's family, with its issues. */ export declare const INVALID_PERMISSION_RULES_CODE = "config.invalid_permission_rules"; /** * Screen and snapshot one `allowedTools` value (`seat` names the key in the refusal): absent ⇒ undefined; a list of strings * ⇒ a frozen copy (the one read every later reader takes); anything else refuses loudly with * {@link INVALID_PERMISSION_RULES_CODE} and an issue per defect — never dropped, never read as "no entries". */ export declare function screenAllowedToolsValue(value: unknown, seat: string): readonly string[] | undefined; /** The disclosure record for one member, minted from the entries it matched: each sanitized (deployment or model-authored * text on a human trust boundary) and capped, the rest counted. */ export declare function allowRuleShadowOf(member: AllowRuleMember, entries: readonly string[]): AllowRuleShadow; /** Read a disclosure value off a decision WITHOUT trusting it (a policy's own value, an ancestor's): every record a known * member with a list of strings, or nothing — a display member must never throw where a card is minted. */ export declare function readAllowRuleShadows(value: unknown): readonly AllowRuleShadow[] | undefined; /** Why a matched entry did not clear the ask, by the ask's class — one row per class, so a class added later is a * compile error here rather than somebody else's reason on the card. Two facts the ask carries are read first, because * they name the reason more exactly than a class shared by several: a table word (`mandate` — the exact mark) and the * deployment's own authorship (`authoredBy`). */ declare const SHADOW_REASON: { readonly content_question: "the call is the engine's own question to a person"; readonly hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person"; readonly ancestor: "the task that delegated this one owns this question"; readonly unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule"; readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule"; readonly org_rule: "an organization policy rule requires approval for this call"; readonly governance_unavailable: "this deployment is org-governed and cannot currently read its organization policy"; readonly rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown"; readonly ask_rule: string; readonly denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides"; readonly requires_user_interaction: "this tool's server declares that a call must be put to a person"; readonly operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule"; readonly shell_floor: string; readonly write_protection_unresolved: string; readonly probe: string; readonly write_protection: string; readonly tool_marks_mcp: string; readonly tool_marks_a2a: string; readonly policy_rra: "the deployment's policy requires a real approval for this call"; readonly artifact_shared_read: "this read targets another task's shared artifact — it is cleared by confirming this call"; readonly mode: "the permission mode puts this question to a person"; readonly unstated: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule"; readonly ordinary: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule"; }; /** The fence over the reason table: `never` while every class has a row. */ export type ShadowReasonTableCoversEveryClass = AssertAllKeysHandled<Exclude<AskClass, keyof typeof SHADOW_REASON>>; /** * The disclosure in the sentence a PERSON reads — rendered where a card or a park row is minted, never onto the decision * the classifier reads: "<members> match(es) this call but do(es) not clear the ask — <why>". */ export declare function allowRuleShadowSentence(shadows: readonly AllowRuleShadow[], ask: { askClass?: AskClass; mandate?: PersistedRuleMandate; authoredBy?: "deployment"; }): string; /** The card's two disclosure members off a surviving ask, minted at every station that builds an approval request: the * sanitized records, and the message with the sentence rendered onto it. */ export declare function cardDisclosureOf(decision: { message?: string; allowRuleShadowed?: unknown; askClass?: AskClass; mandate?: PersistedRuleMandate; authoredBy?: "deployment"; }, fallbackMessage: string): { message: string; allowRuleShadowed?: readonly AllowRuleShadow[]; }; /** The notice for list entries that grant (or refuse) nothing — one code for every list. */ export declare const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted"; /** One entry that reads as nothing, and the list it came from (a skill's names the skill and which of its two lists). */ export type AllowEntryInertNote = { readonly source: "skill"; readonly skill: string; readonly list: "allowed-tools" | "disallowed-tools"; readonly entry: string; readonly reason: AllowEntryInertReason; } | { readonly source: AllowEntrySource; readonly entry: string; readonly reason: AllowEntryInertReason; }; /** * The notice for entries this grammar reads as nothing — delivered only when one exists: once per prepared task for the * skill and request lists (`sessionId` given), once per Runner for the deployment's list. The person whose list it is * reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more). */ export declare function allowedToolsNotGrantedNotice(inert: readonly AllowEntryInertNote[], sessionId: string | undefined): EngineNotice; export {};