@sema-agent/core
Version:
Stateless, task-oriented AI agent core
323 lines (322 loc) • 21.9 kB
JavaScript
import { compileToolSelector, valveLeaves, deniedProtocolTools, delegationListWhere, isCanonicalToolName, isToolSelectorLayer, listReaches, notAListSentence, toolListsDigest, notAStringSentence, readAllowedToolList, readDeniedEntry, readDeniedToolList } from "../../agents/tool-filter.js";
import { readToolNameList, valueTypeLabel } from "../tool-name-list.js";
import { compileAllowList, inertEntryPhrase, renderListEntry, rootFacingToolNames } from "../allow-entry.js";
import { inlineUntrusted } from "../untrusted-text.js";
import { ENGINE_TOOLS } from "../tool-catalog-entries.js";
import { callerToolNameProblem } from "../caller-tool-name.js";
import { mintNamespacePrefix } from "../protocol-naming.js";
import { PROTOCOL_TABLE, protocolOf } from "../protocol-table.js";
import { mountedToolNames, splitNamespacedName } from "../tool-name-aliases.js";
import { projectionToolNames } from "../tool-policy.js";
import { declaredDelegationNameOf } from "../delegation-name.js";
export function renamedDelegationMountSentence(...rosters) {
for (const tools of rosters) {
if (!Array.isArray(tools))
continue;
for (const t of tools) {
const declared = declaredDelegationNameOf(t);
if (declared === undefined)
continue;
try {
const mounted = t.name;
if (declared === mounted)
continue;
return `The delegation tool declared as "${inlineUntrusted(declared, 80)}" is mounted under the name "${inlineUntrusted(String(mounted), 80)}": one tool would answer to two names (the agent definitions' tool lists, the copy a child is handed and the tool's prompt text speak the declared one; the roster and the model's call speak the mounted one). Mount it under its declared name, or declare the name you mount it under (\`createSubagentTool({ name })\`).`;
}
catch {
}
}
}
return undefined;
}
export function refuseRenamedDelegationMounts(...rosters) {
const sentence = renamedDelegationMountSentence(...rosters);
if (sentence !== undefined)
throw refused(sentence, "config.tool_face_control", "renamed_mount");
}
function poolOf(tools) {
return { tools, names: mountedToolNames([...tools.map((t) => ({ name: t.name })), ...tools.flatMap((t) => t.agentToolPool ?? [])]), complete: tools.every((t) => t.agentToolPool !== undefined) };
}
export function delegationPoolReading(callerTools, leaves) {
return poolOf(callerTools.filter((t) => t.agentToolFaces !== undefined && leaves(t)));
}
function peersOf(known, declaredPeers) {
const peers = new Set(PROTOCOL_TABLE.flatMap((ns) => (declaredPeers[ns.id] ?? []).map((p) => mintNamespacePrefix(ns, p.name))));
for (const name of known) {
const ns = protocolOf(name);
const split = ns !== undefined ? splitNamespacedName(name) : undefined;
if (split !== undefined && split.peer !== "")
peers.add(mintNamespacePrefix(split.ns, split.peer));
}
return peers;
}
function rootFacingUniverse(mounted, pool, declaredPeers) {
const known = rootFacingToolNames([...mounted, ...pool.names]);
return { known, peers: peersOf(known, declaredPeers), complete: true, carrierOf: (name) => (mounted.has(name) ? "run" : pool.names.has(name) ? "pool" : "catalog") };
}
function childFacingUniverse(pool, declaredPeers) {
const known = new Set([...mountedToolNames(ENGINE_TOOLS), ...pool.names]);
return { known, peers: peersOf(known, declaredPeers), complete: pool.complete, carrierOf: (name) => (pool.names.has(name) ? "pool" : "catalog") };
}
const DENY_LIST_NAMES_ONLY = "To block by path or command, write a deny rule in the caller's permission policy: a tool-name list only takes tool names (it narrows which tools are mounted).";
function universeCarries(reach, entry, universe) {
for (const name of universe.known)
if (listReaches(reach, { name }))
return true;
const split = splitNamespacedName(entry);
if (split === undefined || split.peer === "")
return false;
return universe.peers.has(mintNamespacePrefix(split.ns, split.peer));
}
function refused(message, code, reason) {
const e = new Error(message);
e.code = code;
e.detail = { reason };
return e;
}
const LIST_WORD = { allowed: "allowed tools", denied: "denied tools" };
const NEAR_MISS_EFFECT = {
allowed: "it admits nothing, so the tool it was written for is not granted",
denied: "it hides nothing, so the tool it was written for stays available to the agent",
};
function nearMissRefusal(where, side, flaw, code) {
return refused(`${where} declares tool ${inertEntryPhrase(flaw)} in its ${LIST_WORD[side]}, a spelling that reaches no tool a run mounts — ${NEAR_MISS_EFFECT[side]}. Write the name as the tool is mounted.`, code, "near_miss");
}
function grammarRefusal(where, side, flaw, code) {
return refused(`${where} declares tool ${inertEntryPhrase(flaw)} in its ${LIST_WORD[side]}, a spelling no tool name can have — fix the entry. ${DENY_LIST_NAMES_ONLY}`, code, "unknown_name");
}
function readableToolNameList(where, side, value, code) {
if (value === undefined || value === null)
return [];
const list = readToolNameList(value);
if (list.kind !== "entries")
throw refused(notAListSentence(where, side, list), code, "not_a_list");
return list.entries.map((entry, index) => {
const read = readDeniedEntry(entry);
if (read.kind === "not_a_string")
throw refused(notAStringSentence(where, side, index, read.type), code, "not_a_string");
if (read.kind === "unreadable")
throw grammarRefusal(where, side, { entry: read.entry, reason: "grammar" }, code);
if (side === "denied" && read.name === "*") {
throw refused(`${where} declares ${renderListEntry(entry)} in its ${LIST_WORD[side]}: a denied list has no wildcard — the entry would hide nothing. To give the agent no tools, write an empty allowed list.`, code, "unsupported_pattern");
}
return { entry: entry, name: read.name, content: read.content };
});
}
const KEPT_ENTRY = {
allowed: "it grants nothing today and stays in force for a tool of that name mounted later",
denied: "it denies nothing today and stays in force for a tool of that name mounted later",
};
const KEPT_ENTRY_UNLISTED_POOL = "so it is kept here: the pool this delegation tool hands its child cannot be listed, and the spawned child judges the entry against its own roster (another spelling of a tool it carries refuses the child before it starts; an exact name takes effect; a name nothing answers to there stays in force for a tool of that name mounted later)";
const PEER_WORD = { mcp: "MCP server", a2a: "A2A peer" };
function judgeToolNameList(where, side, value, universe, advise, code) {
for (const { entry, name: toolName, content: written } of readableToolNameList(where, side, value, code)) {
if (toolName === "*" && !written)
continue;
const name = side === "denied" || !written ? toolName : entry;
const content = side === "denied" && written;
const reach = side === "denied" ? readDeniedToolList([entry]) : (readAllowedToolList([entry]) ?? readDeniedToolList([]));
const split = splitNamespacedName(name);
if (split !== undefined && split.peer === "*" && split.tool === undefined) {
if (side === "denied")
continue;
throw refused(`${where} declares ${renderListEntry(entry)} in its ${LIST_WORD[side]}: an allowed list has no protocol-wide form — name a peer (\`${split.ns.prefix}<peer>\`) or its tools.`, code, "unsupported_pattern");
}
const { inert } = compileAllowList([toolName], "deny", universe.known);
const flaw = inert[0] === undefined ? undefined : { ...inert[0], entry, ...(inert[0].reason === "near_miss" && inert[0].currentName !== undefined ? { carriedBy: universe.carrierOf(inert[0].currentName) } : {}) };
if (universe.complete && flaw?.currentName !== undefined)
throw nearMissRefusal(where, side, flaw, code);
if (toolName !== name) {
advise(new Error(`${where} declares ${renderListEntry(entry)} in its allowed tools: an allowed list names tools, not content, so this entry grants nothing — name the tool "${inlineUntrusted(toolName, 80)}" to grant it, or move the content rule to a denied list or a permission rule.`));
continue;
}
if (flaw === undefined && universeCarries(reach, name, universe)) {
if (content)
advise(new Error(`${where} declares ${renderListEntry(entry)} in its denied tools: this list reads tool names, not content, so the whole tool "${inlineUntrusted(name, 80)}" is denied. ${DENY_LIST_NAMES_ONLY}`));
continue;
}
const phrase = flaw !== undefined ? inertEntryPhrase(flaw) : renderListEntry(entry);
const undeclared = split !== undefined && split.peer !== "" && !split.peer.includes("*") && !universe.peers.has(mintNamespacePrefix(split.ns, split.peer));
const why = undeclared ? `no ${PEER_WORD[split.ns.id]} named "${inlineUntrusted(split.peer, 80)}" is declared for this task` : universe.complete ? "no tool of this task answers to that name" : "no tool this run can list answers to that name";
advise(new Error(`${where} declares tool ${phrase} in its ${LIST_WORD[side]}: ${why}, ${universe.complete ? `so ${KEPT_ENTRY[side]}` : KEPT_ENTRY_UNLISTED_POOL}. If the name is misspelt, fix the entry.`));
}
}
export function deniedToolNamesOf(where, value) {
return readableToolNameList(where, "denied", value, "config.agent.unknown_tool").map((read) => read.name);
}
function inheritedListsWhere(ceilings, i) {
const up = ceilings - 1 - i;
return up === 0 ? "the lists of the run this leg was spawned under" : `the lists of the run ${up} spawn${up === 1 ? "" : "s"} above the run this leg was spawned under`;
}
function readSelectorLayer(layer, layerWhere, listWhere, recorded) {
if (!isToolSelectorLayer(layer)) {
throw refused(`${layerWhere} are a value that is ${valueTypeLabel(layer)}, not a pair of tool-name lists (an allowed and a denied list) — the leg does not start. Fix the layer.`, "config.tool_face_control", "not_a_list");
}
for (const side of ["allowed", "denied"]) {
const where = listWhere(side);
const entries = readableToolNameList(where, side, side === "allowed" ? layer.allow : layer.deny, "config.tool_face_control");
const written = recorded ? entries.find((read) => !isCanonicalToolName(read.entry)) : undefined;
if (written !== undefined) {
throw refused(`${where} declares ${renderListEntry(written.entry)} in its ${LIST_WORD[side]}: a row records each tool name in its bare spelling (\`Read\`, never \`Read()\`, \`Read(*)\` or \`Read(…)\`), and this entry was recorded by an engine that read the list otherwise — it is read here neither as that engine read it nor as this one would`, "config.tool_face_control", "unsupported_pattern");
}
}
}
function ownToolListsFlaw(value) {
try {
if (typeof value !== "object" || value === null || Array.isArray(value))
return `a value that is ${valueTypeLabel(value)}, not a tool name with an agent definition name and the digest of the lists it wrote`;
const { toolName, definition, lists } = value;
if (typeof toolName !== "string" || toolName.length === 0)
return `a tool whose name is ${typeof toolName === "string" ? "empty" : `a value that is ${valueTypeLabel(toolName)}`}, not a tool name`;
const problem = callerToolNameProblem(toolName);
if (problem !== undefined)
return `a tool whose name is not one a caller tool can carry (${problem})`;
if (definition !== undefined && typeof definition !== "string")
return `an agent definition whose name is a value that is ${valueTypeLabel(definition)}, not a name`;
if (definition === "")
return "an agent definition whose name is empty, not a name";
if (lists === undefined)
return "a record with no digest of the lists it wrote";
if (typeof lists !== "string")
return `a digest of the lists it wrote that is a value that is ${valueTypeLabel(lists)}, not a digest`;
if (lists.length === 0)
return "a digest of the lists it wrote that is empty, not a digest";
return undefined;
}
catch {
return "a value that cannot be read";
}
}
export function persistedToolNamesRefusal(ceiling, chain, ownLists) {
const pending = "refused before the claim (the checkpoint stays pending): finish it on a worker of the engine that parked it, or expire it and run the work again";
const ownFlaw = ownLists === undefined ? undefined : ownToolListsFlaw(ownLists);
if (ownFlaw !== undefined)
return { message: `this leg's checkpoint row records the delegation tool that wrote its own lists as ${ownFlaw} — ${pending}`, reason: "invalid_record" };
if (ceiling !== undefined) {
const layers = readToolNameList(ceiling);
if (layers.kind !== "entries")
return { message: `the ceiling this leg's checkpoint row recorded is a value that is ${layers.kind === "not_a_list" ? layers.type : "a list that cannot be read"}, not a list of layers — ${pending}`, reason: "not_a_list" };
try {
layers.entries.forEach((layer, i) => readSelectorLayer(layer, inheritedListsWhere(layers.entries.length, i), () => inheritedListsWhere(layers.entries.length, i), true));
}
catch (e) {
return { message: `${e.message} — as this leg's checkpoint row recorded it; ${pending}`, reason: e.detail.reason };
}
}
if (!Array.isArray(chain))
return undefined;
const flaw = (() => {
for (const [layer, entry] of chain.entries()) {
if (typeof entry !== "object" || entry === null || "opaque" in entry || !Array.isArray(entry.components))
continue;
for (const c of entry.components) {
if (typeof c !== "object" || c === null)
continue;
const { member, list } = projectionToolNames(c);
const where = `layer ${layer + 1}'s ${c.kind === "tool_deny" || c.kind === "tool_allowlist" || c.kind === "shell_command" ? `\`${c.kind}\`` : "component"} ${member}`;
const read = readToolNameList(list);
if (read.kind !== "entries")
return { what: `${where} as a value that is ${read.kind === "not_a_list" ? read.type : "a list that cannot be read"}, not a list of tool names`, reason: "not_a_list" };
const at = read.entries.findIndex((name) => !isCanonicalToolName(name));
if (at < 0)
continue;
const name = read.entries[at];
return typeof name === "string" ? { what: `${renderListEntry(name)} in ${where}`, reason: "unsupported_pattern" } : { what: `in ${where}, as entry ${at + 1}, a value that is ${valueTypeLabel(name)}`, reason: "not_a_string" };
}
}
return undefined;
})();
return flaw === undefined
? undefined
: { reason: flaw.reason, message: `the checkpoint's frozen inherited constraint chain holds ${flaw.what}: a row records each tool name in its bare spelling (\`Read\`, never \`Read()\`, \`Read(*)\` or \`Read(…)\`), and this entry was recorded by a reading that is not the rule syntax's (an earlier engine's name set, or a policy that reads its lists as written) — it is replayed neither as that reading nor as this engine's; ${pending} (with the policy naming the tool itself)` };
}
export function refusePersistedToolNames(ceiling, chain, ownLists) {
const refusal = persistedToolNamesRefusal(ceiling, chain, ownLists);
if (refusal !== undefined)
throw refused(refusal.message, "config.tool_face_control", refusal.reason);
}
export function boundToolListsJudgment(evidence, own) {
return evidence !== undefined && evidence === toolListsDigest(own.allow, own.exclude) ? evidence : undefined;
}
export function boundOwnToolLists(record, own) {
if (record === undefined)
return undefined;
const flaw = ownToolListsFlaw(record);
if (flaw !== undefined) {
throw refused(`this leg's internals (RunInternals.ownToolLists) record the delegation tool that wrote its own lists as ${flaw} — the leg does not start. Only the delegation lane writes that record: leave it out.`, "config.tool_face_control", "invalid_record");
}
const held = record;
return held.lists === toolListsDigest(own.allow, own.exclude) ? held : undefined;
}
export function judgeRunToolLists(input) {
const { spec, tools, onceLedger, sessionId } = input;
const advise = (err) => {
try {
onceLedger.onError(err, { phase: "config", sessionId });
}
catch {
}
};
const ownWhere = input.ownBy !== undefined ? delegationListWhere(input.ownBy.toolName, input.ownBy.definition) : undefined;
const ceilings = input.layers.length - 1;
input.layers.forEach((layer, i) => {
const own = i === ceilings;
const inherited = own ? undefined : inheritedListsWhere(ceilings, i);
readSelectorLayer(layer, inherited ?? "this run's own tool lists", (side) => inherited ?? ownWhere ?? (side === "allowed" ? "TaskSpec.allowTools" : "TaskSpec.excludeTools"), false);
});
const denied = deniedProtocolTools(input.layers, input.legs);
if (denied.length > 0) {
const names = [...new Set(denied.map((h) => `"${h.name}"`))];
const one = names.length === 1;
const deniedBy = (own, whose) => {
const hits = denied.filter((h) => h.own === own).map((h) => `"${h.name}"`);
return hits.length > 0 ? [`${whose} denies ${one ? "it" : hits.join(", ")}`] : [];
};
const by = [...deniedBy(true, ownWhere ?? "this run's own excludeTools"), ...deniedBy(false, "an inherited list (the spawning run's excludeTools, or one it inherits)")];
throw refused(`This kind of leg (${input.legs.join(", ")}) relies on ${names.join(", ")}, but ${by.join(" and ")} — the leg cannot work without ${one ? "it" : "them"}. Remove the entry from the list that names it.`, "config.tool_face_control", "protocol_denied");
}
const selector = compileToolSelector(input.layers, input.legs);
const pool = delegationPoolReading(spec.tools ?? [], valveLeaves(spec, selector));
const mounted = mountedToolNames([...tools, ...(spec.tools ?? [])]);
if (!input.judgedUpstream) {
const root = rootFacingUniverse(input.ownBy !== undefined ? new Set([...mounted, input.ownBy.toolName]) : mounted, pool, input.declaredPeers);
const own = [["allowed", input.own.allow], ["denied", input.own.exclude]];
const tell = input.toldUpstream === true ? () => undefined : advise;
for (const [side, value] of own)
judgeToolNameList(ownWhere ?? (side === "allowed" ? "TaskSpec.allowTools" : "TaskSpec.excludeTools"), side, value, root, tell, "config.tool_face_control");
}
const children = new Map(pool.tools.map((t) => [t, childFacingUniverse(poolOf([t]), input.declaredPeers)]));
const taskTargets = pool.tools.length > 0 ? [...children.values()] : [childFacingUniverse(poolOf([]), input.declaredPeers)];
for (const def of spec.agents ?? []) {
for (const universe of taskTargets) {
judgeToolNameList(`TaskSpec.agents: agent "${def.name}"`, "allowed", def.allowTools, universe, advise, "config.agent.unknown_tool");
judgeToolNameList(`TaskSpec.agents: agent "${def.name}"`, "denied", def.denyTools, universe, advise, "config.agent.unknown_tool");
}
}
const replaced = new Set((spec.agents ?? []).map((d) => d.name));
for (const [t, universe] of children) {
for (const face of t.agentToolFaces ?? []) {
if (face.authored === true && !replaced.has(face.name)) {
const faceWhere = delegationListWhere(t.name, face.name);
judgeToolNameList(faceWhere, "allowed", face.allowTools, universe, advise, "config.agent.unknown_tool");
judgeToolNameList(faceWhere, "denied", face.denyTools, universe, advise, "config.agent.unknown_tool");
}
}
judgeToolNameList(delegationListWhere(t.name, undefined), "allowed", t.agentToolLevelAllowTools, universe, advise, "config.agent.unknown_tool");
judgeToolNameList(delegationListWhere(t.name, undefined), "denied", t.agentToolLevelDenyTools, universe, advise, "config.agent.unknown_tool");
}
}
export function judgeHostToolCeiling(layers, agent, above = []) {
const where = (i) => `RunWorkflowOptions.toolCeiling, layer ${i + 1}`;
layers.forEach((layer, i) => readSelectorLayer(layer, where(i), () => where(i), false));
const callerTools = agent.tools ?? [];
const own = { ...(agent.allowTools != null ? { allow: agent.allowTools } : {}), deny: agent.excludeTools ?? [] };
const selector = compileToolSelector([...above, ...layers, own], ["delegated"]);
const pool = delegationPoolReading(callerTools, valveLeaves(agent, selector));
const universe = rootFacingUniverse(mountedToolNames(callerTools), pool, { mcp: undefined, a2a: undefined });
const unannounced = () => undefined;
layers.forEach((layer, i) => {
judgeToolNameList(where(i), "allowed", layer.allow, universe, unannounced, "config.tool_face_control");
judgeToolNameList(where(i), "denied", layer.deny, universe, unannounced, "config.tool_face_control");
});
}