UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

235 lines (234 loc) • 11.7 kB
import { SAFETY_AXIS_VOCABULARY } from "./safety-axis-vocab.js"; import { INLINE_TRUNCATION_MARK, inlineUntrusted } from "./untrusted-text.js"; import { DECLARATION_RECEIPT_MODE } from "./own-property-receipt.js"; import { isPlainRecord, receive } from "./mods/kernel/receive.js"; import { MODS_VALUE_DEPTH } from "./mods/kernel/site-rules.js"; const TOOL_AXES_SURFACES = { mcp: { key: "tool", declarer: "MCP server" }, a2a: { key: "skill", declarer: "A2A peer" }, }; function invalidArgument(message) { const refusal = new Error(message); refusal.code = "config.invalid_argument"; return refusal; } function toolAxesSurfaceOf(at) { if (typeof at === "string" && Object.hasOwn(TOOL_AXES_SURFACES, at)) return TOOL_AXES_SURFACES[at]; throw invalidArgument(`toolAxes is read for a surface, and the surfaces are ${Object.keys(TOOL_AXES_SURFACES).map((s) => JSON.stringify(s)).join(" | ")} ` + `(an MCP server's tools, an A2A peer's skills); the surface named is not one of them.`); } const TOOL_AXIS_MEMBERS = { effect: true, egress: true, irreversibility: true }; const TOOL_AXIS_MEMBER_NAMES = Object.keys(TOOL_AXIS_MEMBERS); const TOOL_EFFECT_WORDS = SAFETY_AXIS_VOCABULARY.toolEffect; const ENGINE_DERIVED_IRREVERSIBILITY = "maybe"; const IRREVERSIBILITY_POLE_WORDS = SAFETY_AXIS_VOCABULARY.irreversibility.filter((word) => word !== ENGINE_DERIVED_IRREVERSIBILITY); const irreversibilityPolesLockstep = true; void irreversibilityPolesLockstep; const toolAxesDeclarationsAreOne = true; void toolAxesDeclarationsAreOne; function quotedName(name) { return JSON.stringify(name.length > 80 ? `${name.slice(0, 80)}…` : name); } function memberNames(path) { let rendered = ""; for (const [index, name] of path.entries()) { if (rendered.length > 120) return `${rendered}${INLINE_TRUNCATION_MARK}`; rendered += `${index === 0 ? "" : "."}${quotedName(String(name))}`; } return rendered; } function dataKind(value) { return value === null ? "null" : value === undefined ? "undefined" : Array.isArray(value) ? "an array" : typeof value === "object" ? "a record" : `a ${typeof value}`; } const RECEIPT_FAULT_PROBLEMS = { proxy: "it is a Proxy, whose traps would run as it is read; copy it into plain data (a spread or a JSON round trip)", "not-data": "it is not plain data (a function, a bigint, a symbol, or an object whose prototype is neither Object.prototype nor null: a class instance, " + "a map, `process.env`, a record built on a prototype or in another realm — a `vm` context, a worker); copy it into plain data (a spread " + "or a JSON round trip), writing out any member it inherits", "getter-threw": "its getter threw while it was read", "prototype-moved": "its prototype changed while it was read", cycle: "it contains itself; declare it as plain data", "too-deep": `it is nested past ${MODS_VALUE_DEPTH} levels`, uncopyable: "it could not be copied whole", accessor: "it is an accessor", hidden: "it is a member that is not an enumerable data property", }; export function declarationFaultOf(fault) { return { path: fault.path.map((key) => (/^(?:0|[1-9]\d*)$/.test(key) ? Number(key) : key)), problem: RECEIPT_FAULT_PROBLEMS[fault.kind] }; } export const DECLARATION_PATH_RULES = Object.freeze({ mcp: Object.freeze([ { path: ["name"], holds: "a string", required: true }, { path: ["transport", "env"], holds: "names to strings" }, { path: ["transport", "headers"], holds: "names to strings" }, ]), a2a: Object.freeze([ { path: ["name"], holds: "a string", required: true }, { path: ["headers"], holds: "names to strings" }, ]), }); const PROTO_MEMBER = `it is a member named "__proto__", which a copy by assignment would turn into a prototype — not a name a declaration can carry`; function declarationVocabularyFault(root, rules) { const asked = new Set(); const path = []; const stack = []; const ruleAt = () => rules.find((rule) => rule.path.length === path.length && rule.path.every((key, i) => key === path[i])); const onRuleRoute = () => rules.some((rule) => rule.path.length > path.length && path.every((key, i) => key === rule.path[i])); const ruleProblem = (rule, value) => { if (rule.holds === "a string") return typeof value === "string" ? undefined : `it is ${dataKind(value)}, not a string`; if (!isPlainRecord(value)) return `it is ${dataKind(value)}, not a record of names to strings`; for (const [key, member] of Object.entries(value)) { path.push(key); if (key === "__proto__") return PROTO_MEMBER; if (member !== undefined && typeof member !== "string") return `it is ${dataKind(member)}, not a string: this record maps names to strings`; path.pop(); } return undefined; }; const meet = (value) => { if (typeof value !== "object" || value === null || (asked.has(value) && !onRuleRoute())) return undefined; asked.add(value); if (Array.isArray(value)) { for (let index = 0; index < value.length; index++) { const own = Object.hasOwn(value, index); if (own && typeof value[index] === "string") continue; path.push(index); return `it is ${own ? dataKind(value[index]) : "a hole"}, not a string: every list a declaration carries is a list of names`; } const named = Object.keys(value).find((key) => !(/^(?:0|[1-9]\d*)$/.test(key) && Number(key) < value.length)); if (named === undefined) return undefined; path.push(named); return "it is a list member that is not an index: a list declares its names at its indices only"; } stack.push({ members: Object.entries(value), next: 0 }); return undefined; }; for (const rule of rules) { if (rule.required !== true) continue; let at = root; for (const key of rule.path) at = isPlainRecord(at) && Object.hasOwn(at, key) ? at[key] : undefined; if (at === undefined) return { path: [...rule.path], problem: `it is missing: a declaration carries ${rule.holds} here` }; } let problem = meet(root); while (problem === undefined && stack.length > 0) { const frame = stack[stack.length - 1]; if (frame.next === frame.members.length) { stack.pop(); if (stack.length > 0) path.pop(); continue; } const [key, member] = frame.members[frame.next++]; path.push(key); const rule = key === "__proto__" || member === undefined ? undefined : ruleAt(); if (key === "__proto__") problem = PROTO_MEMBER; else if (rule !== undefined) problem = ruleProblem(rule, member); else { const depth = stack.length; problem = meet(member); if (problem === undefined && stack.length > depth) continue; } if (problem === undefined) path.pop(); } return problem === undefined ? undefined : { path, problem }; } export function receiveDeclaration(declared, rules = []) { const received = receive(declared, DECLARATION_RECEIPT_MODE); if (!received.ok) return { ok: false, fault: declarationFaultOf(received.fault) }; const fault = isPlainRecord(received.value) ? declarationVocabularyFault(received.value, rules) : undefined; return fault === undefined ? { ok: true, value: received.value } : { ok: false, fault }; } function isToolEffectWord(value) { return typeof value === "string" && TOOL_EFFECT_WORDS.includes(value); } function isIrreversibilityPoleWord(value) { return typeof value === "string" && IRREVERSIBILITY_POLE_WORDS.includes(value); } function readToolAxisEntry(raw) { if (!isPlainRecord(raw)) return `the declaration is ${dataKind(raw)}, not an object of axes`; const listed = Object.keys(raw); const unknown = listed.find((member) => !TOOL_AXIS_MEMBER_NAMES.includes(member)); if (unknown !== undefined) return `unknown member ${quotedName(unknown)} (the members are ${TOOL_AXIS_MEMBER_NAMES.join(", ")})`; const declared = Object.create(null); for (const member of TOOL_AXIS_MEMBER_NAMES) { const value = raw[member]; if (value !== undefined && !listed.includes(member)) return `member ${quotedName(member)} is a hidden member: its name answers a value the declaration does not list as its own`; declared[member] = value; } const { effect, egress, irreversibility } = declared; if (effect !== undefined && !isToolEffectWord(effect)) return `member "effect" is not one of ${TOOL_EFFECT_WORDS.join(" | ")}`; if (egress !== undefined && typeof egress !== "boolean") return `member "egress" is not a boolean`; if (irreversibility !== undefined && !isIrreversibilityPoleWord(irreversibility)) return `member "irreversibility" is not one of ${IRREVERSIBILITY_POLE_WORDS.join(" | ")}`; return { ...(effect !== undefined ? { effect } : {}), ...(egress !== undefined ? { egress } : {}), ...(irreversibility !== undefined ? { irreversibility } : {}), }; } export function readToolAxes(declared, at) { const unit = toolAxesSurfaceOf(at).key; if (declared === undefined) return { ok: true }; const received = receiveDeclaration(declared); if (!received.ok) { const [key, ...member] = received.fault.path; if (key === undefined) return { ok: false, problem: `toolAxes cannot be read: ${received.fault.problem}` }; const where = member.length > 0 ? `member ${memberNames(member)}: ` : ""; return { ok: false, problem: `the axes declared for ${unit} ${quotedName(String(key))} do not fit: ${where}${received.fault.problem}` }; } const perTool = received.value; if (!isPlainRecord(perTool)) return { ok: false, problem: `toolAxes is ${dataKind(perTool)}, not an object of per-${unit} axis declarations` }; const entries = []; for (const [key, raw] of Object.entries(perTool)) { if (key.length === 0) return { ok: false, problem: `toolAxes has an empty ${unit} name` }; let entry; try { entry = readToolAxisEntry(raw); } catch { entry = "a member threw while it was read"; } if (typeof entry === "string") return { ok: false, problem: `the axes declared for ${unit} ${quotedName(key)} do not fit: ${entry}` }; if (Object.keys(entry).length > 0) entries.push([key, entry]); } return entries.length > 0 ? { ok: true, toolAxes: Object.fromEntries(entries) } : { ok: true }; } export function readMcpToolAxes(declared) { return readToolAxes(declared, "mcp"); } export function declarerWord(at) { return toolAxesSurfaceOf(at).declarer; } export function toolAxesRefusal(at, name, problem) { return invalidArgument(`${toolAxesSurfaceOf(at).declarer} ${name} declares toolAxes the engine cannot read: ${inlineUntrusted(problem, 240)}. A per-tool safety-axis declaration is the ` + `caller's trust root for the approval gate, so it is refused rather than partly applied — dropping one key or defaulting one word ` + `would leave a declaration that reads as in force while it is not.`); }