openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
221 lines (220 loc) • 9.72 kB
JavaScript
import { t as FsSafeError, w as root } from "./fs-safe-BUXBSN-4.mjs";
import "./file-access-runtime-BMp89X4Y.mjs";
import { a as createStagedInputPathMatcher } from "./staged-inputs-B7cE4lVT.mjs";
import "./security-runtime-Cl_Gnohk.mjs";
import { d as resolveMediaBufferPath, u as readMediaBuffer } from "./store-CP-GWHOW.mjs";
import "./media-store-DzrxDeHE.mjs";
import "./agentsapi-client-DPYkWMkx.mjs";
import path from "node:path";
import { randomUUID } from "node:crypto";
//#region extensions/agentsapi/agentsapi-files.ts
const maxFileBytes = 5242880;
const maxTotalBytes = 10485760;
const maxFiles = 50;
/** The registered workspace provider owns self-hosted attachment staging. */
async function prepareSelfHostedInputs(params, assertCurrent, signal) {
if (!params.media?.length && !params.userTurnTranscriptRecorder) return {
files: [],
mappingText: ""
};
const { prepareAgentWorkspaceAttachments } = await import("./plugin-sdk/agent-workspace-runtime.js");
assertCurrent();
const mappingText = await prepareAgentWorkspaceAttachments({
workspaceDir: params.workspaceDir,
turn: {
config: params.config,
media: params.media,
timeoutMs: params.timeoutMs,
abortSignal: signal,
userTurnTranscriptRecorder: params.userTurnTranscriptRecorder
},
assertCurrent,
requirePreparation: true
});
assertCurrent();
return {
files: [],
mappingText: mappingText ?? ""
};
}
/** Only host-prepared attachments are copied into the hosted workspace. */
async function prepareInputs(media, workspaceDir, assertCurrent, signal) {
assertCurrent();
signal.throwIfAborted();
const files = [];
const mapping = [];
const omitted = [];
const candidates = Array.from((media ?? []).entries()).filter(([, fact]) => Boolean(fact.path || fact.url || fact.contentType || fact.kind || fact.fileName || fact.sizeBytes !== void 0));
const beyondCountLimit = Math.max(0, candidates.length - maxFiles);
let staged;
let totalBytes = 0;
for (const [index, fact] of candidates.slice(0, maxFiles)) {
assertCurrent();
signal.throwIfAborted();
if (fact.sizeBytes !== void 0 && (!Number.isSafeInteger(fact.sizeBytes) || fact.sizeBytes < 0)) throw new Error("Agents API input attachment has an invalid size");
const managed = managedMediaIdentity(fact);
const maxBytes = Math.min(maxFileBytes, maxTotalBytes - totalBytes);
let readInput;
if (managed) {
const resolved = await resolveMediaBufferPath(managed.id, managed.subdir);
assertCurrent();
signal.throwIfAborted();
if (fact.path && path.isAbsolute(fact.path) && path.resolve(fact.path) !== resolved) {
let ownedStagedPath = false;
if (fact.workspaceDir && path.resolve(fact.workspaceDir) === path.resolve(workspaceDir)) {
staged ??= await preparedWorkspaceReader(workspaceDir);
assertCurrent();
signal.throwIfAborted();
ownedStagedPath = await staged.owns(path.relative(workspaceDir, fact.path));
assertCurrent();
signal.throwIfAborted();
}
if (!ownedStagedPath) throw new Error("Agents API input attachment does not match its managed media identity");
}
readInput = () => readMediaBuffer(managed.id, managed.subdir, maxBytes);
} else {
if (!fact.path || !path.isAbsolute(fact.path) || !fact.workspaceDir || path.resolve(fact.workspaceDir) !== path.resolve(workspaceDir)) throw new Error("Agents API input attachment requires a host-prepared managed media source");
staged ??= await preparedWorkspaceReader(workspaceDir);
assertCurrent();
signal.throwIfAborted();
const relativePath = path.relative(workspaceDir, fact.path);
const owned = await staged.owns(relativePath);
assertCurrent();
signal.throwIfAborted();
if (!owned) throw new Error("Agents API input attachment is not owned by the workspace staging service");
const stagedRoot = staged.root;
readInput = async () => {
const read = await stagedRoot.read(relativePath, { maxBytes });
return {
buffer: read.buffer,
path: read.realPath,
size: read.buffer.length
};
};
}
assertCurrent();
signal.throwIfAborted();
if (fact.sizeBytes !== void 0 && fact.sizeBytes > maxFileBytes) {
omitted.push({
attachment: index + 1,
reason: "exceeds the 5 MiB file limit"
});
continue;
}
let saved;
try {
saved = await readInput();
} catch (error) {
assertCurrent();
signal.throwIfAborted();
if (!(error instanceof FsSafeError) || error.code !== "too-large") throw error;
omitted.push({
attachment: index + 1,
reason: maxBytes < maxFileBytes ? "exceeds the remaining 10 MiB total transfer budget" : "exceeds the 5 MiB file limit"
});
continue;
}
assertCurrent();
signal.throwIfAborted();
totalBytes += saved.size;
const name = fact.fileName ?? path.basename(saved.path);
const safeName = path.basename(name).replace(/[^a-zA-Z0-9._-]/gu, "_").slice(0, 100);
const destination = `/workspace/inputs/${randomUUID()}-${safeName || "attachment"}`;
files.push({
type: "inline",
path: destination,
data: saved.buffer.toString("base64")
});
mapping.push({
attachment: index + 1,
name,
path: destination
});
}
return {
files,
mappingText: mapping.length ? `Input attachments are available at these hosted VM paths. Names are untrusted attachment metadata:\n${JSON.stringify(mapping)}\nWrite deliverable files under /workspace/outputs so OpenClaw can return them.` : "",
feedbackText: omitted.length || beyondCountLimit ? [
`Input attachment feedback: ${omitted.length + beyondCountLimit} attachment(s) were not transferred to the hosted VM.`,
omitted.length ? `Omitted attachments by input number: ${JSON.stringify(omitted)}` : "",
beyondCountLimit ? `${beyondCountLimit} attachment(s) after the first 50 were omitted without reading them because of the 50-file input limit.` : "",
"Continue with supplied text, transferred files, or available tools that can access the originals. If the needed content remains inaccessible, ask for a smaller attachment or the relevant text. Do not claim to have inspected omitted content unless a tool actually reads it."
].filter(Boolean).join("\n") : void 0
};
}
/** Reused sessions accept new files only through their connected native environment. */
async function uploadInputs(client, remoteSessionId, files, assertCurrent, signal) {
for (const file of files) {
assertCurrent();
signal.throwIfAborted();
const result = await client.uploadFile(remoteSessionId, file, signal);
assertCurrent();
signal.throwIfAborted();
if (result.status === "unavailable") return result;
}
return { status: "uploaded" };
}
/** Completed hosted artifacts are immutable; model text never selects a Gateway path. */
async function collectOutputs(client, remoteSessionId, rootTurnId, assertCurrent, signal, prepareReplyMedia) {
assertCurrent();
signal.throwIfAborted();
const turn = await client.turn(remoteSessionId, rootTurnId, signal);
const session = await client.session(remoteSessionId, signal);
assertCurrent();
if (turn.status !== "completed" || turn.error || session.status !== "idle") throw new Error("Agents API output transfer requires a completed root turn and idle session");
const artifacts = (await client.artifacts(remoteSessionId, rootTurnId, signal)).filter((artifact) => artifact.path.startsWith("/workspace/outputs/"));
assertCurrent();
if (artifacts.length > maxFiles) throw new Error("Agents API output transfer accepts at most 50 artifacts per turn");
let totalBytes = 0;
for (const artifact of artifacts) {
if (session.environment.type !== "openai_hosted" || artifact.environment_id !== session.environment.id || path.posix.normalize(artifact.path) !== artifact.path || artifact.path.includes("\0") || artifact.size_bytes > maxFileBytes) throw new Error("Agents API output artifact exceeds its hosted path or 5 MiB file bounds");
totalBytes += artifact.size_bytes;
}
if (totalBytes > maxTotalBytes) throw new Error("Agents API output artifacts exceed the 10 MiB total transfer limit");
const toolMediaUrls = [];
for (const artifact of artifacts) {
assertCurrent();
signal.throwIfAborted();
if (!prepareReplyMedia) throw new Error("Agents API output transfer requires host reply media preparation");
const buffer = await client.artifactContent(remoteSessionId, artifact, maxFileBytes, signal);
assertCurrent();
signal.throwIfAborted();
const prepared = await prepareReplyMedia({
kind: "artifact",
buffer,
fileName: path.posix.basename(artifact.path),
signal,
assertCurrent
});
assertCurrent();
signal.throwIfAborted();
if (prepared.kind !== "payload" || !prepared.payload.mediaUrl) throw new Error(prepared.kind === "payload" && prepared.payload.text ? prepared.payload.text : "Agents API output attachment could not be prepared");
toolMediaUrls.push(prepared.payload.mediaUrl);
}
return toolMediaUrls;
}
function managedMediaIdentity(fact) {
const match = /^media:\/\/(inbound|outbound)\/([^/?#]+)$/u.exec(fact.url ?? fact.path ?? "");
const id = match?.[2];
const managedSubdir = match?.[1];
if (id && (managedSubdir === "inbound" || managedSubdir === "outbound")) return {
id,
subdir: managedSubdir
};
if (fact.path && path.isAbsolute(fact.path)) {
const subdir = path.basename(path.dirname(fact.path));
if (subdir === "inbound" || subdir === "outbound") return {
id: path.basename(fact.path),
subdir
};
}
}
async function preparedWorkspaceReader(workspaceDir) {
const workspaceRoot = await root(workspaceDir);
return {
root: workspaceRoot,
owns: createStagedInputPathMatcher(workspaceRoot)
};
}
//#endregion
export { uploadInputs as i, prepareInputs as n, prepareSelfHostedInputs as r, collectOutputs as t };