Files
MCPletA2A/dev/build-detail.md
2026-03-30 17:39:13 +09:00

724 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MCPletA2A Platform - 详细开发计划
> 基于 Platform.png 架构图、Flow.png 参考流程及 MCPlet-spec-v202603-03 编写。
---
## 1. 目标
| 产出 | 路径 |
|------|------|
| 平台实现 | `/Users/qingjie.du/HDD/my-prjs/MCPletA2A/platform_impl/` |
| 参考实现 | `/Users/qingjie.du/HDD/my-prjs/MCPletA2A/reference_impl/` |
**平台实现**MCPlet Agent Profile Host含 Director Agent、A2A 协议层、MCPlet Pool 管理与权限执行、Passkey Web Page、Dashboard、可替换 LLM 适配器。
**参考实现**:「降低餐厅取消率」场景——情報収集・分析 Agent → 企画・Plan Agent店长 Passkey 确认)→ 発信・発注・発令 Agent发邮件驱动对应 MCPlets。
---
## 2. 技术栈
| 层次 | 选型 | 理由 |
|------|------|------|
| 语言 | TypeScript 5.x, Node.js 20+ | 与 reference_impl_restaurant 一致 |
| MCP | `@modelcontextprotocol/sdk` | 规范要求 |
| HTTP | Node.js 原生 `http` | 与现有参考实现一致,轻量 |
| 配置 | YAML + `js-yaml` | 可读性强,企业级友好 |
| 定时任务 | `node-cron` | Director Agent schedule |
| LLM 默认 | `@anthropic-ai/sdk` (claude-sonnet-4-6) | 通过适配器接口可替换任意 LLM |
| 测试 | `jest` + `ts-jest` | 标准 TS 测试方案 |
---
## 3. 完整目录结构
```
/MCPletA2A/
├── platform_impl/
│ ├── src/
│ │ ├── types/
│ │ │ ├── a2a.ts # A2AAgentCard / Envelope / TaskRequest / TaskResponse
│ │ │ ├── mcplet.ts # MCPletMeta / MCPletType / Visibility / PoolName
│ │ │ └── config.ts # PlatformConfig / PoolPolicy / AgentConfig / DirectorAgentConfig
│ │ ├── config/
│ │ │ └── loader.ts # 加载并验证 platform.yaml → PlatformConfig
│ │ ├── discovery/
│ │ │ └── mcplet-discovery.ts # MCP tools/list + 热重载 + 合规校验
│ │ ├── pools/
│ │ │ └── pool-registry.ts # Pool 注册表 + canAgentAccessPool + getToolsForAgent
│ │ ├── llm/
│ │ │ ├── llm-adapter.ts # LLMAdapter 接口定义
│ │ │ └── claude-adapter.ts # Claude 实现(@anthropic-ai/sdk
│ │ ├── agents/
│ │ │ ├── base-agent.ts # BaseAgent 抽象类(含 dispatchMCPlet 权限检查)
│ │ │ └── director-agent.ts # Director Agentcron 触发,防并发)
│ │ ├── a2a/
│ │ │ ├── local-bus.ts # A2A local protocol进程内注册+路由)
│ │ │ └── external-endpoint.ts# A2A 外部 HTTP 端点(鉴权+Pool校验
│ │ ├── passkey/
│ │ │ └── passkey-server.ts # Passkey Web Pagelocalhost 模式,动态端口)
│ │ ├── dashboard/
│ │ │ └── dashboard-server.ts # Dashboard HTTP 服务MCPlet 列表+审计日志)
│ │ ├── host/
│ │ │ └── mcplet-host.ts # MCPlet Host 主类,整合全部模块
│ │ └── index.ts # 程序入口
│ ├── config/
│ │ └── platform.yaml # 平台配置模板
│ ├── public/
│ │ ├── passkey/
│ │ │ └── index.html # Passkey Web Pageminimal, strict CSP
│ │ └── dashboard/
│ │ └── index.html # Dashboard 页面
│ ├── package.json
│ └── tsconfig.json
└── reference_impl/
├── mcplets/
│ ├── media-pool/
│ │ ├── site-access/
│ │ │ └── index.ts # read_site_stats (read, media-pool, model-visible)
│ │ ├── email/
│ │ │ └── index.ts # send_email (action, media-pool, passkey strict)
│ │ └── sns/
│ │ └── index.ts # post_sns (action, media-pool, passkey strict)
│ ├── info-pool/
│ │ ├── web-access/
│ │ │ └── index.ts # fetch_web_content (read, info-pool, model-visible)
│ │ └── api-access/
│ │ └── index.ts # call_external_api (read, info-pool, model-visible)
│ └── internal/
│ ├── crm/
│ │ └── index.ts # query_crm (read, no pool, model-visible)
│ ├── erp/
│ │ └── index.ts # query_erp (read, no pool, model-visible)
│ └── hr/
│ └── index.ts # query_hr (read, no pool, model-visible)
├── agents/
│ ├── info-gathering/
│ │ └── index.ts # 情報収集・分析 Agent (accessiblePools: [info-pool])
│ ├── planning/
│ │ └── index.ts # 企画・Plan Agent (accessiblePools: [])
│ └── dispatch/
│ └── index.ts # 発信・発注・発令 Agent (accessiblePools: [media-pool])
├── mock-services/
│ ├── server.ts # Mock HTTP 服务器(端口 5100挂载所有端点
│ └── data/
│ ├── customers.json # CRM含 cancel_tendency 字段的顾客数据5条
│ ├── inventory.json # ERP各商品库存
│ ├── reservations.json # 明日の予約一覧
│ └── weather.json # 天気予報(固定:明日は雨)
├── config/
│ └── reference.yaml # 参考实现配置场景、schedule、agents、pools、mockServices
├── package.json
└── tsconfig.json
```
---
## 4. 核心类型定义(`src/types/`
### 4.1 `a2a.ts`
```typescript
// Spec Section 18
export interface A2AAgentCard {
agentId: string;
displayName?: string;
description?: string;
requestedPools?: string[];
inputSchema?: object;
outputSchema?: object;
version?: string;
}
export interface A2AMessageEnvelope {
messageId: string; // UUID v4
contextId?: string; // UUID v4, stable across delegated workflow
senderId: string;
recipientId: string;
timestamp?: string; // ISO 8601 UTC
locale?: string; // BCP 47
}
export interface A2ATaskRequest extends A2AMessageEnvelope {
type: 'task_request';
payload: {
parameters: Record<string, unknown>;
history?: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
};
}
export interface A2ATaskResponse extends A2AMessageEnvelope {
type: 'task_response';
replyToMessageId: string;
status: 'success' | 'error' | 'timeout' | 'cancelled' | 'partial';
payload?: {
result?: unknown;
error?: { message: string; code?: string };
};
}
```
### 4.2 `mcplet.ts`
```typescript
export type MCPletType = 'read' | 'prepare' | 'action';
export type Visibility = 'model' | 'app';
export interface MCPletMeta {
mcpletType: MCPletType;
pool?: string;
visibility: Visibility[];
mcpletToolResultSchemaUri?: string;
auth?: {
required: 'passkey';
enforcement: 'strict' | 'host-only';
promptMessage?: string;
};
}
// MCPlet 工具结果信封Spec Section 9.1
export interface MCPletToolResult<T = unknown> {
result?: T;
error?: { message: string; code: string };
_meta: {
timestamp: string;
toolId: string;
mcpletType: MCPletType;
visibility: Visibility[];
};
}
```
### 4.3 `config.ts`
```typescript
export interface PoolPolicy {
rateLimitPerMinute?: number;
domainAllowlist?: string[];
}
export interface AgentConfig {
class: string; // Agent 类名,用于注册表查找
accessiblePools: string[];
a2aCard?: Partial<A2AAgentCard>;
}
export interface DirectorAgentConfig {
schedule: string; // cron 表达式
promptTemplate: string;
targetAgentId: string; // 接收指令的 Agent
maxRetries: number;
backoffMs: number;
}
export interface PasskeyConfig {
mode: 'localhost' | 'https';
rpId: string;
fido2ServerUrl?: string;
}
export interface PlatformConfig {
llm: { provider: string; model: string; apiKey?: string };
pools: Record<string, PoolPolicy>;
agents: Record<string, AgentConfig>;
directorAgent?: DirectorAgentConfig;
externalAgents?: Array<{ agentId: string; apiKey: string; accessiblePools: string[] }>;
passkey?: PasskeyConfig;
dashboard?: { port: number };
a2aExternalEndpoint?: { port: number };
}
```
---
## 5. 各模块详细规格
### 5.1 `discovery/mcplet-discovery.ts`
```typescript
class MCPletDiscovery {
constructor(private mcpClient: MCPClient, private poolRegistry: PoolRegistry) {}
async discover(): Promise<ToolDefinition[]>
// 1. 调用 tools/list
// 2. 过滤: 无 _meta.mcpletType → reject
// 3. 过滤: action + visibility含model + 无auth → reject (记录 warn)
// 4. 按 _meta.pool 注册到 poolRegistry
// 5. 返回合规工具列表
subscribeToChanges(): void
// 监听 notifications/tools/list_changed → 重新执行 discover()
// 新增工具重新验证,删除工具从路由表移除
}
```
### 5.2 `pools/pool-registry.ts`
```typescript
class PoolRegistry {
constructor(private poolPolicies: Record<string, PoolPolicy>) {}
registerTool(tool: ToolDefinition, poolName?: string): void
canAgentAccess(agentId: string, poolName: string | undefined, agentPools: string[]): boolean
// - pool-less 工具:任何 Agent 可访问
// - pool 工具agentPools 必须包含该 pool
getToolsForAgent(agentId: string, agentPools: string[]): ToolDefinition[]
// 返回该 Agent 被授权访问的全部工具(用于构建 LLM tool set
checkRateLimit(poolName: string): boolean
}
```
### 5.3 `llm/llm-adapter.ts`
```typescript
export interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface ToolDef {
name: string;
description: string;
inputSchema: object;
}
export interface LLMToolCall {
toolName: string;
arguments: Record<string, unknown>;
}
export interface LLMResponse {
text?: string;
toolCalls?: LLMToolCall[];
}
export interface LLMAdapter {
chat(messages: Message[], tools?: ToolDef[]): Promise<LLMResponse>;
}
```
### 5.4 `agents/base-agent.ts`
```typescript
abstract class BaseAgent {
constructor(
public readonly agentId: string,
public readonly accessiblePools: string[],
protected poolRegistry: PoolRegistry,
protected mcpClient: MCPClient,
protected llm: LLMAdapter,
) {}
abstract handle(task: A2ATaskRequest): Promise<A2ATaskResponse>;
protected getAuthorizedTools(): ToolDef[]
// 调用 poolRegistry.getToolsForAgent仅返回 model-visible 工具
protected async invokeMCPlet(toolName: string, args: object): Promise<MCPletToolResult>
// 1. 检查工具是否在授权 Pool 内(否则抛出 403 等价错误)
// 2. 检查 mcpletTypeaction 工具走 Passkey 拦截流程
// 3. 发送 MCP tools/call
// 4. 返回结果
protected buildSuccessResponse(task: A2ATaskRequest, result: unknown): A2ATaskResponse
protected buildErrorResponse(task: A2ATaskRequest, message: string, code?: string): A2ATaskResponse
}
```
### 5.5 `agents/director-agent.ts`
```typescript
class DirectorAgent {
private running = false;
private cronJob: cron.ScheduledTask;
constructor(
private config: DirectorAgentConfig,
private llm: LLMAdapter,
private localBus: A2ALocalBus,
) {}
start(): void
// node-cron.schedule(config.schedule, this.run.bind(this))
private async run(): Promise<void>
// 1. if (this.running) { log "skipping, previous cycle still active"; return; }
// 2. this.running = true
// 3. try:
// a. LLM(config.promptTemplate) → instruction
// b. 解析失败 → log + return不 dispatch
// c. localBus.sendTask(config.targetAgentId, instruction)
// d. 重试逻辑:最多 config.maxRetries 次,间隔 config.backoffMs
// 4. finally: this.running = false
stop(): void
}
```
### 5.6 `a2a/local-bus.ts`
```typescript
class A2ALocalBus {
private agents = new Map<string, BaseAgent>();
register(agent: BaseAgent): void
async sendTask(request: A2ATaskRequest): Promise<A2ATaskResponse>
// 1. 查找 recipientId 对应 Agent未找到 → error response
// 2. 调用 agent.handle(request)
// 3. local bus 消息 MUST NOT 路由到进程外(纯内存调用)
}
```
### 5.7 `a2a/external-endpoint.ts`
```typescript
// HTTP POST /a2a/task
// Headers: Authorization: Bearer <apiKey>
class A2AExternalEndpoint {
constructor(
private config: PlatformConfig,
private localBus: A2ALocalBus,
private poolRegistry: PoolRegistry,
) {}
start(port: number): void
private async handleTask(req, res): Promise<void>
// 1. 验证 Bearer token → 找到对应外部 Agent 配置(否则 401
// 2. 解析 A2ATaskRequestSpec Section 18 schema
// 3. 检查 recipientId 对应 Agent 的工具 ∩ 外部 Agent 授权 Pool否则 403
// 4. 转发到 localBus.sendTask
// 5. 返回 A2ATaskResponse JSON
}
```
### 5.8 `passkey/passkey-server.ts`
```typescript
class PasskeyServer {
private port: number;
async startCeremony(promptMessage: string): Promise<PasskeyAssertion>
// 1. 动态绑定 loopback 端口
// 2. 在系统浏览器打开 http://localhost:{port}/passkey?msg=...
// 3. 等待回调POST /passkey/callback或超时< 60s
// 4. 超时/取消 → 抛出错误Host 返回 MCP Error
// 5. 回调成功 → 返回 assertion关闭页面和端口
stop(): void
}
// public/passkey/index.html:
// - 读取 ?msg query param
// - 调用 navigator.credentials.get()WebAuthn
// - POST assertion 到 /passkey/callback
// - 严格 CSPno external scripts
```
### 5.9 `host/mcplet-host.ts`
```typescript
class MCPletHost {
async start(configPath: string): Promise<void>
// 按序初始化:
// 1. loadConfig(configPath) → PlatformConfig
// 2. new PoolRegistry(config.pools)
// 3. new MCPletDiscovery(mcpClient, poolRegistry) → discover()
// 4. new LLMAdapter(config.llm) // ClaudeAdapter 或其他
// 5. new A2ALocalBus()
// 6. 注册参考实现中的 AgentInfo/Plan/Dispatch
// 7. new DirectorAgent(config.directorAgent, llm, localBus) → start()
// 8. if (config.passkey) → new PasskeyServer(config.passkey)
// 9. if (config.dashboard) → new DashboardServer(...) → start()
// 10. if (config.a2aExternalEndpoint) → new A2AExternalEndpoint(...) → start()
}
```
---
## 6. 参考实现 MCPlet 规格
所有 MCPlet 遵循以下模式(以 `email/index.ts` 为例):
```typescript
import { registerModelTool, registerAppTool } from '../../../platform_impl/src/mcplet-lib';
// send_email: action, media-pool, app-only需 Passkey 授权后由 dispatch agent 调用)
registerAppTool(server, {
name: 'send_email',
title: 'メール送信',
description: 'Send email to specified recipients',
inputSchema: SendEmailSchema,
mcpletType: 'action',
pool: 'media-pool',
visibility: ['app'],
auth: {
required: 'passkey',
enforcement: 'strict',
promptMessage: 'メール送信を承認してください'
},
handler: sendEmailHandler,
});
```
### Mock Service 端点port 5100
| 端点 | 方法 | 说明 | 对应 MCPlet |
| ---- | ---- | ---- | ---------- |
| `/crm/customers?filter=rain_cancel_tendency` | GET | 雨天取消倾向高的顾客列表5条固定数据 | query_crm |
| `/crm/reservations?date=YYYY-MM-DD` | GET | 指定日期预约列表 | query_crm |
| `/erp/inventory?item=dessert` | GET | 甜点库存 | query_erp |
| `/weather/forecast?date=YYYY-MM-DD` | GET | 天气预报(固定返回「明日は雨」) | fetch_web_content |
| `/site/stats` | GET | EPARK 站点访问数据 | read_site_stats |
| `/email/send` | POST | Mock 发信(记录日志,不真实发送) | send_email |
| `/sns/post` | POST | Mock SNS记录日志 | post_sns |
MCPlet handler 通过 `config.mockServiceUrl``http://localhost:5100`)调以上端点替换真实服务只需修改配置handler 代码不变。
---
## 7. 参考实现 MCPlet 规格
| MCPlet | Tool 名 | mcpletType | pool | visibility | auth |
|--------|---------|-----------|------|------------|------|
| サイトアクセス | `read_site_stats` | read | media-pool | [model] | — |
| Email | `send_email` | action | media-pool | [app] | passkey strict |
| SNS | `post_sns` | action | media-pool | [app] | passkey strict |
| 外部Web | `fetch_web_content` | read | info-pool | [model] | — |
| 外部API | `call_external_api` | read | info-pool | [model] | — |
| CRM | `query_crm` | read | (none) | [model] | — |
| ERP | `query_erp` | read | (none) | [model] | — |
| HR | `query_hr` | read | (none) | [model] | — |
---
## 7. 参考实现 Agent 规格
### 7.1 情報収集・分析 Agent
```typescript
class InfoGatheringAgent extends BaseAgent {
// agentId: 'info-gathering-agent'
// accessiblePools: ['info-pool']
// 可用工具: fetch_web_content, call_external_api, query_crm, query_erp, query_hr (pool-less)
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
// 1. 将 task.payload.parameters 拼入 system prompt
// 2. LLM 根据任务决定调用哪些 read MCPlets
// 3. 执行工具调用循环tool_use → invokeMCPlet → 返回结果给 LLM
// 4. LLM 输出分析总结
// 5. 返回 success responseresult: { analysis, rawData }
}
}
```
### 7.2 企画・Plan Agent
```typescript
class PlanningAgent extends BaseAgent {
// agentId: 'planning-agent'
// accessiblePools: [] (只访问 pool-less MCPlets)
// 可用工具: query_crm, query_erp, query_hr
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
// 1. 接收来自 info-gathering 的分析结果
// 2. 查询必要的 CRM/ERP 数据pool-less MCPlets
// 3. LLM 生成 Plan含具体施策、对象顾客列表、邮件文案
// 4. 通过 Passkey Web Page 请求店长审批auth.enforcement: host-only 模式)
// 5. 审批通过 → 返回 successresult: { plan, approvedAt }
// 6. 审批拒绝/超时 → 返回 cancelled
}
}
```
### 7.3 発信・発注・発令 Agent
```typescript
class DispatchAgent extends BaseAgent {
// agentId: 'dispatch-agent'
// accessiblePools: ['media-pool']
// 可用工具: read_site_stats, send_email, post_sns (需 Passkey)
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
// 1. 接收企划方案plan + 顾客列表 + 邮件文案)
// 2. 对 send_email 工具调用:
// a. BaseAgent.invokeMCPlet 拦截 action 工具
// b. 调用 PasskeyServer.startCeremony()
// c. 拿到 assertion → 注入 params._meta.mcplet_auth
// d. 真正执行 MCP tools/call
// 3. 记录发送结果到审计日志
// 4. 返回 successresult: { sent: n, failed: m }
}
}
```
---
## 8. 参考实现配置 `reference.yaml`
```yaml
llm:
provider: claude
model: claude-sonnet-4-6
apiKey: ${ANTHROPIC_API_KEY}
pools:
media-pool:
rateLimitPerMinute: 60
info-pool:
rateLimitPerMinute: 120
agents:
info-gathering-agent:
class: InfoGatheringAgent
accessiblePools: [info-pool]
planning-agent:
class: PlanningAgent
accessiblePools: []
dispatch-agent:
class: DispatchAgent
accessiblePools: [media-pool]
directorAgent:
schedule: "0 7 * * *"
targetAgentId: info-gathering-agent
promptTemplate: |
今日の天気予報と在庫情報、キャンセル傾向の高い予約客情報を収集・分析し、
キャンセル率を下げる施策を立案してください。
maxRetries: 3
backoffMs: 5000
passkey:
mode: localhost
rpId: localhost
dashboard:
port: 4000
a2aExternalEndpoint:
port: 4001
```
---
## 9. 完整流程Flow.png 场景)
```
[cron 07:00]
DirectorAgent.run()
│ LLM(promptTemplate) → "キャンセル率低減タスク開始"
A2ALocalBus.sendTask("info-gathering-agent", task)
InfoGatheringAgent.handle(task)
├─ invokeMCPlet("fetch_web_content", {url: "天気予報"}) ← info-pool
├─ invokeMCPlet("call_external_api", {source: "在庫API"}) ← info-pool
└─ invokeMCPlet("query_crm", {filter: "雨天キャンセル傾向"}) ← pool-less
│ → LLM分析 → { analysis: "明日は雨、在庫あり、高キャンセル客10名" }
A2ALocalBus.sendTask("planning-agent", { analysis })
PlanningAgent.handle(task)
├─ invokeMCPlet("query_erp", {item: "デザート"}) ← pool-less
└─ LLM生成企划: "無料デザートキャンペーン、対象: 10名"
│ PasskeyServer.startCeremony("キャンペーン計画を承認してください")
│ [店长在浏览器中进行 WebAuthn 认证]
│ → { plan, approvedAt }
A2ALocalBus.sendTask("dispatch-agent", { plan })
DispatchAgent.handle(task)
└─ invokeMCPlet("send_email", { to: [...10名...], body: "..." })
├─ 拦截 action 工具
├─ PasskeyServer.startCeremony("メール送信を承認してください")
├─ 注入 params._meta.mcplet_auth
└─ MCP tools/call → Email MCPlet 后端验证 Passkey → 发送邮件
```
---
## 10. 开发阶段与顺序
```
Phase 1: 项目骨架
platform_impl: package.json, tsconfig.json, 目录结构
reference_impl: package.json, tsconfig.json, 目录结构
Phase 2: 类型层src/types/
a2a.ts, mcplet.ts, config.ts
Phase 3: 配置加载src/config/loader.ts
+ platform.yaml 示例
Phase 4: Pool 管理src/pools/pool-registry.ts
+ MCPlet 发现src/discovery/mcplet-discovery.ts
Phase 5: LLM 适配器src/llm/
llm-adapter.ts接口 + claude-adapter.ts实现
Phase 6: Agent 框架src/agents/
base-agent.ts含 invokeMCPlet 权限检查 + action 拦截骨架)
director-agent.tscron + 防并发 + 重试)
Phase 7: A2A 协议src/a2a/
local-bus.ts进程内路由
external-endpoint.tsHTTP + 鉴权 + Pool校验
Phase 8: Host 主入口src/host/mcplet-host.ts + src/index.ts
整合 Phase 2-7平台实现可独立运行
Phase 9: 参考实现 MCPlets
info-pool: fetch_web_content, call_external_api
internal: query_crm, query_erp, query_hr
media-pool: read_site_stats, send_email, post_sns
Phase 10: 参考实现 Agents
InfoGatheringAgent, PlanningAgent, DispatchAgent
+ reference.yaml
Phase 11: Passkey Web Pagesrc/passkey/ + public/passkey/
localhost 模式动态端口WebAuthn ceremony
Phase 12: Dashboardsrc/dashboard/ + public/dashboard/
MCPlet 列表 + action 审计日志展示
Phase 13: 集成测试
完整流程 E2EDirector → 情報収集 → 企画 → 発信
```
---
## 11. 验收标准Checklist
### 平台实现
- [ ] MCPlet 发现:无 `_meta.mcpletType` 工具被拒绝;`action + model-visible + 无auth` 被拒绝
- [ ] Pool 权限Agent 调用授权范围外工具时得到明确错误
- [ ] Director Agentcron 按时触发LLM 解析失败时跳过不 panic同一 Director 不并发执行
- [ ] A2A Local Bus进程内消息路由正确
- [ ] A2A 外部端点:未授权 → 401Pool 范围外 → 403合法请求转发 → 正确响应
- [ ] Passkey Web Pagelocalhost 动态端口assertion 回调后关闭;超时返回 MCP Error
- [ ] Dashboard可查看 MCPlet 列表 + 最近 action 审计日志
### 参考实现
- [ ] 各 MCPlet 正确注册mcpletType / pool / visibility / auth
- [ ] InfoGatheringAgent 只能访问 info-pool + pool-less MCPlets
- [ ] PlanningAgent 只能访问 pool-less MCPlets
- [ ] DispatchAgent 只能访问 media-pool MCPlets
- [ ] 完整场景流程一次性跑通Director → 情報収集 → 企画 → Passkey 审批 → 発信
- [ ] send_email 调用经过 Passkey 拦截 + assertion 注入 + 后端验证