@ryubyte/dsh-a2a 0.1.3 → 0.1.4

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
@@ -17,14 +17,15 @@ It turns a DSH instance into a full A2A peer in both directions:
17
17
  - **Unary**: `SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, `GetExtendedAgentCard`
18
18
  - **Streaming**: `SendStreamingMessage` (SSE) / `SubscribeToTask`, with `TaskStatusUpdateEvent` + `TaskArtifactUpdateEvent`
19
19
  - **Task lifecycle**: `SUBMITTED → WORKING → COMPLETED / FAILED / CANCELED / REJECTED / INPUT_REQUIRED / AUTH_REQUIRED`
20
+ - **Auth**: outbound sends a per-agent Bearer token (`Authorization: Bearer …`); inbound can protect the endpoint with a shared Bearer token — when set, the AgentCard declares a `bearerAuth` scheme and requests without/with a wrong token get 401 (both unary calls and the SSE stream)
20
21
  - **Types**: `Task`, `Message`, `Part` (text/raw/url/data), `Artifact`, `AgentCard`, security schemes, extensions
21
22
 
22
23
  ### Connection dashboard (设置 → A2A 连接)
23
24
 
24
- - **Outbound agents** — remote agents this DSH connects to (name, connection state, AgentCard URL, skill/tool counts, last activity), each with **Reconnect / Enable / Disable / Delete** controls.
25
+ - **Outbound agents** — remote agents this DSH connects to (name, connection state, AgentCard URL, skill/tool counts, last activity), each with **Reconnect / Enable / Disable / Delete** controls. When adding an agent you can supply an optional **Bearer token** — it is sent as `Authorization: Bearer …` on both the AgentCard fetch and every JSON-RPC call, and persisted alongside the agent in `a2a.json`.
25
26
  - **Inbound peers** — who is calling this DSH (label, source address, first/last seen, task count, streaming flag), with a close control.
26
- - **A2A server** — inbound server status with an online/offline toggle, endpoint and skill overview.
27
- - The panel refreshes every 3 s; `/a2a/api` serves a JSON snapshot and control endpoints, fenced to loopback/same-origin.
27
+ - **A2A server** — inbound server status with an online/offline toggle, endpoint and skill overview; it also shows **who executes inbound tasks** (custom executor / this DSH's agent session / none) and an **inbound auth** control to set or clear the shared Bearer token that gates the endpoint (persisted to `a2a.json`, effective immediately).
28
+ - The panel refreshes every 3 s; `/a2a/api` serves a JSON snapshot and control endpoints, fenced to loopback/same-origin (the token value is never returned in the snapshot — only whether one is configured).
28
29
 
29
30
  ### Runtime configuration (a2a.json)
30
31
 
@@ -105,6 +106,10 @@ AgentCard URL to import and connect — the config is written back to
105
106
  Once connected, the remote agent's skills appear as model tools:
106
107
  `a2a__<name>__<skill>`.
107
108
 
109
+ A local agent session's repeated tool calls to a given remote agent reuse one
110
+ A2A `contextId`, so they land in the same remote session with cross-turn memory;
111
+ different local sessions are isolated into separate conversations.
112
+
108
113
  ### 2. Expose this DSH as an A2A agent (inbound)
109
114
 
110
115
  Enable server mode in `a2a.json`:
@@ -128,9 +133,44 @@ Enable server mode in `a2a.json`:
128
133
  Other A2A clients discover this DSH via
129
134
  `http://<host>:<port>/.well-known/agent-card.json` and call the `/a2a` endpoint.
130
135
 
131
- > Note: the default inbound executor runs shell commands — fine for local
132
- > testing only; for production, inject a constrained executor
133
- > programmatically (see below) and put a TLS reverse proxy in front.
136
+ #### Who executes an inbound task (executor precedence)
137
+
138
+ On each inbound task the server picks an executor in this order:
139
+
140
+ 1. **Explicit `execute`** (injected programmatically) — yours wins.
141
+ 2. **This DSH's own agent session** (the default, when an agent loop is
142
+ present) — **one session per A2A `contextId`**: the first task for a context
143
+ `ctx.agents.create()`s a session (later tasks for the same context reuse it,
144
+ so history accumulates; a restarted process `resume`s it on demand), sends
145
+ the message as a `next-turn` waking item, awaits `whenIdle()`, and takes the
146
+ latest assistant reply as the A2A artifact. Concurrent tasks for one context
147
+ are serialized; distinct contexts run in parallel. Sessions are not disposed
148
+ per task (that would defeat context accumulation) — all are disposed on
149
+ plugin unload/disable. Turning on server mode is enough — no code.
150
+ 3. **`notConfiguredExecutor`** (fallback) — when nothing is injected *and* no
151
+ usable agent loop is composed (e.g. a bare dsh-base profile), it returns
152
+ "no executor configured — inject one" and **runs nothing**.
153
+
154
+ > On "present": `ctx.agents` (the registry) existing ≠ being able to create an
155
+ > agent. `create()` needs a factory registered by an agent-loop plugin; without
156
+ > it `create()` rejects and the DSH agent executor surfaces that as a readable
157
+ > error rather than throwing. So a minimal profile still needs an explicit
158
+ > executor.
159
+ >
160
+ > Session ↔ cwd: a DSH session validates `cwd` as an absolute path and freezes
161
+ > it into the session header at creation. Inbound sessions use a **dedicated**
162
+ > cwd `<profile>/.a2a-sessions` (kept separate from the profile root so they
163
+ > don't crowd your interactive sessions). They appear in the session list's
164
+ > "Ungrouped" bucket, each named `A2A: <first-prompt summary>` (inbound messages
165
+ > carry a plugin source, which DSH's automatic titling ignores, so we name it
166
+ > explicitly). Naming is cosmetic — failures are logged, never failing the task.
167
+ >
168
+ > Cancellation: `CancelTask` aborts the running task's executor signal, actually
169
+ > stopping the agent turn (not just flipping task status).
170
+ >
171
+ > For local testing you may still pass `shellExecutor`, which runs the prompt as
172
+ > a `/bin/sh -c` command — trusted clients only. For production put a TLS
173
+ > reverse proxy in front and set `authToken` as needed.
134
174
 
135
175
  ### 3. Programmatic use
136
176
 
@@ -206,14 +246,17 @@ const server = new A2AServer({
206
246
 
207
247
  - The dashboard API (`/a2a/api`) listens on loopback / same-origin only; it
208
248
  carries no secrets and exists for operator visibility.
209
- - The default inbound executor runs shell commands directly (`/bin/sh -c`), not
210
- an LLM agent; replace it with a constrained executor (e.g. DSH's
211
- sandbox/approval pipeline) in production.
249
+ - Inbound execution follows the [executor precedence](#who-executes-an-inbound-task-executor-precedence):
250
+ with an agent loop present it defaults to this DSH's own cwd-bound
251
+ session; otherwise (and with nothing injected) it **refuses** and returns
252
+ "no executor configured". `shellExecutor` is local-testing only. Whatever the
253
+ executor, never expose server mode to untrusted networks (no auth by default
254
+ unless you set `authToken`).
212
255
  - The A2A spec requires HTTPS for production `AgentInterface.url`; DSH's
213
256
  webserver binds loopback by default, so put a TLS reverse proxy in front
214
257
  before exposing beyond localhost.
215
- - Outbound bearer tokens are stored in plaintext in `a2a.json`; secure the file
216
- yourself.
258
+ - Bearer tokens — both the outbound per-agent token and the inbound endpoint
259
+ `authToken` — are stored in plaintext in `a2a.json`; secure the file yourself.
217
260
 
218
261
  ## Development
219
262
 
package/README.md CHANGED
@@ -17,14 +17,15 @@
17
17
  - **单次调用**:`SendMessage`、`GetTask`、`ListTasks`、`CancelTask`、`GetExtendedAgentCard`
18
18
  - **流式**:`SendStreamingMessage`(SSE)/ `SubscribeToTask`,含 `TaskStatusUpdateEvent` + `TaskArtifactUpdateEvent`
19
19
  - **任务生命周期**:`SUBMITTED → WORKING → COMPLETED / FAILED / CANCELED / REJECTED / INPUT_REQUIRED / AUTH_REQUIRED`
20
+ - **鉴权**:出站按 Agent 配置 Bearer Token(`Authorization: Bearer …`);入站可选共享 Bearer Token 保护端点——配置后 AgentCard 声明 `bearerAuth` 安全方案,未带/错误 token 的请求返回 401(单次调用与 SSE 流均生效)
20
21
  - **类型**:`Task`、`Message`、`Part`(text/raw/url/data)、`Artifact`、`AgentCard`、安全方案、扩展
21
22
 
22
23
  ### 连接面板(设置 → A2A 连接)
23
24
 
24
- - **出站 Agent**:列出本 DSH 连接的远程 Agent(名称、连接状态、AgentCard URL、技能/工具数、最近活动),每个 Agent 支持 **重连 / 启用 / 禁用 / 删除**。
25
+ - **出站 Agent**:列出本 DSH 连接的远程 Agent(名称、连接状态、AgentCard URL、技能/工具数、最近活动),每个 Agent 支持 **重连 / 启用 / 禁用 / 删除**。添加 Agent 时可填 **Bearer Token**(可选),导入与后续 JSON-RPC 调用都会带上 `Authorization: Bearer …`,并随 Agent 一起持久化到 `a2a.json`。
25
26
  - **入站连接**:谁在调用本 DSH(来源、地址、首/末次连接、任务数、流式标记),支持关闭。
26
- - **A2A 服务**:入站服务的运行状态,上线 / 下线开关,端点与技能一览。
27
- - 面板每 3 秒自动刷新;`/a2a/api` 提供 JSON 快照与控制接口,受 loopback/same-origin 保护。
27
+ - **A2A 服务**:入站服务的运行状态,上线 / 下线开关,端点与技能一览;并显示 **入站任务由谁执行**(自定义 executor / 本 DSH agent 会话 / 未配置),以及 **入站鉴权** 开关——可设置或清除保护入站端点的共享 Bearer Token(持久化到 `a2a.json`,实时生效)。
28
+ - 面板每 3 秒自动刷新;`/a2a/api` 提供 JSON 快照与控制接口,受 loopback/same-origin 保护(token 值不会通过快照回传,只暴露「是否已配置」)。
28
29
 
29
30
  ### 运行时配置(a2a.json)
30
31
 
@@ -96,6 +97,8 @@
96
97
 
97
98
  连接成功后,对方 Agent 的 skills 会作为模型工具出现:`a2a__<name>__<skill>`。
98
99
 
100
+ 同一个本地 agent 会话对某远程 Agent 的多次工具调用会复用同一个 A2A `contextId`,因此在远端落到同一会话、具备跨轮记忆;不同本地会话相互隔离,各自独立对话。
101
+
99
102
  ### 2. 对外提供 A2A 服务(入站)
100
103
 
101
104
  同样在 `a2a.json` 里开启 server 模式:
@@ -118,7 +121,21 @@
118
121
 
119
122
  之后其他 A2A 客户端通过 `http://<host>:<port>/.well-known/agent-card.json` 发现本 DSH,通过 `/a2a` 端点调用。
120
123
 
121
- > 注:默认入站 executor 会执行 shell 命令,仅适合本机自测;生产部署请通过编程方式注入受限 executor(见下文),并置于 TLS 反向代理之后。
124
+ #### 入站任务由谁执行(executor 优先级)
125
+
126
+ server 模式收到任务后,按以下优先级选择 executor:
127
+
128
+ 1. **显式 `execute`**(编程接入时注入)—— 用你的。
129
+ 2. **本 DSH 的 agent 会话**(默认,当 agent 循环就位时)—— 按 A2A `contextId` **一个对话一个会话**:某 `contextId` 首个任务 `ctx.agents.create()` 建会话(后续同 `contextId` 复用同一会话,历史累积;进程重启后按需 `resume`),把消息作为 `next-turn` 唤醒项发给它,等 `whenIdle()`,取最新 assistant 回复作为 A2A artifact。同一 `contextId` 的并发任务按序串行,不同 `contextId` 并行。会话不逐任务释放(否则无法累积上下文),插件卸载/下线时统一释放。开 server 即可「接收并处理任务」,无需写代码。
130
+ 3. **`notConfiguredExecutor`**(兜底)—— 既没注入 `execute`、composition 里又没有可用 agent 循环(如仅 dsh-base 的最小 profile)时,返回「no executor configured — inject one」,**不执行任何命令**。
131
+
132
+ > 关于「就位」:`ctx.agents`(registry)存在 ≠ 能创建 agent。`create()` 需要 agent-loop 插件注册的 factory;缺 factory 时 `create()` 会 reject,此时 DSH agent executor 会把它作为可读错误返回(而非抛出)。因此最小 profile 仍需显式注入 executor。
133
+ >
134
+ > 会话与工作区:DSH 会话在创建时把 `cwd` 校验为绝对路径并冻结进会话头。入站会话用**专用目录** `<profile>/.a2a-sessions` 作为 cwd(与 profile 根分开,避免入站会话挤占你的交互会话)。它们出现在会话列表的「未分组」里,并按首条消息摘要命名为 `A2A: <摘要>`(入站消息带 plugin 来源,DSH 的自动标题只认 human 消息,故显式命名)。命名是装饰性的,失败只记日志、绝不影响任务。
135
+ >
136
+ > 取消:`CancelTask` 会 abort 正在运行任务的 executor 信号,真正中止 agent 回合(不只是改任务状态)。
137
+ >
138
+ > 本机自测也可显式传入 `shellExecutor`(把 prompt 当作 shell 命令执行,仅限可信客户端);生产请置于 TLS 反向代理之后并按需配 `authToken`。
122
139
 
123
140
  ### 3. 编程接入
124
141
 
@@ -183,9 +200,9 @@ const server = new A2AServer({
183
200
  ## 已知限制
184
201
 
185
202
  - 面板 API(`/a2a/api`)只监听 loopback / same-origin,供运维可见性使用,不承载机密。
186
- - 默认入站 executor 直接执行 shell 命令(`/bin/sh -c`),非 LLM 代理;生产环境必须替换为受限 executor(如 DSH sandbox / 审批管线)。
203
+ - 入站服务的执行策略见[入站任务由谁执行](#入站任务由谁执行executor-优先级):有 agent 循环时默认由本 DSH 会话执行(绑定工作目录 cwd),否则(且未注入 executor)**拒绝执行**并返回「no executor configured」。`shellExecutor` 仅限本地自测。无论哪种 executor,都切勿向不受信网络暴露 server 模式(默认无鉴权,除非配置 `authToken`)。
187
204
  - A2A 规范要求生产 `AgentInterface.url` 使用 HTTPS;DSH webServer 默认仅绑 loopback,对外暴露前需自行加 TLS。
188
- - 出站的 bearer token 明文存于 `a2a.json`,请自行保证该文件权限。
205
+ - Bearer token(出站按 Agent 配置的、入站保护端点的 `authToken`)均明文存于 `a2a.json`,请自行保证该文件权限。
189
206
 
190
207
  ## 开发
191
208
 
package/lib/client.js CHANGED
@@ -54,6 +54,10 @@ window.__ModuleLoader__.load({
54
54
  const [tab, setTab] = (0, react.useState)("out");
55
55
  /** connection ids whose skill list is expanded in the outbound tab. */
56
56
  const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set());
57
+ /** Optional bearer token for the add-agent form (outbound). */
58
+ const [addToken, setAddToken] = (0, react.useState)("");
59
+ /** Inbound authToken input (serve tab); empty string means "clear". */
60
+ const [authInput, setAuthInput] = (0, react.useState)("");
57
61
  const refresh = async () => {
58
62
  try {
59
63
  setError(null);
@@ -101,7 +105,8 @@ window.__ModuleLoader__.load({
101
105
  headers: { "content-type": "application/json" },
102
106
  body: JSON.stringify({
103
107
  action: "discover-agent",
104
- agentCardUrl: u
108
+ agentCardUrl: u,
109
+ bearerToken: addToken.trim() || void 0
105
110
  })
106
111
  })).json();
107
112
  if (!body.ok || !body.message) {
@@ -118,9 +123,13 @@ window.__ModuleLoader__.load({
118
123
  const doConnect = async () => {
119
124
  if (!discovered) return;
120
125
  const n = discovered.name.trim() || "remote";
121
- await act("add-agent", n, { agentCardUrl: discovered.agentCardUrl });
126
+ await act("add-agent", n, {
127
+ agentCardUrl: discovered.agentCardUrl,
128
+ bearerToken: addToken.trim() || void 0
129
+ });
122
130
  if (!busy) {
123
131
  setAddUrl("");
132
+ setAddToken("");
124
133
  setDiscovered(null);
125
134
  }
126
135
  };
@@ -196,7 +205,25 @@ window.__ModuleLoader__.load({
196
205
  flex: 1,
197
206
  minWidth: 260
198
207
  }
199
- }), actionBtn("导入", false, () => void doDiscover(), discovering || !addUrl.trim())), discovering ? (0, react.createElement)("div", { style: {
208
+ }), actionBtn("导入", false, () => void doDiscover(), discovering || !addUrl.trim())), (0, react.createElement)("div", { style: {
209
+ display: "flex",
210
+ alignItems: "center",
211
+ gap: 8,
212
+ flexWrap: "wrap"
213
+ } }, (0, react.createElement)("input", {
214
+ type: "password",
215
+ placeholder: "Bearer Token(可选,发送为 Authorization: Bearer …)",
216
+ value: addToken,
217
+ onChange: (e) => setAddToken(e.target.value),
218
+ style: {
219
+ ...inputStyle,
220
+ flex: 1,
221
+ minWidth: 260
222
+ }
223
+ })), (0, react.createElement)("div", { style: {
224
+ fontSize: 11.5,
225
+ color: "var(--dsw-alias-label-tertiary, #999)"
226
+ } }, "若远程 Agent 需要鉴权,填入 token;导入与后续调用都会带上。token 会明文存入 a2a.json。"), discovering ? (0, react.createElement)("div", { style: {
200
227
  fontSize: 13,
201
228
  color: "var(--dsw-alias-label-tertiary, #999)"
202
229
  } }, "正在读取 Agent Card…") : discovered ? (0, react.createElement)("div", { style: {
@@ -243,7 +270,7 @@ window.__ModuleLoader__.load({
243
270
  display: "flex",
244
271
  gap: 8,
245
272
  marginTop: 2
246
- } }, actionBtn("连接", true, () => void doConnect(), busy === "add-agent"), actionBtn("取消", false, () => {
273
+ } }, actionBtn("连接", true, () => void doConnect(), busy?.startsWith("add-agent:") ?? false), actionBtn("取消", false, () => {
247
274
  setDiscovered(null);
248
275
  setAddUrl("");
249
276
  }, false))) : (0, react.createElement)("div", { style: {
@@ -263,7 +290,7 @@ window.__ModuleLoader__.load({
263
290
  } }, sec("A2A 服务(本 DSH 作为 Agent 对外)", 0), stateDot(serve.enabled ? "connected" : "disconnected"), (0, react.createElement)("span", { style: {
264
291
  fontSize: 12.5,
265
292
  color: serve.enabled ? "#2e7d32" : "#9e9e9e"
266
- } }, serve.enabled ? "运行中" : "已下线"), (0, react.createElement)("span", { style: { marginLeft: "auto" } }, serve.enabled ? actionBtn("下线", false, () => void act("server-disable", ""), busy === "server-disable") : actionBtn("上线", true, () => void act("server-enable", ""), busy === "server-enable"))), (0, react.createElement)("div", { style: {
293
+ } }, serve.enabled ? "运行中" : "已下线"), (0, react.createElement)("span", { style: { marginLeft: "auto" } }, serve.enabled ? actionBtn("下线", false, () => void act("server-disable", ""), busy === "server-disable:") : actionBtn("上线", true, () => void act("server-enable", ""), busy === "server-enable:"))), (0, react.createElement)("div", { style: {
267
294
  display: "flex",
268
295
  flexDirection: "column",
269
296
  gap: 6
@@ -301,13 +328,7 @@ window.__ModuleLoader__.load({
301
328
  fontSize: 12.5,
302
329
  color: "var(--dsw-alias-label-primary, #222)"
303
330
  }
304
- }, `• ${s.name}${s.description ? ` — ${s.description}` : ""}`))) : null), serve.enabled && !serve.customExecutor ? (0, react.createElement)("div", { style: {
305
- fontSize: 12,
306
- color: "var(--dsw-warning, #b26a00)",
307
- background: "rgba(178,106,0,.08)",
308
- borderRadius: 6,
309
- padding: "6px 10px"
310
- } }, "⚠ 默认 executor 会执行 shell 命令。生产环境请通过配置指定受限的 execute 实现。") : null) : null, tab === "in" ? (0, react.createElement)("div", { style: {
331
+ }, `• ${s.name}${s.description ? ` — ${s.description}` : ""}`))) : null), serve.enabled ? executorNote(serve) : null, serve.enabled ? authControl(serve, busy, act, authInput, setAuthInput) : null) : null, tab === "in" ? (0, react.createElement)("div", { style: {
311
332
  display: "flex",
312
333
  flexDirection: "column",
313
334
  gap: 10
@@ -557,6 +578,83 @@ window.__ModuleLoader__.load({
557
578
  }
558
579
  }, label);
559
580
  }
581
+ /**
582
+ * A semantically-correct note about how inbound tasks are executed. Dispatches
583
+ * on the `executor` field; falls back to the legacy `customExecutor` boolean
584
+ * for snapshots from an older host that didn't send `executor`.
585
+ */
586
+ function executorNote(serve) {
587
+ const kind = serve.executor ?? (serve.customExecutor ? "custom" : void 0);
588
+ if (kind === "custom") return null;
589
+ if (kind === "dsh-agent") return (0, react.createElement)("div", { style: {
590
+ fontSize: 12,
591
+ color: "var(--dsw-alias-label-secondary, #666)",
592
+ background: "rgba(74,125,255,.08)",
593
+ borderRadius: 6,
594
+ padding: "6px 10px"
595
+ } }, "ℹ 入站任务由本 DSH 的 agent 会话执行,按 A2A contextId 一个对话一个会话。");
596
+ if (kind === "none") return (0, react.createElement)("div", { style: {
597
+ fontSize: 12,
598
+ color: "var(--dsw-warning, #b26a00)",
599
+ background: "rgba(178,106,0,.08)",
600
+ borderRadius: 6,
601
+ padding: "6px 10px"
602
+ } }, "⚠ 未配置 executor,且未检测到可用的 agent 循环:入站任务会被拒绝。通过配置注入 execute,或加载 agent-loop 插件。");
603
+ return null;
604
+ }
605
+ /**
606
+ * Inbound bearer-token control for the serve tab: shows whether the endpoint is
607
+ * token-gated, and lets the operator set or clear the shared token. The token
608
+ * value is never read back from the host — only whether one is configured.
609
+ */
610
+ function authControl(serve, busy, act, authInput, setAuthInput) {
611
+ return (0, react.createElement)("div", { style: {
612
+ display: "flex",
613
+ flexDirection: "column",
614
+ gap: 6,
615
+ marginTop: 4
616
+ } }, (0, react.createElement)("div", { style: {
617
+ display: "flex",
618
+ alignItems: "center",
619
+ gap: 8
620
+ } }, (0, react.createElement)("span", { style: {
621
+ fontSize: 12.5,
622
+ fontWeight: 600,
623
+ color: "var(--dsw-alias-label-primary, #222)"
624
+ } }, "入站鉴权"), stateDot(serve.authConfigured ? "connected" : "disabled"), (0, react.createElement)("span", { style: {
625
+ fontSize: 12,
626
+ color: serve.authConfigured ? "#2e7d32" : "#9e9e9e"
627
+ } }, serve.authConfigured ? "已启用 Bearer 鉴权" : "未鉴权(任何客户端可调用)")), (0, react.createElement)("div", { style: {
628
+ display: "flex",
629
+ alignItems: "center",
630
+ gap: 8,
631
+ flexWrap: "wrap"
632
+ } }, (0, react.createElement)("input", {
633
+ type: "password",
634
+ placeholder: serve.authConfigured ? "输入新 token 以替换" : "设置 Bearer Token 以开启鉴权",
635
+ value: authInput,
636
+ onChange: (e) => setAuthInput(e.target.value),
637
+ style: {
638
+ padding: "4px 8px",
639
+ fontSize: 13,
640
+ borderRadius: 6,
641
+ border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3))",
642
+ background: "var(--dsw-alias-fill-l1, rgba(127,127,127,.06))",
643
+ color: "var(--dsw-alias-label-primary, #222)",
644
+ flex: 1,
645
+ minWidth: 220
646
+ }
647
+ }), actionBtn("保存", true, () => {
648
+ act("set-server-auth", "", { authToken: authInput.trim() });
649
+ setAuthInput("");
650
+ }, busy === "set-server-auth:" || !authInput.trim()), serve.authConfigured ? actionBtn("清除", false, () => {
651
+ act("set-server-auth", "", { authToken: "" });
652
+ setAuthInput("");
653
+ }, busy === "set-server-auth:") : null), (0, react.createElement)("div", { style: {
654
+ fontSize: 11.5,
655
+ color: "var(--dsw-alias-label-tertiary, #999)"
656
+ } }, "token 会明文存入 a2a.json,重启后仍生效。设置后,入站 JSON-RPC 请求需带 Authorization: Bearer <token>。"));
657
+ }
560
658
  function inboundRow(p, busy, act) {
561
659
  return (0, react.createElement)("tr", { key: p.id }, (0, react.createElement)("td", { style: tdStyle }, p.label), (0, react.createElement)("td", { style: {
562
660
  ...tdStyle,