English SAFA iconSAFA

SAFA 访问模式:所有资源类型、拓扑,以及不落明文的密码

上一篇文章论证了"为什么 Agent 不该持有可复用凭据"。这篇讲真正的机制:数据库、对象存储、缓存、主机是如何被登记而密码从不变成明文;受保护值如何通过系统认证的进程录入;跳板路由与钉住的主机身份如何工作;拓扑答案如何基于观测证据计算——而不是基于假设。

SAFA — 面向智能体的安全访问层2026-08-19github.com/juju-w/safa第一部分:为什么

一个资源目录,容纳所有类型

SAFA 的资产清单是一个加密、与传输方式无关的目录。"资源"是一个通用聚合模型——host(主机)、database(数据库)、object-storage(对象存储)、cache(缓存)、messaging(消息)、search(搜索)、graph(图)、service(服务)全部复用同一条记录。每条记录绑定一个不可变的模板,标明适配器/配置 schema,如 ssh@1mysql@1redis@1s3@1http@1

登记一个类型不等于授予访问,也不等于声称适配器已存在。目录回答的是"用户登记了什么、受保护数据放在哪";真正执行某个类型的操作,由对应的适配器决定。

字段可见性说明
canonical alias(规范别名)公开小写逻辑名;Agent 唯一可用的选择器
resource kind / type公开database.mysqlobject-storage.s3host.linux
template公开不可变的适配器/schema 绑定,如 mysql@1
endpoint / username / route授权后加密;仅授权详情视图披露
typed metadata(类型化元数据)白名单 / 授权后未知键一律默认私有
credential bindings(凭据绑定)仅 Broker只有不透明 ID,永不出现在 Agent DTO
credential values / locators(凭据值)仅 Keychain / Broker永不作为资源元数据
让一切成立的那条规则:凭据值从来不是领域模型里的字段。记录只存一个不透明引用;秘密本身放在 Keychain 或 Secure Enclave,位于 Broker 边界内部。

密码如何进入系统,却不变成明文

首次录入密码是整个系统最敏感的时刻,所以 SAFA 把它与 Agent 能影响的一切隔离开。一个单独签名的 safa-trusted-setup 进程在 Mac 上以系统认证方式运行:受保护字段输入时关闭终端回显,值以类型化的 ProtectedResourceSetupPayload 经认证的本地 XPC 直达 Broker——绝不经过 Agent CLI 的 argv、环境变量、stdin、stdout 或 stderr。如果没有可信的控制终端,SAFA 会返回一条 safe_for_agent: false 的命令,由用户亲自执行。

最终产物是一个 CredentialReference(凭据引用):一个不透明 UUID,同时作为 Keychain 账户标识;加上类型(当前是 SSH;数据库密码、对象存储访问密钥、API token 无需改动保险箱结构即可扩展)、健康状态,以及最关键的一项——访问类别(access class)

设备绑定的私钥没有任何导出路径;删除引用会在依赖资源停用后清除对应 Keychain 材料。秘密不是领域模型字段——保险箱 schema 本身就没有可泄露的东西。

一个录入到系统认证本地进程、以不透明 Keychain 引用存储、只在 Broker 边界内注入的密码,永远不会成为模型输入、工具输出或聊天记录。

怎么连过去:直连、跳板、钉住的身份

连接元数据(endpoint、username、route)是加密的,不在 Agent 可见面内。需要路由时,资源模型携带 jumpRoute:一个有序、无环的 SSH 资源列表,Broker 依次穿越。也支持直连——包括指向本机已运行的 Core Tunnel 监听端口,隧道前置而不暴露任何东西。

每次执行都是一次全新的、隔离的 SSH 调用,基于每次请求的随机别名和一个临时 ssh_config + known_hosts(目录权限 0700):

主机身份记录守卫目的地:状态为 trusted(可信)、changed(已变更)或 revoked(已吊销),并记录验证方式。身份变更或缺失一律失败关闭——永远不存在原始 SSH 回退。

拓扑:答案来自证据,而不是示意图

资源关系以有向、类型化、带属性的多重图存储——树结构表达不了"这台主机用了另一个安全域的路由、又依赖三个存储"。边携带层级(layer)验证状态(verification)

一次成功的有界 SSH 配置或执行,会刷新一条持续五分钟的 runtime.local can-reach <resource> 观测边;OpenSSH 的 exit 255 永远不会创建它。可达性只有在 Broker 找到一条完全由新鲜、已验证证据构成的路径时才返回 confirmed

下面是真实命令的返回——在真实注册环境中录制(别名已脱敏,数值原样):

$ safa topology show data.home --limit 64   # 数据库主机在哪里?
schema: dev.safa.cli/v2
command: topology.show
status: completed
task: placement
answer:
  outcome: found
  source: data.home
count:
  nodes: 6
  edges: 5
nodes[6]{alias,kind,resource_kind}:
  data.home,resource,host
  site.home,site,null
  desk.win,resource,host
  router.home,resource,host
  virt.host,resource,host
  nas.primary,resource,host
edges[5]{id,from,relation,to}:
  ...,data.home,located-in,site.home
  ...
$ safa topology path data.home nas.primary --limit 64   # 数据库主机能到 NAS 吗?
schema: dev.safa.cli/v2
command: topology.path
status: completed
task: reachability
answer:
  outcome: not-found
  source: data.home
  target: nas.primary
count:
  nodes: 2
  edges: 0

对比这两条输出:数据库主机与 NAS 同处一个站点(placement found,6 个节点 5 条 located-in 边),但 Broker 报告两者之间没有已验证的路径。这正是重点——同站不连通。Agent 若问"这个节点能到数据库吗",得到的是 not-found,且被禁止编造路由或尝试直连。同一个图版本还产出 topology impact(反向依赖搜索)与 topology link/unlink(变更关系,需 macOS 用户授权)。

什么可见,什么需要用户在场

默认的非交互摘要只暴露经过源码评审的白名单键:host.os.familyhost.docker.availabledatabase.engineobject-storage.providercache.engineservice.protocol。IP、内核版本、CPU/内存/磁盘细节、Docker 版本、路由以及一切未知键,都需要 resource show --details——而这条命令依赖 macOS 自有的用户在场提示。受保护详情与提权动作共用同一条规则:要么用户在 Mac 前,要么值保持隐藏。

授权在代码里不是模糊概念:待审批请求携带 TrustedApprovalPresentation(资源别名、特权级别、精确命令、意图、预期影响、风险级别、发现项),用户通过 safa-trusted-setup request approve 或拒绝来决策——sudo 还支持可选的作用域授权(见 spec 002)。Agent 的自我评估只是建议,永远不能替代这个决定。

当前范围与后续

当前可执行的配置是 SSH 主机 + 白名单只读诊断。数据库、对象存储、缓存与服务已作为类型化记录登记,其操作在评审前保持门控。通用目录、不透明凭据引用模型、跳板路由字段、访问类别与用户在场授权路径,都已经在设计与契约中——接下来交付的是各适配器,且每个都附带一致性测试夹具,而不是空头承诺。

看真实排查 在 GitHub 查看