为什么需要 Contract
没有 Contract 时,downstream Unit(如 auth-fe)只能读 upstream Unit 的源码(如 auth-be 的 server.js)。这会引发:
- 实现细节耦合,任何重构都连环失败
- Coder Agent 沙箱必须开放跨目录读权限(不安全)
- contract_hash 无法计算 → L2 gate 失效
有 Contract 后,downstream 只读 upstream 的 contract.json。这是跨 Unit 唯一允许的读路径。
Contract Schema(TypeBox)
const Contract = Type.Object({ unit_id: Type.String(), version: Type.String({ pattern: '^sha256:' }), exports: Type.Object({ http: Type.Optional(Type.Array(HttpEndpoint)), ts_types: Type.Optional(Type.String()), // 相对路径 ./types.d.ts events: Type.Optional(Type.Array(EventSpec)), cli: Type.Optional(Type.Array(CliSpec)), }), consumes: Type.Optional(Type.Array(Type.String())), // 上游 Unit id }); const HttpEndpoint = Type.Object({ method: Type.Union([Type.Literal('GET'), Type.Literal('POST'), Type.Literal('PUT'), Type.Literal('DELETE'), Type.Literal('PATCH')]), path: Type.String(), request: Type.Optional(Type.Object({}, { additionalProperties: true })), responses: Type.Record(Type.String(), // "200" | "401" | ... Type.Object({}, { additionalProperties: true })), auth: Type.Optional(Type.Union([ Type.Literal('none'), Type.Literal('jwt'), Type.Literal('apikey') ])), }); const EventSpec = Type.Object({ name: Type.String(), // "unit.state" payload: Type.Object({}, { additionalProperties: true }), channel: Type.Union([Type.Literal('sse'), Type.Literal('ws')]), }); const CliSpec = Type.Object({ cmd: Type.String(), // "auto-dev" args: Type.Array(Type.Object({ name: Type.String(), required: Type.Boolean() })), });
完整示例 · auth-be
{ "unit_id": "auth-be", "version": "sha256:7f3a...", "exports": { "http": [ { "method": "POST", "path": "/api/auth/login", "request": { "email": "string", "password": "string" }, "responses": { "200": { "token": "string", "user_id": "string" }, "401": { "error": "\"invalid_credentials\"" } }, "auth": "none" }, { "method": "POST", "path": "/api/auth/register", "request": { "email": "string", "password": "string" }, "responses": { "201": { "user_id": "string" }, "409": { "error": "\"email_exists\"" } }, "auth": "none" } ], "ts_types": "./types.d.ts" }, "consumes": [] }
contract_hash 计算
function hashContract(contract) { // 1. 序列化 exports 字段(不包含 version 自身) const normalized = canonicalJSON(contract.exports); // 2. sha256 完整 hex const full = crypto.createHash('sha256').update(normalized).digest('hex'); // 3. 截前 16 位(加 'sha256:' 前缀) return `sha256:${full.slice(0, 16)}`; } // canonicalJSON:key 按字典序排列,无空白,统一引号 // 保证同一 exports 在任意机器上 hash 完全相同
截 16 位的概率碰撞计算 = 2^-64,对单项目内 ≤ 100 个 Unit 安全。
SDD ↔ contract.json 一致性
Architect 在 SDD 写的 <pre data-contract> 内容必须与 Coder 写的 contract.json 一致。校验流程:
- Architect 写 SDD 时把 contract 文本固化在
<pre> - Coder 实现完成后写 contract.json,必须包含 SDD 描述的全部 endpoints
- DocStore 计算两者 hash 对比 — 不一致 → Coder 视为 fail,触发 retry
- 反过来 Coder 不许新增 SDD 未声明的 endpoint(沙箱拒绝)
contract 变化触发的连锁
跨 Unit 引用约定
auth-fe 想用 auth-be 的契约,不读 auth-be 的代码。流程:
// auth-fe 的 Coder Agent 在 system prompt 指引下: const contract = JSON.parse(fs.readFileSync('../auth-be/contract.json')); const endpoint = contract.exports.http.find(e => e.path === '/api/auth/login'); // → 写 fetch 调用基于 endpoint.responses 推导出 200 / 401 分支
沙箱白名单允许 ../{otherUnit}/contract.json 只读,不允许读 ../{otherUnit}/code/。