已合并
feat(ws-channel): WSServiceMessageChannel 支持链路握手鉴权(向后兼容) #291
feat(ws-channel): WSServiceMessageChannel 支持链路握手鉴权(向后兼容) #291
已合并
Wal1et创建于 5月27日
21 个文件变更+1013-36
@@ -1,4 +1,13 @@
1-介绍 Runtime Management 的安全 SDK(`openjiuwen_runtime.management.security`):以通用接口提供**加解密**、**加签验签**、**证书/密钥管理**块能力,供节点管理、配置下发等场景按需引入。实现可插拔(默认 Ed25519 + X25519/AES-256-GCM),调用方只依赖接口。1+介绍 Runtime 的安全 SDK:以通用接口提供**加解密**、**加签验签**、**证书/密钥管理**、**控制链路握手鉴权**四块能力,供节点管理、配置下发、链路握手等场景按需引入。实现可插拔(默认 Ed25519 + X25519/AES-256-GCM),调用方只依赖接口。
2+ 
3+## 分层落点(foundation / management)
4+ 
5+| 层 | 模块 | 内容 | 依赖 |
6+| --- | --- | --- | --- |
7+| **foundation** | `openjiuwen_runtime.foundation.security` | 密码学原语、加解密、加签验签、**link-auth 握手鉴权**(含进程内 TOFU) | 仅 `cryptography` |
8+| **management** | `openjiuwen_runtime.management.security` | **落库**`CertificateManager` + 落库显式绑定 `CertificatePinStore`;并 re-export foundation 全部符号(历史导入路径兼容) | foundation + `DBHandler` |
9+ 
10+纯能力放 foundation,是为了让**进程级**组件(如独立 AgentServer)零控制面依赖即可使用;只有需要落库密钥管理时才依赖 management。历史代码 `from openjiuwen_runtime.management.security import ...` 继续可用。
2 11 
3# 概述12# 概述
4 13 
@@ -8,6 +17,7 @@
8| 加签 | Ed25519 | `ISigner` |17| 加签 | Ed25519 | `ISigner` |
9| 验签 | Ed25519 | `IVerifier` |18| 验签 | Ed25519 | `IVerifier` |
10| 证书/密钥管理 | 基于 foundation `DBHandler` 落库 | `ICertificateManager` |19| 证书/密钥管理 | 基于 foundation `DBHandler` 落库 | `ICertificateManager` |
20+| 控制链路握手鉴权 | Ed25519 一次性令牌 + nonce 防重放 + 指纹固定 | `IPinStore`(指纹存储) |
11 21 
12设计要点:22设计要点:
13 23 
@@ -189,13 +199,50 @@ gw_enc_priv = (await client_cm.get_or_create_keypair(KeyPurpose.ENCRYPT)).privat
189plaintext = crypto.open(gw_enc_priv, sealed)199plaintext = crypto.open(gw_enc_priv, sealed)
190```200```
191 201 
202+# 控制链路握手鉴权(link-auth)
203+ 
204+为两条 WebSocket 控制链路(Manager ↔ Gateway、Gateway ↔ AgentServer)提供握手期**双向身份鉴权**:连接发起方在握手头出示一枚 Ed25519 **一次性令牌**(载荷含签发者身份/类型/签发时间/nonce/签发者公钥,由其私钥签名),接收方用令牌内嵌公钥验签 → 校验有效期/类型 → nonce 防重放 → **指纹固定**比对,确认对端身份未被冒充。与加签验签复用同一套 Ed25519 原语,但作用在握手挑战上而非配置帧。
205+ 
206+**三档开关 `CLAW_LINK_AUTH_MODE`**:`off`(默认,零行为变更)/ `observe`(验签记日志但放行,灰度)/ `enforce`(验不过即拒)。令牌有效期 `CLAW_LINK_TOKEN_TTL`(秒,默认 300)。
207+ 
208+**指纹固定的两种后端(由集成方按链路选择:构造哪种 store + 调对应的验证入口)**
209+ 
210+| 后端 | 类 / 入口 | 适用 | 语义 |
211+| --- | --- | --- | --- |
212+| 进程内 TOFU | `InMemoryPinStore` + `verify_and_pin`(同步,foundation) | 进程级、无持久 DB 的端(如独立 AgentServer) | 首次见到即记录指纹,进程重启后重新 TOFU |
213+| 落库显式绑定 | `CertificatePinStore` + `verify_and_bind`(异步,management) | 有 DB 的持久端(Manager / Gateway) | 对端公钥按 `(peer_id, sign)` 落库 `bound`,可轮换/解绑,与配置下发同一套密钥管理 |
214+ 
215+```python
216+# 发起方(持本端密钥对):握手头带令牌
217+from openjiuwen_runtime.foundation.security import build_token_header, generate_keypair
218+priv, pub = generate_keypair()
219+headers = build_token_header(service_id="gateway-1", service_type="gateway",
220+ private_b64=priv, public_b64=pub) # off 时返回 {}
221+ 
222+# 接收方 A:进程内 TOFU(同步)
223+from openjiuwen_runtime.foundation.security import InMemoryPinStore, NonceCache, verify_and_pin
224+res = verify_and_pin(InMemoryPinStore(), token, expect_type="gateway", nonce_cache=NonceCache())
225+if not res.allowed:
226+ ... # enforce 下拒绝连接
227+ 
228+# 接收方 B:落库显式绑定(异步,需 CertificateManager)
229+from openjiuwen_runtime.management.security import CertificatePinStore, verify_and_bind
230+store = CertificatePinStore(cert_manager) # purpose 默认 sign
231+res = await verify_and_bind(store, token, expect_type="gateway", nonce_cache=NonceCache())
232+```
233+ 
234+> 信任引导(首次见到的公钥是否可信)与配置下发一致,暂由 wss/TLS、指纹预共享或 TOFU 兜底;显式绑定解决的是绑定关系的持久化与生命周期(轮换/解绑),不替代首信任。
235+ 
192# 相关代码236# 相关代码
193 237 
194| 模块 | 路径 |238| 模块 | 路径 |
195| --- | --- |239| --- | --- |
196-| 通用接口 | `openjiuwen_runtime/management/security/interfaces.py` |240+| 通用接口 | `openjiuwen_runtime/foundation/security/interfaces.py` |
197-| 加解实现 | `.../security/crypto.py` |241+| 密码学原语 | `.../foundation/security/_primitives.py` |
198-| 加签验签实现 | `.../security/signing.py` |242+| 加解密实现 | `.../foundation/security/crypto.py` |
199-| 证书/密钥管理 | `.../security/certificate.py` |243+| 加签验签实现 | `.../foundation/security/signing.py` |
200-| 数据模型与表定义 | `.../security/models.py` |244+| 数据模型与表定义 | `.../foundation/security/models.py` |
201-| 单元测试 | `management/tests/unit_tests/management_security/` |245+| 链路握手鉴权(含进程内 TOFU) | `.../foundation/security/link_auth.py` |
246+| 证书/密钥管理(落库) | `openjiuwen_runtime/management/security/certificate.py` |
247+| 落库显式绑定指纹固定 | `.../management/security/certificate_pin_store.py` |
248+| 单元测试 | `foundation/tests/unit_tests/test_link_auth.py``management/tests/unit_tests/management_security/` |
@@ -0,0 +1,100 @@
1+# coding: utf-8
2+# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved
3+ 
4+"""OpenJiuwen Runtime 基础安全能力(foundation 层,谁都可轻量依赖)。
5+ 
6+放在 foundation 而非 management,是为了让**进程级**组件(如独立 AgentServer)也能
7+零控制面依赖地使用:
8+ 
9+- 密码学原语:Ed25519(签验)、X25519+HKDF 信封、AES-256-GCM(``_primitives``)。
10+- 加解密 / 加签验签:``EnvelopeCryptoProvider`` / ``Ed25519Signer`` / ``Ed25519Verifier``。
11+- 控制链路握手鉴权:``link_auth``(一次性令牌 + nonce 防重放 + 指纹固定,
12+ off/observe/enforce 三档开关)。
13+ 
14+DB 落库的密钥/证书管理(``CertificateManager``)在 management 层
15+(``openjiuwen_runtime.management.security``),需要 ``DBHandler``。
16+"""
17+ 
18+from .interfaces import (
19+ ICertificateManager,
20+ ICryptoProvider,
21+ ISigner,
22+ IVerifier,
23+)
24+from .models import (
25+ DEK_ALGORITHM,
26+ EncAlgorithm,
27+ KeyPair,
28+ KeyPurpose,
29+ PeerKey,
30+ SealedMessage,
31+ SECURITY_LOCAL_KEY_TABLE_DEF,
32+ SECURITY_PEER_KEY_TABLE_DEF,
33+ SignAlgorithm,
34+)
35+from .crypto import EnvelopeCryptoProvider
36+from .signing import Ed25519Signer, Ed25519Verifier, make_signer, make_verifier
37+from .link_auth import (
38+ AuthMode,
39+ Claims,
40+ InMemoryPinStore,
41+ IPinStore,
42+ LINK_TOKEN_HEADER,
43+ LinkAuthError,
44+ NonceCache,
45+ VerifyResult,
46+ build_token,
47+ build_token_header,
48+ generate_keypair,
49+ get_auth_mode,
50+ get_ttl,
51+ fingerprint,
52+ sign_token,
53+ verify_and_pin,
54+ verify_signature,
55+ verify_token,
56+)
57+ 
58+__all__ = (
59+ # interfaces
60+ "ICryptoProvider",
61+ "ISigner",
62+ "IVerifier",
63+ "ICertificateManager",
64+ # models
65+ "KeyPurpose",
66+ "SignAlgorithm",
67+ "EncAlgorithm",
68+ "DEK_ALGORITHM",
69+ "KeyPair",
70+ "PeerKey",
71+ "SealedMessage",
72+ "SECURITY_LOCAL_KEY_TABLE_DEF",
73+ "SECURITY_PEER_KEY_TABLE_DEF",
74+ # crypto
75+ "EnvelopeCryptoProvider",
76+ # signing
77+ "Ed25519Signer",
78+ "Ed25519Verifier",
79+ "make_signer",
80+ "make_verifier",
81+ # link-auth handshake
82+ "LINK_TOKEN_HEADER",
83+ "AuthMode",
84+ "LinkAuthError",
85+ "Claims",
86+ "VerifyResult",
87+ "NonceCache",
88+ "IPinStore",
89+ "InMemoryPinStore",
90+ "get_auth_mode",
91+ "get_ttl",
92+ "generate_keypair",
93+ "fingerprint",
94+ "sign_token",
95+ "verify_signature",
96+ "verify_token",
97+ "verify_and_pin",
98+ "build_token",
99+ "build_token_header",
100+)
Rmanagement/openjiuwen_runtime/management/security/_primitives.pyfoundation/openjiuwen_runtime/foundation/security/_primitives.py+0-0
文件重命名但无更改。
Rmanagement/openjiuwen_runtime/management/security/crypto.pyfoundation/openjiuwen_runtime/foundation/security/crypto.py+0-0
文件重命名但无更改。
Rmanagement/openjiuwen_runtime/management/security/interfaces.pyfoundation/openjiuwen_runtime/foundation/security/interfaces.py+0-0
文件重命名但无更改。
Rmanagement/openjiuwen_runtime/management/security/models.pyfoundation/openjiuwen_runtime/foundation/security/models.py+0-0
文件重命名但无更改。
Rmanagement/openjiuwen_runtime/management/security/signing.pyfoundation/openjiuwen_runtime/foundation/security/signing.py+0-0
文件重命名但无更改。
@@ -171,6 +171,13 @@ class DockerDeployer(Deployer[DockerParams]):
171 proxy_env_filtered = {k: v for k, v in proxy_env.items() if v}171 proxy_env_filtered = {k: v for k, v in proxy_env.items() if v}
172 # Merge proxy env with custom env, custom env has higher priority172 # Merge proxy env with custom env, custom env has higher priority
173 env_vars = {**proxy_env_filtered, **env_vars}173 env_vars = {**proxy_env_filtered, **env_vars}
174+ # link-auth:透传控制面的链路握手鉴权开关给 AgentServer 容器(容器不继承宿主
175+ # 环境);显式 env 优先,故用 setdefault。enforce 下若缺,pod 不签 ack 令牌、
176+ # Gateway 反向校验失败。
177+ for _link_env in ("CLAW_LINK_AUTH_MODE", "CLAW_LINK_TOKEN_TTL"):
178+ _link_val = os.getenv(_link_env)
179+ if _link_val:
180+ env_vars.setdefault(_link_env, _link_val)
174 181 
175 # 非低码情况182 # 非低码情况
176 if not ir_path:183 if not ir_path:
@@ -539,6 +539,13 @@ class K8sDeployer(Deployer[K8sParams]):
539 }539 }
540 if settings.userdata:540 if settings.userdata:
541 env_vars["RUNTIME_USERDATA"] = settings.userdata541 env_vars["RUNTIME_USERDATA"] = settings.userdata
542+ # link-auth:把控制面的链路握手鉴权开关透传给 AgentServer pod。k8s pod 不继承
543+ # 宿主/控制面环境,须显式注入;否则 enforce 模式下 pod 不会签 connection.ack
544+ # 反向令牌,Gateway 侧反向校验将失败、握手建立不起来。
545+ for _link_env in ("CLAW_LINK_AUTH_MODE", "CLAW_LINK_TOKEN_TTL"):
546+ _link_val = os.getenv(_link_env)
547+ if _link_val:
548+ env_vars[_link_env] = _link_val
542 549 
543 core_api, apps_api = await self._get_apis()550 core_api, apps_api = await self._get_apis()
544 secret_body = self._build_secret_body(551 secret_body = self._build_secret_body(
@@ -1,14 +1,13 @@
1# coding: utf-81# coding: utf-8
2# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved2# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved
3 3 
4-"""OpenJiuwen Runtime Security SDK.4+"""OpenJiuwen Runtime Security SDK(management 层)。
5 5 
6-通用安全能力实现可插拔,调用方仅依赖接口6+通用安全原语加解密 / 加签验签 / 链路握手鉴权已下沉到 foundation
7- 7+(``openjiuwen_runtime.foundation.security``),供进程级组件零控制面依赖地复用;
8-- 加解:AES-256-GCM对称)+ X25519 信封(面向公钥的混合加密8+本模块保留 **落库** 的钥/证书管理``CertificateManager`` ``DBHandler``与其
9-- 加签验签:Ed25519。9+指纹固定适配器(``CertificatePinStore`` + 异步 ``verify_and_bind``),并 **re-export**
10-- 证书/密钥管理:密钥与证书录入、存、读取、删除(client/server 通用),10+foundation安全符号以 ``openjiuwen_runtime.management.security`` 历史导入路径兼容。
11- 并提供传输无关的握手密钥交换(``KeyExchange`` / server_/client_key_exchange)。
12 11 
13示例:12示例:
14 from openjiuwen_runtime.management.security import (13 from openjiuwen_runtime.management.security import (
@@ -17,31 +16,54 @@
17 )16 )
18"""17"""
19 18 
20-from .interfaces import (19+# --- re-export foundation 安全能力(向后兼容历史导入路径)---
20+from openjiuwen_runtime.foundation.security import (
21+ DEK_ALGORITHM,
22+ AuthMode,
23+ Claims,
24+ Ed25519Signer,
25+ Ed25519Verifier,
26+ EncAlgorithm,
27+ EnvelopeCryptoProvider,
21 ICertificateManager,28 ICertificateManager,
22 ICryptoProvider,29 ICryptoProvider,
30+ InMemoryPinStore,
31+ IPinStore,
23 ISigner,32 ISigner,
24 IVerifier,33 IVerifier,
25-)
26-from .models import (
27- DEK_ALGORITHM,
28- EncAlgorithm,
29 KeyPair,34 KeyPair,
30 KeyPurpose,35 KeyPurpose,
36+ LINK_TOKEN_HEADER,
37+ LinkAuthError,
38+ NonceCache,
31 PeerKey,39 PeerKey,
32 SealedMessage,40 SealedMessage,
33 SECURITY_LOCAL_KEY_TABLE_DEF,41 SECURITY_LOCAL_KEY_TABLE_DEF,
34 SECURITY_PEER_KEY_TABLE_DEF,42 SECURITY_PEER_KEY_TABLE_DEF,
35 SignAlgorithm,43 SignAlgorithm,
44+ VerifyResult,
45+ build_token,
46+ build_token_header,
47+ fingerprint,
48+ generate_keypair,
49+ get_auth_mode,
50+ get_ttl,
51+ make_signer,
52+ make_verifier,
53+ sign_token,
54+ verify_and_pin,
55+ verify_signature,
56+ verify_token,
36)57)
37-from .crypto import EnvelopeCryptoProvider58+ 
38-from .signing import Ed25519Signer, Ed25519Verifier, make_signer, make_verifier59+# --- 本层独有:落库密钥/证书管理 + 落库显式绑定的指纹固定 ---
39from .certificate import (60from .certificate import (
40 CertificateManager,61 CertificateManager,
41 KeyExchange,62 KeyExchange,
42 client_key_exchange,63 client_key_exchange,
43 server_key_exchange,64 server_key_exchange,
44)65)
66+from .certificate_pin_store import CertificatePinStore, verify_and_bind
45 67 
46__all__ = (68__all__ = (
47 # interfaces69 # interfaces
@@ -66,9 +88,30 @@ __all__ = (
66 "Ed25519Verifier",88 "Ed25519Verifier",
67 "make_signer",89 "make_signer",
68 "make_verifier",90 "make_verifier",
69- # certificate / key management91+ # link-auth handshake (foundation)
92+ "LINK_TOKEN_HEADER",
93+ "AuthMode",
94+ "LinkAuthError",
95+ "Claims",
96+ "VerifyResult",
97+ "NonceCache",
98+ "IPinStore",
99+ "InMemoryPinStore",
100+ "get_auth_mode",
101+ "get_ttl",
102+ "generate_keypair",
103+ "fingerprint",
104+ "sign_token",
105+ "verify_signature",
106+ "verify_token",
107+ "verify_and_pin",
108+ "build_token",
109+ "build_token_header",
110+ # certificate / key management (management)
70 "CertificateManager",111 "CertificateManager",
71 "KeyExchange",112 "KeyExchange",
72 "server_key_exchange",113 "server_key_exchange",
73 "client_key_exchange",114 "client_key_exchange",
115+ "CertificatePinStore",
116+ "verify_and_bind",
74)117)
@@ -17,10 +17,9 @@ from datetime import datetime, timezone
17from typing import Any, Optional17from typing import Any, Optional
18 18 
19from openjiuwen_runtime.foundation.db.handler import DBHandler19from openjiuwen_runtime.foundation.db.handler import DBHandler
20- 20+from openjiuwen_runtime.foundation.security import _primitives as _p
21-from . import _primitives as _p21+from openjiuwen_runtime.foundation.security.interfaces import ICertificateManager
22-from .interfaces import ICertificateManager22+from openjiuwen_runtime.foundation.security.models import (
曹玉哲6月22日

这里依赖了foundation的最新代码,需要改一下pyproject.toml里的依赖,可以先改成源码依赖

likedislike
23-from .models import (
24 EncAlgorithm,23 EncAlgorithm,
25 KeyPair,24 KeyPair,
26 KeyPurpose,25 KeyPurpose,
@@ -0,0 +1,88 @@
1+# coding: utf-8
2+# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved
3+ 
4+"""落库显式绑定的指纹固定(link-auth 的 DB 后端)。
5+ 
6+把 foundation 层 link-auth 的「指纹固定」接到 management 层 :class:`CertificateManager`:
7+对端公钥按 ``(peer_id=iss, purpose=sign)`` 落库(``status=bound``),握手时比对指纹。
8+相对进程内 TOFU(:class:`InMemoryPinStore`),它是**显式绑定**:持久、可按 key_version
9+轮换、解绑即删行——与配置下发的密钥管理同一套模型。
10+ 
11+因 :class:`CertificateManager` 为异步 DB 操作,本模块提供**异步** :func:`verify_and_bind`,
12+与 foundation 的同步 :func:`verify_and_pin`(内存 TOFU)并列;集成方按链路是否有持久 DB
13+二选一(构造 ``CertificatePinStore`` 走落库绑定,或 ``InMemoryPinStore`` 走进程内 TOFU)。
14+"""
15+ 
16+from __future__ import annotations
17+ 
18+import logging
19+from typing import Optional
20+ 
21+from openjiuwen_runtime.foundation.security import _primitives as _p
22+from openjiuwen_runtime.foundation.security.link_auth import (
23+ AuthMode,
24+ NonceCache,
25+ VerifyResult,
26+ verify_token,
27+)
28+from openjiuwen_runtime.foundation.security.models import KeyPurpose
29+ 
30+from .certificate import CertificateManager
31+ 
32+logger = logging.getLogger(__name__)
33+ 
34+ 
35+class CertificatePinStore:
36+ """:class:`CertificateManager` 的指纹固定适配器(异步、落库、显式绑定)。
37+ 
38+ - ``pinned(iss)``:读对端 ``sign`` 公钥的已绑定指纹(无则 ``None``)。
39+ - ``remember(iss, pub_b64, fp)``:录入/更新对端 ``sign`` 公钥(``status=bound``)。
40+ """
41+ 
42+ def __init__(
43+ self, manager: CertificateManager, *, purpose: str = KeyPurpose.SIGN.value
44+ ) -> None:
45+ self._cm = manager
46+ self._purpose = purpose
47+ 
48+ async def pinned(self, iss: str) -> Optional[str]:
49+ peer = await self._cm.load_peer_key(iss, self._purpose)
50+ return peer.fingerprint if peer is not None else None
51+ 
52+ async def remember(self, iss: str, pub_b64: str, fp: str) -> None:
53+ await self._cm.save_peer_key(iss, self._purpose, _p.b64d(pub_b64))
54+ 
55+ 
56+async def verify_and_bind(
57+ store: CertificatePinStore,
58+ token: str | None,
59+ *,
60+ expect_type: str | None = None,
61+ nonce_cache: NonceCache | None = None,
62+) -> VerifyResult:
63+ """验令牌 + 落库显式绑定(持久端首选入口,异步)。
64+ 
65+ 首次见到该身份:验签通过即落库绑定其公钥并放行;之后:比对已绑定指纹,不一致即拒。
66+ 内存 TOFU 版见 foundation 的 :func:`verify_and_pin`。
67+ """
68+ res = verify_token(token, expect_type=expect_type, nonce_cache=nonce_cache)
69+ if not res.ok or res.claims is None or res.peer_fp is None:
70+ return res
71+ iss = res.claims.iss
72+ pinned = await store.pinned(iss)
73+ if pinned is not None and pinned != res.peer_fp:
74+ mode = res.mode
75+ logger.warning(
76+ "[link_auth] %s: fingerprint changed for iss=%s (possible impersonation), rejecting",
77+ mode.value, iss,
78+ )
79+ return VerifyResult(
80+ allowed=(mode is AuthMode.OBSERVE),
81+ ok=False,
82+ reason="fingerprint mismatch",
83+ mode=mode,
84+ peer_fp=res.peer_fp,
85+ claims=res.claims,
86+ )
87+ await store.remember(iss, res.claims.pub, res.peer_fp)
88+ return res
@@ -6,6 +6,7 @@
6from __future__ import annotations6from __future__ import annotations
7 7 
8import asyncio8import asyncio
9+import contextlib
9import os10import os
10import subprocess11import subprocess
11import sys12import sys
@@ -159,10 +160,11 @@ class ProcessServiceHandler:
159 return info160 return info
160 161 
161 async def _wait_until_ready(self) -> None:162 async def _wait_until_ready(self) -> None:
162- import websockets163+ # 探活仅需确认 AgentServer 的监听端口已就绪,故用 TCP 连接探测而非
163- 164+ # WebSocket 握手——这与 K8s 的 TCP readiness 探针语义一致,且不会触发
165+ # 链路握手鉴权:enforce 模式下,不带 X-Claw-Link-Token 的 WS 握手会被
166+ # AgentServer 拒绝(401),令探活永远失败。TCP 探测则不涉及握手鉴权。
164 deadline = asyncio.get_running_loop().time() + self._ready_timeout167 deadline = asyncio.get_running_loop().time() + self._ready_timeout
165- url = f"ws://{self._host}:{self._port}"
166 last_error: Exception | None = None168 last_error: Exception | None = None
167 169 
168 while asyncio.get_running_loop().time() < deadline:170 while asyncio.get_running_loop().time() < deadline:
@@ -172,14 +174,21 @@ class ProcessServiceHandler:
172 f"AgentServer process exited early with code {proc.returncode}"174 f"AgentServer process exited early with code {proc.returncode}"
173 )175 )
174 try:176 try:
175- async with websockets.connect(url, open_timeout=2.0):177+ _, writer = await asyncio.wait_for(
176- return178+ asyncio.open_connection(self._host, self._port),
179+ timeout=2.0,
180+ )
181+ writer.close()
182+ with contextlib.suppress(Exception):
183+ await writer.wait_closed()
184+ return
177 except Exception as exc: # noqa: BLE001185 except Exception as exc: # noqa: BLE001
178 last_error = exc186 last_error = exc
179 await asyncio.sleep(self._ready_poll_interval)187 await asyncio.sleep(self._ready_poll_interval)
180 188 
181 raise TimeoutError(189 raise TimeoutError(
182- f"AgentServer not ready within {self._ready_timeout}s url={url} last_error={last_error}"190+ f"AgentServer not ready within {self._ready_timeout}s "
191+ f"host={self._host} port={self._port} last_error={last_error}"
183 )192 )
184 193 
185 async def delete(self) -> str:194 async def delete(self) -> str:
@@ -86,6 +86,8 @@ class WSServiceMessageChannel:
86 ws_use_tls: bool = False,86 ws_use_tls: bool = False,
87 payload_from_raw: Optional[PayloadBuilder] = None,87 payload_from_raw: Optional[PayloadBuilder] = None,
88 connect_timeout: float = 30.0,88 connect_timeout: float = 30.0,
89+ additional_headers: Optional[Any] = None,
90+ verify_peer: Optional[Callable[[dict], bool]] = None,
89 ) -> None:91 ) -> None:
90 self._fallback_port = int(target_port) if target_port is not None else None92 self._fallback_port = int(target_port) if target_port is not None else None
91 self._port = self._fallback_port or 093 self._port = self._fallback_port or 0
@@ -96,6 +98,15 @@ class WSServiceMessageChannel:
96 payload_from_raw or serialize_request_payload98 payload_from_raw or serialize_request_payload
97 )99 )
98 self._connect_timeout = connect_timeout100 self._connect_timeout = connect_timeout
101+ # 可选:握手期附加的 HTTP 头(如链路鉴权令牌)。
102+ # 可传 dict(静态),或无参回调 ``() -> Optional[dict]``(每次连接现取,
103+ # 用于令牌需逐次刷新的场景,避免重连复用同一令牌被对端的 nonce/有效期校验拦截)。
104+ # 默认 None=不附加,行为与既有调用方完全一致。
105+ self._additional_headers: Optional[Any] = additional_headers
106+ # 可选:对端核验回调 ``(connection_ack_frame: dict) -> bool``。设置后,连接建立即
107+ # 消费首帧 connection.ack 交其核验(如反向链路鉴权:确认连到的是合法对端),返回
108+ # False 则断开。默认 None=不核验,行为与既有调用方完全一致。
109+ self._verify_peer: Optional[Callable[[dict], bool]] = verify_peer
99 110 
100 # 强引用:接收循环须稳定持有 ServiceHandler 以 dispatch;弱引用在部分 GC/嵌入场景下111 # 强引用:接收循环须稳定持有 ServiceHandler 以 dispatch;弱引用在部分 GC/嵌入场景下
101 # 可能在 recv 首帧前失效,导致接收协程空跑退出、全链路无下行(表现为「能连上但不转发」)。112 # 可能在 recv 首帧前失效,导致接收协程空跑退出、全链路无下行(表现为「能连上但不转发」)。
@@ -209,15 +220,28 @@ class WSServiceMessageChannel:
209 if not self._ws_url:220 if not self._ws_url:
210 raise RuntimeError("WebSocket URL 未设置")221 raise RuntimeError("WebSocket URL 未设置")
211 logger.info("WSS 正在连接: %s", self._ws_url)222 logger.info("WSS 正在连接: %s", self._ws_url)
223+ # dict 直接用;回调则每次连接现取一份(如刷新链路令牌:新 nonce/新签发时间)。
224+ hdrs = self._additional_headers
225+ if callable(hdrs):
226+ hdrs = hdrs()
212 new_ws = await asyncio.wait_for(227 new_ws = await asyncio.wait_for(
213 websockets.connect(228 websockets.connect(
214 self._ws_url,229 self._ws_url,
215 open_timeout=self._connect_timeout,230 open_timeout=self._connect_timeout,
216 ping_interval=20.0,231 ping_interval=20.0,
217 ping_timeout=20.0,232 ping_timeout=20.0,
233+ additional_headers=hdrs,
218 ),234 ),
219 timeout=self._connect_timeout,235 timeout=self._connect_timeout,
220 )236 )
237+ # link-auth 反向:消费首帧 connection.ack 并核验对端令牌;不通过则断开。
238+ if self._verify_peer is not None:
239+ first_raw = await asyncio.wait_for(new_ws.recv(), timeout=self._connect_timeout)
240+ first = _decode_ws_message(first_raw)
241+ if not self._verify_peer(first if isinstance(first, dict) else {}):
242+ with contextlib.suppress(Exception):
243+ await new_ws.close()
244+ raise RuntimeError("WSS 对端 link-auth 校验失败")
221 self._ws = new_ws245 self._ws = new_ws
222 p = self._default_parser246 p = self._default_parser
223 if p is not None and (self._recv_task is None or self._recv_task.done()):247 if p is not None and (self._recv_task is None or self._recv_task.done()):
@@ -24,7 +24,7 @@ dependencies = [
24 "redis==7.1.0",24 "redis==7.1.0",
25 # lint25 # lint
26 "ruff==0.9.10",26 "ruff==0.9.10",
27- "openjiuwen-runtime-foundation==0.1.0",27+ "openjiuwen_runtime_foundation @ git+https://gitcode.com/openJiuwen/agent-runtime.git@develop#subdirectory=foundation",
28 "fastapi>=0.110.0",28 "fastapi>=0.110.0",
29 "httpx>=0.27.0",29 "httpx>=0.27.0",
30 "uvicorn[standard]>=0.29.0",30 "uvicorn[standard]>=0.29.0",
@@ -14,7 +14,7 @@ from openjiuwen_runtime.management.security import (
14 client_key_exchange,14 client_key_exchange,
15 server_key_exchange,15 server_key_exchange,
16)16)
17-from openjiuwen_runtime.management.security._primitives import (17+from openjiuwen_runtime.foundation.security._primitives import (
18 ed25519_generate,18 ed25519_generate,
19 fingerprint,19 fingerprint,
20 x25519_generate,20 x25519_generate,
@@ -9,7 +9,7 @@ import pytest
9from cryptography.exceptions import InvalidTag9from cryptography.exceptions import InvalidTag
10 10 
11from openjiuwen_runtime.management.security import EnvelopeCryptoProvider, SealedMessage11from openjiuwen_runtime.management.security import EnvelopeCryptoProvider, SealedMessage
12-from openjiuwen_runtime.management.security._primitives import x25519_generate12+from openjiuwen_runtime.foundation.security._primitives import x25519_generate
13 13 
14 14 
15def test_envelope_seal_open_roundtrip():15def test_envelope_seal_open_roundtrip():
@@ -13,7 +13,7 @@ from openjiuwen_runtime.management.security import (
13 make_signer,13 make_signer,
14 make_verifier,14 make_verifier,
15)15)
16-from openjiuwen_runtime.management.security._primitives import ed25519_generate16+from openjiuwen_runtime.foundation.security._primitives import ed25519_generate
17 17 
18 18 
19def test_sign_verify_roundtrip():19def test_sign_verify_roundtrip():