UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

506 lines (505 loc) • 33 kB
import { arbitrateByMode, classifyAsk } from "../../core/ask-origin.js"; import { deliverEngineNotice } from "../../core/engine-notice.js"; import { createHash } from "node:crypto"; import { Type } from "typebox"; import { ARTIFACT_ACTION_CONCURRENCY_SAFE, ARTIFACT_COMMIT_THROW_OUTCOME, ARTIFACT_FAILURE_RESULT_CODE, ARTIFACT_HOST_ERROR_CODE, ARTIFACT_LIST_SCOPES, ARTIFACT_PLANE_CONTENT_ORIGIN, ARTIFACT_PUBLISH_CONTEXTS, } from "../../core/artifact-host.js"; import { ARTIFACT_TOOL_NAME, toolFace } from "../../core/tool-catalog-entries.js"; import { resolveAsk } from "../../core/tool-policy.js"; import { NOT_BINDABLE } from "../../core/skill-grant-binding.js"; import { approvalCardOf } from "../../core/tool-roster.js"; import { errorResult } from "../../core/tools.js"; import { inlineUntrusted } from "../../core/untrusted-text.js"; import { actionDescription, artifactDescription, PARAM_TEXT, urlDescription } from "./artifact-text.js"; import { ARTIFACT_REJECT_BREAKER_THRESHOLD, artifactRejectBannerFor, artifactRejectKeyOf, createArtifactRejectBreaker } from "./reject-breaker.js"; import { thrownText } from "../../core/thrown-value.js"; export const MAX_FILES_PER_PUBLISH = 255; const LIST_LIMIT_DEFAULT = 25; const LIST_LIMIT_MAX = 100; const BINARY_NOTE_MEDIA = new Set(["application/json", "application/javascript", "text/javascript", "image/svg+xml", "application/xml"]); const MEDIA_BY_EXT = new Map(Object.entries({ html: "text/html", htm: "text/html", css: "text/css", js: "text/javascript", mjs: "text/javascript", json: "application/json", md: "text/markdown", txt: "text/plain", csv: "text/csv", xml: "application/xml", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", wasm: "application/wasm", pdf: "application/pdf", mp3: "audio/mpeg", mp4: "video/mp4", })); export function mediaTypeOf(path) { const dot = path.lastIndexOf("."); return (dot >= 0 ? MEDIA_BY_EXT.get(path.slice(dot + 1).toLowerCase()) : undefined) ?? "application/octet-stream"; } export function isTextMedia(mediaType) { if (mediaType === undefined) return false; const essence = mediaType.split(";")[0].trim().toLowerCase(); return essence.startsWith("text/") || BINARY_NOTE_MEDIA.has(essence); } export function artifactAttemptId(runId, toolCallId, occurrence = 0) { const h = createHash("sha256").update(runId).update("|").update(toolCallId).update("|").update(String(occurrence)).digest("hex"); return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`; } const basenameOf = (p) => { const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")); return i >= 0 ? p.slice(i + 1) : p; }; export function normalizePublishedPath(raw) { if (raw.length === 0) return { ok: false, reason: "is empty" }; if (raw.includes("\0")) return { ok: false, reason: "contains a NUL byte" }; if (raw.includes("\\")) return { ok: false, reason: "contains a backslash (use forward slashes)" }; if (raw.startsWith("/")) return { ok: false, reason: "is root-relative (no leading slash)" }; const path = raw.normalize("NFC"); for (const seg of path.split("/")) if (seg === "" || seg === "." || seg === "..") return { ok: false, reason: "contains an empty, `.` or `..` segment" }; return { ok: true, path }; } function renderSnapshotMeta(s) { const meta = ["title", "description", "favicon", "label"].filter((k) => s[k] !== undefined).map((k) => `${k}: ${inlineUntrusted(s[k], 200)}`); return meta.length === 0 ? "" : `${meta.join("\n")}\n`; } function renderFile(f) { const head = `--- ${inlineUntrusted(f.path, 200)} (${inlineUntrusted(f.mediaType ?? "application/octet-stream", 80)}, ${String(f.content.byteLength).slice(0, 20)} bytes) ---`; return isTextMedia(f.mediaType) ? `${head}\n${Buffer.from(f.content).toString("utf8")}` : `${head}\n[binary content, not shown]`; } const isRecord = (x) => x !== null && typeof x === "object"; const withDefaultAction = (args) => (isRecord(args) && args["action"] === undefined ? { ...args, action: "publish" } : args); const everyItem = (x, pred) => Array.isArray(x) && x.every(pred); const optString = (v) => v === undefined || typeof v === "string"; const isRef = (x) => isRecord(x) && typeof x["url"] === "string" && typeof x["slug"] === "string" && typeof x["version"] === "string" && optString(x["title"]); const isFile = (x) => isRecord(x) && typeof x["path"] === "string" && x["content"] instanceof Uint8Array && optString(x["mediaType"]); const isSnapshot = (x) => isRecord(x) && everyItem(x["files"], isFile) && optString(x["title"]) && optString(x["description"]) && optString(x["favicon"]) && optString(x["label"]); const failureWord = (x, allowed) => isRecord(x) && typeof x["code"] === "string" && allowed.includes(x["code"]); const OUTCOME_SHAPE = { publish: (o) => isRecord(o) && (o["ok"] === true ? isRef(o["ref"]) : o["ok"] === false && (o["code"] === "conflict" ? typeof o["currentVersion"] === "string" && (o["current"] === "reread" || isSnapshot(o["current"])) : failureWord(o, ["url_foreign", "forbidden", "payload_too_large", "not_found", "force_refused", "refused"]) && typeof o["message"] === "string")), list: (o) => isRecord(o) && (o["ok"] === true ? everyItem(o["rows"], (r) => isRef(r) && isRecord(r) && (r["owner"] === "mine" || r["owner"] === "shared") && typeof r["updatedAt"] === "string") && typeof o["truncated"] === "boolean" : o["ok"] === false && failureWord(o, ["refused"]) && typeof o["message"] === "string"), read: (o) => isRecord(o) && (o["ok"] === true ? isRef(o["ref"]) && typeof o["content"] === "string" && typeof o["isolatedSummary"] === "boolean" : o["ok"] === false && failureWord(o, ["not_found", "forbidden"]) && typeof o["message"] === "string"), list_files: (o) => isRecord(o) && (o["ok"] === true ? everyItem(o["entries"], (e) => isRecord(e) && typeof e["path"] === "string" && typeof e["bytes"] === "number" && optString(e["mediaType"])) : o["ok"] === false && failureWord(o, ["not_found", "forbidden"]) && typeof o["message"] === "string"), read_file: (o) => isRecord(o) && (o["ok"] === true ? isFile(o["file"]) : o["ok"] === false && failureWord(o, ["not_found", "forbidden"]) && typeof o["message"] === "string"), }; function callHost(action, fn, signal) { if (signal?.aborted) return Promise.resolve({ ok: false, kind: "aborted", message: "the call was aborted before the host was reached" }); return new Promise((resolve) => { const onAbort = () => resolve({ ok: false, kind: "aborted", message: "the call was aborted while the host was working" }); signal?.addEventListener("abort", onAbort, { once: true }); let p; try { p = Promise.resolve(fn()); } catch (err) { signal?.removeEventListener("abort", onAbort); resolve({ ok: false, kind: "threw", message: thrownText(err) }); return; } p.then((value) => resolve(OUTCOME_SHAPE[action](value) ? { ok: true, value } : { ok: false, kind: "malformed", message: `the ${action} answer was outside the host contract` }), (err) => resolve({ ok: false, kind: "threw", message: thrownText(err) })).finally(() => signal?.removeEventListener("abort", onAbort)); }); } function buildSchema(caps) { const has = (a) => caps.actions.includes(a); const props = { action: Type.Optional(Type.Unsafe({ type: "string", enum: [...caps.actions], description: actionDescription(caps.actions) })), file_path: Type.Optional(Type.String({ description: PARAM_TEXT.filePath })), url: Type.Optional(Type.String({ maxLength: 2048, description: urlDescription(caps.actions) })), title: Type.Optional(Type.String({ maxLength: 200, description: PARAM_TEXT.title })), description: Type.Optional(Type.String({ maxLength: 1000, description: PARAM_TEXT.description })), favicon: Type.Optional(Type.String({ maxLength: 32, description: PARAM_TEXT.favicon })), label: Type.Optional(Type.String({ maxLength: 60, description: PARAM_TEXT.label })), baseVersion: Type.Optional(Type.String({ maxLength: 200, description: PARAM_TEXT.baseVersion })), force: Type.Optional(Type.Boolean({ description: PARAM_TEXT.force })), publishContext: Type.Optional(Type.Unsafe({ type: "string", enum: [...ARTIFACT_PUBLISH_CONTEXTS], description: PARAM_TEXT.publishContext })), }; if (caps.multiFile) { props["files"] = Type.Optional(Type.Array(Type.Object({ path: Type.String(), file_path: Type.String() }), { maxItems: MAX_FILES_PER_PUBLISH, description: PARAM_TEXT.files })); } if (has("list")) { props["scope"] = Type.Optional(Type.Unsafe({ type: "string", enum: [...ARTIFACT_LIST_SCOPES], description: PARAM_TEXT.scope })); props["limit"] = Type.Optional(Type.Integer({ minimum: 1, maximum: LIST_LIMIT_MAX, description: PARAM_TEXT.limit })); } if (has("read_file")) props["path"] = Type.Optional(Type.String({ maxLength: 1024, description: PARAM_TEXT.path })); return Type.Object(props); } const invalid = (text) => errorResult(`Error (Artifact): ${text}`, { type: "artifact", invalidArguments: true }); const typed = (code, text, extra = {}) => errorResult(`Error (Artifact): ${text}`, { type: "artifact", code: ARTIFACT_FAILURE_RESULT_CODE[code], ...extra }); export function createArtifactTool(opts) { const { host, capabilities: caps, runId, taskId, sessionId, agentName, readLocalFile, onAsk, permissionMode, onNotice } = opts; let releaseAnnounced = false; const versions = new Map(); const bindings = new Map(); const occurrences = new Map(); const breaker = createArtifactRejectBreaker(); const ownUrls = new Set(); const sharedUrls = new Set(); const sharedReadApproved = new Set(); const consentInFlight = new Map(); const activeTargets = new Map(); const hasAction = (a) => caps.actions.includes(a); const rememberRef = (ref) => { versions.set(ref.url, ref.version); }; const rememberOwner = (url, owner) => { (owner === "mine" ? ownUrls : sharedUrls).add(url); }; const nextContext = (ctx) => { const n = occurrences.get(ctx.toolCallId) ?? 0; occurrences.set(ctx.toolCallId, n + 1); return { taskId, ...(sessionId !== undefined ? { sessionId } : {}), ...(agentName !== undefined ? { agentName } : {}), runId, attemptId: artifactAttemptId(runId, ctx.toolCallId, n), }; }; const hostError = (verb, fault, url) => { const outcome = fault.kind === "threw" ? ARTIFACT_COMMIT_THROW_OUTCOME[caps.commit] : "unknown"; const details = { type: "artifact", code: ARTIFACT_HOST_ERROR_CODE, outcome, plane: caps.plane, commit: caps.commit }; if (outcome === "not_executed") { return errorResult(`Error (Artifact): the artifact host failed before committing (${inlineUntrusted(fault.message, 200)}). Nothing was published; the call can be repeated.`, details); } const verifiable = hasAction("read") && url !== ""; return errorResult(`Error (Artifact): the artifact host did not answer the ${verb.slice(0, 20)} (${inlineUntrusted(fault.message, 200)}). Whether it was applied is not known: it may already have been committed, so do not simply repeat the call.${verifiable ? ` Verify with action: "read" on ${url.slice(0, 200)} first.` : ""}`, details); }; const notFoundOrForbidden = (code, url, message) => code === "not_found" ? typed(code, `no artifact at ${inlineUntrusted(url, 200)} (${inlineUntrusted(message, 200)}).`, { url }) : typed(code, `${inlineUntrusted(url, 200)} is not accessible to this session (${inlineUntrusted(message, 200)}).`, { url }); const readRefusal = (path, r) => { switch (r.reason) { case "refused": return errorResult(r.text.slice(0, 4000), r.details); case "too_large": return typed("payload_too_large", `${inlineUntrusted(path, 200)} is ${String(r.size).slice(0, 20)} bytes; this host accepts at most ${String(r.limit).slice(0, 20)} bytes for it${r.judgedOn === "metadata" ? " (the file was not read)" : " (it grew past its declared size while being read)"}.`, { limit: r.limit, measured: r.size, judgedOn: r.judgedOn }); case "not_a_file": return invalid(`${inlineUntrusted(path, 200)} is a ${inlineUntrusted(r.kind, 20)}, not a regular file.`); default: { const _exhaustive = r; return _exhaustive; } } }; const manifestOf = (a) => { const mainNorm = normalizePublishedPath(basenameOf(a.file_path)); if (!mainNorm.ok) return { ok: false, result: invalid(`the published name of ${inlineUntrusted(a.file_path, 200)} ${mainNorm.reason.slice(0, 80)}.`) }; const entries = []; if (a.files === undefined || (Array.isArray(a.files) && a.files.length === 0)) return { ok: true, main: mainNorm.path, entries }; if (!caps.multiFile) return { ok: false, result: invalid("this artifact host publishes single files only; `files` is not accepted.") }; if (!Array.isArray(a.files)) return { ok: false, result: invalid("`files` must be an array of { path, file_path } entries.") }; if (a.files.length > MAX_FILES_PER_PUBLISH) return { ok: false, result: invalid(`\`files\` carries ${String(a.files.length).slice(0, 20)} entries; at most ${MAX_FILES_PER_PUBLISH} supporting files are accepted per publish.`) }; const seen = new Set([mainNorm.path.toLowerCase()]); for (const entry of a.files) { if (!isRecord(entry) || typeof entry["path"] !== "string" || typeof entry["file_path"] !== "string") return { ok: false, result: invalid("every `files` entry must carry a string `path` and a string `file_path`.") }; const norm = normalizePublishedPath(entry["path"]); if (!norm.ok) return { ok: false, result: invalid(`files entry ${inlineUntrusted(entry["path"], 120)} ${norm.reason.slice(0, 80)}.`) }; const fold = norm.path.toLowerCase(); if (seen.has(fold)) return { ok: false, result: invalid(`files entry ${inlineUntrusted(norm.path, 120)} collides with another published path.`) }; seen.add(fold); entries.push({ path: norm.path, file_path: entry["file_path"] }); } return { ok: true, main: mainNorm.path, entries }; }; async function publish(a, ctx) { if (typeof a.file_path !== "string") return invalid("`file_path` is required to publish."); const manifest = manifestOf(a); if (!manifest.ok) return manifest.result; const files = []; const budget = Math.min(caps.maxFileBytes, caps.maxTotalBytes); const first = await readLocalFile(a.file_path, budget, ctx.signal); if (!first.ok) return readRefusal(a.file_path, first); files.push({ path: manifest.main, content: first.bytes, mediaType: mediaTypeOf(manifest.main) }); let total = first.bytes.byteLength; const url = a.url ?? bindings.get(first.key); if (url === undefined && (a.force === true || a.baseVersion !== undefined)) { return invalid("`force` and `baseVersion` only apply to an update — pass `url`, or republish the same file path this session already published."); } for (const entry of manifest.entries) { const left = caps.maxTotalBytes - total; if (left <= 0) return typed("payload_too_large", `the files already total ${String(total).slice(0, 20)} bytes; this host accepts at most ${String(caps.maxTotalBytes).slice(0, 20)} bytes per version.`, { limit: caps.maxTotalBytes, measured: total }); const r = await readLocalFile(entry.file_path, Math.min(caps.maxFileBytes, left), ctx.signal); if (!r.ok) return readRefusal(entry.file_path, r); total += r.bytes.byteLength; files.push({ path: entry.path, content: r.bytes, mediaType: mediaTypeOf(entry.path) }); } const baseVersion = a.baseVersion ?? (url !== undefined ? versions.get(url) : undefined); const req = { ...(url !== undefined ? { url } : {}), files, ...(a.title !== undefined ? { title: a.title } : {}), ...(a.description !== undefined ? { description: a.description } : {}), ...(a.favicon !== undefined ? { favicon: a.favicon } : {}), ...(a.label !== undefined ? { label: a.label } : {}), ...(baseVersion !== undefined ? { baseVersion } : {}), ...(a.force === true ? { force: true } : {}), publishContext: a.publishContext ?? "plain", }; const call = await callHost("publish", () => host.publish(req, nextContext(ctx)), ctx.signal); if (!call.ok) return hostError("publish", call, inlineUntrusted(url ?? "", 200)); const out = call.value; if (out.ok) { rememberRef(out.ref); rememberOwner(out.ref.url, "mine"); bindings.set(first.key, out.ref.url); const title = out.ref.title !== undefined ? ` — ${inlineUntrusted(out.ref.title, 120)}` : ""; return { content: `Published ${inlineUntrusted(out.ref.url, 300)} (version ${inlineUntrusted(out.ref.version, 60)})${title}.`, details: { type: "artifact", action: "publish", plane: caps.plane, ref: out.ref, update: url !== undefined }, }; } if (out.code === "conflict") { let snapshot = out.current; if (snapshot !== "reread") { const withheld = url === undefined || (await filesOwnership(url, ctx)) !== undefined; if (withheld || (url !== undefined && sharedUrls.has(url) && !sharedReadApproved.has(url))) snapshot = "reread"; } if (snapshot !== "reread" && url !== undefined) versions.set(url, out.currentVersion); const handedBack = snapshot === "reread" ? hasAction("read") ? `The current content was not handed back; read it with action: "read" on ${inlineUntrusted(url ?? "", 200)}.` : "The current content was not handed back by the host." : `Current content follows.\n${renderSnapshotMeta(snapshot)}${snapshot.files.map(renderFile).join("\n")}`; return errorResult(`Error (Artifact): publish conflict — ${inlineUntrusted(url ?? "", 200)} is at version ${inlineUntrusted(out.currentVersion, 60)}, this publish was based on ${inlineUntrusted(baseVersion ?? "", 60)}. Merge your changes onto the current content and publish again. ${handedBack}`, { type: "artifact", code: ARTIFACT_FAILURE_RESULT_CODE.conflict, url, currentVersion: out.currentVersion, current: snapshot === "reread" ? "reread" : { ...snapshot, files: snapshot.files.map((f) => ({ path: f.path, bytes: f.content.byteLength, mediaType: f.mediaType })) } }); } switch (out.code) { case "url_foreign": return typed(out.code, `${inlineUntrusted(url ?? "", 200)} is not an artifact this host (or this run) published; nothing was published (${inlineUntrusted(out.message, 200)}).`, { url }); case "forbidden": return typed(out.code, `${inlineUntrusted(url ?? "", 200)} is not this session's to update; nothing was published (${inlineUntrusted(out.message, 200)}).`, { url }); case "payload_too_large": return typed(out.code, `the host refused the payload as too large; nothing was published (${inlineUntrusted(out.message, 200)}).`); case "not_found": return typed(out.code, `no artifact at ${inlineUntrusted(url ?? "", 200)}; nothing was published (${inlineUntrusted(out.message, 200)}).`, { url }); case "force_refused": return typed(out.code, `the host refused to overwrite with force; nothing was published (${inlineUntrusted(out.message, 200)}). Merge onto the current version instead.`, { url }); case "refused": return typed(out.code, `the host refused the publish; nothing was published (${inlineUntrusted(out.message, 200)}).`); default: { const _exhaustive = out; return _exhaustive; } } } async function list(a, ctx) { const scope = a.scope ?? "mine"; const limit = a.limit ?? LIST_LIMIT_DEFAULT; const call = await callHost("list", () => host.list({ scope, limit }, nextContext(ctx)), ctx.signal); if (!call.ok) return hostError("list", call, ""); const out = call.value; if (!out.ok) return typed("refused", `the host refused the listing (${inlineUntrusted(out.message, 200)}).`); for (const r of out.rows) rememberOwner(r.url, r.owner); const lines = out.rows.map((r) => `${inlineUntrusted(r.url, 300)} v${inlineUntrusted(r.version, 40)} ${inlineUntrusted(r.updatedAt, 40)} (${r.owner})${r.title !== undefined ? ` ${inlineUntrusted(r.title, 120)}` : ""}`); const body = lines.length === 0 ? "nothing listed" : lines.join("\n"); return { content: `${body}${out.truncated ? "\n(more artifacts exist; raise `limit` to see them)" : ""}`, details: { type: "artifact", action: "list", plane: caps.plane, scope, rows: out.rows, truncated: out.truncated } }; } const sharedReadConsent = (url, ref, ctx) => { if (sharedReadApproved.has(url)) return Promise.resolve({ ok: true }); if (permissionMode !== undefined) { const verdict = arbitrateByMode(permissionMode(), classifyAsk({ action: "ask", requiresRealApproval: true, guards: ["artifact_shared_read"] }, { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} })); if (verdict.kind === "released") { if (!releaseAnnounced && onNotice !== undefined) { releaseAnnounced = true; deliverEngineNotice(onNotice, { code: "permission.ask_released_by_mode", message: `the ${verdict.mode} permission mode released a shared-artifact read (${inlineUntrusted(url, 300)}) without asking anyone; further shared reads of this run under the mode are released the same way`, detail: { mode: verdict.mode, askClass: verdict.askClass, count: 1, firstToolCallId: ctx.toolCallId, ...(sessionId !== undefined ? { sessionId } : {}), runId } }); } return Promise.resolve({ ok: true }); } if (verdict.kind === "denied_by_mode") return Promise.resolve({ ok: false, reason: `the ${verdict.mode} permission mode refuses to read another person's artifact without approval` }); } const pending = consentInFlight.get(url); if (pending !== undefined) return pending; const ask = (async () => { const title = ref?.title !== undefined ? ` (${inlineUntrusted(ref.title, 120)})` : ""; const version = ref !== undefined ? `, version ${inlineUntrusted(ref.version, 60)}` : ""; const resolved = await resolveAsk({ toolName: ARTIFACT_TOOL_NAME, toolCallId: ctx.toolCallId, args: { action: "read", url }, message: `Read a shared artifact: ${inlineUntrusted(url, 300)}${title}${version} — an artifact another person shared with this user; approving covers re-reads of it for the rest of this run.`, requiresRealApproval: true, approvalCard: approvalCardOf({ toolName: ARTIFACT_TOOL_NAME }, toolFace("artifact").renderHints?.approvalCard), ...NOT_BINDABLE, }, onAsk, ctx.signal); if (resolved.action !== "allow") return { ok: false, reason: resolved.message ?? "not approved" }; if (resolved.updatedInput !== undefined) return { ok: false, reason: "the approval edited the request; an edited approval does not authorize the original target" }; sharedReadApproved.add(url); return { ok: true }; })().finally(() => { if (!activeTargets.has(url)) consentInFlight.delete(url); }); consentInFlight.set(url, ask); return ask; }; const filesOwnership = async (url, ctx) => { if (!ownUrls.has(url) && !sharedUrls.has(url)) { if (!hasAction("read")) return typed("refused", `${inlineUntrusted(url, 200)} is not an artifact this run published or listed as its own, and this host declares no read to establish who owns it; its files were not delivered.`, { url, sharedRead: "ownership_unknown" }); const probe = await callHost("read", () => host.read({ url }, nextContext(ctx)), ctx.signal); if (!probe.ok) return hostError("read", probe, inlineUntrusted(url, 200)); if (!probe.value.ok) return notFoundOrForbidden(probe.value.code, url, probe.value.message); if (probe.value.ref.url !== url) return hostError("read", { kind: "malformed", message: "read.ref.url does not match the requested target" }, inlineUntrusted(url, 200)); rememberOwner(url, probe.value.isolatedSummary ? "shared" : "mine"); } if (!sharedUrls.has(url)) return undefined; const consent = await sharedReadConsent(url, undefined, ctx); return consent.ok ? undefined : typed("refused", `reading the shared artifact ${inlineUntrusted(url, 200)} was not approved (${inlineUntrusted(consent.reason, 200)}); its files were not delivered.`, { url, sharedRead: "not_approved" }); }; async function read(a, ctx) { if (a.url === undefined) return invalid("`url` is required to read an artifact."); const call = await callHost("read", () => host.read({ url: a.url }, nextContext(ctx)), ctx.signal); if (!call.ok) return hostError("read", call, inlineUntrusted(a.url, 200)); const out = call.value; if (!out.ok) return notFoundOrForbidden(out.code, a.url, out.message); if (out.ref.url !== a.url) return hostError("read", { kind: "malformed", message: "read.ref.url does not match the requested target" }, inlineUntrusted(a.url, 200)); rememberOwner(a.url, out.isolatedSummary ? "shared" : "mine"); if (sharedUrls.has(a.url)) { const consent = await sharedReadConsent(a.url, out.ref, ctx); if (!consent.ok) return typed("refused", `reading the shared artifact ${inlineUntrusted(a.url, 200)} was not approved (${inlineUntrusted(consent.reason, 200)}); its content was not delivered.`, { url: a.url, sharedRead: "not_approved" }); } rememberRef(out.ref); const head = out.isolatedSummary ? `[isolated summary of a shared artifact — ${inlineUntrusted(out.ref.url, 300)}, version ${inlineUntrusted(out.ref.version, 60)}]\n` : ""; return { content: `${head}${out.content}`, details: { type: "artifact", action: "read", plane: caps.plane, ref: out.ref, isolatedSummary: out.isolatedSummary } }; } async function listFiles(a, ctx) { if (a.url === undefined) return invalid("`url` is required to list an artifact's files."); const gated = await filesOwnership(a.url, ctx); if (gated !== undefined) return gated; const call = await callHost("list_files", () => host.listFiles({ url: a.url }, nextContext(ctx)), ctx.signal); if (!call.ok) return hostError("list_files", call, inlineUntrusted(a.url, 200)); const out = call.value; if (!out.ok) return notFoundOrForbidden(out.code, a.url, out.message); if (sharedUrls.has(a.url)) { const denied = await filesOwnership(a.url, ctx); if (denied !== undefined) return denied; } const lines = out.entries.map((e) => `${inlineUntrusted(e.path, 200)} ${String(e.bytes).slice(0, 20)} bytes${e.mediaType !== undefined ? ` ${inlineUntrusted(e.mediaType, 80)}` : ""}`); return { content: lines.length === 0 ? "no files" : lines.join("\n"), details: { type: "artifact", action: "list_files", plane: caps.plane, url: a.url, entries: out.entries } }; } async function readFile(a, ctx) { if (a.url === undefined || a.path === undefined) return invalid("`url` and `path` are required to read a published file."); const gated = await filesOwnership(a.url, ctx); if (gated !== undefined) return gated; const call = await callHost("read_file", () => host.readFile({ url: a.url, path: a.path }, nextContext(ctx)), ctx.signal); if (!call.ok) return hostError("read_file", call, inlineUntrusted(a.url, 200)); const out = call.value; if (!out.ok) return notFoundOrForbidden(out.code, a.url, out.message); if (sharedUrls.has(a.url)) { const denied = await filesOwnership(a.url, ctx); if (denied !== undefined) return denied; } return { content: renderFile(out.file), details: { type: "artifact", action: "read_file", plane: caps.plane, url: a.url, path: out.file.path, bytes: out.file.content.byteLength, mediaType: out.file.mediaType } }; } return { name: ARTIFACT_TOOL_NAME, ...toolFace("artifact"), contentOrigin: ARTIFACT_PLANE_CONTENT_ORIGIN[caps.plane], description: artifactDescription(caps.plane, caps.actions), parameters: buildSchema(caps), prepareArguments: withDefaultAction, isConcurrencySafe: (args) => isRecord(args) && typeof args["action"] === "string" && ARTIFACT_ACTION_CONCURRENCY_SAFE[args["action"]] === true, execute: async (args, ctx) => { const a = withDefaultAction(args); const action = a.action; const target = typeof a.url === "string" ? a.url : undefined; if (target !== undefined) activeTargets.set(target, (activeTargets.get(target) ?? 0) + 1); let result; try { result = await dispatch(action, a, ctx); } finally { if (target !== undefined) { const left = activeTargets.get(target) - 1; if (left > 0) activeTargets.set(target, left); else { activeTargets.delete(target); consentInFlight.delete(target); } } } if (typeof result === "string" || result.isError !== true) { breaker.clear(); return result; } const details = isRecord(result.details) ? result.details : {}; const reason = typeof details["code"] === "string" ? details["code"] : details["invalidArguments"] === true ? "invalid_arguments" : "error"; const text = typeof result.content === "string" ? result.content : ""; const count = breaker.hit(artifactRejectKeyOf(a), reason, text); if (count < ARTIFACT_REJECT_BREAKER_THRESHOLD || typeof result.content !== "string") return result; return { ...result, content: `${artifactRejectBannerFor(reason)}\n\n${result.content}`, details: { ...details, rejectStreak: count } }; }, }; function dispatch(action, a, ctx) { if (!hasAction(action)) return invalid(`action ${inlineUntrusted(String(action), 40)} is not available on this artifact host.`); switch (action) { case "publish": return publish(a, ctx); case "list": return list(a, ctx); case "read": return read(a, ctx); case "list_files": return listFiles(a, ctx); case "read_file": return readFile(a, ctx); default: { const _exhaustive = action; return _exhaustive; } } } }