## 大多数构建者面临的问题

如果您针对一家商店运行一名代理，那么归因就微不足道了。你知道每一行是谁写的。你写了代理。

当您针对同一个 Neotoma 实例运行两个、三个、五个代理时，[情况发生了变化](/posts/when-agents-share-state-everything-breaks)。每个代理都会写下观察结果、关系、来源和解释。商店积累所有这些的状态。如果其中一个代理开始编写错误的数据、微妙错误的摘要、陈旧的日期、错误的关系，那么推断哪些记录值得信任的唯一方法就是推断哪个代理编写了这些数据。

如果没有每行归因，出现问题时您的选择很艰难：擦除存储并重新摄取，或者保留坏行并接受漂移。随着商店的发展，这两种情况都会变得更糟。

对于任何运输代理驱动产品的人来说，这个问题变得更加严重。您的客户记录由一群人写入：您自己的代理、通过 MCP 集成的第三方代理，也许是某人上周安装的插件。当客户询问“谁将此信息写入我的帐户并根据谁的权限？”时，该问题必须根据数据来回答，而不是通过服务器日志和对话记录之间的关联练习来回答。

我在 Cursor、Claude Code、Codex 和 ChatGPT 上运行代理，所有代理都写入一个 Neotoma 实例。我写了关于[该堆栈实际上做了什么](/posts/what-my-agentic-stack-actually-does)。 Neotoma 的 AAuth 集成缩小了我的堆栈和在其上构建的任何人的差距：每个代理都有自己的密钥，并且随着车队的增长，商店可以保持值得信赖。

## 为什么要进行 AAuth

归因层基于 [AAuth](https://www.aauth.dev/) 构建，这是一种开放协议，为每个 HTTP 客户端提供自己的加密身份。无需预先登记。没有共同的秘密。没有不记名令牌。每个请求都使用 [RFC 9421](https://datatracker.ietf.org/doc/html/rfc9421) HTTP 消息签名进行签名，因此如果没有签名密钥，被盗的令牌就毫无价值。

我选择 AAuth 是因为它背后的人迪克·哈特 (Dick Hardt) 是我的朋友，也是我认识的最深入的身份专家之一。他编辑了 OAuth 2.0 ([RFC 6749](https://www.rfc-editor.org/rfc/rfc6749))，共同撰写了 OpenID Authentication 2.0，并且是 [OpenID Foundation](https://openid.net/foundation/members/) 的创始董事会成员。这与大多数开发人员通过授权代码流和联合登录遇到的血统相同。当有这样历史的人开始专门为代理制定新协议时，这就是值得构建的协议。

## 现在每个写入都包含什么

[v0.6.0](https://github.com/markmhendrickson/neotoma/releases/tag/v0.6.0) 在每个写入表面上提供每行代理归因：`/store`、`/observations/create`、`/create_relationship`、`/ Correct`、`/entities/split`、MCP 存储工具以及通过 MCP 和 HTTP 进行的 CLI 写入。每个观察、关系、来源和解释都印有：

- 经过验证的代理标识符（签名作者的公钥指纹、代理令牌的 JWT 主题和发行者、作为后备的 clientInfo 名称和版本）。
- 信任层，对身份证明的强度进行分类。
- 写入到达的传输。

五层覆盖范围：

- “硬件”：代理提供了一个“cnf.attestation”信封（Apple Secure Enclave、WebAuthn 打包或 TPM2），服务器根据受信任的根进行验证。
- `operator_attested`：签名已验证，并且运营商已将发行者或发行者-主题对列入白名单。操作员为代理的流程提供担保，无需硬件认证。
-“软件”：代理使用有效密钥签署请求，并由服务器验证。这是今天大多数代理登陆的地方，包括我自己的 Cursor 代理使用文件支持的 ES256 JWK 进行签名。
- `unverified_client`：代理使用可识别的 clientInfo 声明自己，但未签名。
- `匿名`：根本没有身份。

结果：您可以查看商店中的任何行并回答“哪个代理写了这个”作为对一流数据的读取。

## 授予而不是配置文件

在 v0.6.0 周期的早期，功能是从环境变量 JSON 文件加载的。这适用于一组静态代理，但当您想要在不重新启动服务器的情况下暂停一个代理时，就会出现问题。

现在：每个“agent_grant”都是一个一流的 Neotoma 实体。它匹配 AAuth 身份（按主题、颁发者、指纹或组合），携带按操作和实体类型限定范围的功能条目，并具有生命周期：“活动”、“暂停”、“撤销”。准入中间件将经过验证的 AAuth 身份解析为每个请求上的匹配授权，将授权的用户和功能标记到请求上下文上，下游强制执行根据授权检查每个操作。

授权通过 Inspector UI、REST API（“POST /agents/grants”、“PATCH”、挂起、撤销、恢复）进行管理，或通过“neotoma Agents grants import”从旧环境配置中迁移一次。如果仍然设置旧版环境变量（NEOTOMA_AGENT_CAPABILITIES_*），则会导致启动时失败，并出现指向迁移命令的结构化错误。

暂停资助是即时的。代理的下一个请求失败。恢复同样是即时的。无需重新启动服务器，无需重新加载配置。

对于任何使用面向客户的代理运行产品的人来说，这意味着事件响应从“使用新配置重新启动服务”转变为“暂停一项授权并进行调查”。行为不当代理的爆炸半径仅限于授予授权的操作。

## 身份预检

每个代理现在都可以在 Neotoma 生成任何数据之前询问它是否被认为是可信的写入者。

三个等效的入口点：

- 通过 HTTP 获取/会话。
- `get_session_identity` 作为 MCP 工具。
- CLI 上的“neotoma auth session”。

每个都返回已解析的信任层、授予状态（是否允许，有原因）、匿名写入策略和布尔值“eligible_for_trusted_writes”。该响应包括一个诊断块，解释该层是如何解析的。新代理在会话开始时会大声失败，而不是写入匿名行，直到有人注意到。

附带的 MCP 指令告诉每个连接的代理在启用写入之前运行此检查。

## 我在哪里运行这个

我的堆栈中的三个不同的服务代理今天在 AAuth 下写入 Neotoma。

**Cursor MCP 代理。** 来自 Cursor 的每个 MCP 请求都会流经签名代理 (`mcp_identity_proxy.py`)，该代理使用 `aa-agent+jwt` 代理令牌注入 RFC 9421 签名。 Neotoma 验证签名，解析身份（`sub=cursor@markmhendrickson.com`、`iss=https://markmhendrickson.com`），匹配 `agent_grant`，并允许在 `tier=software` 处写入。代理还在启动时运行会话预检，如果服务器报告匿名层，代理可能会失败关闭。

**反馈管道。** `agent.neotoma.io` 处的 Netlify 中继通过 AAuth 签名的 [Cloudflare Access](https://www.cloudflare.com/zero-trust/) 隧道将代理错误报告转发到 Neotoma。其授权范围仅限于“neotoma_feedback”操作。

**[Darkmesh](https://github.com/markmhendrickson/darkmesh) Warm-intro writeback。** 我的 [Darkmesh fork](https://github.com/markmhendrickson/darkmesh/blob/main/docs/neotoma_integration.md) ([context](/posts/the-substrate-plancast-needed)) 通过 RFC 将warm-intro 记录回 Neotoma 9421 个签名和一个“aa-agent+jwt”令牌。每个揭示都带有节点的“agent_sub”、“agent_iss”和密钥指纹，范围由每个节点授予。

Darkmesh 联合测试证明了对抗性的执行。来自对等节点的第二个模拟代理尝试编写一个“warm_intro_reveal”，但在其授权中没有该实体类型。 Neotoma 拒绝了写入。授权节点的写入未发生变化。

路线图的下一步：[markmhendrickson.com 上的公共代理](https://markmhendrickson.com/agent/) 将 Neotoma 实例包装为其内存，并且今天只为我明确标记为公共的实体提供服务。我计划添加 AAuth 门控读取，以便授权访问者可以查询特定的非公共实体类型。相同的签名身份加授权机制，应用于读取路径。

## 舰队范围内的升级

Neotoma 在每次握手时将其规范的 MCP 指令从服务器发送到每个连接的客户端。在 v0.6.0 中，这些指令现在将归因预检、“observation_source”标记、回复引用的出处边缘、用于启发式合并警告的“模糊 (N)”显示组以及结构化反馈提交循环编入代码。

当我升级服务器时，我的 Cursor、Claude Code、Codex 和 OpenCode 挂钩都采用了新行为。没有客户端版本。没有每个工具的迁移。一台服务器升级，五个代理更新。对于任何运行客户队列的人，都适用相同的模式：升级 Neotoma 实例，每个连接的代理都会采用新的默认值，而无需部署客户端。

## 审核面

对于受监管市场中的产品制造商来说，客户的后续问题很少是“您的系统是否记得这一点”。是“谁写的，你能证明他们是授权的吗”。

在 v0.6.0 之后，这是对一流数据的读取：

- `GET /agents` 枚举服务器已经看到的每个代理身份。
- `GET /agents/{key}` 返回每个代理的详细信息视图。
- `GET /agents/{key}/records` 审核记录了给定代理的创作。
- `GET /agents/grants` 列出所有授权、其功能及其生命周期状态。

如果您向医疗保健、金融、法律或企业垂直领域的客户提供代理功能，这就是您的客户最终需要的界面。

## 如何开启

````bash
neotoma auth 注册机 --alg ES256
neotoma auth 签名示例
neotoma 授权会话
````

通过 Inspector 或 REST API 为新身份创建授权，将功能范围确定为代理所需的操作。如果您要从旧的 env-config 模型升级，请运行一次“neotoma Agents grants import --owner-user-id <your_user_id>”，然后取消设置旧变量。

对于编程签名，[`@aauth/local-keys`](https://www.aauth.dev/) 或等效的 AAuth 库使用 RFC 9421 HTTP 消息签名加上 `aa-agent+jwt` 令牌来签署请求。 Neotoma 验证客户端签名的原始字节上的签名。

没有 AAuth 的写入仍然有效。他们处于“匿名”层。想要硬失败的构建者可以翻转“NEOTOMA_AAUTH_STRICT=1”并将特定主题添加到“NEOTOMA_STRICT_AAUTH_SUBS”。

## 也已发货

v0.6.0 不仅仅是 AAuth。同一版本针对过度合并的记录进行了实体拆分、队列快照导出和漂移工具、通过“conversation_message”和“sender_kind”进行一流的多代理对话，以及更严格的 API 边界。完整的补充位于 [v0.6.0 发行说明](https://github.com/markmhendrickson/neotoma/releases/tag/v0.6.0)。

## 安装和升级

````bash
npm install -g neotoma@[0.6.0](https://github.com/markmhendrickson/neotoma/releases/tag/v0.6.0)
新番茄初始化
Neotoma 授权注册机
neotoma 授权会话
````

升级服务器将为您提供新的归因标记，并在下次客户端握手时刷新 MCP 指令。已通过 MCP 连接的代理无需安装客户端。

完整安装：[neotoma.io/install](https://neotoma.io/install)。仓库：[github.com/markmhendrickson/neotoma](https://github.com/markmhendrickson/neotoma)。发行说明：[v0.6.0](https://github.com/markmhendrickson/neotoma/releases/tag/v0.6.0)。