@sema-agent/core
Version:
Stateless, task-oriented AI agent core
235 lines (234 loc) • 11.7 kB
JavaScript
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.`);
}