@ryubyte/dsh-a2a 0.1.4 → 0.1.5
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 +6 -2
- package/README.md +2 -2
- package/lib/index.js +82 -5
- package/lib/types/a2a-config.d.ts +23 -1
- package/lib/types/a2a-config.d.ts.map +1 -1
- package/lib/types/index.d.ts +18 -0
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/a2a-config.ts +39 -3
- package/src/index.ts +64 -4
package/README.en.md
CHANGED
|
@@ -32,7 +32,8 @@ It turns a DSH instance into a full A2A peer in both directions:
|
|
|
32
32
|
Configuration is driven by **`a2a.json`** (per profile, UI-editable) — manage
|
|
33
33
|
agents without touching the composition. The file is resolved from the active
|
|
34
34
|
profile, not the launch directory: the plugin parses `--profile <name>` from
|
|
35
|
-
the process command line
|
|
35
|
+
the process command line (the `dsh web` alias is recognized as the `web`
|
|
36
|
+
profile too) and reads/writes `~/.dsh/profiles/<name>/a2a.json`.
|
|
36
37
|
|
|
37
38
|
## Screenshots
|
|
38
39
|
|
|
@@ -232,7 +233,10 @@ const server = new A2AServer({
|
|
|
232
233
|
that half; nothing crashes.
|
|
233
234
|
- **`baseUrl` is optional in server mode**: when omitted the AgentCard
|
|
234
235
|
advertises the webserver's real listen address, so ephemeral ports never
|
|
235
|
-
publish a stale URL.
|
|
236
|
+
publish a stale URL. When bound to `127.0.0.1` that loopback address is
|
|
237
|
+
advertised; when bound to `0.0.0.0` the plugin derives the machine's LAN
|
|
238
|
+
address (reachable by remote Agents), falling back to `127.0.0.1` only when
|
|
239
|
+
none exists. Set it explicitly only when a reverse proxy fronts the
|
|
236
240
|
instance.
|
|
237
241
|
- Client-mode connection lifecycle hooks (`onReady` / `onDispose`) feed a
|
|
238
242
|
**process-wide shared registry**; multiple instances (client + server) share
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
### 运行时配置(a2a.json)
|
|
31
31
|
|
|
32
|
-
配置以 **`a2a.json`(按 profile 存放,UI 可编辑)为准**,无需改动组合配置即可管理 Agent。文件定位不依赖启动目录:插件解析进程命令行里的 `--profile <name
|
|
32
|
+
配置以 **`a2a.json`(按 profile 存放,UI 可编辑)为准**,无需改动组合配置即可管理 Agent。文件定位不依赖启动目录:插件解析进程命令行里的 `--profile <name>`(`dsh web` 别名同样识别为 `web` profile),读写 `~/.dsh/profiles/<name>/a2a.json`。
|
|
33
33
|
|
|
34
34
|
## 截图
|
|
35
35
|
|
|
@@ -192,7 +192,7 @@ const server = new A2AServer({
|
|
|
192
192
|
## 工作原理
|
|
193
193
|
|
|
194
194
|
- 插件在 `ctx.inject(...)` / `ctx.effect(...)` 内读取 `ctx.webServer` 与 `ctx.tools`(原生 Cordis 模式,与 `dsh-client-hmr` 等 bundle 一致);缺少任一半的组合只会闲置该半区,不会崩溃。
|
|
195
|
-
- **server 模式 `baseUrl` 可选**:省略时 AgentCard 公布 webServer 的真实监听地址,临时端口不会发布过期 URL
|
|
195
|
+
- **server 模式 `baseUrl` 可选**:省略时 AgentCard 公布 webServer 的真实监听地址,临时端口不会发布过期 URL。绑定 `127.0.0.1` 时公布该回环地址;绑定 `0.0.0.0` 时自动推导本机 LAN 地址(供远程 Agent 可达),无法推导才退回 `127.0.0.1`。仅在反向代理前置时才需显式设置。
|
|
196
196
|
- 客户端模式的连接生命周期钩子(`onReady` / `onDispose`)上报到**进程级共享注册表**;多个实例(client + server)共用,`/a2a/api` 每个进程注册一次。
|
|
197
197
|
- 所有注册 fiber 作用域化:插件停止 / 更新时由 Cordis 自动清理。
|
|
198
198
|
- 禁用(`enabled:false`)的 Agent 显示为灰化占位卡,**不自动连接**,直到重新启用。
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, networkInterfaces } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
4
|
//#region src/protocol.ts
|
|
5
5
|
/**
|
|
6
6
|
* A2A (Agent2Agent) Protocol v1.0 — TypeScript type definitions.
|
|
@@ -1479,6 +1479,30 @@ function profileNameFromArgv() {
|
|
|
1479
1479
|
}
|
|
1480
1480
|
}
|
|
1481
1481
|
/**
|
|
1482
|
+
* Best-effort profile-name fallback for launchers that resolve a profile
|
|
1483
|
+
* WITHOUT writing `--profile` back into argv.
|
|
1484
|
+
*
|
|
1485
|
+
* The dsh `web` subcommand is a hardcoded alias for `--profile web`: the
|
|
1486
|
+
* launcher resolves the name internally and boots the web profile, but the
|
|
1487
|
+
* process argv keeps the original tokens (`node …/dsh web [flags]`) — there
|
|
1488
|
+
* is no `--profile` flag and no environment variable naming the profile. So
|
|
1489
|
+
* when {@link profileNameFromArgv} finds nothing, check whether the first
|
|
1490
|
+
* non-flag token after argv[0] is a known dsh subcommand alias:
|
|
1491
|
+
*
|
|
1492
|
+
* - `web` → boot profile `web` (alias, never followed by a name)
|
|
1493
|
+
* - `--profile <n>` → already handled above
|
|
1494
|
+
* - anything else → unknown/not a profile alias (e.g. `plugin`, which
|
|
1495
|
+
* is a management mode, not a boot; returns undefined)
|
|
1496
|
+
*
|
|
1497
|
+
* Returns undefined when argv carries no recognizable alias, letting the
|
|
1498
|
+
* caller fall through to the cwd-based resolution steps.
|
|
1499
|
+
*/
|
|
1500
|
+
function profileNameFromDshAlias() {
|
|
1501
|
+
const argv = process.argv ?? [];
|
|
1502
|
+
if (argv.includes("--profile") || argv.some((a) => a.startsWith("--profile="))) return void 0;
|
|
1503
|
+
if (argv[2] === "web") return "web";
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1482
1506
|
* Resolve the a2a.json path for the current process, without depending on
|
|
1483
1507
|
* the launch directory. The plugin must work no matter where `dsh` was
|
|
1484
1508
|
* started from (dsh does NOT chdir into the profile directory), so this
|
|
@@ -1488,7 +1512,9 @@ function profileNameFromArgv() {
|
|
|
1488
1512
|
*
|
|
1489
1513
|
* Order:
|
|
1490
1514
|
* 0. `$DSH_HOME/profiles/<argv --profile>/a2a.json` — the active profile
|
|
1491
|
-
* as named on the command line (most authoritative; works from ANY cwd)
|
|
1515
|
+
* as named on the command line (most authoritative; works from ANY cwd).
|
|
1516
|
+
* Also matches the `dsh web` alias, which boots profile `web` without
|
|
1517
|
+
* writing `--profile` into argv.
|
|
1492
1518
|
* 1. `cwd/a2a.json` — explicit when launched inside a profile dir
|
|
1493
1519
|
* 2. outward from cwd: the nearest ancestor holding `a2a.json` — covers
|
|
1494
1520
|
* launching from e.g. a repo checkout whose parent chain includes the
|
|
@@ -1506,7 +1532,7 @@ function resolveConfigPath() {
|
|
|
1506
1532
|
const cwd = process.cwd();
|
|
1507
1533
|
const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
1508
1534
|
const base = cwd.split(/[\\/]/).pop() ?? "";
|
|
1509
|
-
const profileName = profileNameFromArgv();
|
|
1535
|
+
const profileName = profileNameFromArgv() ?? profileNameFromDshAlias();
|
|
1510
1536
|
if (profileName && profileName !== "." && profileName !== ".." && !profileName.includes("/") && !profileName.includes("\\")) {
|
|
1511
1537
|
const argvFile = join(home, "profiles", profileName, A2A_CONFIG_FILENAME);
|
|
1512
1538
|
if (existsSync(argvFile)) return argvFile;
|
|
@@ -2015,7 +2041,7 @@ function apply(ctx, config = {}) {
|
|
|
2015
2041
|
ctx.inject(["webServer"], (ctx) => {
|
|
2016
2042
|
ctx.effect(() => {
|
|
2017
2043
|
const webServer = ctx.webServer;
|
|
2018
|
-
const baseUrl = mergedConfig.baseUrl ??
|
|
2044
|
+
const baseUrl = mergedConfig.baseUrl ?? inferBaseUrl(webServer.host, webServer.port);
|
|
2019
2045
|
const endpointPath = mergedConfig.endpointPath ?? DEFAULT_ENDPOINT;
|
|
2020
2046
|
let execute = mergedConfig.execute;
|
|
2021
2047
|
let executorKind = execute ? "custom" : "none";
|
|
@@ -2281,6 +2307,57 @@ function inboundSessionCwd(configPath) {
|
|
|
2281
2307
|
return process.cwd();
|
|
2282
2308
|
}
|
|
2283
2309
|
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Derive the public base URL advertised in the AgentCard from the real wire
|
|
2312
|
+
* listen address, when the operator did not pin `baseUrl` in config.
|
|
2313
|
+
*
|
|
2314
|
+
* The A2A AgentCard must carry a URL a REMOTE caller can reach. When the web
|
|
2315
|
+
* server binds only loopback (`127.0.0.1`), that address is correct. But when
|
|
2316
|
+
* it binds every interface (`0.0.0.0`), advertising `127.0.0.1` is wrong for
|
|
2317
|
+
* every non-local caller — it points at the caller's own loopback. In that
|
|
2318
|
+
* case pick the machine's best-known LAN address (the primary non-loopback
|
|
2319
|
+
* IPv4 of the interface that routes to the default gateway), and only fall
|
|
2320
|
+
* back to loopback when no such address exists. IPv6 link-local is skipped
|
|
2321
|
+
* (not reachable without a scope id).
|
|
2322
|
+
*
|
|
2323
|
+
* @param host - the web server's configured bind host ('127.0.0.1' | '0.0.0.0').
|
|
2324
|
+
* @param port - the OS-assigned (or configured) listen port.
|
|
2325
|
+
* @returns a scheme+host+port URL for Agents to call back into this DSH.
|
|
2326
|
+
*/
|
|
2327
|
+
function inferBaseUrl(host, port) {
|
|
2328
|
+
return `http://${host === "0.0.0.0" ? primaryLanAddress() ?? "127.0.0.1" : host || "127.0.0.1"}:${port}`;
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Best single LAN IPv4 for this machine: the address of the non-loopback
|
|
2332
|
+
* interface whose route reaches the default gateway (the address a remote
|
|
2333
|
+
* peer on the same subnet would use to reach us). Returns undefined when the
|
|
2334
|
+
* machine has no non-loopback IPv4 (e.g. offline / containers).
|
|
2335
|
+
*
|
|
2336
|
+
* Prefers IPv4 (A2A URLs here are v4-style), skips link-local (169.254.0.0/16)
|
|
2337
|
+
* and CGNAT (100.64.0.0/10) ranges, which are useless to advertise.
|
|
2338
|
+
*/
|
|
2339
|
+
function primaryLanAddress() {
|
|
2340
|
+
try {
|
|
2341
|
+
const ifaces = networkInterfaces();
|
|
2342
|
+
let candidate;
|
|
2343
|
+
for (const name of Object.keys(ifaces)) {
|
|
2344
|
+
const addrs = ifaces[name] ?? [];
|
|
2345
|
+
for (const a of addrs) {
|
|
2346
|
+
if (a.family !== "IPv4" || a.internal) continue;
|
|
2347
|
+
const ip = a.address;
|
|
2348
|
+
if (ip.startsWith("169.254.")) continue;
|
|
2349
|
+
const first = Number(ip.split(".")[0]);
|
|
2350
|
+
const second = Number(ip.split(".")[1]);
|
|
2351
|
+
if (first === 100 && second >= 64 && second <= 127) continue;
|
|
2352
|
+
if (!candidate) candidate = ip;
|
|
2353
|
+
if (name === "en0" || name === "eth0" || name === "ens3" || name === "ens5") return ip;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return candidate;
|
|
2357
|
+
} catch {
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2284
2361
|
/** Drain the request body from a Node IncomingMessage. */
|
|
2285
2362
|
async function readBody(req) {
|
|
2286
2363
|
return new Promise((resolve, reject) => {
|
|
@@ -2321,4 +2398,4 @@ function toServerReq(req) {
|
|
|
2321
2398
|
};
|
|
2322
2399
|
}
|
|
2323
2400
|
//#endregion
|
|
2324
|
-
export { A2AClient, A2AError, A2AServer, A2A_ERROR_CODES, A2A_METHODS, DashboardRegistry, Role, TaskState, TaskStore, apply, createDshAgentExecutor, defaultExecutor, defaultSkills, fetchAgentCard, getSharedRegistry, inject, isTerminal, name, notConfiguredExecutor, registerAgentTools, shellExecutor };
|
|
2401
|
+
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 };
|
|
@@ -68,6 +68,26 @@ export interface PersistedA2AConfig {
|
|
|
68
68
|
* guessing from cwd's basename.
|
|
69
69
|
*/
|
|
70
70
|
export declare function profileNameFromArgv(): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Best-effort profile-name fallback for launchers that resolve a profile
|
|
73
|
+
* WITHOUT writing `--profile` back into argv.
|
|
74
|
+
*
|
|
75
|
+
* The dsh `web` subcommand is a hardcoded alias for `--profile web`: the
|
|
76
|
+
* launcher resolves the name internally and boots the web profile, but the
|
|
77
|
+
* process argv keeps the original tokens (`node …/dsh web [flags]`) — there
|
|
78
|
+
* is no `--profile` flag and no environment variable naming the profile. So
|
|
79
|
+
* when {@link profileNameFromArgv} finds nothing, check whether the first
|
|
80
|
+
* non-flag token after argv[0] is a known dsh subcommand alias:
|
|
81
|
+
*
|
|
82
|
+
* - `web` → boot profile `web` (alias, never followed by a name)
|
|
83
|
+
* - `--profile <n>` → already handled above
|
|
84
|
+
* - anything else → unknown/not a profile alias (e.g. `plugin`, which
|
|
85
|
+
* is a management mode, not a boot; returns undefined)
|
|
86
|
+
*
|
|
87
|
+
* Returns undefined when argv carries no recognizable alias, letting the
|
|
88
|
+
* caller fall through to the cwd-based resolution steps.
|
|
89
|
+
*/
|
|
90
|
+
export declare function profileNameFromDshAlias(): string | undefined;
|
|
71
91
|
/**
|
|
72
92
|
* Resolve the a2a.json path for the current process, without depending on
|
|
73
93
|
* the launch directory. The plugin must work no matter where `dsh` was
|
|
@@ -78,7 +98,9 @@ export declare function profileNameFromArgv(): string | undefined;
|
|
|
78
98
|
*
|
|
79
99
|
* Order:
|
|
80
100
|
* 0. `$DSH_HOME/profiles/<argv --profile>/a2a.json` — the active profile
|
|
81
|
-
* as named on the command line (most authoritative; works from ANY cwd)
|
|
101
|
+
* as named on the command line (most authoritative; works from ANY cwd).
|
|
102
|
+
* Also matches the `dsh web` alias, which boots profile `web` without
|
|
103
|
+
* writing `--profile` into argv.
|
|
82
104
|
* 1. `cwd/a2a.json` — explicit when launched inside a profile dir
|
|
83
105
|
* 2. outward from cwd: the nearest ancestor holding `a2a.json` — covers
|
|
84
106
|
* launching from e.g. a repo checkout whose parent chain includes the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"a2a-config.d.ts","sourceRoot":"","sources":["../../src/a2a-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,eAAO,MAAM,mBAAmB,aAAa,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CACzG;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAaxD;AAED
|
|
1
|
+
{"version":3,"file":"a2a-config.d.ts","sourceRoot":"","sources":["../../src/a2a-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,eAAO,MAAM,mBAAmB,aAAa,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CACzG;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAaxD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,SAAS,CAW5D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAuC1C;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,IAAI,kBAAkB,CAW/C;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,MAAM,EAAE,kBAAkB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAatG;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,GAAG,eAAe,CAmBpG"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -125,4 +125,22 @@ export declare const name = "a2a";
|
|
|
125
125
|
*/
|
|
126
126
|
export declare const inject: readonly ["webServer", "tools"];
|
|
127
127
|
export declare function apply(ctx: Context, config?: A2APluginConfig): void;
|
|
128
|
+
/**
|
|
129
|
+
* Derive the public base URL advertised in the AgentCard from the real wire
|
|
130
|
+
* listen address, when the operator did not pin `baseUrl` in config.
|
|
131
|
+
*
|
|
132
|
+
* The A2A AgentCard must carry a URL a REMOTE caller can reach. When the web
|
|
133
|
+
* server binds only loopback (`127.0.0.1`), that address is correct. But when
|
|
134
|
+
* it binds every interface (`0.0.0.0`), advertising `127.0.0.1` is wrong for
|
|
135
|
+
* every non-local caller — it points at the caller's own loopback. In that
|
|
136
|
+
* case pick the machine's best-known LAN address (the primary non-loopback
|
|
137
|
+
* IPv4 of the interface that routes to the default gateway), and only fall
|
|
138
|
+
* back to loopback when no such address exists. IPv6 link-local is skipped
|
|
139
|
+
* (not reachable without a scope id).
|
|
140
|
+
*
|
|
141
|
+
* @param host - the web server's configured bind host ('127.0.0.1' | '0.0.0.0').
|
|
142
|
+
* @param port - the OS-assigned (or configured) listen port.
|
|
143
|
+
* @returns a scheme+host+port URL for Agents to call back into this DSH.
|
|
144
|
+
*/
|
|
145
|
+
export declare function inferBaseUrl(host: string, port: number): string;
|
|
128
146
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
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.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/a2a-config.ts
CHANGED
|
@@ -85,6 +85,38 @@ export function profileNameFromArgv(): string | undefined {
|
|
|
85
85
|
return undefined;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Best-effort profile-name fallback for launchers that resolve a profile
|
|
90
|
+
* WITHOUT writing `--profile` back into argv.
|
|
91
|
+
*
|
|
92
|
+
* The dsh `web` subcommand is a hardcoded alias for `--profile web`: the
|
|
93
|
+
* launcher resolves the name internally and boots the web profile, but the
|
|
94
|
+
* process argv keeps the original tokens (`node …/dsh web [flags]`) — there
|
|
95
|
+
* is no `--profile` flag and no environment variable naming the profile. So
|
|
96
|
+
* when {@link profileNameFromArgv} finds nothing, check whether the first
|
|
97
|
+
* non-flag token after argv[0] is a known dsh subcommand alias:
|
|
98
|
+
*
|
|
99
|
+
* - `web` → boot profile `web` (alias, never followed by a name)
|
|
100
|
+
* - `--profile <n>` → already handled above
|
|
101
|
+
* - anything else → unknown/not a profile alias (e.g. `plugin`, which
|
|
102
|
+
* is a management mode, not a boot; returns undefined)
|
|
103
|
+
*
|
|
104
|
+
* Returns undefined when argv carries no recognizable alias, letting the
|
|
105
|
+
* caller fall through to the cwd-based resolution steps.
|
|
106
|
+
*/
|
|
107
|
+
export function profileNameFromDshAlias(): string | undefined {
|
|
108
|
+
const argv = process.argv ?? [];
|
|
109
|
+
// Only the `dsh <subcommand>` alias form counts: when a `--profile` flag is
|
|
110
|
+
// present, the flag parser owns the name and this fallback must yield.
|
|
111
|
+
if (argv.includes('--profile') || argv.some((a) => a.startsWith('--profile='))) return undefined;
|
|
112
|
+
// `dsh web …` puts the subcommand at argv[2] (argv[0]=node, argv[1]=script).
|
|
113
|
+
// The dsh launcher rejects parent flags before a subcommand, so the alias
|
|
114
|
+
// must be the FIRST user token — a later `web` is an app argument, not a
|
|
115
|
+
// profile alias.
|
|
116
|
+
if (argv[2] === 'web') return 'web';
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
88
120
|
/**
|
|
89
121
|
* Resolve the a2a.json path for the current process, without depending on
|
|
90
122
|
* the launch directory. The plugin must work no matter where `dsh` was
|
|
@@ -95,7 +127,9 @@ export function profileNameFromArgv(): string | undefined {
|
|
|
95
127
|
*
|
|
96
128
|
* Order:
|
|
97
129
|
* 0. `$DSH_HOME/profiles/<argv --profile>/a2a.json` — the active profile
|
|
98
|
-
* as named on the command line (most authoritative; works from ANY cwd)
|
|
130
|
+
* as named on the command line (most authoritative; works from ANY cwd).
|
|
131
|
+
* Also matches the `dsh web` alias, which boots profile `web` without
|
|
132
|
+
* writing `--profile` into argv.
|
|
99
133
|
* 1. `cwd/a2a.json` — explicit when launched inside a profile dir
|
|
100
134
|
* 2. outward from cwd: the nearest ancestor holding `a2a.json` — covers
|
|
101
135
|
* launching from e.g. a repo checkout whose parent chain includes the
|
|
@@ -114,8 +148,10 @@ export function resolveConfigPath(): string {
|
|
|
114
148
|
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
115
149
|
const base = cwd.split(/[\\/]/).pop() ?? '';
|
|
116
150
|
|
|
117
|
-
// 0. Active profile from argv — authoritative regardless of cwd.
|
|
118
|
-
|
|
151
|
+
// 0. Active profile from argv — authoritative regardless of cwd. Also
|
|
152
|
+
// handles the `dsh web` alias, which boots profile `web` without putting
|
|
153
|
+
// `--profile` in argv.
|
|
154
|
+
const profileName = profileNameFromArgv() ?? profileNameFromDshAlias();
|
|
119
155
|
if (profileName && profileName !== '.' && profileName !== '..' && !profileName.includes('/') && !profileName.includes('\\')) {
|
|
120
156
|
const argvFile = join(home, 'profiles', profileName, A2A_CONFIG_FILENAME);
|
|
121
157
|
if (existsSync(argvFile)) return argvFile;
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import type { Context } from '@deepseek-ai/cordis';
|
|
25
25
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
26
26
|
import { mkdirSync } from 'node:fs';
|
|
27
|
+
import { networkInterfaces } from 'node:os';
|
|
27
28
|
import { dirname, join } from 'node:path';
|
|
28
29
|
import { registerAgentTools } from './outbound.js';
|
|
29
30
|
import { fetchAgentCard, pickInterface } from './card.js';
|
|
@@ -508,10 +509,9 @@ export function apply(ctx: Context, config: A2APluginConfig = {}) {
|
|
|
508
509
|
ctx.effect(() => {
|
|
509
510
|
const webServer = ctx.webServer;
|
|
510
511
|
// Derive the advertised base URL from the real listen address when the
|
|
511
|
-
// config doesn't pin one; avoids advertising a stale port.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
`http://${webServer.host === '0.0.0.0' ? '127.0.0.1' : webServer.host}:${webServer.port}`;
|
|
512
|
+
// config doesn't pin one; avoids advertising a stale port. When bound
|
|
513
|
+
// to 0.0.0.0, advertise a reachable LAN address, not loopback.
|
|
514
|
+
const baseUrl = mergedConfig.baseUrl ?? inferBaseUrl(webServer.host, webServer.port);
|
|
515
515
|
const endpointPath = mergedConfig.endpointPath ?? DEFAULT_ENDPOINT;
|
|
516
516
|
|
|
517
517
|
// ── executor precedence ───────────────────────────────────────────────
|
|
@@ -817,6 +817,66 @@ function inboundSessionCwd(configPath: string): string {
|
|
|
817
817
|
}
|
|
818
818
|
}
|
|
819
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Derive the public base URL advertised in the AgentCard from the real wire
|
|
822
|
+
* listen address, when the operator did not pin `baseUrl` in config.
|
|
823
|
+
*
|
|
824
|
+
* The A2A AgentCard must carry a URL a REMOTE caller can reach. When the web
|
|
825
|
+
* server binds only loopback (`127.0.0.1`), that address is correct. But when
|
|
826
|
+
* it binds every interface (`0.0.0.0`), advertising `127.0.0.1` is wrong for
|
|
827
|
+
* every non-local caller — it points at the caller's own loopback. In that
|
|
828
|
+
* case pick the machine's best-known LAN address (the primary non-loopback
|
|
829
|
+
* IPv4 of the interface that routes to the default gateway), and only fall
|
|
830
|
+
* back to loopback when no such address exists. IPv6 link-local is skipped
|
|
831
|
+
* (not reachable without a scope id).
|
|
832
|
+
*
|
|
833
|
+
* @param host - the web server's configured bind host ('127.0.0.1' | '0.0.0.0').
|
|
834
|
+
* @param port - the OS-assigned (or configured) listen port.
|
|
835
|
+
* @returns a scheme+host+port URL for Agents to call back into this DSH.
|
|
836
|
+
*/
|
|
837
|
+
export function inferBaseUrl(host: string, port: number): string {
|
|
838
|
+
const hostname =
|
|
839
|
+
host === '0.0.0.0'
|
|
840
|
+
? primaryLanAddress() ?? '127.0.0.1'
|
|
841
|
+
: host || '127.0.0.1';
|
|
842
|
+
return `http://${hostname}:${port}`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Best single LAN IPv4 for this machine: the address of the non-loopback
|
|
847
|
+
* interface whose route reaches the default gateway (the address a remote
|
|
848
|
+
* peer on the same subnet would use to reach us). Returns undefined when the
|
|
849
|
+
* machine has no non-loopback IPv4 (e.g. offline / containers).
|
|
850
|
+
*
|
|
851
|
+
* Prefers IPv4 (A2A URLs here are v4-style), skips link-local (169.254.0.0/16)
|
|
852
|
+
* and CGNAT (100.64.0.0/10) ranges, which are useless to advertise.
|
|
853
|
+
*/
|
|
854
|
+
function primaryLanAddress(): string | undefined {
|
|
855
|
+
try {
|
|
856
|
+
const ifaces = networkInterfaces();
|
|
857
|
+
let candidate: string | undefined;
|
|
858
|
+
for (const name of Object.keys(ifaces)) {
|
|
859
|
+
const addrs = ifaces[name] ?? [];
|
|
860
|
+
for (const a of addrs) {
|
|
861
|
+
if (a.family !== 'IPv4' || a.internal) continue;
|
|
862
|
+
const ip = a.address;
|
|
863
|
+
if (ip.startsWith('169.254.')) continue;
|
|
864
|
+
const first = Number(ip.split('.')[0]);
|
|
865
|
+
const second = Number(ip.split('.')[1]);
|
|
866
|
+
// CGNAT 100.64.0.0/10: first octet 100, second in [64..127]. The rest
|
|
867
|
+
// of 100.x.y.z is ordinary public space — keep it.
|
|
868
|
+
if (first === 100 && second >= 64 && second <= 127) continue;
|
|
869
|
+
if (!candidate) candidate = ip;
|
|
870
|
+
// Prefer addresses that match a default-route interface if visible.
|
|
871
|
+
if (name === 'en0' || name === 'eth0' || name === 'ens3' || name === 'ens5') return ip;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return candidate;
|
|
875
|
+
} catch {
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
820
880
|
/** Drain the request body from a Node IncomingMessage. */
|
|
821
881
|
async function readBody(req: IncomingMessage): Promise<string> {
|
|
822
882
|
return new Promise<string>((resolve, reject) => {
|