@sema-agent/core
Version:
Stateless, task-oriented AI agent core
468 lines (467 loc) • 24.9 kB
JavaScript
import { heldBy } from "./tool-name-readings.js";
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "./permission-rules.js";
import { parsePermissionRule, ruleSpellingOf } from "./permission-rule-syntax.js";
import { judgeRuleToolName, splitNamespacedName } from "./tool-name-aliases.js";
import { catalogToolNames, isShellFaceToolName } from "./tool-registry.js";
import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
import { COMMAND_RULE_TOOL, adjudicatePersistedDomainRules, adjudicatePersistedNameRules, commandCoverageOf, domainRuleSubjectOf, laneToolNamesOf, nameRuleSubjectOf, noticeRuleText, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
import { callTargetReadingOf } from "./effective-path-target.js";
import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
import { mandateDisclosureOf } from "./ask-origin.js";
import { inlineUntrusted } from "./untrusted-text.js";
import { valueTypeLabel } from "./tool-name-list.js";
import { deliverEngineNotice } from "./engine-notice.js";
export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "content_unsupported", "grammar", "tool_name", "near_miss", "earlier_grammar"]);
const NEAR_MISS_CARRIER = { run: "the mounted spelling is", pool: "a delegation pool carries", catalog: "the engine's tool catalog names" };
export function nonStringMemberLabel(value) {
return `<not a string: ${valueTypeLabel(value)}>`;
}
export function nonListValueLabel(read) {
return read.kind === "not_a_list" ? `<not a list: ${read.type}>` : "<a list whose entries cannot be read>";
}
function ruleRowOf(parsed) {
return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
}
function mountedSpellingOf(name, roster) {
const folded = name.toLowerCase();
for (const mounted of roster)
if (mounted.toLowerCase() === folded)
return mounted;
return undefined;
}
export function nearMissSpellingOf(name, roster) {
if (roster.has(name) || catalogToolNames().has(name) || splitNamespacedName(name) !== undefined)
return undefined;
return mountedSpellingOf(name, roster);
}
export function rootFacingToolNames(carried) {
return new Set([...catalogToolNames(), ...carried]);
}
function toolNameReading(name, roster) {
const verdict = judgeRuleToolName(name, roster);
if (verdict.kind === "unprefixed_namespace")
return { kind: "inert", reason: "tool_name", verdict: verdict.kind };
if (verdict.kind !== "current") {
return { kind: "inert", reason: "tool_name", verdict: verdict.kind, guidance: verdict.guidance, ...(verdict.currentName !== undefined ? { currentName: verdict.currentName } : {}) };
}
const mounted = nearMissSpellingOf(name, roster);
return mounted === undefined ? undefined : { kind: "inert", reason: "near_miss", currentName: mounted };
}
export function readRuleEntry(raw, behavior, roster) {
const inert = (reason) => ({ kind: "inert", reason });
const row = (compiled) => ({ kind: "row", compiled });
if (typeof raw !== "string")
return { kind: "inert", reason: "grammar", valueType: valueTypeLabel(raw) };
const parsed = ruleSpellingOf(raw);
if (parsed === undefined)
return inert("grammar");
const name = parsed.toolName;
if (name === "*")
return inert("wildcard_all");
if (roster !== undefined) {
const stale = toolNameReading(name, roster);
if (stale !== undefined)
return stale;
}
if (parsed.ruleContent === undefined) {
const split = splitNamespacedName(name);
if (split !== undefined) {
if (split.peer.includes("*"))
return inert("namespace_wildcard");
if (isNamespacedCoveringRuleName(name))
return row({ kind: "namespace", pattern: name });
return row({ kind: "tool", name });
}
return row(isShellFaceToolName(name) ? { kind: "shell", name } : { kind: "tool", name });
}
const grammar = ruleToolGrammarOf(name);
if (grammar === undefined)
return inert("content_unsupported");
const read = parseRuleText(raw, behavior);
if ("rule" in read)
return row({ kind: grammar, rule: ruleRowOf(read.rule) });
if (grammar === "command" && behavior === "allow" && read.reject.code === "invalid.bare_interpreter_prefix") {
const wide = parseRuleText(raw, "deny");
if ("rule" in wide)
return row({ kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) });
}
return inert(inertReasonOf(read.reject.code));
}
export function compiledEntryName(compiled) {
switch (compiled.kind) {
case "tool":
case "shell":
return compiled.name;
case "namespace":
return compiled.pattern;
default:
return compiled.rule.tool;
}
}
function inertReasonOf(code) {
return code === "unsupported.tool" ? "content_unsupported" : "grammar";
}
export function compileAllowList(entries, behavior, roster) {
const voided = behavior === "allow" && entries.some(namesWholeRoster);
const rows = [];
const inert = [];
for (const entry of entries) {
const read = typeof entry !== "string" || !voided ? readRuleEntry(entry, behavior, roster) : { kind: "inert", reason: "wildcard_all" };
if (read.kind === "row")
rows.push({ entry, compiled: read.compiled });
else {
const { kind: _kind, ...why } = read;
void _kind;
inert.push({ entry: typeof entry === "string" ? entry : nonStringMemberLabel(entry), ...why });
}
}
return { rows, inert };
}
function namesWholeRoster(entry) {
return typeof entry === "string" && entry === entry.trim() && parsePermissionRule(entry).toolName === "*";
}
function answersTo(req, has) {
return heldBy("allowEntry.names", { has }, req);
}
export const AUTO_STRIPPED_PROGRAMS = Object.freeze([
"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",
]);
function shellContentStripped(content) {
if (content === undefined || content === "" || /^[\s*]+$/.test(content))
return true;
const n = content.trim().toLowerCase();
if (n === "*")
return true;
for (const program of AUTO_STRIPPED_PROGRAMS) {
const g = program.toLowerCase();
if (n === g || n === `${g}:*` || n === `${g} *` || n === `${g}*`)
return true;
if (n.startsWith(`${g} `) && n.endsWith("*")) {
const tail = n.slice(g.length + 1);
if (tail.startsWith("-") && !(/^python[\d.]*$/.test(g) && /^-m\s+\w+\.[\w.]+(\s*:|\s+)$/.test(tail.slice(0, -1))))
return true;
}
}
return false;
}
function shellRuleContent(rule) {
const split = splitShellCompoundSegments(rule.command, { trailingTerminator: "keep", redirection: "reject" });
const body = "reject" in split ? rule.command : (split.segments[split.segments.length - 1] ?? rule.command).trim();
return rule.match === "prefix" ? `${body}:*` : rule.match === "wildcard" ? `${body} *` : body;
}
export function autoStripped(compiled, call) {
switch (compiled.kind) {
case "shell":
return true;
case "tool":
return call !== undefined && answersTo(call, isShellFaceToolName);
case "command":
return shellContentStripped(shellRuleContent(compiled.rule));
case "namespace":
case "path":
case "name":
case "domain":
return false;
default: {
const exhaustive = compiled;
return exhaustive;
}
}
}
export function autoStrippedRule(rule, call) {
return rule.behavior === "allow" && rule.tool === COMMAND_RULE_TOOL && autoStripped({ kind: "command", rule }, call);
}
const NO_LEADS = new Map();
function nameRowReaches(compiled, req) {
const covers = compiled.kind === "namespace" ? (n) => namespacedRuleNameCovers(compiled.pattern, n) : compiled.kind === "tool" || compiled.kind === "shell" ? (n) => n === compiled.name : undefined;
return covers !== undefined && answersTo(req, covers);
}
function commandOf(req) {
const tool = laneToolNamesOf(req).find((n) => ruleToolGrammarOf(n) === "command");
const command = req.args?.command;
return tool !== undefined && typeof command === "string" ? { tool, command } : undefined;
}
function admissionCallOf(call, ctx, liveCwd, posture) {
return { tool: call.tool, command: call.command, cwd: ctx.root, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}), ...(liveCwd !== undefined ? { execCwd: liveCwd } : {}), posture };
}
export function uncoveredCommandSegment(rows, req, ctx, posture) {
const call = commandOf(req);
if (call === undefined)
return undefined;
const rules = rows.flatMap((r) => (r.behavior === "allow" && r.compiled.kind === "command" ? [r.compiled.rule] : []));
const coverage = commandCoverageOf(rules, admissionCallOf(call, ctx, ctx.liveCwd(), posture));
return "uncovered" in coverage ? coverage.uncovered : undefined;
}
export function judgeAllowEntries(rows, req, behavior, ctx, posture = "grant") {
const live = rows.filter((r) => r.behavior === behavior);
if (live.length === 0)
return undefined;
for (const row of live)
if (nameRowReaches(row.compiled, req))
return [row];
const call = commandOf(req);
const liveCwd = ctx.liveCwd();
if (call !== undefined) {
const byRule = new Map();
for (const r of live)
if (r.compiled.kind === "command")
byRule.set(r.compiled.rule, r);
if (behavior === "allow") {
const coverage = commandCoverageOf([...byRule.keys()], admissionCallOf(call, ctx, liveCwd, posture));
const hits = ("covering" in coverage ? coverage.covering : []).flatMap((rule) => byRule.get(rule) ?? []);
if (hits.length > 0)
return hits;
}
else {
for (const [rule, row] of byRule) {
if (programRunReachOf(rule, call.command).reach !== "not_reached")
return [row];
}
}
}
const pathRows = live.filter((r) => r.compiled.kind === "path");
if (pathRows.length > 0) {
const table = pathRows.map((r) => r.compiled.rule);
for (const name of laneToolNamesOf(req).filter((n) => ruleToolGrammarOf(n) === "path")) {
const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, anchorsOf: () => ctx.anchors, leads: callTargetReadingOf(req)?.leads ?? NO_LEADS });
const spoke = answer !== undefined && ("unreadable" in answer ? behavior === "deny" : "behavior" in answer && answer.behavior === behavior);
if (spoke) {
const rule = answer !== undefined && "rules" in answer ? answer.rules[0]?.rule : undefined;
return [pathRows.find((r) => r.compiled.rule.rule === rule) ?? pathRows[0]];
}
}
}
for (const grammar of ["name", "domain"]) {
const familyRows = live.filter((r) => r.compiled.kind === grammar);
if (familyRows.length === 0)
continue;
const table = familyRows.map((r) => r.compiled.rule);
for (const name of laneToolNamesOf(req).filter((n) => ruleToolGrammarOf(n) === grammar)) {
const at = { tool: name, cwd: ctx.root, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}) };
const verdict = grammar === "name" ? adjudicatePersistedNameRules(table, { ...at, name: nameRuleSubjectOf(name, req.args) }) : adjudicatePersistedDomainRules(table, { ...at, host: domainRuleSubjectOf(name, req.args) });
if (verdict !== undefined && verdict.behavior === behavior)
return [familyRows.find((r) => r.compiled.rule === verdict.rules[0]) ?? familyRows[0]];
}
}
return undefined;
}
export function allowingRows(rows, req, mode) {
return mode === "auto" ? rows.filter((r) => !autoStripped(r.compiled, req)) : rows;
}
export class AllowEntryTable {
held = [];
heldEntries = new Set();
seed(entries, roster) {
const { rows, inert } = compileAllowList(entries, "allow", roster);
for (const r of rows) {
if (this.heldEntries.has(r.entry))
continue;
this.heldEntries.add(r.entry);
this.held.push({ behavior: "allow", entry: r.entry, compiled: r.compiled });
}
return inert;
}
entries() {
return this.held.map((r) => r.entry);
}
holdsPathRows() {
return this.held.some((r) => r.compiled.kind === "path");
}
lane(ctx) {
return {
allowing: (req, mode) => {
const hit = judgeAllowEntries(allowingRows(this.held, req, mode), req, "allow", ctx);
return hit === undefined ? undefined : { entries: hit.map((r) => r.entry) };
},
};
}
}
export function seedAllowEntryTables(tables, lists, roster) {
tables.request.seed(lists.inherited ?? []);
const requestInert = tables.request.seed(lists.request ?? [], roster);
const deploymentInert = tables.deployment.seed(lists.deployment ?? [], roster).filter((e) => e.reason === "tool_name" || e.reason === "near_miss");
return [
...lists.skillInert.map((e) => ({ source: "skill", ...e })),
...requestInert.map((e) => ({ source: "request", ...e })),
...deploymentInert.map((e) => ({ source: "deployment", ...e })),
];
}
export function withDeploymentAllowedTools(deps) {
if (deps.allowedTools === undefined)
return deps;
const allowedTools = screenAllowedToolsValue(deps.allowedTools, "RunnerDeps.allowedTools");
const inert = compileAllowList(allowedTools ?? [], "allow").inert;
if (inert.length > 0)
deliverEngineNotice(deps.onNotice, allowedToolsNotGrantedNotice(inert.map((e) => ({ source: "deployment", ...e })), undefined));
return { ...deps, allowedTools };
}
export const INVALID_PERMISSION_RULES_CODE = "config.invalid_permission_rules";
export function screenAllowedToolsValue(value, seat) {
if (value === undefined)
return undefined;
const issues = [];
if (!Array.isArray(value)) {
issues.push({ rule: "", code: "invalid.entry_type", message: `${seat} must be a list of permission-rule strings (got ${value === null ? "null" : typeof value})` });
}
else {
const list = value;
list.forEach((entry, i) => {
if (typeof entry !== "string")
issues.push({ rule: "", code: "invalid.entry_type", message: `${seat}[${i}] must be a permission-rule string (got ${entry === null ? "null" : typeof entry})` });
});
}
if (issues.length > 0) {
const e = new Error(`${seat}: ${issues.length} invalid entr${issues.length === 1 ? "y" : "ies"} — refusing rather than running without them:\n${issues.map((i) => ` [${i.code}] ${i.message}`).join("\n")}`);
e.code = INVALID_PERMISSION_RULES_CODE;
e.issues = issues;
throw e;
}
return Object.freeze([...value]);
}
const SHADOW_ENTRIES_MAX = 5;
const SHADOW_ENTRY_MAX_CHARS = 200;
export function allowRuleShadowOf(member, entries) {
const shown = entries.slice(0, SHADOW_ENTRIES_MAX).map((e) => inlineUntrusted(e, SHADOW_ENTRY_MAX_CHARS));
return { member, entries: shown, ...(entries.length > shown.length ? { omitted: entries.length - shown.length } : {}) };
}
const MEMBER_SET = new Set(ALLOW_RULE_MEMBERS);
export function readAllowRuleShadows(value) {
try {
if (!Array.isArray(value) || value.length === 0)
return undefined;
const list = value;
const out = [];
for (const r of list) {
if (typeof r !== "object" || r === null)
return undefined;
const { member, entries, omitted } = r;
if (!MEMBER_SET.has(member) || !Array.isArray(entries) || !entries.every((e) => typeof e === "string"))
return undefined;
const shadow = allowRuleShadowOf(member, entries);
const carried = typeof omitted === "number" && Number.isInteger(omitted) && omitted > 0 ? omitted : 0;
out.push(carried > 0 ? { ...shadow, omitted: (shadow.omitted ?? 0) + carried } : shadow);
}
return out;
}
catch {
return undefined;
}
}
export const SHADOW_REASON = Object.freeze({
content_question: "the call is the engine's own question to a person",
hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
ancestor: "the task that delegated this one owns this question",
dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule",
unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule",
unverifiable_shell_text: "the text this shell will run cannot be read by the engine — it is cleared by confirming this call, never by an allow rule",
transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
org_rule: "an organization policy rule requires approval for this call",
rule_unreadable: "a rule that may govern this call cannot currently be read (this deployment's organization policy, a partition of the person's standing rules, or this call against a deny or ask rule), so whether one forbids it is unknown",
ask_rule: mandateDisclosureOf(undefined),
denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides",
requires_user_interaction: "this tool's server declares that a call must be put to a person",
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",
shell_floor: mandateDisclosureOf("operator_always"),
target_unresolved: mandateDisclosureOf("target_unresolved"),
probe: mandateDisclosureOf("probe_mandate"),
write_protection: mandateDisclosureOf("write_protection"),
tool_marks_mcp: mandateDisclosureOf("tool_marks"),
tool_marks_a2a: mandateDisclosureOf("tool_marks"),
policy_rra: "the deployment's policy requires a real approval for this call",
artifact_shared_read: "this read targets another task's shared artifact — it is cleared by confirming this call",
mode: "the permission mode puts this question to a person",
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",
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",
});
function memberPhrase(s) {
const listed = `${s.entries.join(", ")}${s.omitted !== undefined ? ` and ${s.omitted} more` : ""}`;
switch (s.member) {
case "hook":
return "a PreToolUse hook's allow";
case "deployment":
return `the deployment's allowedTools (${listed})`;
case "persisted":
return s.entries.length + (s.omitted ?? 0) === 1 ? `a persisted allow rule (${listed})` : `persisted allow rules (${listed})`;
case "skill":
return `an active skill's allowed-tools (${listed})`;
case "request":
return `this run's allowedTools (${listed})`;
default: {
const exhaustive = s.member;
return exhaustive;
}
}
}
export function allowRuleShadowSentence(shadows, ask) {
const one = shadows.length === 1 && (shadows[0].member === "hook" || shadows[0].entries.length + (shadows[0].omitted ?? 0) === 1);
const why = ask.mandate !== undefined
? mandateDisclosureOf(ask.mandate)
: ask.authoredBy === "deployment"
? "this deployment requires a person's confirmation for this call"
: SHADOW_REASON[ask.askClass ?? "ordinary"];
return `${shadows.map(memberPhrase).join("; ")} ${one ? "matches this call but does not clear the ask" : "match this call but do not clear the ask"} — ${why}`;
}
export function cardDisclosureOf(decision, fallbackMessage) {
const base = decision.message ?? fallbackMessage;
const shadows = readAllowRuleShadows(decision.allowRuleShadowed);
if (shadows === undefined)
return { message: base };
return { message: `${base} (${allowRuleShadowSentence(shadows, decision)})`, allowRuleShadowed: shadows };
}
export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
const NOTICE_ENTRIES_SHOWN = 5;
const UNSEEN_IN_ENTRY = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]/gu;
const ENTRY_RENDER_MAX = 122;
function escapedCodePoint(ch) {
const cp = ch.codePointAt(0) ?? 0;
const hex = cp.toString(16).padStart(4, "0");
return cp > 0xffff ? `\\u{${hex}}` : `\\u${hex}`;
}
export function spelledListEntry(entry) {
return JSON.stringify(entry)
.replace(/ {2,}/g, (run) => "\\u0020".repeat(run.length))
.replace(UNSEEN_IN_ENTRY, (ch) => (ch === " " ? ch : escapedCodePoint(ch)));
}
export function renderListEntry(entry) {
return inlineUntrusted(spelledListEntry(entry), ENTRY_RENDER_MAX);
}
export function inertEntryPhrase(e) {
if (e.valueType !== undefined)
return `a value that is ${e.valueType} (grammar: not a string)`;
const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
const why = (() => {
if (e.reason === "near_miss")
return `near_miss: ${NEAR_MISS_CARRIER[e.carriedBy ?? "run"]} "${spelling}"`;
if (e.reason !== "tool_name" || e.verdict === undefined)
return e.reason;
switch (e.verdict) {
case "retired":
return `tool_name: a retired tool name${spelling !== undefined ? `, now "${spelling}"` : ""}`;
case "unprefixed_namespace":
return 'tool_name: a "__" name with no protocol prefix';
case "unmintable_segment":
return `tool_name: a namespaced name with a segment the engine never mints${spelling !== undefined ? `, write "${spelling}"` : ""}`;
default: {
const exhaustive = e.verdict;
return exhaustive;
}
}
})();
return `${renderListEntry(e.entry)} (${why})`;
}
export function allowedToolsNotGrantedNotice(inert, sessionId) {
const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)} ${inertEntryPhrase(e)}`);
const rest = inert.length - shown.length;
const refusing = inert.some((e) => "list" in e && e.list === "disallowed-tools");
return {
code: ALLOWED_TOOL_NOT_GRANTED_CODE,
message: `${inert.length} allowed-tools entr${inert.length === 1 ? "y grants" : "ies grant"} or refuse${inert.length === 1 ? "s" : ""} nothing — ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. ` +
"A list holding `*` voids the list, `mcp__*` names no server, a rule the grammar refuses (`grammar`) or one on a tool with no such rule form (`content_unsupported`: an allow by path is an `Edit(…)` or `Read(…)` rule) grants nothing, and a tool name must be spelled as the tool is mounted; name the tools (or `mcp__<server>`) instead." +
(refusing ? " Such an entry in a disallowed-tools list keeps its Skill from activating: a refusal that reads as nothing is not dropped." : ""),
detail: { ...(sessionId !== undefined ? { sessionId } : {}), entries: inert.map((e) => ({ ...e, entry: noticeRuleText(e.entry) })) },
};
}