fix: confine Agent and MCP data access

This commit is contained in:
2026-07-10 16:38:08 +08:00
parent 73fa28a178
commit f4968d6657
25 changed files with 1521 additions and 221 deletions
+62 -12
View File
@@ -1,17 +1,19 @@
import { createSdkMcpServer, tool, type McpSdkServerConfigWithInstance, type SdkMcpToolDefinition } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
import { sendApprovalCard, sendFile, type FeishuRuntime, type SendMessageOptions } from "./client.js";
import { sendApprovalCard, sendFileData, type FeishuRuntime, type SendMessageOptions } from "./client.js";
import { resolveDeliverableFile } from "./fileDelivery.js";
import { downloadFeishuMessageResource } from "./download.js";
import { readFeishuContext } from "./read.js";
import type { ApprovalManager } from "./approval.js";
import { CPH_HUB_MCP_TOOL_IDS, type CphHubMcpToolId } from "../agent/roleTools.js";
import { WorkspaceFileBoundaryError } from "../security/workspaceFiles.js";
export interface FileDeliveryToolOptions {
readonly rt: FeishuRuntime;
readonly chatId: string;
readonly projectId: string;
readonly runId: string;
readonly workspaceRoot?: string | undefined;
readonly workspaceDir: string;
readonly sendOptions?: SendMessageOptions | undefined;
readonly approvalManager: ApprovalManager;
@@ -27,13 +29,46 @@ export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): M
tools.push(
tool(
"send_file",
"Upload an existing project/repository file to the current Feishu chat. The path must point to a concrete file, for example build/student.pdf or README.md.",
"Upload an existing file from the current project workspace to the current Feishu chat. The path must point to a concrete no-symlink file, for example build/student.pdf or README.md.",
{
path: z.string().describe("Workspace-relative, repository-relative, or absolute path to the existing file."),
path: z.string().describe("Workspace-relative path, or an absolute path physically inside the current workspace."),
name: z.string().optional().describe("Optional display filename. Defaults to the file's basename."),
},
async (args) => {
const file = await resolveDeliverableFile(args.path, options.workspaceDir);
const workspaceRoot = options.workspaceRoot?.trim();
if (workspaceRoot === undefined || workspaceRoot === "") {
options.rt.logger.error(
{ runId: options.runId, projectId: options.projectId },
"Agent file delivery missing configured workspace root",
);
return {
isError: true,
content: [{ type: "text", text: "File delivery is unavailable because workspace isolation is not configured." }],
};
}
let file;
try {
file = await resolveDeliverableFile(args.path, workspaceRoot, options.workspaceDir);
} catch (error) {
const boundaryViolation = error instanceof WorkspaceFileBoundaryError && error.reason === "boundary";
const log = boundaryViolation ? options.rt.logger.warn.bind(options.rt.logger) : options.rt.logger.error.bind(options.rt.logger);
log(
{
runId: options.runId,
projectId: options.projectId,
requestedPath: args.path,
err: error,
},
boundaryViolation
? "Agent file delivery refused by workspace boundary"
: "Agent file delivery failed during workspace file access",
);
if (!boundaryViolation) throw error;
return {
isError: true,
content: [{ type: "text", text: "File path is outside the current workspace or uses a symlink." }],
};
}
if (file === null) {
return {
isError: true,
@@ -41,13 +76,13 @@ export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): M
};
}
const messageId = await sendFile(options.rt, options.chatId, file.path, args.name ?? file.name, options.sendOptions);
if (messageId === null) {
return {
isError: true,
content: [{ type: "text", text: `Failed to send file: ${file.path}` }],
};
}
const messageId = await sendFileData(
options.rt,
options.chatId,
file.data,
args.name ?? file.name,
options.sendOptions,
);
options.onDelivered?.(file.path);
return {
@@ -97,13 +132,28 @@ export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): M
resource_type: z.enum(["image", "file"]).describe("Use image for image_key and file for file_key."),
},
async (args) => {
const workspaceRoot = options.workspaceRoot?.trim();
if (workspaceRoot === undefined || workspaceRoot === "") {
options.rt.logger.error(
{ runId: options.runId, projectId: options.projectId },
"Feishu resource download missing configured workspace root",
);
return {
isError: true,
content: [{ type: "text", text: "Resource download is unavailable because workspace isolation is not configured." }],
};
}
const downloaded = await downloadFeishuMessageResource(
{
messageId: args.message_id,
fileKey: args.file_key,
resourceType: args.resource_type,
},
{ boundChatId: options.chatId, workspaceDir: options.workspaceDir },
{
boundChatId: options.chatId,
workspaceRoot,
workspaceDir: options.workspaceDir,
},
options.rt,
);
return { content: [{ type: "text", text: JSON.stringify(downloaded) }] };