Skip to content

Commit 84dfaf2

Browse files
authored
Merge pull request #120 from Serverless-Devs/worktree-sts-refresh
feat(credential): 支持从函数计算请求头静默刷新 STS 临时凭证
2 parents 9344549 + d9354ba commit 84dfaf2

23 files changed

Lines changed: 1291 additions & 297 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,25 @@ jobs:
5454
id: changes
5555
run: |
5656
echo "Checking if agentrun directory has changes..."
57-
# 获取最近两次提交之间的差异;如果没有父提交,则将所有跟踪文件视为已更改
58-
if git rev-parse HEAD^ >/dev/null 2>&1; then
57+
# 与默认分支(main)的分叉点比较,取整个分支引入的全部改动,
58+
# 而非仅最近一次提交(避免多 commit 分支漏检早先提交的改动)。
59+
# CI 以 fetch-depth: 0 检出,origin/main 可用;优先用它,退回本地 main。
60+
BASE_REF=""
61+
for ref in origin/main main; do
62+
if git rev-parse --verify "$ref" >/dev/null 2>&1; then
63+
BASE_REF="$ref"; break
64+
fi
65+
done
66+
MERGE_BASE=""
67+
if [ -n "$BASE_REF" ]; then
68+
MERGE_BASE=$(git merge-base "$BASE_REF" HEAD 2>/dev/null || echo "")
69+
fi
70+
if [ -n "$MERGE_BASE" ] && [ "$MERGE_BASE" != "$(git rev-parse HEAD)" ]; then
71+
echo "Diffing against base ($BASE_REF, merge-base ${MERGE_BASE})"
72+
git diff --name-only "$MERGE_BASE" HEAD > changed_files.txt
73+
elif git rev-parse HEAD^ >/dev/null 2>&1; then
74+
# 回退(如直接 push 到 main,与 base 无分叉):比较最近一次提交
75+
echo "No divergence from base; falling back to HEAD^..HEAD"
5976
git diff --name-only HEAD^ HEAD > changed_files.txt
6077
else
6178
echo "No parent commit; treating all tracked files as changed."

AGENTS.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,61 @@ A: 建议:
471471
3. 执行相关模块的 ut 测试,确保可以正确执行
472472
4. 进行修改内容的总结汇报
473473
5. 根据汇报内容进行检查,重新检查底层 SDK 和 AgentRun SDK 的定义
474+
475+
## 凭证注入与 STS 静默刷新约定(强制)
476+
477+
STS 临时凭证(ak/sk/security_token)会过期。部署在函数计算(FC)时,最新轮转
478+
后的 STS 通过**每次请求的 HTTP 头**下发,而非进程级环境变量。为让所有 client 在
479+
凭证过期后静默刷新,本仓库采用统一机制:
480+
481+
1. **请求级 overlay**`agentrun/server/sts_middleware.py` 解析 FC 头
482+
(默认 `x-fc-access-key-id` / `x-fc-access-key-secret` / `x-fc-security-token`
483+
可经构造参数或 `AGENTRUN_STS_HEADER_*` 环境变量覆盖),写入
484+
`agentrun/utils/credential_context.py``contextvars` overlay。中间件本身
485+
只是 `use_sts_from_headers` 的薄封装(加 FC 门控),二者共用同一套解析逻辑。
486+
487+
**非 agentrun server 场景**(自有 FastAPI / Flask / Django、或非 HTTP 任务):
488+
中间件不会运行,需用户手动注入。SDK 顶层导出两个上下文管理器:
489+
- `agentrun.use_sts_credentials(ak, sk, sts)` —— 显式传值;
490+
- `agentrun.use_sts_from_headers(headers)` —— 从任意请求头映射解析(同 `x-fc-*`)。
491+
492+
```python
493+
from agentrun import use_sts_from_headers
494+
with use_sts_from_headers(request.headers):
495+
... # 块内所有 SDK 调用使用最新 STS,退出自动复位
496+
```
497+
498+
2. **Config 懒解析**`Config` 的三个凭证 getter 按
499+
**显式传入 > 请求级 overlay(仅当三者均未显式传入)> 环境变量** 解析。
500+
切勿在 `Config.__init__` 里把凭证快照成固定字符串。
501+
502+
3. **client 一律用 credential provider,禁止传静态 ak/sk/sts**
503+
504+
- **alibabacloud OpenAPI**(控制面 / Bailian / GPDB / Devs 等):
505+
构造 `open_api_util_models.Config` 时传
506+
`credential=build_openapi_credential(cfg)`
507+
(见 `agentrun/utils/credential_providers.py`),**不要**再传
508+
`access_key_id` / `access_key_secret` / `security_token`
509+
注意 tea_openapi 的优先级是「静态 ak/sk 优先于 credential」,传了静态值
510+
会让 provider 失效。
511+
512+
- **TableStore `OTSClient` / `AsyncOTSClient`**
513+
构造时传 `credentials_provider=build_ots_credentials_provider(cfg)`
514+
(见 `agentrun/conversation_service/utils.py`),**不要**再传
515+
`access_key_id` / `access_key_secret` / `sts_token`
516+
517+
原因:直接传静态凭证会在 client 构造时把凭证冻结,长生命周期 client(如
518+
server 启动时仅创建一次的 OTSClient)在 STS 过期后所有请求都会失败。
519+
provider 会在**每次请求**被底层 SDK 调用,从而拿到最新 STS。
520+
521+
4. **数据面手写签名**`agentrun/utils/data_api.py` 的 RAM 签名)无需 provider:
522+
它本就每次请求调用 `cfg.get_*()`,已随 Config 懒解析自动刷新。
523+
524+
5. **自定义 httpx 签名器**(如 `_AgentrunRamAuth`,用于 MCP SSE / OpenAPI 工具)
525+
必须**持有 `Config`**、在 `auth_flow` 内调用 `cfg.get_*()` 取证,
526+
**不要**`__init__` 把 ak/sk/sts 快照成字段。否则长连接(SSE 一次建连、
527+
多请求复用)会冻结建连时的 STS。
528+
529+
新增任何与阿里云 / TableStore 交互的 client 时,必须遵循第 3 条;新增单测应覆盖
530+
「overlay 生效」与「显式凭证不被 overlay 覆盖」两种情况
531+
(参考 `tests/unittests/test_sts_refresh.py`)。

agentrun/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@
135135
# ToolSet
136136
from agentrun.toolset import ToolSet, ToolSetClient
137137
from agentrun.utils.config import Config
138+
from agentrun.utils.credential_context import (
139+
StsCredential,
140+
use_sts_credentials,
141+
use_sts_from_headers,
142+
)
138143
from agentrun.utils.exception import (
139144
ResourceAlreadyExistError,
140145
ResourceNotExistError,
@@ -335,6 +340,10 @@
335340
"ResourceNotExistError",
336341
"ResourceAlreadyExistError",
337342
"Config",
343+
######## STS 凭证刷新(非 server 场景手动注入) ########
344+
"StsCredential",
345+
"use_sts_credentials",
346+
"use_sts_from_headers",
338347
]
339348

340349
# Memory Collection 模块的所有导出(延迟加载)

agentrun/conversation_service/__session_store_async_template.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ async def from_memory_collection_async(
880880
"vector_store_config.instance_name 为空。"
881881
)
882882

883-
# 3. 获取凭证
883+
# 3. 校验凭证存在(fail-fast;运行时由 CredentialsProvider 动态取证)
884884
effective_config = config if isinstance(config, Config) else Config()
885885
access_key_id = effective_config.get_access_key_id()
886886
access_key_secret = effective_config.get_access_key_secret()
@@ -891,17 +891,13 @@ async def from_memory_collection_async(
891891
"AGENTRUN_ACCESS_KEY_ID / AGENTRUN_ACCESS_KEY_SECRET。"
892892
)
893893

894-
security_token = effective_config.get_security_token()
895-
sts_token = security_token if security_token else None
896-
897894
# 4. 构建 OTSClient + AsyncOTSClient 和 OTSBackend
898895
# 使用 utils.build_ots_clients 避免 codegen 替换 AsyncOTSClient
896+
# 传入 config:OTS 经 CredentialsProvider 每次请求动态取最新 STS。
899897
ots_client, async_ots_client = build_ots_clients(
900898
endpoint,
901-
access_key_id,
902-
access_key_secret,
903899
instance_name,
904-
sts_token=sts_token,
900+
config=effective_config,
905901
)
906902

907903
backend = OTSBackend(

agentrun/conversation_service/session_store.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,7 +1645,7 @@ async def from_memory_collection_async(
16451645
"vector_store_config.instance_name 为空。"
16461646
)
16471647

1648-
# 3. 获取凭证
1648+
# 3. 校验凭证存在(fail-fast;运行时由 CredentialsProvider 动态取证)
16491649
effective_config = config if isinstance(config, Config) else Config()
16501650
access_key_id = effective_config.get_access_key_id()
16511651
access_key_secret = effective_config.get_access_key_secret()
@@ -1656,17 +1656,13 @@ async def from_memory_collection_async(
16561656
"AGENTRUN_ACCESS_KEY_ID / AGENTRUN_ACCESS_KEY_SECRET。"
16571657
)
16581658

1659-
security_token = effective_config.get_security_token()
1660-
sts_token = security_token if security_token else None
1661-
16621659
# 4. 构建 OTSClient + AsyncOTSClient 和 OTSBackend
16631660
# 使用 utils.build_ots_clients 避免 codegen 替换 AsyncOTSClient
1661+
# 传入 config:OTS 经 CredentialsProvider 每次请求动态取最新 STS。
16641662
ots_client, async_ots_client = build_ots_clients(
16651663
endpoint,
1666-
access_key_id,
1667-
access_key_secret,
16681664
instance_name,
1669-
sts_token=sts_token,
1665+
config=effective_config,
16701666
)
16711667

16721668
backend = OTSBackend(
@@ -1749,7 +1745,7 @@ def from_memory_collection(
17491745
"vector_store_config.instance_name 为空。"
17501746
)
17511747

1752-
# 3. 获取凭证
1748+
# 3. 校验凭证存在(fail-fast;运行时由 CredentialsProvider 动态取证)
17531749
effective_config = config if isinstance(config, Config) else Config()
17541750
access_key_id = effective_config.get_access_key_id()
17551751
access_key_secret = effective_config.get_access_key_secret()
@@ -1760,17 +1756,13 @@ def from_memory_collection(
17601756
"AGENTRUN_ACCESS_KEY_ID / AGENTRUN_ACCESS_KEY_SECRET。"
17611757
)
17621758

1763-
security_token = effective_config.get_security_token()
1764-
sts_token = security_token if security_token else None
1765-
17661759
# 4. 构建 OTSClient + OTSClient 和 OTSBackend
17671760
# 使用 utils.build_ots_clients 避免 codegen 替换 OTSClient
1761+
# 传入 config:OTS 经 CredentialsProvider 每次请求动态取最新 STS。
17681762
ots_client, async_ots_client = build_ots_clients(
17691763
endpoint,
1770-
access_key_id,
1771-
access_key_secret,
17721764
instance_name,
1773-
sts_token=sts_token,
1765+
config=effective_config,
17741766
)
17751767

17761768
backend = OTSBackend(

agentrun/conversation_service/utils.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
if TYPE_CHECKING:
1313
from tablestore import AsyncOTSClient # type: ignore[import-untyped]
1414
from tablestore import OTSClient
15+
from tablestore.credentials import CredentialsProvider
16+
17+
from agentrun.utils.config import Config
1518

1619
# OTS 单个属性列值上限为 2MB,留 0.5MB 余量(按字符数计)
1720
MAX_COLUMN_SIZE: int = 1_500_000 # 1.5M 字符
@@ -103,38 +106,73 @@ def from_chunks(chunks: list[str]) -> str:
103106
return "".join(chunks)
104107

105108

109+
def build_ots_credentials_provider(config: "Config") -> "CredentialsProvider":
110+
"""构建 TableStore CredentialsProvider,每次请求从 Config 实时取最新 STS。
111+
112+
TableStore client 在**每个请求**调用 ``credentials_provider.get_credentials()``
113+
(见 tablestore client 的 ``_request_helper``),因此长生命周期的 OTSClient
114+
也能在每次操作时拿到请求级 overlay 注入的最新 STS(再回退环境变量)。
115+
116+
Args:
117+
config: agentrun Config 对象,凭证经其 getter 解析(overlay 优先)。
118+
119+
Returns:
120+
TableStore ``CredentialsProvider`` 实例。
121+
"""
122+
from tablestore.credentials import Credentials, CredentialsProvider
123+
124+
class _AgentrunOtsCredentialsProvider(CredentialsProvider):
125+
def __init__(self, cfg: "Config") -> None:
126+
self._cfg = cfg
127+
128+
def get_credentials(self) -> Credentials:
129+
cfg = self._cfg
130+
return Credentials(
131+
access_key_id=cfg.get_access_key_id(),
132+
access_key_secret=cfg.get_access_key_secret(),
133+
security_token=cfg.get_security_token() or None,
134+
)
135+
136+
return _AgentrunOtsCredentialsProvider(config)
137+
138+
106139
def build_ots_clients(
107140
endpoint: str,
108-
access_key_id: str,
109-
access_key_secret: str,
110141
instance_name: str,
111142
*,
112-
sts_token: str | None = None,
143+
config: "Config",
113144
) -> tuple[OTSClient, AsyncOTSClient]:
114145
"""构建 OTSClient 和 AsyncOTSClient 实例。
115146
116147
独立于 codegen 模板,避免 AsyncOTSClient 被替换为 OTSClient。
117148
149+
凭证统一通过 :func:`build_ots_credentials_provider` 注入:client 每次请求
150+
动态从 ``config`` 取最新 STS(请求级 overlay 优先),STS 过期可静默刷新。
151+
遵循 AGENTS.md 约定——不接受静态 ak/sk/sts。
152+
153+
Args:
154+
endpoint: OTS endpoint。
155+
instance_name: OTS 实例名。
156+
config: agentrun Config 对象,凭证经其 getter 动态解析。
157+
118158
Returns:
119159
(ots_client, async_ots_client) 二元组。
120160
"""
121161
from tablestore import AsyncOTSClient # type: ignore[import-untyped]
122162
from tablestore import OTSClient, WriteRetryPolicy
123163

164+
# 同一个 provider 可被 sync / async client 共享:无状态,按请求读 overlay。
165+
provider = build_ots_credentials_provider(config)
124166
ots_client = OTSClient(
125167
endpoint,
126-
access_key_id,
127-
access_key_secret,
128-
instance_name,
129-
sts_token=sts_token,
168+
instance_name=instance_name,
169+
credentials_provider=provider,
130170
retry_policy=WriteRetryPolicy(),
131171
)
132172
async_ots_client = AsyncOTSClient(
133173
endpoint,
134-
access_key_id,
135-
access_key_secret,
136-
instance_name,
137-
sts_token=sts_token,
174+
instance_name=instance_name,
175+
credentials_provider=provider,
138176
retry_policy=WriteRetryPolicy(),
139177
)
140178
return ots_client, async_ots_client

agentrun/memory_collection/memory_conversation.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,18 @@ async def _init_memory_store(self):
189189
ots_config = await self._get_ots_config_from_memory_collection()
190190

191191
# 创建 AsyncOTSClient
192-
# 支持使用 STS 临时凭证访问 TableStore
193-
client_kwargs = {
194-
"end_point": ots_config["endpoint"],
195-
"access_key_id": ots_config["access_key_id"],
196-
"access_key_secret": ots_config["access_key_secret"],
197-
"instance_name": ots_config["instance_name"],
198-
}
199-
200-
# 如果提供了 security_token,则添加到参数中(支持 STS 临时凭证)
201-
if ots_config.get("security_token"):
202-
client_kwargs["sts_token"] = ots_config["security_token"]
192+
# 通过 CredentialsProvider 注入凭证:长生命周期的 client(server 启动时
193+
# 仅创建一次)也能在每次请求动态取到请求级 overlay 注入的最新 STS
194+
# (再回退环境变量),无需重建连接。
195+
from agentrun.conversation_service.utils import (
196+
build_ots_credentials_provider,
197+
)
203198

204-
self._ots_client = tablestore.AsyncOTSClient(**client_kwargs)
199+
self._ots_client = tablestore.AsyncOTSClient(
200+
end_point=ots_config["endpoint"],
201+
instance_name=ots_config["instance_name"],
202+
credentials_provider=build_ots_credentials_provider(self.config),
203+
)
205204

206205
# 配置会话表的二级索引元数据字段
207206
# agent_id 字段用于标识会话所属的 Agent
@@ -259,10 +258,10 @@ async def _get_ots_config_from_memory_collection(self) -> Dict[str, Any]:
259258
Returns:
260259
Dict[str, Any]: OTS 配置字典,包含:
261260
- endpoint: OTS endpoint
262-
- access_key_id: 访问密钥 ID
263-
- access_key_secret: 访问密钥 Secret
264-
- security_token: STS 安全令牌(可选,用于临时凭证)
265261
- instance_name: OTS 实例名称
262+
263+
凭证(ak/sk/sts)不在此返回:OTSClient 通过 CredentialsProvider
264+
每次请求动态从 Config 取最新 STS,无需在此快照。
266265
"""
267266
from agentrun.memory_collection import MemoryCollection
268267

@@ -307,15 +306,10 @@ async def _get_ots_config_from_memory_collection(self) -> Dict[str, Any]:
307306
f" {original_endpoint} -> {endpoint}"
308307
)
309308

310-
# 构建 OTS 配置
309+
# 构建 OTS 配置(仅连接信息;凭证由 CredentialsProvider 动态注入)
311310
ots_config = {
312311
"endpoint": endpoint,
313312
"instance_name": vs_config.instance_name or "",
314-
"access_key_id": self.config.get_access_key_id(),
315-
"access_key_secret": self.config.get_access_key_secret(),
316-
"security_token": (
317-
self.config.get_security_token()
318-
), # 支持 STS 临时凭证
319313
}
320314

321315
return ots_config

0 commit comments

Comments
 (0)