@sema-agent/core
Version:
Stateless, task-oriented AI agent core
242 lines (241 loc) • 15 kB
TypeScript
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 {};