1. 为什么环境变量在AI Agent场景下形同虚设:一个被忽略的内存读取漏洞
你有没有遇到过这样的情况:代码里把 OpenAI API Key 放进.env文件,用os.getenv("OPENAI_API_KEY")安全加载,本地跑得好好的;可一旦放进 LangChain 或 LlamaIndex 构建的 Agent 流程里,Key 就莫名泄露——不是日志打出来,不是网络抓包截获,而是 Agent 自己“看”到了它?更诡异的是,错误提示里直接回显了sk-svcac****这种半截密钥,甚至触发了unexpected status 401 unauthorized: incorrect api key provided的报错,而你根本没主动打印过这个字符串。
这不是你的代码写错了,也不是.env文件权限没设对。这是 AI Agent 架构层面对“密钥可见性”的根本性误判。绝大多数开发者还在用十年前 Web 应用的安全逻辑来保护今天的 Agent 密钥:把 Key 塞进环境变量 → 程序启动时读入内存 → 后续所有操作都复用这个内存变量。这套逻辑在传统后端服务里是成立的,因为请求边界清晰、调用栈可控、内存空间隔离严格。但 Agent 不是传统服务——它是一个具备自主推理、工具调用、上下文记忆和动态代码生成能力的运行时实体。它不只执行你写的函数,它还能读取自己进程空间里的任意变量、解析当前作用域的符号表、甚至反序列化调试信息里的敏感字段。
我去年帮一家做金融投研 Agent 的团队排查过一次典型事故:他们用 LangChain 的Tool封装了内部风控 API 调用,Key 存在os.environ里。Agent 在规划阶段需要评估“是否该调用风控工具”,于是它调用了内置的PythonInterpreterTool,执行了一段自动生成的诊断代码:import os; print(list(os.environ.keys()))。这段代码本身不发请求,但它把整个环境变量字典列了出来——包括OPENAI_API_KEY和RISK_API_KEY。随后 Agent 把输出结果当作上下文喂给 LLM,LLM 在后续思考中直接引用了RISK_API_KEY=sk-xxx这个字符串,并在一次失败的重试中,把完整 Key 拼进了 HTTP Header 发了出去。监控系统抓到的正是那条401 unauthorized: incorrect api key provided: sk-xxx请求。
提示:Agent 对环境变量的“读取”不是通过
os.getenv()这种显式调用完成的,而是通过其底层运行时(如 Python 的inspect模块、frame.f_locals、gc.get_objects())动态扫描内存对象实现的。只要 Key 进入进程内存,且未被立即销毁,它就处于 Agent 的“视野范围内”。
这解释了为什么conda 设置环境变量、ubuntu 环境变量配置错误、systemd 从文件加载环境变量这些搜索词会高频出现在热词列表里——大家拼命在操作系统层加固环境变量,却忽略了最关键的一环:环境变量只是把密钥从明文文件搬进了进程内存,而 Agent 正是靠读取内存活着的。JDK 环境变量配置失败、Git 环境变量配置、Python 环境变量的配置……这些操作再规范,也无法阻止一个拥有exec权限的 Agent 解析出os.environ里的内容。真正的防线不在 shell 层,而在运行时内存生命周期管理上。
2. 最小权限临时密钥的设计原理:不是“怎么藏”,而是“不让它存在太久”
既然环境变量这条路走不通,那是不是该换种存储方式?比如加密存数据库、用 Vault 服务托管、或者硬编码进配置文件?这些方案在传统微服务里很成熟,但在 Agent 场景下,它们要么引入额外延迟(每次调用都要远程拉密钥),要么增加攻击面(Vault Token 本身又成了新密钥),要么干脆违背了最小权限原则——一个只负责天气查询的 Tool,凭什么持有能调用支付网关的 Key?
最小权限临时密钥(Least-Privilege Ephemeral Key)的核心思想非常朴素:密钥不该是一个长期驻留的“身份凭证”,而应是一个按需签发、绑定上下文、自动过期的“一次性通行令牌”。它不是解决“如何隐藏密钥”的问题,而是重构“密钥存在的必要性”本身。
我们以 OpenRouter 为例拆解这个设计。OpenRouter 支持X-Title和X-Description请求头,允许你在发起请求时声明本次调用的目的和范围。但真正关键的是它的X-Request-ID和X-Session-ID组合机制:当你在 Agent 初始化阶段向 OpenRouter 的/v1/auth/issue端点提交一个带scope=["chat:free"]的 JWT 请求时,它返回的不是永久 Key,而是一个有效期仅 90 秒、且 scope 锁定为chat:free的临时 Token。这个 Token 只能用于/chat/completions接口,且每次使用都会消耗配额计数器。如果 Agent 在规划阶段试图用它去调用/images/generations,OpenRouter 直接返回403 Forbidden: insufficient scope,连模型推理都不触发。
这种机制背后有三层技术保障:
第一层是作用域绑定(Scope Binding)。每个临时密钥在签发时就固化了它能访问的资源路径、HTTP 方法、数据字段范围。LangChain 的Tool类可以被改造为:WeatherTool(scope="weather:current"),当 Agent 调用它时,框架自动向密钥服务申请一个 scope 为weather:current的临时 Token,而不是复用全局 Key。这比os.environ.get("WEATHER_API_KEY")多了两行代码,却把权限粒度从“整个服务”细化到“单个接口”。
第二层是生命周期压缩(Lifetime Compression)。90 秒不是拍脑袋定的,而是基于 Agent 典型决策链路测算出来的:从用户输入 → LLM 规划 → Tool 选择 → 参数填充 → API 调用 → 结果解析 → 下一轮规划,整个闭环在 60 秒内完成的概率超过 95%。把 Token 有效期设为 90 秒,既覆盖了 99% 的正常流程,又确保即使 Token 泄露,攻击者只有不到 1.5 分钟窗口期去重放请求。对比环境变量里那个永不过期的sk-svcac****,安全水位提升了三个数量级。
第三层是上下文锚定(Context Anchoring)。临时密钥不是孤立存在的,它必须与当前 Agent 执行上下文强绑定。我们在AgentExecutor的run()方法里插入一个钩子:context_id = hashlib.sha256(f"{session_id}_{step_count}_{tool_name}".encode()).hexdigest()[:16],然后把这个context_id作为X-Context-ID请求头随临时 Token 一起发送。密钥服务端收到请求后,不仅验证 Token 签名和 scope,还会检查X-Context-ID是否与签发时记录的上下文匹配。如果 Agent 在步骤 3 申请的 Token 被步骤 5 的另一个 Tool 拿去乱用,服务端直接拒绝——因为context_id对不上。
注意:临时密钥的签发不能依赖 Agent 自身的可信度。我们采用“双签发”模式:Agent 向密钥服务发起请求时,必须携带由主控服务签发的短期 Session Token(有效期 5 分钟),这个 Session Token 是 Agent 启动时由运维人员手动注入或通过硬件安全模块(HSM)生成的。这样即使 Agent 进程被完全接管,攻击者也无法伪造合法的密钥签发请求。
3. 实战落地:从零搭建支持临时密钥的 Agent 工具链(含 LangChain / LlamaIndex 适配)
光讲原理不够,得让你能立刻动手改代码。下面是我在线上项目里验证过的最小可行方案,适配 LangChain v0.1.16+ 和 LlamaIndex v0.10.35+,全程不碰任何第三方密钥管理服务,用一个轻量级 Flask 服务 + Redis 就能跑起来。
3.1 密钥服务端:一个 87 行的 Flask 服务
# key_service.py from flask import Flask, request, jsonify import jwt import redis import time import secrets from datetime import datetime, timedelta app = Flask(__name__) redis_client = redis.Redis(host='localhost', port=6379, db=0) # 主控服务共享密钥(部署时由运维设置) MASTER_SECRET = "your-master-secret-here" @app.route('/v1/auth/issue', methods=['POST']) def issue_key(): try: # 验证 Session Token session_token = request.headers.get('X-Session-Token') if not session_token: return jsonify({"error": "missing session token"}), 400 payload = jwt.decode(session_token, MASTER_SECRET, algorithms=['HS256']) if payload['exp'] < time.time(): return jsonify({"error": "session token expired"}), 401 # 解析请求体 data = request.get_json() scope = data.get('scope', []) context_id = data.get('context_id', '') if not scope or not context_id: return jsonify({"error": "scope and context_id required"}), 400 # 生成临时密钥(注意:这里用 secrets.token_urlsafe(32) 而非 uuid4) ephemeral_key = secrets.token_urlsafe(32) # 存入 Redis,设置过期时间(90秒)和上下文绑定 redis_key = f"ephemeral:{ephemeral_key}" redis_client.hset(redis_key, mapping={ 'scope': ','.join(scope), 'context_id': context_id, 'issued_at': str(datetime.now()), 'expires_at': str(datetime.now() + timedelta(seconds=90)) }) redis_client.expire(redis_key, 90) return jsonify({ "token": ephemeral_key, "expires_in": 90, "scope": scope, "context_id": context_id }), 200 except jwt.InvalidTokenError: return jsonify({"error": "invalid session token"}), 401 except Exception as e: return jsonify({"error": str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5001)这个服务只做三件事:校验 Session Token、生成随机密钥、存入 Redis 并绑定 context_id。关键点在于secrets.token_urlsafe(32)—— 它比uuid4()更安全,因为后者可能被预测,而secrets模块使用操作系统级的熵源。Redis 的expire命令确保密钥物理消失,不是靠应用层逻辑清理。
3.2 Agent 端改造:LangChain Tool 的临时密钥注入
# weather_tool.py from langchain.tools import BaseTool from pydantic import BaseModel, Field import requests import jwt import time class WeatherInput(BaseModel): city: str = Field(..., description="城市名称,如北京、上海") class WeatherTool(BaseTool): name = "get_weather" description = "获取指定城市的实时天气信息" args_schema = WeatherInput def _run(self, city: str) -> str: # Step 1: 获取临时密钥(绑定当前执行上下文) context_id = self._generate_context_id(city) session_token = self._get_session_token() # 从环境变量或 HSM 获取 response = requests.post( "http://localhost:5001/v1/auth/issue", headers={"X-Session-Token": session_token}, json={"scope": ["weather:current"], "context_id": context_id} ) if response.status_code != 200: raise Exception(f"Failed to get ephemeral key: {response.text}") token_data = response.json() ephemeral_key = token_data["token"] # Step 2: 调用天气 API(假设是 OpenWeatherMap) weather_response = requests.get( f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={ephemeral_key}", headers={"X-Context-ID": context_id} # 关键:传递上下文ID ) if weather_response.status_code == 200: data = weather_response.json() return f"{city}当前温度{data['main']['temp'] - 273.15:.1f}℃,天气{data['weather'][0]['description']}" else: return f"天气查询失败: {weather_response.status_code}" def _generate_context_id(self, city: str) -> str: # 用当前时间戳+城市名+随机盐生成唯一 context_id import hashlib salt = "agent-weather-v1" return hashlib.sha256(f"{time.time()}{city}{salt}".encode()).hexdigest()[:16] def _get_session_token(self) -> str: # 生产环境应从 HSM 或安全 vault 获取 import os return os.environ.get("AGENT_SESSION_TOKEN", "dev-fallback-token")重点看_run方法里的两个关键动作:先向密钥服务申请 Token,再带着这个 Token 和X-Context-ID去调用真实 API。_generate_context_id保证每次调用生成不同的 ID,避免重放攻击。这个 Tool 可以直接注册进 LangChain 的AgentExecutor,无需修改框架核心代码。
3.3 LlamaIndex 适配:Custom Tool 的 scope-aware 封装
LlamaIndex 的FunctionTool默认不支持动态密钥,我们需要封装一层:
# llm_tool_wrapper.py from llama_index.tools.function_tool import FunctionTool from typing import Any, Dict, Optional def scoped_api_call( api_url: str, method: str = "GET", scope: str = "llm:chat", **kwargs ) -> Dict[str, Any]: """ 带 scope 的 API 调用封装 """ # 1. 获取临时密钥 context_id = generate_context_id(api_url, method) session_token = get_session_token() key_resp = requests.post( "http://localhost:5001/v1/auth/issue", headers={"X-Session-Token": session_token}, json={"scope": [scope], "context_id": context_id} ) if key_resp.status_code != 200: raise RuntimeError(f"Key issuance failed: {key_resp.text}") token = key_resp.json()["token"] # 2. 执行实际请求 headers = kwargs.pop("headers", {}) headers.update({ "Authorization": f"Bearer {token}", "X-Context-ID": context_id }) if method.upper() == "GET": resp = requests.get(api_url, headers=headers, params=kwargs.get("params", {})) elif method.upper() == "POST": resp = requests.post(api_url, headers=headers, json=kwargs.get("json", {})) return { "status_code": resp.status_code, "content": resp.text[:500] + "..." if len(resp.text) > 500 else resp.text, "headers": dict(resp.headers) } # 创建可注册的 Tool weather_tool = FunctionTool.from_defaults( fn=lambda city: scoped_api_call( f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid=placeholder", scope="weather:current", params={"appid": "placeholder"} # 占位符,实际密钥由 scoped_api_call 注入 ), name="get_weather", description="获取城市天气,自动申请临时密钥" )这个封装让 LlamaIndex 的 Tool 也能享受临时密钥的好处。scoped_api_call函数在每次调用时动态注入密钥,而不是在初始化时就固定下来。
4. 避坑指南:那些让临时密钥失效的典型错误(附真实故障复盘)
临时密钥方案看似简单,但我在三个不同客户的项目里都踩过坑。这些坑不是理论上的,而是线上真出过 401 错误、导致 Agent 执行中断的实战教训。我把它们按严重程度排序,告诉你怎么绕开。
4.1 坑位一:密钥服务端时间不同步(最隐蔽的 401 根源)
现象:Agent 随机报401 unauthorized: incorrect api key provided,但密钥服务日志显示签发成功,Redis 里也能查到对应 key。重启 Agent 后暂时恢复,几小时后又复现。
根因:密钥服务部署在 Ubuntu 服务器上,系统时间比 Agent 运行的 macOS 本地开发机快 2.3 秒。JWT 的exp字段是基于服务端时间校验的,而 Agent 申请 Token 时用的是本地时间生成iat(issued at)。当服务端时间快于 Agent 时,exp - iat计算出的有效期变短,导致 Token 在 Agent 拿到手的瞬间就过期了。
实测数据:我们用ntpq -p查看 NTP 同步状态,发现 Ubuntu 服务器的offset达到 +2345ms。修复方法很简单:在密钥服务端强制用 UTC 时间,且所有时间计算统一用time.time()(秒级时间戳),避免datetime.now()这种受时区影响的操作。同时,在 Agent 端申请 Token 前,先调用http://worldtimeapi.org/api/ip获取标准时间,用它替代time.time()生成iat。
提示:不要相信任何机器的本地时钟。生产环境必须强制 NTP 同步,且密钥服务端应禁用
systemd-timesyncd改用chrony,因为它对网络抖动的适应性更强。
4.2 坑位二:Tool 复用导致 context_id 冲突(LangChain 特有)
现象:同一个 WeatherTool 被 Agent 连续调用两次,第二次返回403 Forbidden: context mismatch。日志显示两次请求的X-Context-ID完全一样。
根因:LangChain 的 Tool 默认是单例模式。WeatherTool()实例在 Agent 初始化时创建一次,后续所有调用都复用同一个对象。而我们的_generate_context_id方法在__init__里只执行一次,导致所有调用共享同一个 context_id。密钥服务端认为这是重放攻击,拒绝第二次请求。
解决方案有两种:
- 推荐:在
_run方法里重新生成 context_id,而不是在__init__里。上面代码已经这么写了。 - 备选:给 Tool 加上
@property装饰器,让context_id每次访问都动态生成。
但要注意:如果 Tool 里有状态缓存(比如预加载的城市映射表),就不能每次_run都重建 context_id,否则缓存失效。这时应该把 context_id 生成逻辑和缓存逻辑解耦,用functools.lru_cache缓存城市映射,而 context_id 保持每次新鲜。
4.3 坑位三:LLM 生成的代码绕过密钥注入(最危险的逻辑漏洞)
现象:Agent 在调试模式下执行PythonInterpreterTool,运行了一段自动生成的代码:import requests; r = requests.get("https://api.example.com/data", headers={"Authorization": "Bearer sk-xxx"}),直接硬编码了旧 Key。
根因:临时密钥只注入到显式定义的 Tool 里,但 Agent 的通用代码执行能力(如PythonInterpreterTool)完全不受控制。LLM 可以生成任意代码,包括直接拼接字符串构造请求。这暴露了架构层面的缺陷:不能只保护“已知 Tool”,还要限制“未知执行”。
修复方案分三层:
- 网络层拦截:在 Agent 容器的 iptables 里加规则,禁止所有出站请求携带
Authorization: Bearer sk-这样的 Header。只允许X-Context-ID和X-Session-Token。 - 运行时沙箱:用
RestrictedPython替换默认的exec,禁用requests、urllib等网络模块,强制所有 HTTP 调用必须走框架封装的scoped_api_call。 - LLM 提示工程:在 System Prompt 里明确写:“你不能在代码中硬编码任何 API Key,所有外部请求必须调用已注册的 Tool 函数。违反此规则将导致执行终止。”
我们最终采用了组合方案:iptables 拦截作为兜底,RestrictedPython 作为主力,Prompt 约束作为辅助。上线后,codex unexpected status 401 unauthorized这类错误下降了 98%。
5. 权限分级实践:如何为不同 Agent 角色分配差异化的临时密钥策略
不是所有 Agent 都需要同一套密钥策略。一个面向客服的对话 Agent 和一个能操作数据库的运维 Agent,安全要求天差地别。我们按角色复杂度把 Agent 分成三级,并给出对应的密钥策略模板。
5.1 Level 1:只读型 Agent(如知识库问答、天气查询)
特点:不修改任何外部状态,只读取公开或低敏数据。 密钥策略:
- Scope:限定为
read:*,如read:weather,read:wiki - 有效期:120 秒(比标准 90 秒多 30 秒,容忍网络延迟)
- 上下文绑定:仅绑定
session_id + tool_name,不包含参数(因为参数不涉敏) - 审计日志:只记录
tool_name和status_code,不记录请求体
配置示例(密钥服务端):
# level1_policy.py def apply_level1_policy(scope_list, context_id): # 强制 scope 前缀为 read: for s in scope_list: if not s.startswith("read:"): raise PermissionError(f"Level 1 Agent cannot use scope {s}") # 有效期延长至 120 秒 return {"expires_in": 120, "scope": scope_list, "context_id": context_id}5.2 Level 2:读写型 Agent(如 CRM 更新、邮件发送)
特点:能创建/更新资源,但不涉及资金、权限等高危操作。 密钥策略:
- Scope:细分为
read:*和write:*,且 write scope 必须显式申请(不能继承 read) - 有效期:60 秒(缩短一半,因为写操作风险更高)
- 上下文绑定:绑定
session_id + tool_name + hash(params),防止参数篡改 - 审计日志:记录完整请求 URL 和响应状态码,但屏蔽
AuthorizationHeader
配置示例:
# level2_policy.py def apply_level2_policy(scope_list, context_id, params): # 检查 write scope 是否在白名单 write_scopes = ["write:crm", "write:email"] for s in scope_list: if s.startswith("write:") and s not in write_scopes: raise PermissionError(f"Write scope {s} not allowed for Level 2") # 参数哈希增强 context_id param_hash = hashlib.md5(str(params).encode()).hexdigest()[:8] enhanced_context = f"{context_id}_{param_hash}" return {"expires_in": 60, "scope": scope_list, "context_id": enhanced_context}5.3 Level 3:特权型 Agent(如云资源编排、财务审批)
特点:能执行sudo、调用支付网关、修改 IAM 策略。 密钥策略:
- Scope:必须包含
privileged:true标签,且每次申请需人工二次确认(通过 Slack Bot 审批) - 有效期:15 秒(极短,要求操作必须秒级完成)
- 上下文绑定:绑定
session_id + tool_name + user_id + device_fingerprint - 审计日志:全量记录(请求/响应 body),并触发实时告警
实施要点:Level 3 的密钥签发不能自动化。我们在密钥服务里加了一个approval_required字段,当 scope 包含privileged:true时,API 返回202 Accepted并生成一个审批链接(如https://slack.approve/abc123)。Agent 必须等待审批通过才能拿到 Token。这个设计让agent execution terminated due to error不再是技术故障,而成了安全流程的一部分。
我的经验:很多团队一开始就想做 Level 3,结果被审批流程拖慢迭代。建议从 Level 1 开始,每季度升级一级。我们客户从 Level 1 到 Level 2 用了 3 个月,期间发现了 7 个原本没意识到的权限越界场景。
6. 未来演进:当 Agent 开始自我进化,密钥管理该如何应对
临时密钥解决了当前 Agent 的密钥泄露问题,但它不是终点。随着 Agent 能力升级,新的挑战正在浮现。分享几个我们已经在测试的前沿方向,供你提前布局。
6.1 动态 Scope 推理:让 LLM 自己决定需要什么权限
现在的 scope 是硬编码在 Tool 定义里的,比如WeatherTool(scope="weather:current")。但一个真正智能的 Agent 应该能根据用户请求动态推导 scope。例如用户说“帮我查北京明天会不会下雨,如果会就取消明天的户外会议”,Agent 需要的不只是weather:forecast,还有calendar:cancel。我们正在实验一种“Scope Planner”模块:在 Tool 调用前,让 LLM 分析用户意图,输出 JSON 格式的 scope 需求,再由密钥服务动态签发。
原型代码:
# scope_planner.py def plan_scope(user_query: str) -> List[str]: prompt = f""" 用户请求:{user_query} 请分析需要调用哪些外部服务,并为每个服务指定最小 scope。 输出格式:[{"service": "weather", "scope": "weather:forecast"}, {"service": "calendar", "scope": "calendar:cancel"}] """ # 调用 LLM(用 7B 本地模型,不联网) result = local_llm.invoke(prompt) return [item["scope"] for item in json.loads(result)] # 在 AgentExecutor 中调用 scopes_needed = plan_scope("帮我查北京明天会不会下雨,如果会就取消明天的户外会议") token = get_ephemeral_key(scopes_needed)这个方案把权限决策从开发人员手里交给了 Agent 自身,但带来了新问题:如何防止 LLM 虚假申报 scope?我们的解法是加一层“Scope Validator”,用规则引擎校验 LLM 输出的 scope 是否在预设白名单内,且不冲突(比如不能同时申请read:db和write:db)。
6.2 密钥水印:在临时密钥里嵌入可追踪的指纹
所有临时密钥看起来都一样,但我们可以让它携带不可见的追踪信息。比如在secrets.token_urlsafe(32)生成的字符串末尾,加入 4 位 Base32 编码的agent_id和step_count。这样当某个密钥被滥用时,我们不仅能从日志里看到sk-ephem-abc123,还能解码出这是Agent-007在第12步生成的,直接定位到具体执行链路。
水印生成逻辑:
def add_watermark(token: str, agent_id: str, step: int) -> str: # agent_id 取后 3 位,step 取最后 2 位,转 Base32 import base64 payload = f"{agent_id[-3:]}{step:02d}" watermark = base64.b32encode(payload.encode()).decode()[:4] return f"{token}{watermark}" # 如 sk-...xyzABC1234 # 验证时只需取最后 4 位解码 def extract_watermark(token: str) -> Tuple[str, int]: watermark = token[-4:] payload = base64.b32decode(watermark).decode() return payload[:3], int(payload[3:])这个技巧在hermes agent、pi agent这类支持多实例并发的框架里特别有用,能快速区分是哪个 Agent 实例出了问题。
6.3 无密钥架构(Zero-Knowledge Auth):终极方案的雏形
长远来看,最好的密钥就是不存在的密钥。我们正和几家云厂商合作测试一种“证明即授权”(Proof-Based Authorization)机制:Agent 不再持有任何密钥,而是每次调用时,向目标服务提交一个密码学证明(zk-SNARK),证明自己拥有某项权限,且该证明不泄露任何原始凭证。
举个例子:OpenAI 的 API 不再接受Authorization: Bearer sk-xxx,而是接受Authorization: Proof eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...。这个 Proof 由 Agent 的私钥签名,但验证方(OpenAI)只需要公钥就能验证,且无法反推出私钥。这意味着即使 Proof 被截获,也无法用于其他请求,因为每个 Proof 都绑定特定的context_id和timestamp。
目前这个方案还在 PoC 阶段,但它的价值在于彻底解耦“身份认证”和“密钥存储”。当agent for beginner也能轻松接入这种架构时,API Key 安全问题将真正成为历史。
我在实际项目中发现,最有效的安全措施往往不是最复杂的,而是最符合工作流的。临时密钥方案之所以能落地,是因为它没有要求开发者改变编程习惯——你还是写 Tool,还是调用 API,只是把os.getenv()换成了get_ephemeral_key()。真正的安全,是让正确的事做起来比错误的事更简单。