跳到主要内容

dsh-a2a-server

已验证

dsh-a2a-server · v0.1.1 · MIT

Inbound A2A (Agent2Agent) protocol server for DeepSeek Harness: publish an Agent Card and let any A2A-compliant peer submit tasks to a harness agent

安装

dsh plugin add dsh-a2a-server

dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南

源码

标签

作者

说明文档

dsh-a2a-server

为 DeepSeek Harness 提供入站 A2A 协议服务。
发布 Agent Card,接收任何合规 peer 提交的任务。
万物皆「插件」,它就是其中之一。

English · 简体中文

社区维护的插件,并非 DeepSeek 官方产品。

npm 版本 MIT License A2A v0.3.0 JSON-RPC 绑定 基于 DSH 0.1.0-rc.6 构建 129 项测试

dsh-a2a-serverDeepSeek Harness 上的 agent 可以通过 A2A(Agent2Agent)协议被访问。它在 well-known 路径发布 Agent Card,并实现 v0.3.0 JSON-RPC 绑定——任何知道本部署 URL 的合规 peer 都能发现这个 agent 并向它提交任务。

本插件只做入站:从不主动连接其他 agent,没有 client、没有 peer 目录、没有 A2A subagent provider。它是 ctx.agents 之上的传输适配层,不是能力接缝。

安装

dsh plugin --profile web add dsh-a2a-server

dsh plugin 会在 profile 目录里转发给 pnpm,并把这个 bundle 追加进 dsh.profile.bundles——因为包中声明了 dsh.bundle

若想直接使用本地检出,把路径指过去即可:

dsh plugin --profile web add ./path/to/dsh-a2a-server
依赖 由谁提供
ctx.agents dsh-base
ctx.credentials dsh-base
ctx.webServer dsh-web-app
ctx.sessionProjections(可选) 组合层;启用后 tasks/get 在任务结算后仍可应答

三个必需服务齐备之前,插件保持 PENDING。ctx.webServerdsh-web-app 提供而不在 dsh-base 中,因此 headless profile 需要先挂载 @deepseek-ai/dsh-host-webserverdsh --profile <name> --dump-config 可以打印组合出来的配置行。

快速开始

仓库自带的示例组合会运行真实模型并启动一个监听服务。

pnpm install
A2A_PEERS="alice:demo123" A2A_PORT=9922 pnpm serve
$env:A2A_PEERS = "alice:demo123"
$env:A2A_PORT = "9922"
pnpm serve

peer 名字是任意的——alice 只是这个 demo 的默认值,不是协议规定。可以声明任意多个, token 既可以内联,也可以存在派生出来的凭据引用下:

A2A_PEERS="ops:tok1,research:tok2"   # 内联
A2A_PEERS="ops,research"             # token 取自 A2A_PEER_OPS / A2A_PEER_RESEARCH

模型凭据经 ctx.credentials 解析,因此 harness home、任一 .env 层或进程环境中已有的 DEEPSEEK_API_KEY 都会被直接采用。缺少凭据时启动失败。

获取 Card 并提交任务:

curl -s http://127.0.0.1:9922/.well-known/agent-card.json

curl -s http://127.0.0.1:9922/a2a \
  -H "authorization: Bearer demo123" \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{
        "message":{"kind":"message","messageId":"m1","role":"user",
                   "parts":[{"kind":"text","text":"hello"}]}}}'
变量 默认值 含义
A2A_PEERS alice 名字[:token] 列表;每个 peer 都必须有 token
A2A_PORT 9900 监听端口
A2A_SEND_MODE block blockimmediate
A2A_WORKSPACE_ROOT 临时目录 per-peer 工作目录的父目录
DEEPSEEK_MODEL deepseek-v4-flash 向适配器请求的模型 id

一次性探针会对运行中的服务执行 48 项检查,任何一项不符即以非零码退出:

pnpm probe                                        # 默认 :9922 / demo123
node example/probe.mjs http://127.0.0.1:9922 demo123

对外接口

路由 方法 认证
/.well-known/agent-card.json GET 默认公开
/.well-known/agent.json GET 默认公开
{basePath}(默认 /a2a POST 必须携带 Bearer
JSON-RPC 方法 v1.0 别名 状态
message/send SendMessage 是否阻塞逐请求协商
message/stream SendStreamingMessage SSE
tasks/get GetTask 幂等;结算后仍可应答
tasks/cancel CancelTask 取消正在执行的 turn
tasks/resubscribe SubscribeToTask 续订活任务,或给一帧终态
tasks/pushNotificationConfig/* *TaskPushNotificationConfig -32003
tasks/list ListTasks -32601
agent/getAuthenticatedExtendedCard GetExtendedAgentCard -32601

两种方言都接受。v0.3(message/send"working"、带 kind 的 part)是主线;v1.0 拼写(SendMessageTASK_STATE_WORKING、成员存在式 part)在入站时被归一化,并按请求 所用的方言渲染回去。

入站消息可携带 text、file、data 三类 part。file 与 data 会以方括号引用的形式进入模型 上下文。回复为纯文本。

认证、限流与信任门分别以 HTTP 401429403 应答,响应体仍是合法的 JSON-RPC 错误 信封。属于其他 peer 的任务,应答方式与不存在的任务完全一致。

配置

- id: a2a-server
  name: dsh-a2a-server
  config:
    basePath: /a2a
    publicUrl: https://agents.example.com/a2a   # 写入 Card 的对外地址
    protocolVersion: 0.3.0
    provider: deepseek-official
    model: deepseek-v4-flash

    card:
      name: dsh-harness
      description: 读代码、执行命令、给出结论。
      public: true
      skills:
        - id: general
          name: general
          description: 通用任务执行。
          tags: [coding, research]
      provider:
        organization: Example Inc.
        url: https://example.com

    peers:
      alice: { tokenEnv: A2A_PEER_ALICE }
      bob:   { tokenEnv: A2A_PEER_BOB }
    trustedPeers: [alice]
    rateLimitPerMinute: 60
    maxContextTurns: 5

    sendMode: block
    blockTimeoutMs: 60000
    contextIdleTtlMs: 1800000
    maxResidentContexts: 64

    isolation:
      workspaceMode: per-peer
      workspaceRoot: /srv/dsh/a2a
      peerWorkspaces:
        alice: /srv/project

    push:
      enabled: false
配置项 默认值 含义
basePath /a2a JSON-RPC 路由
publicUrl Host 推导 写入 Card 的可路由地址
protocolVersion 0.3.0 Card 上声明的协议版本
provider · model 本服务创建的每个 agent 使用的模型路由
card.public true 无需凭据即可获取 Card
card.skills [] 声明的 skills;为空时回退到一条 general
peers {} 身份 → 凭据引用名
trustedPeers 全部已认证身份 允许执行任务的身份白名单
rateLimitPerMinute 60 按身份的滑动窗口
maxContextTurns 5 单个 context 接受的消息数上限,超出后 rejected
sendMode block 客户端未表态时的默认行为
blockTimeoutMs 60000 超过后拒绝继续阻塞
contextIdleTtlMs 1800000 context 的 agent 被释放前的空闲时长
maxResidentContexts 64 常驻 context 数量上限
isolation.workspaceMode per-peer per-peershared
isolation.workspaceRoot 必填;父目录或共享 cwd
isolation.peerWorkspaces {} 按身份覆盖工作目录
push.enabled false 保留字段,见边界

以下情况在加载期即被拒绝:缺少 isolation.workspaceRoot;peer 名不匹配 [A-Za-z0-9][A-Za-z0-9_-]*tokenEnv 不是 POSIX 标识符;trustedPeerspeerWorkspaces 引用了未声明的 peer;basePath 不以 / 开头。

凭据

配置中携带的是凭据引用名,而非凭据值:

peers:
  alice: { tokenEnv: A2A_PEER_ALICE }
# ~/.dsh/.credentials.yaml
A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>

ctx.credentials 在每次请求时跨四层解析该引用——进程环境、托管文档、<cwd>/.env$DSH_HOME/.env——因此轮换 token 在下一次请求即生效,无需重启。peer 身份只来自所出示 的凭据,请求体中的任何内容都无法声明身份。本插件不提供共享 bearer token:隔离建立在 互不相同的身份之上。

隔离

保证 机制
模型上下文 一个 peer 的对话不会进入另一个 peer 的模型请求 不同 contextId → 不同 Session → 不同 log
协议访问 peer 无法读取、续接或取消他人的 context 与任务 按认证身份判定归属
工具层 peer 的 agent 无法借工具读取他人的 session workspaceMode: per-peer

per-peer(默认)依据 workspaceRoot 为每个身份派生独立 cwdshared 则把所有 peer 放进同一个目录,适用于协作维护同一个仓库的场景;此模式下一个 peer 写入的文件对其他 peer 可读。

边界

  • 只做入站。没有出站 client、peer 目录或 A2A subagent provider。
  • 只提供 JSONRPC 绑定。不提供 gRPC 与 HTTP+JSON,Card 上如实声明。
  • 未实现推送通知。push.enabled 仅决定推送方法返回哪个错误码,Card 上声明 pushNotifications: false
  • 不支持扩展 Agent Card、stateTransitionHistory、协议扩展与 Card 签名。
  • 流式只推送已提交的 assistant 消息,未实现逐 chunk 推送。
  • 无孤儿任务看门狗:卡在非终态的任务会一直保持该状态。
  • 未实现跨会话工具拒绝;隔离依赖 workspaceMode
  • 触及 token 上限的任务结算为 completed,真实的 turn 结束原因放在 Task.metadata.dsh.stopReason——A2A 的状态枚举无法表达它。
  • 任务状态可以跨结算存活,但无法跨进程重启:未组合 session 持久化,重启后 projection 没有日志可供冷折叠。
  • ctx.webServer 不提供 TLS。任何非回环地址的暴露都应置于反向代理之后。
  • 修改配置会重启插件并取消进行中的任务。

架构

src/
├── protocol/          零依赖库:不碰 Cordis、不碰 HTTP、不碰 harness
│   ├── wire.ts        A2A 词汇表,统一归一到 v0.3 拼写
│   ├── normalize.ts   v0.3 <-> v1.0 方言双向翻译
│   ├── jsonrpc.ts     信封框架与 A2A 错误码
│   ├── card.ts        Agent Card 构造
│   └── sse.ts         SSE 帧编码
├── index.ts           Cordis 插件本体:接线、agent 归属、拆卸
├── router.ts          HTTP + JSON-RPC 分发;不依赖 Cordis,因此可单测
├── contexts.ts        contextId -> Activation 注册表与驻留策略
├── tasks.ts           任务槽位与三段式 turn 关联
├── projection.ts      在 session log 上折叠出的 a2aTask 读模型
├── security.ts        认证、限流、注入去势、外发脱敏
├── config.ts          schema,以及加载期就会拒绝的跨字段校验
└── types.ts           向 SessionEventMap / MessageSourceMap 的声明合并

task 是一个区间,不是一个 turn。 一条提交进来的消息,如果工具排出了更多工作,可能 横跨好几个 turn,因此结算使用三个钩子:agent/inbox/claimed 把消息绑到某个 turn, turn/end 记录该 turn 的结束原因,agent.whenIdle() 在整个 agent 安静后才结算。

任务状态活在 session log 里。 生命周期迁移是 a2a/task 事件,由 projection unit 折叠成读模型。终态那条边携带已提交的输出,因此折叠结果可以直接给出答案,无需回头翻消息 历史。

驻留是显式管理的。 HTTP 没有连接生命周期,因此每个 contextId 对应一个 Activation, 空闲后被驱逐,持久化的 Session 留在原地。

开发

pnpm install
pnpm typecheck
pnpm test       # 9 个文件共 129 项测试
pnpm serve      # 启动监听服务
pnpm probe      # 对运行中的服务执行 48 项检查
pnpm build      # 产出 lib/

端到端测试会启动真实的 Cordis 组合与真实的 agent loop,并通过 HTTP 驱动它;模型使用 确定性的 stub 适配器,使断言不依赖模型输出。

与官方项目的关系

本项目基于 deepseek-ai/deepseek-harness 构建。

官方项目提供 agent 运行时、插件系统,以及本插件所消费的各个能力接缝。本项目提供:

  • 入站的 A2A v0.3.0 JSON-RPC 绑定
  • Agent Card 构造与方言归一化
  • A2A task 与 harness turn 之间的映射
  • 按 peer 的认证、隔离与工作目录策略

harness 处于 pre-release 阶段,不承诺跨重命名或重新打包的兼容性,因此 peer 依赖精确锁定 在 0.1.0-rc.6

社区交流

扫码加入微信群 A2A 产品应用和探索 —— 交流 A2A 的实际应用,也包括这个插件。

微信群二维码

若二维码已过期,欢迎提 issue,我们会更新。

许可

MIT