mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix: explain why chat attachments could not be sent (#151913)
This commit is contained in:
parent
4a4462c85b
commit
f0a1c4f0ce
4 changed files with 86 additions and 24 deletions
|
|
@ -226,6 +226,11 @@ Local-path behavior follows the same file-read trust model as the agent:
|
|||
|
||||
Keep sensitive files outside the agent-readable filesystem, or keep `tools.fs.workspaceOnly: true` for stricter local-path sends.
|
||||
|
||||
When the message tool cannot stage an attachment for the current conversation,
|
||||
its error identifies the file and the reported reason, such as a missing file or
|
||||
an unsupported local format. Being inside the workspace does not make every
|
||||
file type eligible for host-local attachment reads.
|
||||
|
||||
## Operations checklist
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -136,20 +136,23 @@ describe("WebChat message tool internal source reply", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("stages buffer media before acknowledging the current-source send", async () => {
|
||||
it.each([
|
||||
{ filename: "proof.txt", contentType: "text/plain", content: "current-source attachment" },
|
||||
{ filename: "proof.html", contentType: "text/html", content: "<!doctype html><h1>Proof</h1>" },
|
||||
])("stages $filename buffer before acknowledging the current-source send", async (fixture) => {
|
||||
await withOpenClawTestState(
|
||||
{ layout: "state-only", prefix: "message-tool-source-buffer-" },
|
||||
async (state) => {
|
||||
await fs.mkdir(state.workspaceDir, { recursive: true });
|
||||
const tool = createCurrentSourceMessageTool({ workspaceDir: state.workspaceDir });
|
||||
const attachment = Buffer.from("current-source attachment");
|
||||
const attachment = Buffer.from(fixture.content);
|
||||
|
||||
const toolResult = await tool.execute("message-buffer-call", {
|
||||
action: "send",
|
||||
message: "Attached proof.",
|
||||
buffer: attachment.toString("base64"),
|
||||
filename: "proof.txt",
|
||||
contentType: "text/plain",
|
||||
filename: fixture.filename,
|
||||
contentType: fixture.contentType,
|
||||
});
|
||||
|
||||
const sourceReply = extractMessagingToolSourceReplyPayload(toolResult);
|
||||
|
|
@ -157,8 +160,8 @@ describe("WebChat message tool internal source reply", () => {
|
|||
expect(sourceReply?.mediaUrls).toHaveLength(1);
|
||||
expect(sourceReply?.attachments).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "proof.txt",
|
||||
mimeType: "text/plain",
|
||||
name: fixture.filename,
|
||||
mimeType: fixture.contentType,
|
||||
trustedLocalMedia: true,
|
||||
}),
|
||||
]);
|
||||
|
|
@ -169,6 +172,64 @@ describe("WebChat message tool internal source reply", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
filename: "report.html",
|
||||
content: "<!doctype html><h1>Report</h1>",
|
||||
reason: "Rejected by the local attachment allowlist. Send a supported file type.",
|
||||
},
|
||||
{
|
||||
filename: "missing.txt",
|
||||
content: undefined,
|
||||
reason: "File not found. Check the path and try again.",
|
||||
},
|
||||
])("reports the staging reason for workspace $filename", async (fixture) => {
|
||||
await withOpenClawTestState(
|
||||
{ layout: "state-only", prefix: "message-tool-source-error-" },
|
||||
async (state) => {
|
||||
await fs.mkdir(state.workspaceDir, { recursive: true });
|
||||
const media = path.join(state.workspaceDir, fixture.filename);
|
||||
if (fixture.content !== undefined) {
|
||||
await fs.writeFile(media, fixture.content);
|
||||
}
|
||||
const tool = createCurrentSourceMessageTool({ workspaceDir: state.workspaceDir });
|
||||
|
||||
await expect(
|
||||
tool.execute("message-path-error", {
|
||||
action: "send",
|
||||
message: "Private draft that must not enter the error.",
|
||||
attachments: [{ media, type: "file", name: fixture.filename }],
|
||||
final: true,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new Error(
|
||||
`Current-source media could not be staged.\n⚠️ ${fixture.filename}: ${fixture.reason}`,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["proof.txt", "proof.md"])("stages the supported workspace file %s", async (filename) => {
|
||||
await withOpenClawTestState(
|
||||
{ layout: "state-only", prefix: "message-tool-source-document-" },
|
||||
async (state) => {
|
||||
await fs.mkdir(state.workspaceDir, { recursive: true });
|
||||
const media = path.join(state.workspaceDir, filename);
|
||||
const content = "# Attachment proof\n";
|
||||
await fs.writeFile(media, content);
|
||||
const tool = createCurrentSourceMessageTool({ workspaceDir: state.workspaceDir });
|
||||
const result = await tool.execute("message-document", {
|
||||
action: "send",
|
||||
attachments: [{ media, type: "file", name: filename }],
|
||||
});
|
||||
const reply = extractMessagingToolSourceReplyPayload(result);
|
||||
expect(reply?.mediaUrls).toHaveLength(1);
|
||||
await expect(fs.readFile(reply?.mediaUrls?.[0] as string, "utf8")).resolves.toBe(content);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("uses policy-scoped bridge access for remote-only current-source media", async () => {
|
||||
await withOpenClawTestState(
|
||||
{ layout: "state-only", prefix: "message-tool-source-remote-media-" },
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../../agents/ag
|
|||
import type { AgentToolResult } from "../../agents/runtime/index.js";
|
||||
import { readStringArrayParam, readToolStringParam } from "../../agents/tools/common.js";
|
||||
import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/reply-payload.js";
|
||||
import {
|
||||
appendReplyMediaFailures,
|
||||
getReplyPayloadMetadata,
|
||||
type ReplyPayload,
|
||||
} from "../../auto-reply/reply-payload.js";
|
||||
import type { ChannelId, ChannelPlugin } from "../../channels/plugins/types.public.js";
|
||||
import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js";
|
||||
import { readBooleanParam } from "../../plugin-sdk/boolean-param.js";
|
||||
|
|
@ -407,8 +411,14 @@ async function handleInternalSourceReplySendAction(
|
|||
if (
|
||||
resolveSendableOutboundReplyParts(sourceReplyPayload).mediaUrls.length !== requestedMediaCount
|
||||
) {
|
||||
const failureMessage = appendReplyMediaFailures(
|
||||
undefined,
|
||||
getReplyPayloadMetadata(sourceReplyPayload)?.assistantMediaFailures ?? [],
|
||||
);
|
||||
throw new Error(
|
||||
"Current-source media could not be staged. Use an accessible URL, a file inside the agent workspace, or the buffer field.",
|
||||
failureMessage
|
||||
? `Current-source media could not be staged.\n${failureMessage}`
|
||||
: "Current-source media could not be staged. Use an accessible URL, a file inside the agent workspace, or the buffer field.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -482,21 +492,7 @@ function buildInternalSourceReplyToolResult(payload: {
|
|||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
dryRun: boolean;
|
||||
}): AgentToolResult<{
|
||||
status: string;
|
||||
deliveryStatus: string;
|
||||
channel: ChannelId;
|
||||
target: string;
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
idempotencyKey?: string;
|
||||
sourceReplyTranscriptOwner?: true;
|
||||
sourceReplySink?: "internal-ui";
|
||||
sourceReply: ReplyPayload;
|
||||
message?: string;
|
||||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
dryRun: boolean;
|
||||
}> {
|
||||
}): AgentToolResult<typeof payload> {
|
||||
const action = payload.dryRun ? "Prepared" : "Sent";
|
||||
const sink = payload.sourceReplySink ? ` via ${payload.sourceReplySink}` : "";
|
||||
const cards = readClawHubRecommendations(payload.sourceReply.channelData);
|
||||
|
|
|
|||
|
|
@ -1130,7 +1130,7 @@ async function loadWebMediaInternal(
|
|||
? await resolveTrustedGeneratedHostReadHtml(mediaUrl)
|
||||
: undefined;
|
||||
if (hostReadDeclaredMime === "text/html" && !htmlTrust) {
|
||||
throw new LocalMediaAccessError("path-not-allowed", HOST_READ_DECLARED_TEXT_ERROR);
|
||||
throw new HostReadMediaTypeError(HOST_READ_DECLARED_TEXT_ERROR);
|
||||
}
|
||||
|
||||
// Local path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue