api-contracts.md
4.48 KB
Unit Contract 规范
每个 Unit 暴露给「外部」的契约协议。Architect 在 SDD 中规定,Coder 实现后产出
contract.json。两者必须一致;DocStore 校验。Contract 是跨 Unit 协同的唯一接口。
为什么需要 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 文本固化在
2. Coder 实现完成后写 contract.json,必须包含 SDD 描述的全部 endpoints 3. DocStore 计算两者 hash 对比 — 不一致 → Coder 视为 fail,触发 retry 4. 反过来 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/。