Agent 只保存最少的身份信息
Cloud Agent 有意只保存最少的身份信息:网关的用户 ID,以及签发这个 ID 的签发方。网关账号的其他资料不会抄进身份声明,权限也不从账号角色推出来。能不能访问,由网关当场判定;Cloud Agent 每次都向网关重新确认,而不是从会话里读权限。具体怎么检查,见 Agent 准入契约。
令牌与会话
凭据 |
Agent 保留的声明 |
校验 |
|---|---|---|
网关身份令牌 |
|
通过 JWKS 校验 RS256 签名;签发方必须和配置完全一致,受众必须是 |
Agent 会话 |
|
用 |
旧版网关可能还会带上额外的个人资料声明,Agent 在校验时直接丢掉。会话里的 gateway_issuer 标明上游的身份签发方,而会话本身由 Agent 签名,有效期 30 天。缺少签发方或带有个人资料声明的旧 cookie,以及来自其他签发方的 cookie,都会被拒绝。所以从这些旧格式升级之后,用户需要重新登录一次。
cookie 带 Secure 时(默认如此),会话只从 __Host-cloud_agent_session 读取,进行到一半的登录只从 __Host-cloud_agent_login 读取。同一站点下的其他主机能设置普通名字的 cookie,所以加前缀之前留下的 cloud_agent_session cookie 不会让任何人登录;浏览器里如果只剩这一个,就要重新登录。纯 HTTP 的本地环境里,浏览器会拒绝这个前缀,所以仍用普通名字;见 ENVIRONMENT.md 里的 AGENT_SESSION_COOKIE_SECURE。
状态正常、但没有 Agent 权限的账号,可以建立一个最小会话,用来显示"需要访问权限"页面。不存在或已停用的账号,会收到访问契约里带类型的 subject_unavailable 错误,无法登录。响应无法解析或代理出错时,都不会放行。
GET /v1/session/me 返回 id 和 agent_access。My Settings 里有一个 Profile 页面:账号邮箱只读,名和姓可以编辑,还可以上传头像。邮箱是需要时才从网关现有的 /internal/users/{user_id}/status 接口查来的,Agent 从不存储,也不放进会话声明。这个接口返回的角色和账号状态字段不授予任何 Agent 权限;当前能否访问,仍以 agent-access 的检查结果为准。
名字和头像是所有者自己填写的 Cloud Agent 显示信息,和网关的登录身份无关,也和首页上自动生成的伙伴形象无关。首页和 Profile 编辑页优先显示上传的头像;没有上传时,显示所有者的宝可梦伙伴;删掉头像,就会换回伙伴形象。伙伴的栖息地和徽章来自工作洞察(work_insights.py),每次读取时都根据所有者最近的请求重新计算;它们不属于记忆功能,也永远不会改动已保存的头像。GET 和带修订号检查的 PATCH /v1/agent/profile 都要求当前有 Agent 准入,并返回 Cache-Control: no-store。名和姓各自最多 100 个字符。上传的 PNG、JPEG 和 WebP 不能超过 2 MiB、1600 万像素;服务器保存前会把它们重新编码成不带元数据的 PNG,尺寸最大 512×512 像素。迁移 0063_user_profiles 新建了 agent_user_profiles 表,里面没有邮箱、角色或凭据。编辑 Profile 不会改动网关里的账号信息。
前端会把成功读到的个人资料和首页洞察放在内存里,按账号共享。回到 New task 页面时,先立即显示缓存的头像,同时在后台刷新;刷新失败就继续显示上一张。保存 Profile 会立即更新这份共享缓存,并让更早发出的读取结果作废。缓存只在当前浏览器标签页里有效,退出登录或切换账号之前会被清空;它不影响各 API 的 Cache-Control: no-store 策略。
任务、消息、连接标识、加密存储的 Git/MCP 凭据,以及用户编写的其他内容,仍然是 Agent 自己的数据。
系统没有逐个吊销会话的表。被人复制走的 cookie 在过期前一直有效,但受保护的请求仍然要求网关当前允许访问。轮换 AGENT_SESSION_SECRET 会让所有会话一起失效。在网关上收回权限,之后的受保护请求就都会被拒绝。
数据库身份边界
agent_identity_namespace 表里只有一条记录,把这个数据库绑定到某个 GATEWAY_ISSUER。主 API 和 MCP 监听器在启动 worker 之前都会检查这个绑定。绑定不会改写所有者 ID,也不会根据旧数据去猜它们来自哪个签发方。这张表属于 0001_schema_baseline;在已经退役的旧迁移链里,这项改动叫 0029_identity_namespace。
数据库状态 |
启动或检查时的行为 |
|---|---|
空库,没有绑定,已配置签发方 |
自动绑定到这个签发方 |
已有所有者数据,没有绑定,已配置签发方 |
拒绝启动,直到运维人员明确认定原来的签发方 |
已绑定,且和配置的签发方一致 |
正常继续 |
绑定的是另一个签发方,或者绑定后又删掉了签发方配置 |
拒绝启动 |
既没有网关配置,也没有绑定 |
允许本地的基础设施管理;身份和会话认证不可用 |
多个进程同时初始化时会排队依次执行。认定(adopt)只能补上缺失的绑定,不能替换已有的绑定。请使用原网关实际签发的 iss 值,一字不差。GATEWAY_BASE_URL 和 JWKS URL 只是访问地址,可以改,不影响身份命名空间。相互独立的网关必须使用不同的签发方。在同一个签发方下,把网关的账号数据库换成一批毫不相干的身份,这种做法不受支持。
升级现有部署
在发行版把 Agent 升级到新版本之前,先把每个环境准备好。合并上游源代码并不会部署到任何一个具体的环境;而且光看已有的所有者数据,也看不出原来的网关签发方是什么。
从原网关的运行环境里,确认它实际使用的签发方。在最初上线时使用的网关实现中,
serving.utils.identity_tokens.issuer()先读IDENTITY_ISSUER,没有就用base_url,并去掉末尾的斜杠。把确认后的实际值同时固定写进网关的IDENTITY_ISSUER和 Agent 的GATEWAY_ISSUER。staging 和 production 要分别确认。备份 Agent 数据库。如果它还在用 2026-09-10 之前的迁移链,运行迁移前先按运维手册里的转换或重建流程处理;
upgrade head无法直接处理已退役的修订号标记。如果已经是受支持的 schema,就用准备好的发布镜像迁移到 head。把已有的所有者数据认定为属于原来的签发方,然后检查绑定。在装有 Agent 包、并配置好数据库的环境里运行:
python -m cloud_agent.identity_namespace --adopt-existing --issuer '<original-issuer>' python -m cloud_agent.identity_namespace --check
认定时传入的签发方必须和
GATEWAY_ISSUER完全一致。这一步只会补上缺失的绑定,任务、连接和所有者 ID 都原样保留。没有可以长期开着的自动认定开关。用 Compose 部署时,沿用现有的项目名、当前生效的环境文件和准备好的本地仓库:
export AGENT_DEPLOYMENT_SHA="$(git rev-parse HEAD)" dc() { docker compose -p '<existing-project>' --env-file '<active-env-file>' \ -f deploy/control-plane/docker-compose.yml "$@" } dc build control-plane migrate dc run --rm --no-deps migrate dc run --rm --no-deps control-plane python -m cloud_agent.identity_namespace \ --adopt-existing --issuer '<original-issuer>' dc run --rm --no-deps control-plane python -m cloud_agent.identity_namespace --check
这些命令需要现有的 Postgres 服务正在运行。它们只负责准备绑定,不会替换 API 或 MCP 中继。
通过部署流程,同时发布 Agent 后端和 Web 应用。检查全新登录、
/me、所有者和管理员权限,以及已有任务的归属是否正确。确认旧 cookie 会被拒绝。
deploy/control-plane/deploy.sh 会在迁移完成之后、启动初始化、等待主机就绪和替换 API/MCP 之前,先做身份检查。只要有未认定归属的所有者数据、缺少必需的签发方,或者绑定冲突,这次部署就会中止。主机的上线可能同时在进行,所以要先把身份边界准备好。两个 Compose 监听器用的是同一个签发方。
支持权限判定的网关,在这次 Agent 发布之前或之后去掉个人资料声明都可以,Agent 把这些声明当作可有可无。不支持访问契约的旧版网关仍然不兼容。本地 Docker 环境使用固定的签发方 freeinference-local;已有但还没绑定的演示数据,同样需要明确认定这个签发方。
回滚和恢复时的限制
不要用 alembic downgrade base 来撤销身份绑定:baseline 会把所有者相关的表和 agent_identity_namespace 一起删掉。没有哪种局部降级能只删掉这条记录、同时保住所有者数据。
对空数据库重新升级时,会自动绑定当前配置的签发方;但降级时已经删掉的历史,它也救不回来。
如果恢复了所有者数据,却没有恢复对应的绑定,身份检查就会失败。这时要么恢复原来的绑定,要么按上面的流程,明确认定核实过的原签发方。
旧版 Agent 可能会接受带个人资料的 cookie,并且无视已保存的签发方。所以出问题时,优先做兼容的向前修复;或者先停掉 Agent,让网关继续运行。备份和恢复绑定时,要和它所指向的那批所有者数据放在一起。
实现代码在 identity.py、identity_namespace.py 和 lifespan.py。