Block 凭证系统
Block 常需要 API Key/OAuth 才能调外部服务。今天讲凭证怎么被安全地声明、存储、注入——"引用 vs 实体"两层设计是安全的核心。
👶 小白:为什么不干脆把 API Key 直接存进 Graph 里,用起来多方便?
👨🏫 老师:那等于把保险柜密码写在明信片上——图会被分享、fork、导出,Key 就跟着泄露了。所以平台在图里只存一个"引用"(房卡号),真正的 Key(实体)加密锁在别处,执行的一瞬间才注入。看得到"用哪张卡",却偷不走"钥匙本身",这就是引用 / 实体两层分离的安全价值。
凭证难题
Block 要调 OpenAI 需要 API Key、要发 GitHub PR 需要 OAuth token。这些敏感凭证怎么管,既安全又好用?难点:① 不能明文存在图里(图会导出/分享);② 不能硬编码在 Block 代码里;③ OAuth token 会过期需刷新;④ 多个执行并发用同一凭证要防冲突。
引用 vs 实体两层(核心安全设计)
引用 CredentialsMetaInput
- 只有
id/title/provider/type - 不含密钥本身
- 图里、DB 里、前端流转的都是它
- 只是指向凭证库某条记录的指针
实体 APIKeyCredentials/OAuth2
- 含真实
api_key: SecretStr/ token - 只在执行瞬间由执行器解析出
- 以 kwarg 注入 Block 的 run()
- 用完即释放
data/model.py:514)是"指针"——只说"用哪条凭证",不含密钥。实体(model.py:357)才是真密钥,只在执行时短暂出现。密钥用 SecretStr 包裹,序列化时才解包(防止意外打印/日志泄漏)。CredentialsField 声明
Block 声明凭证字段用 CredentialsField(data/model.py:743)。LLM 块的封装(blocks/llm.py:90):
def AICredentialsField() -> AICredentials:
return CredentialsField(
description="LLM 供应商的 API key",
discriminator="model", # ★ 根据用户选的 model 字段...
discriminator_mapping={m.value: m.metadata.provider for m in LlmModel}, # ...自动决定用哪个 provider 的凭证
)
discriminator:智能推断该用哪个凭证
上面 discriminator="model" 是个精妙设计——凭证类型由另一个字段的值决定:
model = "claude-3-5-sonnet" → mapping 表查到它的 provider 是 anthropic → 前端自动要求"连接 Anthropic 账号 / 填 Anthropic API Key"。若改选
model = "gpt-4o" → provider 变成 openai → UI 自动切换成要 OpenAI 的 key。用户从头到尾没手动选过"我用哪家的 key",也就不可能出现"选了 GPT 模型却填了 Claude key"的错配。model 下拉(选 GPT-4 / Claude / ...)。discriminator="model" + mapping 表让框架自动推断:用户选了某个 Anthropic 模型 → 前端就自动知道要连 Anthropic 的凭证,而不是让用户再手动选一次"我要用哪家的 key"。你选了模型,凭证类型就定了——少一步操作,且不会选错(选了 GPT 模型却填 Claude key)。这种"一个字段的值决定另一个字段的行为"的联动,让可视化界面更智能、更防错。命名强约束(回扣 Day 02)
Day 02 提到的自动校验(_base.py:337)在这里发挥作用:名为 credentials 或 *_credentials 的字段必须是凭证类型,反之亦然(双向绑定)。
openai_credentials + e2b_credentials,同时用两个服务),但 webhook 类块只允许一个(_base.py:616)。约定命名 + 强制校验 = 前端能自动识别、开发者不易出错。执行时注入与加锁
Day 05 见过执行时的凭证注入(executor/manager.py:263),核心是 creds_manager.acquire:
credentials, lock = await creds_manager.acquire(user_id, credentials_meta.id)
extra_exec_kwargs[field_name] = credentials # 注入到 run 的 kwarg
# ... block.execute(...) ...
# finally: 释放所有凭证锁
acquire 做两件事:① 从凭证库取出已解密、已刷新的真实凭证实体;② 拿一把 Redis 分布式锁——保证"同一套凭证同一时刻只被一个 Block 用"。执行完 finally 里释放锁。user:{id}/credentials:{id}。OAuth 刷新
OAuth token 会过期。IntegrationCredentialsManager(integrations/creds_manager.py:74)的 get() 取凭证时,若是 OAuth2 且快过期就 refresh_if_needed()——加锁调对应 provider 的 oauth_handler.refresh_tokens() 换新 token 再存回。
integrations/oauth/:google.py/github.py/discord.py/notion.py...),都继承 BaseOAuthHandler(提供 get_login_url/exchange_code_for_tokens/refresh_tokens)。自动刷新让用户"连一次账号,长期可用"——不用每次 token 过期就重新授权。这套凭证管理(引用/实体分离 + 加锁 + 自动刷新)是 Day 17 集成系统的核心,Day 17 会从平台视角再看一遍。今日小结 + 动手
🧠 今天你应该能回答
- 凭证管理的核心矛盾是什么?
- 引用 vs 实体两层为什么是安全关键?
- discriminator 怎么智能推断凭证类型?
- 命名强约束换来什么?
- 执行时凭证为什么要加锁?OAuth 怎么自动刷新?
✋ 动手
P=autogpt_platform/backend/backend
sed -n '327,360p' $P/data/model.py # 凭证实体
grep -n 'class CredentialsMetaInput\|def CredentialsField' $P/data/model.py
sed -n '85,100p' $P/blocks/llm.py # AICredentialsField + discriminator
grep -n 'def acquire\|def refresh_if_needed' $P/integrations/creds_manager.py