Skip to content
dsh.fish
Bundle

@hanphone/dsh-a2a

Agent2Agent (A2A) v1.0.1 dual-end plugin for DeepSeek Harness: multiple inbound servers (per-instance agent presets; skills derived from each preset's skill directory), multiple outbound connections, GUI-managed instance lifecycle.

Source
HanphoneJan
License
MIT
Updated
Updated 2 days ago

Readme

# dsh-a2a

Agent2Agent (A2A) Protocol v1.0.1 dual-end plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — **English** · [中文](README.zh.md)

`@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](docs/architecture.md).

## Features

- **A2A v1.0.1 protocol surface, aligned with the official spec** —
  `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`,
  `GetExtendedAgentCard`, `SubscribeToTask` over JSON-RPC; SSE streaming with
  catch-up frames; official `TASK_STATE_*` / `ROLE_*` enums and AgentCard
  structure (including `supportedInterfaces`, `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 standard `agentPresets` resolve+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-in `chat` skill. Remotes
  call a skill via `metadata.skill`, and the preset session executes it
  through its `tool-skill` loader.
- **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 `a2a` storage 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 per `contextId`) and `subagent`
  (delegates to `ctx.subagents`, streams tool-call artifacts back).
- **Governed inbound** — every inbound task passes through the
  `a2a/inbound-task` waterfall 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 `Config` only carries host-level defaults
  (`baseUrl`, `subagentProvider`, `defaultTimeoutMs`).

## Installation

### From npm

```sh
dsh plugin --profile web add @hanphone/dsh-a2a
```

Any profile name works (`web`, custom profiles, headless etc.):

```sh
dsh plugin --profile <name> add @hanphone/dsh-a2a
```

### From a local build

```sh
cd dsh-a2a
pnpm build
npm pack
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-<version>.tgz
```

## Quick start

1. **Install** — `dsh plugin --profile web add @hanphone/dsh-a2a`.
2. **Restart the GUI** — the browser half is scanned at host startup, so
   restart once after installing (`pnpm dsh web` or your profile launcher).
3. **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:

```sh
# 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):

```sh
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:

```yaml
- 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:

```yaml
- 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 `InboundServerManager` owns every instance: one preset-bound
  session pool + `A2AServer` + routes per instance. Each instance persists in
  the `inbound_servers` table and serves its own endpoint + AgentCard. Tasks
  flow through `a2a/inbound-task` → executor → task store, with SSE frames
  streamed to subscribers.
- **Outbound** — an `OutboundServerManager` owns every connection: one
  `OutboundAgentRegistry` with an isolated agent store per instance,
  persisted in the `outbound_servers` table. `A2AClient` discovers an
  AgentCard, and each skill registers as a tool.
- **Dashboard** — the browser half (React, `settings.section`) reads/writes
  the loopback-only `/a2a/api` route; the host half feeds it snapshots of
  inbound/outbound server views and the preset roster (`/a2a/api/presets`).

See [docs/architecture.md](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.

```sh
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](https://github.com/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

Install

dsh plugin --profile web add github:HanphoneJan/dsh-a2a

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source