UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

200 lines (199 loc) • 10.6 kB
import { types } from "node:util"; import { DECLARATION_PATH_RULES, declarationFaultOf, declarerWord, readToolAxes, receiveDeclaration, toolAxesRefusal } from "./mcp-server-spec.js"; import { isPlainRecord, receive } from "./mods/kernel/receive.js"; import { CONFIG_RECEIPT_MODE } from "./own-property-receipt.js"; import { isToolContentOrigin, TOOL_CONTENT_ORIGINS } from "./memory-engine/content-origin.js"; import { inlineUntrusted } from "./untrusted-text.js"; import { assertNoRetiredToolPolicyKeys } from "./retired-keys.js"; export const LOCKED_KEY_REGISTRY = { mcp: { specFields: ["mcp"] }, toolPolicy: { specFields: ["toolPolicy", "basePolicyForResumeEdit"] }, compliancePosture: { specFields: [] }, retentionPolicy: { specFields: [] }, }; export const ENTER_AS_IS = (_seat, door) => door(); function codedError(code, message) { const e = new Error(message); e.code = code; return e; } export function resolveLockedKeys(config) { if (config === undefined) return new Set(); const keys = new Set(); const listed = declaredLockKeys(config); for (let index = 0; index < listed.length; index++) { const key = listed[index]; if (!Object.hasOwn(LOCKED_KEY_REGISTRY, key)) { throw lockDeclarationRefusal(`RunnerDeps.lockedConfig.keys[${index}]`, `is ${inlineUntrusted(JSON.stringify(key), 80)}, which is not a member of the locked-key registry (${Object.keys(LOCKED_KEY_REGISTRY).join(", ")})`); } keys.add(key); } return keys; } function declaredLockKeys(config) { const received = receiveDeclaration(config); if (!received.ok) throw lockDeclarationRefusal(memberPath(received.fault.path, "RunnerDeps.lockedConfig"), `cannot be read: ${received.fault.problem}`); const copy = received.value; if (!isPlainRecord(copy)) throw lockDeclarationRefusal("RunnerDeps.lockedConfig", `is ${inlineUntrusted(describeDeclared(copy), 80)}, not an object with a \`keys\` list`); const keys = Object.hasOwn(copy, "keys") ? copy["keys"] : undefined; if (!Array.isArray(keys)) throw lockDeclarationRefusal("RunnerDeps.lockedConfig.keys", `is ${inlineUntrusted(describeDeclared(keys), 80)}, not a list of locked keys`); return keys; } function lockDeclarationRefusal(member, problem) { return codedError("config.locked_registry", `${member} ${problem}. A lock that does not parse must not half-arm — fix the deployment's lock declaration.`); } export function snapshotMcpEntries(entries) { return entries.map((entry) => snapshotDeclaredEntry(entry, "mcp")); } function memberPath(path, base = "") { let rendered = base; for (const key of path) { const name = typeof key === "string" && /^\d+$/.test(key) ? JSON.stringify(key) : String(key); rendered += typeof key === "number" ? `[${key}]` : rendered === "" ? name : `.${name}`; if (rendered.length > 120 + base.length) break; } return rendered; } function unreadableDeclaration(at, declarer, member, problem) { const refusal = codedError("config.invalid_argument", `${declarerWord(at)}${declarer === undefined ? "" : ` ${declarerNameOf(declarer)}`} declares ${inlineUntrusted(member, 80)} the engine cannot read: ${problem}. A declaration the engine cannot read is refused ` + `rather than partly applied — what the caller wrote and what the engine runs on would otherwise differ without anyone being told.`); if (declarer !== undefined) Object.defineProperty(refusal, "declarer", { value: declarer, enumerable: true, writable: false, configurable: false }); return refusal; } function ownDataString(holder, key) { const slot = Object.getOwnPropertyDescriptor(holder, key); const value = slot !== undefined && Object.hasOwn(slot, "value") ? slot.value : undefined; return typeof value === "string" ? value : undefined; } export function refusedDeclarer(refusal) { return typeof refusal === "object" && refusal !== null && !types.isProxy(refusal) ? ownDataString(refusal, "declarer") : undefined; } function capturedName(entry) { return typeof entry === "object" && entry !== null && !types.isProxy(entry) ? ownDataString(entry, "name") : undefined; } function entryFaultRefusal(at, declarer, path, problem) { return unreadableDeclaration(at, declarer, path.length === 0 ? "an entry" : memberPath(path), problem); } export function snapshotDeclaredEntry(entry, at) { const declared = entry; const declarer = capturedName(declared); const received = receiveDeclaration(declared, DECLARATION_PATH_RULES[at]); if (!received.ok) throw entryFaultRefusal(at, declarer, received.fault.path, received.fault.problem); if (!isPlainRecord(received.value)) throw unreadableDeclaration(at, declarer, "an entry", `it is ${inlineUntrusted(describeDeclared(received.value), 80)}, not an object`); return received.value; } function describeDeclared(value) { try { return JSON.stringify(value) ?? String(value); } catch { try { return String(value); } catch { return `[unrepresentable ${typeof value}]`; } } } const declarerNameOf = (name) => inlineUntrusted(typeof name === "string" ? JSON.stringify(name) : describeDeclared(name), 80); export function judgeEntryToolAxes(copy, at) { const { toolAxes: declaredAxes, ...rest } = copy; const axes = readToolAxes(declaredAxes, at); if (!axes.ok) return { ok: false, error: toolAxesRefusal(at, declarerNameOf(copy.name), axes.problem) }; if (axes.toolAxes !== undefined) { for (const perTool of Object.values(axes.toolAxes)) Object.freeze(perTool); Object.freeze(axes.toolAxes); } return { ok: true, entry: Object.freeze((axes.toolAxes === undefined ? rest : { ...rest, toolAxes: axes.toolAxes })) }; } function declaredEntriesOf(declared, at) { const field = at; const received = receive(declared, CONFIG_RECEIPT_MODE); let problem; if (received.ok) { const list = received.value; if (Array.isArray(list) && list !== declared) return Array.from({ length: list.length }, (_, index) => list[index]); problem = typeof list === "object" && list !== null ? "it is not an array" : `it is ${inlineUntrusted(describeDeclared(list), 80)}`; } else { const fault = declarationFaultOf(received.fault); if (fault.path.length > 0) throw unreadableDeclaration(at, undefined, "an entry", fault.problem); problem = fault.problem; } throw codedError("config.invalid_argument", `TaskSpec.${field} must be a readable array of ${declarerWord(at)} declarations; ${problem}. A list the engine cannot read ` + `is refused rather than read as "no entries" — the declared ${declarerWord(at)}s would otherwise be dropped without anyone being told.`); } function snapshotAndValidateA2aEntries(declared) { const entries = declaredEntriesOf(declared, "a2a"); if (entries.length === 0) return undefined; return entries.map((entry) => { const judged = judgeEntryToolAxes(snapshotDeclaredEntry(entry, "a2a"), "a2a"); if (!judged.ok) throw judged.error; return judged.entry; }); } function snapshotAndValidateMcpEntries(declared) { return snapshotMcpEntries(declaredEntriesOf(declared, "mcp")).map((copy) => { const declared = copy.contentOrigin; if (declared !== undefined && !isToolContentOrigin(declared)) { throw codedError("config.mcp_content_class", `MCP server ${declarerNameOf(copy.name)} declares contentOrigin ` + `${inlineUntrusted(describeDeclared(declared), 80)}, which is not one of ` + `${TOOL_CONTENT_ORIGINS.map((v) => `"${v}"`).join(" | ")}. The declaration decides whether this server's tools ` + `mark the session's memory externally exposed — a value this engine cannot read is refused at the door rather ` + `than folded to a class, because folding it would let a deployment believe a declaration is in force while every ` + `call keeps writing a one-way durable mark.`); } const judged = judgeEntryToolAxes(copy, "mcp"); if (!judged.ok) throw judged.error; return judged.entry; }); } export function preflightLockedConfig(spec, deps, enter = ENTER_AS_IS) { const lockedKeys = enter("deps", () => resolveLockedKeys(deps.lockedConfig)); enter("taskConfig", () => { for (const key of lockedKeys) { for (const field of LOCKED_KEY_REGISTRY[key].specFields) { if (spec[field] !== undefined) { throw codedError("config.locked_key", `TaskSpec.${field} is administratively locked by this deployment (locked key "${key}") — ` + `the task-supplied value is refused wholesale (locks are config-time; there is no request-time ` + `unlock). Remove the field from the task, or change the deployment's lock configuration.`); } } } }); const toolPolicyLocked = lockedKeys.has("toolPolicy"); const task = toolPolicyLocked ? {} : { toolPolicy: spec.toolPolicy, basePolicyForResumeEdit: spec.basePolicyForResumeEdit }; const policies = callerPolicySeats(task, deps, toolPolicyLocked); enter(!toolPolicyLocked && task.toolPolicy != null ? "taskConfig" : "deps", () => assertNoRetiredToolPolicyKeys(policies.toolPolicy)); enter(!toolPolicyLocked && task.basePolicyForResumeEdit != null ? "taskConfig" : "deps", () => assertNoRetiredToolPolicyKeys(policies.basePolicyForResumeEdit)); const declaredServers = lockedKeys.has("mcp") ? undefined : spec.mcp; const mcp = declaredServers === undefined ? undefined : enter("taskConfig", () => snapshotAndValidateMcpEntries(declaredServers)); const declaredPeers = spec.a2a; return { lockedKeys, mcp, a2a: declaredPeers === undefined ? undefined : enter("taskConfig", () => snapshotAndValidateA2aEntries(declaredPeers)), ...policies, }; } export function callerPolicySeats(spec, deps, toolPolicyLocked) { return { toolPolicy: toolPolicyLocked ? deps.toolPolicy : (spec.toolPolicy ?? deps.toolPolicy), basePolicyForResumeEdit: toolPolicyLocked ? deps.basePolicyForResumeEdit : (spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit), }; }