@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 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 and reads/writes `~/.dsh/profiles/<name>/a2a.json`.
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. Set it explicitly only when a reverse proxy fronts the
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>`,读写 `~/.dsh/profiles/<name>/a2a.json`。
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 ?? `http://${webServer.host === "0.0.0.0" ? "127.0.0.1" : webServer.host}:${webServer.port}`;
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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAqC1C;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"}
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"}
@@ -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
@@ -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.1.4",
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
- const profileName = profileNameFromArgv();
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
- const baseUrl =
513
- mergedConfig.baseUrl ??
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) => {