@ryubyte/dsh-a2a 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -9,6 +9,8 @@ It turns a DSH instance into a full A2A peer in both directions:
9
9
  - **Client mode (outbound)** — discover a remote agent via its **AgentCard**, map each **skill** to a DSH model tool (`a2a__<name>__<skill>`), and execute remote tasks through A2A **JSON-RPC**.
10
10
  - **Server mode (inbound)** — expose this DSH to other agents: `/.well-known/agent-card.json` plus a JSON-RPC endpoint (`SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SendStreamingMessage` over SSE) on the DSH webserver.
11
11
 
12
+ Supports DSH `0.1.7-rc.2`, `0.2.0-rc.1–2`, `0.2.1-alpha.1–5`, and `0.2.1-rc.1–5` (13 versions), checked individually in CI and before release. [Compatibility details](docs/dsh-compat.md).
13
+
12
14
  ## Installation
13
15
 
14
16
  Install into a profile with one command and restart:
package/README.md CHANGED
@@ -9,6 +9,8 @@
9
9
  - **Client 模式(出站)**——通过远程 Agent 的 **AgentCard** 发现它,把每个 **skill** 映射为 DSH 模型工具(`a2a__<name>__<skill>`),通过 A2A **JSON-RPC** 执行远程任务。
10
10
  - **Server 模式(入站)**——把本 DSH 暴露给其他 Agent:在 DSH webserver 上提供 `/.well-known/agent-card.json` 与 JSON-RPC 端点(`SendMessage`、`GetTask`、`ListTasks`、`CancelTask`、基于 SSE 的 `SendStreamingMessage`)。
11
11
 
12
+ 兼容 DSH `0.1.7-rc.2`、`0.2.0-rc.1~2`、`0.2.1-alpha.1~5` 与 `0.2.1-rc.1~5`,共 13 个版本;CI 和发布流程逐版本验证。[兼容性说明](docs/dsh-compat.md)。
13
+
12
14
  ## 安装
13
15
 
14
16
  一行命令装进 profile,重启即生效:
package/lib/index.js CHANGED
@@ -2174,7 +2174,7 @@ function apply(ctx, config = {}) {
2174
2174
  ctx.inject(["webServer"], (ctx) => {
2175
2175
  ctx.effect(() => {
2176
2176
  const webServer = ctx.webServer;
2177
- const baseUrl = mergedConfig.baseUrl ?? inferBaseUrl(webServer.host, webServer.port);
2177
+ const baseUrl = mergedConfig.baseUrl ?? webServerBaseUrl(webServer);
2178
2178
  const endpointPath = mergedConfig.endpointPath ?? DEFAULT_ENDPOINT;
2179
2179
  if (mergedConfig.baseUrl) try {
2180
2180
  const pinnedPort = new URL(mergedConfig.baseUrl).port;
@@ -2382,7 +2382,7 @@ function apply(ctx, config = {}) {
2382
2382
  baseUrl: void 0
2383
2383
  } : patch;
2384
2384
  const { endpointChanged } = server.updateCard(cardPatch);
2385
- if (clearBaseUrl) server.setBaseUrl(inferBaseUrl(webServer.host, webServer.port));
2385
+ if (clearBaseUrl) server.setBaseUrl(webServerBaseUrl(webServer));
2386
2386
  if (endpointChanged && enabled) {
2387
2387
  unregisterRoutes();
2388
2388
  registerRoutes();
@@ -2561,12 +2561,20 @@ function inboundSessionCwd(configPath) {
2561
2561
  * back to loopback when no such address exists. IPv6 link-local is skipped
2562
2562
  * (not reachable without a scope id).
2563
2563
  *
2564
- * @param host - the web server's configured bind host ('127.0.0.1' | '0.0.0.0').
2564
+ * @param host - the web server's configured bind address.
2565
2565
  * @param port - the OS-assigned (or configured) listen port.
2566
+ * @param protocol - the host listener's protocol (older hosts default to HTTP).
2566
2567
  * @returns a scheme+host+port URL for Agents to call back into this DSH.
2567
2568
  */
2568
- function inferBaseUrl(host, port) {
2569
- return `http://${host === "0.0.0.0" ? primaryLanAddress() ?? "127.0.0.1" : host || "127.0.0.1"}:${port}`;
2569
+ function inferBaseUrl(host, port, protocol = "http:") {
2570
+ const hostname = host === "0.0.0.0" ? primaryLanAddress() ?? "127.0.0.1" : host || "127.0.0.1";
2571
+ return `${protocol}//${hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname}:${port}`;
2572
+ }
2573
+ /** Prefer the host's authoritative origin; retain inference for older DSH. */
2574
+ function webServerBaseUrl(webServer) {
2575
+ if (typeof webServer.getOrigin === "function") return webServer.getOrigin();
2576
+ if (webServer.port === void 0) throw new Error("a2a: webServer has not bound a port");
2577
+ return inferBaseUrl(webServer.host, webServer.port, webServer.protocol);
2570
2578
  }
2571
2579
  /**
2572
2580
  * Best single LAN IPv4 for this machine: the address of the non-loopback
@@ -2648,8 +2656,15 @@ function corsHeadersFor(reqOrigin, allowed) {
2648
2656
  function trustedLoopback(req) {
2649
2657
  const host = req.headers.host;
2650
2658
  if (!host) return false;
2651
- const hostname = host.split(":")[0];
2652
- if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "::1") return false;
2659
+ let hostname;
2660
+ try {
2661
+ const authority = new URL(`http://${host}`);
2662
+ if (authority.username || authority.password || authority.pathname !== "/" || authority.search || authority.hash) return false;
2663
+ hostname = authority.hostname;
2664
+ } catch {
2665
+ return false;
2666
+ }
2667
+ if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "[::1]") return false;
2653
2668
  if (req.headers["sec-fetch-site"] === "cross-site") return false;
2654
2669
  const origin = req.headers.origin;
2655
2670
  if (!origin) return true;
@@ -2675,4 +2690,4 @@ function toServerReq(req) {
2675
2690
  };
2676
2691
  }
2677
2692
  //#endregion
2678
- export { A2AClient, A2AError, A2AServer, A2A_ERROR_CODES, A2A_METHODS, DashboardRegistry, Role, TaskState, TaskStore, apply, createDshAgentExecutor, defaultExecutor, defaultSkills, fetchAgentCard, getSharedRegistry, inferBaseUrl, inject, isTerminal, name, notConfiguredExecutor, registerAgentTools, shellExecutor };
2693
+ export { A2AClient, A2AError, A2AServer, A2A_ERROR_CODES, A2A_METHODS, DashboardRegistry, Role, TaskState, TaskStore, apply, createDshAgentExecutor, defaultExecutor, defaultSkills, fetchAgentCard, getSharedRegistry, inferBaseUrl, inject, isTerminal, name, notConfiguredExecutor, registerAgentTools, shellExecutor, webServerBaseUrl };
@@ -77,9 +77,12 @@ export type { DashboardSnapshot, InboundPeer, OutboundAgent, ControlResult, } fr
77
77
  */
78
78
  export interface WebServerService {
79
79
  /** The OS-assigned (or configured) listen port. */
80
- readonly port: number;
81
- /** The configured bind host literal ('127.0.0.1' | '0.0.0.0'). */
80
+ readonly port: number | undefined;
81
+ /** The configured bind address. Older hosts also allow '0.0.0.0'. */
82
82
  readonly host: string;
83
+ /** Added by DSH 0.2.1: preserve TLS and canonical IPv6/mapped addresses. */
84
+ getOrigin?(): string;
85
+ readonly protocol?: 'http:' | 'https:';
83
86
  register(route: {
84
87
  kind: 'exact' | 'prefix';
85
88
  path: string;
@@ -154,9 +157,12 @@ export declare function apply(ctx: Context, config?: A2APluginConfig): void;
154
157
  * back to loopback when no such address exists. IPv6 link-local is skipped
155
158
  * (not reachable without a scope id).
156
159
  *
157
- * @param host - the web server's configured bind host ('127.0.0.1' | '0.0.0.0').
160
+ * @param host - the web server's configured bind address.
158
161
  * @param port - the OS-assigned (or configured) listen port.
162
+ * @param protocol - the host listener's protocol (older hosts default to HTTP).
159
163
  * @returns a scheme+host+port URL for Agents to call back into this DSH.
160
164
  */
161
- export declare function inferBaseUrl(host: string, port: number): string;
165
+ export declare function inferBaseUrl(host: string, port: number, protocol?: 'http:' | 'https:'): string;
166
+ /** Prefer the host's authoritative origin; retain inference for older DSH. */
167
+ export declare function webServerBaseUrl(webServer: WebServerService): string;
162
168
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ryubyte/dsh-a2a",
3
3
  "description": "Agent2Agent (A2A) protocol v1.0 plugin for DeepSeek Harness (DSH): outbound client (remote AgentCard skills as ctx.tools), inbound server (A2A JSON-RPC + AgentCard endpoint), and a settings dashboard of live A2A connections",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "keywords": [
6
6
  "a2a",
7
7
  "agent2agent",
@@ -50,21 +50,26 @@
50
50
  "license": "MIT",
51
51
  "peerDependencies": {
52
52
  "@deepseek-ai/cordis": "^4.0.4",
53
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.2",
54
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
55
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
56
- "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
57
- "@deepseek-ai/dsh-tools": "^0.1.7-rc.2",
53
+ "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || >=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-alpha.1 <0.2.2-0",
54
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.2 || >=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-alpha.1 <0.2.2-0",
58
55
  "react": "^18.2.0",
59
56
  "react-dom": "^18.2.0"
60
57
  },
61
58
  "devDependencies": {
59
+ "@deepseek-ai/cordis": "^4.0.5",
60
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
61
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.5",
62
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
63
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.5",
64
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.5",
62
65
  "@types/node": "^22.0.0",
63
66
  "@types/react": "~18.3.1",
64
67
  "@types/react-dom": "^18.3.5",
68
+ "@types/semver": "^7.7.1",
65
69
  "esbuild": "^0.28.2",
66
70
  "react": "^18.3.1",
67
71
  "react-dom": "^18.3.1",
72
+ "semver": "^7.8.5",
68
73
  "tsdown": "^0.22.14",
69
74
  "tsx": "^4.19.0",
70
75
  "typescript": "^5.5.0"
package/src/index.ts CHANGED
@@ -104,9 +104,12 @@ export type {
104
104
  */
105
105
  export interface WebServerService {
106
106
  /** The OS-assigned (or configured) listen port. */
107
- readonly port: number;
108
- /** The configured bind host literal ('127.0.0.1' | '0.0.0.0'). */
107
+ readonly port: number | undefined;
108
+ /** The configured bind address. Older hosts also allow '0.0.0.0'. */
109
109
  readonly host: string;
110
+ /** Added by DSH 0.2.1: preserve TLS and canonical IPv6/mapped addresses. */
111
+ getOrigin?(): string;
112
+ readonly protocol?: 'http:' | 'https:';
110
113
  register(route: {
111
114
  kind: 'exact' | 'prefix';
112
115
  path: string;
@@ -570,7 +573,7 @@ export function apply(ctx: Context, config: A2APluginConfig = {}) {
570
573
  // Derive the advertised base URL from the real listen address when the
571
574
  // config doesn't pin one; avoids advertising a stale port. When bound
572
575
  // to 0.0.0.0, advertise a reachable LAN address, not loopback.
573
- const baseUrl = mergedConfig.baseUrl ?? inferBaseUrl(webServer.host, webServer.port);
576
+ const baseUrl = mergedConfig.baseUrl ?? webServerBaseUrl(webServer);
574
577
  const endpointPath = mergedConfig.endpointPath ?? DEFAULT_ENDPOINT;
575
578
  // Surface a stale pinned baseUrl: if the config pins a port that does
576
579
  // not match the real listen port, the advertised AgentCard URL is
@@ -845,7 +848,7 @@ export function apply(ctx: Context, config: A2APluginConfig = {}) {
845
848
  const { endpointChanged } = server.updateCard(cardPatch);
846
849
  if (clearBaseUrl) {
847
850
  // Re-derive from the actual listen address and re-advertise.
848
- server.setBaseUrl(inferBaseUrl(webServer.host, webServer.port));
851
+ server.setBaseUrl(webServerBaseUrl(webServer));
849
852
  }
850
853
  // The JSON-RPC path moved: re-register on the new path, but only
851
854
  // while serving (disabled → routes get the new path on next enable).
@@ -1033,16 +1036,25 @@ function inboundSessionCwd(configPath: string): string {
1033
1036
  * back to loopback when no such address exists. IPv6 link-local is skipped
1034
1037
  * (not reachable without a scope id).
1035
1038
  *
1036
- * @param host - the web server's configured bind host ('127.0.0.1' | '0.0.0.0').
1039
+ * @param host - the web server's configured bind address.
1037
1040
  * @param port - the OS-assigned (or configured) listen port.
1041
+ * @param protocol - the host listener's protocol (older hosts default to HTTP).
1038
1042
  * @returns a scheme+host+port URL for Agents to call back into this DSH.
1039
1043
  */
1040
- export function inferBaseUrl(host: string, port: number): string {
1044
+ export function inferBaseUrl(host: string, port: number, protocol: 'http:' | 'https:' = 'http:'): string {
1041
1045
  const hostname =
1042
1046
  host === '0.0.0.0'
1043
1047
  ? primaryLanAddress() ?? '127.0.0.1'
1044
1048
  : host || '127.0.0.1';
1045
- return `http://${hostname}:${port}`;
1049
+ const urlHost = hostname.includes(':') && !hostname.startsWith('[') ? `[${hostname}]` : hostname;
1050
+ return `${protocol}//${urlHost}:${port}`;
1051
+ }
1052
+
1053
+ /** Prefer the host's authoritative origin; retain inference for older DSH. */
1054
+ export function webServerBaseUrl(webServer: WebServerService): string {
1055
+ if (typeof webServer.getOrigin === 'function') return webServer.getOrigin();
1056
+ if (webServer.port === undefined) throw new Error('a2a: webServer has not bound a port');
1057
+ return inferBaseUrl(webServer.host, webServer.port, webServer.protocol);
1046
1058
  }
1047
1059
 
1048
1060
  /**
@@ -1135,8 +1147,15 @@ function corsHeadersFor(reqOrigin: string | undefined, allowed: readonly string[
1135
1147
  function trustedLoopback(req: IncomingMessage): boolean {
1136
1148
  const host = req.headers.host;
1137
1149
  if (!host) return false;
1138
- const hostname = host.split(':')[0];
1139
- if (hostname !== '127.0.0.1' && hostname !== 'localhost' && hostname !== '::1') return false;
1150
+ let hostname: string;
1151
+ try {
1152
+ const authority = new URL(`http://${host}`);
1153
+ if (authority.username || authority.password || authority.pathname !== '/' || authority.search || authority.hash) return false;
1154
+ hostname = authority.hostname;
1155
+ } catch {
1156
+ return false;
1157
+ }
1158
+ if (hostname !== '127.0.0.1' && hostname !== 'localhost' && hostname !== '[::1]') return false;
1140
1159
  if (req.headers['sec-fetch-site'] === 'cross-site') return false;
1141
1160
  const origin = req.headers.origin;
1142
1161
  if (!origin) return true;
@@ -1165,4 +1184,4 @@ function toServerReq(req: IncomingMessage): {
1165
1184
  headers,
1166
1185
  socket: req.socket ? { remoteAddress: req.socket.remoteAddress, remotePort: req.socket.remotePort } : undefined,
1167
1186
  };
1168
- }
1187
+ }