Day 08 / 共 20 天 · 第 2 周 Block 系统

Block 凭证系统

Block 常需要 API Key/OAuth 才能调外部服务。今天讲凭证怎么被安全地声明、存储、注入——"引用 vs 实体"两层设计是安全的核心。

📍 你在整门课的位置 · 第 2 周 Block 深入
D7 真实 Block 精读 D8 Block 凭证 D9 Block 成本 D10 SDK
💡 今天的类比世界观:凭证两层 = 酒店的"房卡"与"保险柜" 安全的核心是把"引用"和"实体"分开引用(房卡号)= 图里只存一个卡号,能指认用哪张卡,但卡号本身不是钥匙实体(保险柜里的真钥匙)= 真正的 API Key,加密单独存CredentialsField = 登记"这个块要用哪类卡"discriminator = 前台按你选的服务,智能发对应那张卡注入时加锁 = 同一把钥匙刷新时不许两人同时动。今天都用"房卡 / 保险柜"来想。

👶 小白:为什么不干脆把 API Key 直接存进 Graph 里,用起来多方便?

👨‍🏫 老师:那等于把保险柜密码写在明信片上——图会被分享、fork、导出,Key 就跟着泄露了。所以平台在图里只存一个"引用"(房卡号),真正的 Key(实体)加密锁在别处,执行的一瞬间才注入。看得到"用哪张卡",却偷不走"钥匙本身",这就是引用 / 实体两层分离的安全价值。

L01

凭证难题

Block 要调 OpenAI 需要 API Key、要发 GitHub PR 需要 OAuth token。这些敏感凭证怎么管,既安全又好用?难点:① 不能明文存在图里(图会导出/分享);② 不能硬编码在 Block 代码里;③ OAuth token 会过期需刷新;④ 多个执行并发用同一凭证要防冲突。

核心矛盾 凭证既要"能用"(执行时 Block 得拿到真 key 去调 API)又要"安全"(别处不该出现明文)。AutoGPT 的解法是"引用 vs 实体分离"——平时到处流转的只是"引用"(指针),真正的密钥只在执行瞬间"实体化"注入。下面拆解。
L02

引用 vs 实体两层(核心安全设计)

🤔 痛点:图要能分享,密钥又不能泄露,二者怎么共存? 你把一个"自动发推"的 Agent 图导出发到社区,别人一导入就能用——但如果图里存着你的推特 token,别人也就拿到了你的账号。可要是图里啥都不存,执行时块又拿什么去调 API?
💡 本质:把"指哪条凭证"和"凭证是什么"彻底拆开 图里只存一个引用("用 id=xxx 那条凭证",就是个指针,没有密钥);真正的密钥(实体)只在你自己执行的那一瞬间从加密库里解出来、注入 run()、用完即弃。能流通的东西不含密钥,含密钥的东西不流通。

引用 CredentialsMetaInput

  • 只有 id/title/provider/type
  • 不含密钥本身
  • 图里、DB 里、前端流转的都是它
  • 只是指向凭证库某条记录的指针

实体 APIKeyCredentials/OAuth2

  • 含真实 api_key: SecretStr / token
  • 只在执行瞬间由执行器解析出
  • 以 kwarg 注入 Block 的 run()
  • 用完即释放
到处流通的世界(图 / DB / 导出文件 / 前端) 引用 CredentialsMetaInput { id, title, provider, type } · 无密钥 只在"你的执行 + 那一瞬间"的世界 实体 APIKeyCredentials api_key: SecretStr = "sk-真实密钥" 🔒 加密凭证库(每用户私有) creds_manager.acquire 解密+加锁 按 id 查 实体化注入 run()
上层的"引用"随图到处走却不含密钥;下层的"实体"密钥只在执行瞬间由加密库解出注入。密钥的暴露面被压缩到一次执行的生命周期内。
读法:引用(data/model.py:514)是"指针"——只说"用哪条凭证",不含密钥。实体(model.py:357)才是真密钥,只在执行时短暂出现。密钥用 SecretStr 包裹,序列化时才解包(防止意外打印/日志泄漏)。
为什么这个分离是安全关键? 你导出一个 Agent 图分享给别人时,图里只有凭证引用("用 id=xxx 的凭证"),没有真密钥——别人拿到图也拿不到你的 key。真密钥只在你自己的执行执行的那一瞬间、从你自己的加密凭证库解出来注入 Block。"图里存指针、执行时才实体化"——密钥的暴露面被压缩到极致。这和 OpenHands 的密钥按需下发(Day 09)是同一安全哲学。
L03

CredentialsField 声明

Block 声明凭证字段用 CredentialsFielddata/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 的凭证
    )
读法:声明一个凭证字段,告诉框架"这个字段需要凭证"。前端看到凭证字段就弹出"连接账号/填 API Key"的 UI。
L04

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"的错配。
怎么工作的? LLM 块有个 model 下拉(选 GPT-4 / Claude / ...)。discriminator="model" + mapping 表让框架自动推断:用户选了某个 Anthropic 模型 → 前端就自动知道要连 Anthropic 的凭证,而不是让用户再手动选一次"我要用哪家的 key"。你选了模型,凭证类型就定了——少一步操作,且不会选错(选了 GPT 模型却填 Claude key)。这种"一个字段的值决定另一个字段的行为"的联动,让可视化界面更智能、更防错。
L05

命名强约束(回扣 Day 02)

Day 02 提到的自动校验(_base.py:337)在这里发挥作用:名为 credentials*_credentials 的字段必须是凭证类型,反之亦然(双向绑定)。

强约束换来什么? 因为命名规范被强制,前端只要看字段名就知道"这是个凭证字段,该弹连接账号 UI"——不用额外标记。而且防止开发者写错(把凭证字段起个普通名字,或把普通字段误标成凭证类型)。一个 Block 可以有多个凭证字段(如 openai_credentials + e2b_credentials,同时用两个服务),但 webhook 类块只允许一个(_base.py:616)。约定命名 + 强制校验 = 前端能自动识别、开发者不易出错。
L06

执行时注入与加锁

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 里释放锁。
为什么凭证要加锁? 想象两个执行同时用同一个 OAuth 凭证,而这个凭证恰好过期需要刷新。如果两个执行同时刷新,可能互相覆盖 token、导致混乱。加锁保证"同一凭证同一时刻只有一个使用者"——刷新、使用都串行化,避免竞态。共享的可变资源(凭证)+ 并发访问 = 必须加锁。锁 key 形如 user:{id}/credentials:{id}
L07

OAuth 刷新

OAuth token 会过期。IntegrationCredentialsManagerintegrations/creds_manager.py:74)的 get() 取凭证时,若是 OAuth2 且快过期就 refresh_if_needed()——加锁调对应 provider 的 oauth_handler.refresh_tokens() 换新 token 再存回。

每个第三方一个 OAuth handler(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 会从平台视角再看一遍。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 凭证管理的核心矛盾是什么?
  • 引用 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
明天预告 · Day 09Block 成本计量——每个 Block 怎么定价(BlockCost 六种类型)、静态 vs 动态成本、cost_filter 按参数匹配价格。这是计量 SaaS 的地基。
← Day 07 真实 Block Day 09 · 成本计量 →