dsh-a2a
No description
- Stars
- 0
- Language
- TypeScript
- Created
- Sep 6, 2026
- Updated
- Sep 7, 2026
Introduction
dsh-a2a
Agent2Agent (A2A) Protocol v1.0.1 dual-end plugin for DeepSeek Harness — English · 中文
@hanphone/dsh-a2a is an independent, open-source A2A plugin that turns a
DeepSeek Harness profile into a multi-faced agent-to-agent citizen: it can
simultaneously serve multiple inbound A2A servers, each bound to its own
agent preset with its own endpoint, AgentCard, preset-derived skills and auth, and
connect to multiple outbound A2A servers, each with its own preset, whose
remote skills appear as model tools. Every server instance is created,
started, stopped, edited and removed entirely from the GUI — no config-file
editing.
Architecture and design decisions: docs/architecture.md.
Features
- A2A v1.0.1 protocol surface, aligned with the official spec —
SendMessage,SendStreamingMessage,GetTask,ListTasks,CancelTask,GetExtendedAgentCard,SubscribeToTaskover JSON-RPC; SSE streaming with catch-up frames; officialTASK_STATE_*/ROLE_*enums and AgentCard structure (includingsupportedInterfaces,capabilities, full error-code table). - Multiple inbound servers — one per persona. Each instance gets its own
endpoint (
/a2a/<id>), AgentCard route, authentication env, and skill declarations. - Per-instance agent preset — every inbound server binds one concrete
agent preset (e.g.
ptc,standard,minimal, …); the picker lists only real roster presets (matching the in-app selector) and defaults to the deployment default. Inbound tasks execute in sessions composed from that preset through the standardagentPresetsresolve+mount path. - Preset-derived skill declarations — the AgentCard's skills are the
model-invocable entries of the bound preset's skill directory
(
agentPresets.standingKeyFor+ctx.skills.list), derived automatically — "the preset decides its skills; everything is a plugin". No typed skill form; a missing skills service falls back to the built-inchatskill. Remotes call a skill viametadata.skill, and the preset session executes it through itstool-skillloader. - Multiple outbound servers — each connection has its own remote URL,
auth env, timeout and preset; enabled instances map remote skills
to
a2a__<name>__<skill>model tools. - Durable task store — tasks live in the
a2astorage domain (JSON backend by default, SQLite per deployment choice); server-generated ids survive restarts, and each task records the inbound server it arrived through. - Executors —
session(one DSH session percontextId) andsubagent(delegates toctx.subagents, streams tool-call artifacts back). - Governed inbound — every inbound task passes through the
a2a/inbound-taskwaterfall so policy plugins can veto or audit. - Inbound connection monitoring — the dashboard shows which remote peers are talking to each instance and can close a peer.
- Direct bearer-token entry — the GUI's Bearer Token field writes each
instance's token through the harness credentials service (managed
.env/ credential store,0o700); the record keeps only an auto-generated env-var name and the value never lands in the a2a domain or the AgentCard. Runtime resolution layers credentials → process environment, so externally exported env vars keep working. - Minimal plugin config — instances are created through the GUI and live
in the domain; the plugin
Configonly carries host-level defaults (baseUrl,subagentProvider,defaultTimeoutMs).
Installation
From npm
dsh plugin --profile web add @hanphone/dsh-a2a
Any profile name works (web, custom profiles, headless etc.):
dsh plugin --profile <name> add @hanphone/dsh-a2a
From a local build
cd dsh-a2a
pnpm build
npm pack
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-<version>.tgz
Quick start
- Install —
dsh plugin --profile web add @hanphone/dsh-a2a. - Restart the GUI — the browser half is scanned at host startup, so
restart once after installing (
pnpm dsh webor your profile launcher). - Open Settings → A2A 连接 — create your first inbound server (pick a preset — its skills are derived automatically — optionally set an auth env). It is enabled immediately and publishes its own endpoint and AgentCard.
Each inbound server listens on the profile's webServer:
# the created instance's AgentCard (see the GUI for the exact id)
curl http://127.0.0.1:3080/a2a/<id>/agent-card.json
Send a task to an instance (its declared chat skill):
curl -X POST http://127.0.0.1:3080/a2a/<id> \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"SendMessage","params":{"message":{"role":"user","parts":[{"text":"hello"}],"metadata":{"skill":"chat"}}}}'
GUI dashboard
The browser half registers an A2A 连接 page under Settings, organized in three tabs. From it you can, without touching any file:
- 入站 Servers — create inbound servers (name/description/version, a preset picker listing real roster presets with the deployment default preselected, and a Bearer Token field), start/stop, edit (including clearing auth), and remove them; each card shows its endpoint, preset badge, auth state, preset-derived skill chips and live AgentCard URL.
- 出站 Servers — add outbound connections with a two-phase flow: enter the remote AgentCard URL (± bearer token) → 导入 to preview the remote card (name/version/skills/endpoint) → 连接 to confirm; start/stop, refresh, edit (name/preset/timeout/token) and remove them. Cards show connection state (state dot), tool counts and errors.
- 连接与任务 — the inbound-peer table (who is calling, task counts, streaming, close control) and the task list (per-source view, cancel).
All dashboard traffic goes through the loopback-only /a2a/api route —
remote peers can never drive it.
Configuration
The GUI covers instance management day-to-day. The plugin Config only has
host-level defaults, set through the profile's user patch layer
($DSH_HOME/profiles/<name>/cordis.patch.yml) if you want to override them:
- id: a2a
config:
baseUrl: http://127.0.0.1:<port> # omit to derive from the webServer address
subagentProvider: in-process
defaultTimeoutMs: 60000 # default outbound connection timeout
Instances are not configured via patches — they are created in the GUI and
persisted in the a2a domain (inbound_servers / outbound_servers tables).
Required host services
Base-backed profiles mount them all: webServer (@deepseek-ai/dsh-host-webserver),
the storage stack (@deepseek-ai/dsh-storage + @deepseek-ai/dsh-storage-domain),
the tools registry (@deepseek-ai/dsh-tools), an agent loop
(@deepseek-ai/dsh-agent + @deepseek-ai/dsh-agent-loop), and agent presets
(@deepseek-ai/dsh-agent-presets; the preset pickers and preset-bound session
composition need it). The subagent executor additionally needs
@deepseek-ai/dsh-subagent.
Storage backend
The task and instance stores live in the a2a storage domain. The base
composition routes storage through the json backend; to use SQLite, route
the domain and add the backend in the same patch layer:
- id: storage-domain
config:
backend: json
routes:
a2a: sqlite
- insert:
- id: storage-sqlite
name: '@deepseek-ai/dsh-storage-sqlite'
config:
path: /absolute/path/to/a2a.sqlite
CLI
A /a2a chat command mirrors the dashboard (a text backup to the GUI):
a2a status | presets | peers |
inbound list|create|remove|enable|disable |
outbound list|create|remove|enable|disable|refresh |
tasks | task get|cancel <id> | help
How it works
- Inbound — an
InboundServerManagerowns every instance: one preset-bound session pool +A2AServer+ routes per instance. Each instance persists in theinbound_serverstable and serves its own endpoint + AgentCard. Tasks flow througha2a/inbound-task→ executor → task store, with SSE frames streamed to subscribers. - Outbound — an
OutboundServerManagerowns every connection: oneOutboundAgentRegistrywith an isolated agent store per instance, persisted in theoutbound_serverstable.A2AClientdiscovers an AgentCard, and each skill registers as a tool. - Dashboard — the browser half (React,
settings.section) reads/writes the loopback-only/a2a/apiroute; the host half feeds it snapshots of inbound/outbound server views and the preset roster (/a2a/api/presets).
See docs/architecture.md for the full design.
Directory structure
src/
api.ts # loopback dashboard API (/a2a/api, /a2a/api/presets)
index.ts # Cordis plugin entry (apply)
protocol.ts # A2A v1.0.1 protocol constants + types
jsonrpc.ts # JSON-RPC framing
servers/ # multi-instance managers
inbound-manager.ts # inbound server instances (CRUD, routes, lifecycle)
outbound-manager.ts # outbound connection instances (CRUD, tools)
server/ # single-instance internals: store, card, a2a-server,
# routes, executors, inbound-registry
outbound/ # outbound internals: A2AClient, registry, tools
client/ # browser half: settings dashboard (React)
service.ts # ctx.a2a service facade
commands.ts # /a2a chat command
tests/
unit/ # protocol, framing, card, store, registry, server,
# client, api, inbound-registry, identity
composition/ # apply() on a real Cordis Context with stub host services
cordis.patch.yml # bundle patch (mounts the plugin; instances are GUI-managed)
Development
The plugin typechecks against the harness source graph through project references; a harness checkout with its built host aggregate is required.
pnpm typecheck # host (tsc -b) + client (tsc -p tsconfig.client.json)
pnpm test # vitest run (unit + composition suites)
pnpm build # tsc + tsdown → lib/index.js (host) + lib/client.js (browser)
Acknowledgements
This plugin was inspired by and developed alongside ryubyte/dsh-a2a, an earlier A2A plugin for DeepSeek Harness. That project's design — dual-end scope, AgentCard advertisement, and the settings-dashboard pattern — set the direction this implementation follows. Our protocol layer, task store, and executor seam are independent implementations; the GUI management model owes a direct debt to ryubyte's connection dashboard.
License
MIT