UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

147 lines (146 loc) • 9.49 kB
import { MCP_NAMESPACE } from "../protocol-table.js"; import { mintNamespacePrefix } from "../protocol-naming.js"; import { RESERVED_APPROVAL_CARDS, TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ABSENCES, TOOL_PATH_ACCESSES, TOOL_PATH_BASES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS, } from "../tool-face.js"; import { engineCardOf, inputKeysOf } from "../tool-roster.js"; import { deliverEngineNotice } from "../engine-notice.js"; export const APPROVAL_CARD_REQUIRED_KEYS = Object.freeze({ "file-edit": ["file_path"], "file-write": ["file_path"], "notebook-edit": ["notebook_path"], shell: ["command"], fetch: ["url"], plan: [], question: [], generic: [], }); export function toolFaceProblem(face, schema, tool) { const { keys, scalar } = inputKeysOf(schema); const top = new Set(keys.map((k) => k.name)); if (face.family !== undefined && !TOOL_FAMILIES.includes(face.family)) return `family ${JSON.stringify(face.family)} is outside the closed set`; const overlong = (label, values) => { const k = values.find((v) => v.length > TOOL_KEY_MAX_CHARS); return k === undefined ? undefined : `${label} ${JSON.stringify(k.slice(0, 40) + "…")} is ${k.length} characters; a roster key is at most ${TOOL_KEY_MAX_CHARS}`; }; const vocabulary = new Set(scalar); if (face.pathTarget !== undefined) { if (typeof face.pathTarget.param !== "string" || !top.has(face.pathTarget.param)) return `pathTarget.param ${JSON.stringify(face.pathTarget.param)} is not a top-level property of the tool's schema`; if (!TOOL_PATH_ACCESSES.includes(face.pathTarget.access)) return `pathTarget.access ${JSON.stringify(face.pathTarget.access)} is outside the closed set`; const base = face.pathTarget.base; if (base !== undefined && !TOOL_PATH_BASES.includes(base)) return `pathTarget.base ${JSON.stringify(base)} is outside the closed set`; if (base === "root" && face.pathTarget.access !== "read") return `pathTarget.base "root" needs pathTarget.access "read" — two write guards (the session-transcript directory guard and the frozen-spec deny) resolve a write target against the call's working directory only, so a write face declaring a root base would be judged on two different files (#638)`; const absent = face.pathTarget.absent; if (absent !== undefined && !TOOL_PATH_ABSENCES.includes(absent)) return `pathTarget.absent ${JSON.stringify(absent)} is outside the closed set`; if (absent === "base" && face.pathTarget.access !== "read") return `pathTarget.absent "base" needs pathTarget.access "read" — it says the call is about the whole base directory when the slot is empty, which is a search SCOPE; a write with no path names no file, and the containment fences judge a write on its own target`; const patternParam = face.pathTarget.patternParam; if (patternParam !== undefined) { if (typeof patternParam !== "string" || !top.has(patternParam)) return `pathTarget.patternParam ${JSON.stringify(patternParam)} is not a top-level property of the tool's schema`; if (absent !== "base") return `pathTarget.patternParam ${JSON.stringify(patternParam)} is declared but pathTarget.absent is ${JSON.stringify(absent ?? "none")} — the pattern base is only ever read for an absent slot, so this declaration would be inert`; } const long = overlong("pathTarget.param", [face.pathTarget.param]) ?? overlong("pathTarget.aliases", face.pathTarget.aliases ?? []) ?? overlong("pathTarget.patternParam", patternParam === undefined ? [] : [patternParam]); if (long !== undefined) return long; vocabulary.add(face.pathTarget.param); for (const a of face.pathTarget.aliases ?? []) vocabulary.add(a); } const hints = face.renderHints; if (hints !== undefined) { const card = hints.approvalCard; if (card !== undefined) { if (!TOOL_APPROVAL_CARDS.includes(card)) return `renderHints.approvalCard ${JSON.stringify(card)} is outside the closed set`; if (RESERVED_APPROVAL_CARDS.includes(card) && (tool === undefined || engineCardOf(tool) !== card)) return `renderHints.approvalCard ${JSON.stringify(card)} is reserved for the engine's own tool that owns it; a declared tool takes another card or none`; const missing = APPROVAL_CARD_REQUIRED_KEYS[card].filter((k) => !top.has(k)); if (missing.length > 0) return `renderHints.approvalCard ${JSON.stringify(card)} needs ${missing.join(", ")} on the tool's schema`; } if (hints.userFacingName !== undefined && hints.userFacingName.length > RENDER_HINT_MAX_CHARS) return `renderHints.userFacingName is ${hints.userFacingName.length} characters; the bound is ${RENDER_HINT_MAX_CHARS}`; if (hints.activity !== undefined && hints.activity.length > RENDER_HINT_ACTIVITY_MAX_CHARS) return `renderHints.activity is ${hints.activity.length} characters; the bound is ${RENDER_HINT_ACTIVITY_MAX_CHARS}`; if ((hints.summaryParams?.length ?? 0) > RENDER_HINT_MAX_LIST) return `renderHints.summaryParams lists ${hints.summaryParams.length} entries; the bound is ${RENDER_HINT_MAX_LIST}`; if ((hints.resultCards?.length ?? 0) > RENDER_HINT_MAX_LIST) return `renderHints.resultCards lists ${hints.resultCards.length} entries; the bound is ${RENDER_HINT_MAX_LIST}`; const long = overlong("renderHints.summaryParams", hints.summaryParams ?? []) ?? overlong("renderHints.resultCards", hints.resultCards ?? []); if (long !== undefined) return long; const stray = (hints.summaryParams ?? []).find((p) => !top.has(p)); if (stray !== undefined) return `renderHints.summaryParams names ${JSON.stringify(stray)}, which is not a top-level property`; } for (const p of face.ruleFace?.params ?? []) if (!vocabulary.has(p)) return `ruleFace.params names ${JSON.stringify(p)}, which is neither a scalar top-level property nor the declared path target`; for (const p of face.ruleFace?.primaryParams ?? []) if (!vocabulary.has(p)) return `ruleFace.primaryParams names ${JSON.stringify(p)}, which is neither a top-level property nor the declared path target`; const long = overlong("ruleFace.params", face.ruleFace?.params ?? []) ?? overlong("ruleFace.primaryParams", face.ruleFace?.primaryParams ?? []); if (long !== undefined) return long; return undefined; } export function applyMcpToolFaces(input) { const faces = input.server.toolFaces; if (faces === undefined) return [...input.tools]; const prefix = mintNamespacePrefix(MCP_NAMESPACE, input.server.name); const accepted = new Set(input.tools.filter((t) => t.name.startsWith(prefix)).map((t) => t.mcpRemoteName).filter((r) => typeof r === "string")); for (const raw of Object.keys(faces)) { if (accepted.has(raw)) continue; const key = `undeclared:${input.server.name}/${raw}`; if (input.announced.has(key)) continue; input.announced.add(key); deliverEngineNotice(input.onNotice, { code: "config.tool_face_undeclared", message: `MCP server "${input.server.name}": toolFaces declares a face for "${raw}", which the server's tool listing does not carry — the declaration is inert`, detail: { server: input.server.name, tool: raw, sessionId: input.sessionId, runId: input.runId }, }); } return input.tools.flatMap((t) => { const stamped = stampOne(t); return stamped === undefined ? [] : [stamped]; }); function stampOne(t) { const raw = t.mcpRemoteName; const face = raw !== undefined && t.name.startsWith(prefix) ? faces?.[raw] : undefined; if (raw === undefined || face === undefined) return t; const hit = { raw, face }; const problem = toolFaceProblem(hit.face, t.parameters, { toolName: t.name }); if (problem !== undefined) { const key = `invalid:${t.name}`; if (!input.announced.has(key)) { input.announced.add(key); deliverEngineNotice(input.onNotice, { code: "config.tool_face_invalid", message: `MCP server "${input.server.name}": the toolFaces overlay for "${hit.raw}" does not fit the tool's materialized schema (${problem}) — the tool is NOT mounted until the overlay fits (mounting it bare would drop the protection the overlay declares)`, detail: { server: input.server.name, tool: hit.raw, problem, sessionId: input.sessionId, runId: input.runId }, }); } return undefined; } const f = hit.face; return { ...t, ...(f.family !== undefined ? { family: f.family } : {}), ...(f.pathTarget !== undefined ? { pathTarget: f.pathTarget } : {}), ...(f.renderHints !== undefined ? { renderHints: f.renderHints } : {}), ...(f.ruleFace !== undefined ? { ruleFace: f.ruleFace } : {}), }; } }