ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入解读 Beads 的 `bd serve`:v0 HTTP 契约的设计、实现与安全姿态

深入解读 Beads 的 `bd serve`:v0 HTTP 契约的设计、实现与安全姿态 深入解读 Beads 的bd servev0 HTTP 契约的设计、实现与安全姿态【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beadsbd serve是 Beads 项目为自动化客户端与编排器提供的 HTTP 服务入口它把 CLI 已经覆盖的同一套工作面issue 的查询、认领、关闭、依赖编辑、配置与记忆读写等以/v0前缀的 HTTP 接口对外暴露替代每次调用 fork 一个bd子进程并解析其 stdout 文本的旧模式。本文以 bd-serve-v0.md 为核心结合 SERVE_RUNBOOK.md 运维手册与仓库源码系统讲解该契约的权威来源、操作面形态、错误码词汇表、游标契约、环回安全姿态、写路径的三大约束无 hooks、无自动提交、按请求持久化以及工作区模式的准入规则读完你将能独立部署、探测、排障并正确编写 v0 客户端。一、契约的权威来源三处真源与防漂移机制设计文档开宗明义地声明了本页内容的真源source of truth顺序且明确Nothing here may contradict those threeinternal/httpapi/spec/openapi.v0.yaml——线上契约wire contract。所有请求/响应类型都由它生成make api-gen而make api-check会在只改了文档没改代码或反之时让 CI 失败强制二者同步。internal/httpapi/doc.go—— 说明本构建实际启用的内容以及防漂移属性如何被机器强制。issueops/reader.go——读角色read roleCLI 与 HTTP 两个前端入口共同到达的底层实现。设计文档刻意不在此页复述操作列表原因在internal/httpapi/routes.go中体现得淋漓尽致routeTable就是整个操作面TestSpecRouteParity见 spec_parity_test.go以双向集合相等的方式把路由表焊接到 OpenAPI 文档上而握手接口ContextResponse.capabilities又由同一张表派生而来。也就是说文档、路由表、运行中服务器的握手声明是同一答案的三种拼写在页面里再抄第四份只会像历史上某次一样过期。从源码看路由表每一行都携带op对应 spec 的operationId、method、patternServeMux 模式、specPath文档拼写、customMethod自定义方法后缀、capability能力令牌、bypassSemaphore是否绕过数据库槽位、streaming、authExempt、implemented等元数据并最终由s.handler构建真实路由器。二、操作面的形态路由、方法与capabilities2.1 路由空间概览一切都在/v0之下只有存活探针GET /healthz例外。实际路由以internal/httpapi/routes.go的routeTable与 openapi.v0.yaml 为准目前覆盖/healthz、/v0/beads/context、/v0/beads/ready含:count、/v0/beads/stats、/v0/beads/issues含:query、:count、:batchCreate、:batchApply、:batchClose、:claimNext、:sweep、:delete、/v0/beads/issues/{id}含:claim、:release、:close、:reopen、:casMetadata及子资源/related、/comments、/v0/beads/config含/{key}、/v0/beads/dependencies含:count、:add、:remove及/cycles、/blocking、/tree、/v0/beads/memories含/{key}、/v0/beads/events含:watch。2.2 两种写的拼写设计文档指出写入有两种拼写routes.go逐行声明规则自定义方法custom method当操作不是 CRUD 时使用例如:claim、:close、:reopen、:sweep、:delete、:add、:remove、:batchCreate等。由于 ServeMux 的通配符只能匹配整个路径段{id}:claim无法直接表达为模式因此这些行共享一个通配注册/v0/beads/issues/{idop}由customMethodTarget把后缀从匹配段上切分出来。一个未注册后缀的 POST 会得到 404——这正是宽泛模式是路由细节而非未文档化表面的体现TestCustomMethodsNarrowThePOSTSurface钉住这一点见 claim_test.go。普通方法plain method当操作确实是 CRUD 时使用例如单条 issue 的PATCH、单条 memory 的POST/DELETE。routes.go的注释还揭示了一些设计取舍/v0/beads/issues的普通POST是单条创建createIssue批量创建刻意拼成自定义方法:batchCreate以把这条路径留给单条创建GET /v0/beads/issues/{id}/comments不存在——评论线程通过GET /v0/beads/issues/{id}?include_commentstrue读取集合本身没有 405。2.3capabilities从注册表派生而非手工维护GET /v0/beads/context报告本构建实际实现了哪些操作。Capabilities()遍历routeTable仅收集implemented true且声明了capability的行外加行为级令牌因此切片中途发布的版本绝不会宣传一个还没实现的操作。客户端按操作级能力探测capabilities按参数级支持探测 400invalid_argument/reason: unknown_parameter。没有更细粒度的能力文档也刻意没有字段宣传绑定模式bind mode。2.4 响应体直接编组internal/types设计文档强调没有 wire struct响应体直接序列化internal/types的值。这意味着bd show --json的输出与 HTTP 响应体属于同一个兼容域——types.Issue上一个被序列化的字段不能随意改名或删除否则破坏的不只是 CLI还包括 HTTP 契约。三、错误码词汇表唯一的错误形态与机器可读的code3.1 一个错误形状RFC 9457 problemjson所有非 2xx 响应都是application/problemjson文档。哨兵错误到状态码/错误码的映射全部位于internal/httpapi/problem.go并且只用errors.Is/errors.As匹配绝不用err ! nil - status这种粗放写法。code是机器可读成员也是客户端唯一可以分派的成员。3.2 完整词汇表摘自设计文档源码逐条对应CodeStatus含义恢复invalid_argument400请求校验拒绝。携带param与reason换一种请求。永不重试invalid_cursor400游标不是本服务器签发、无法解码或签发于不同的编码版本不带cursor重新开始分页not_found404不存在该 id 的 issue 或 wisp—already_claimed409其他 actor 持有认领。携带assignee—not_claimable409issue 不在可认领状态。携带issue_status—not_closable409关闭策略拒绝非强制关闭存在打开的 children或存在 blocker关闭 children / 清除 blocker或带force重发dependency_cycle409请求的边永远无法清空调度环或阻塞边指向该 issue 自身的祖先/后代。层级情形携带issue_id、blocker_id、blocker_is_ancestor三者缺席即标识普通环换一组边。什么都没写入dependency_exists409该对已存在不同类型的一条边。携带existing_type与requested_type先移除已有边再添加busy503可重试争用事务重试预算耗尽或在途上限饱和。携带Retry-After按头部延迟重试db_unavailable503可重试的数据库连通性故障。携带Retry-After按头部延迟重试internal500其他一切—源码problem.go中的词汇表比设计文档表更长——还包含unauthenticated、not_releasable、already_exists、precondition_failed、events_journal_disabled、events_journal_truncated、events_watch_saturated等并以operationCodes逐操作声明可发射集合。TestSpecStatusCodesMatchHandlerTable见 spec_parity_test.go在两个方向上断言处理器表与 spec 集合相等未文档化的发射和不可发射的文档化状态都会让 CI 失败。3.3reason拆分两种 400 客户端姿态invalid_argument背后的两种情形客户端区分时永远不需要解析detailunknown_parameter—— 本服务器不认识该参数属于版本偏差version skew客户端降级或回退invalid_value—— 服务器不会对那个值采取行动格式错误、超出词汇表或在本服务器配置下合法但被拒绝。词汇表是单向门重命名或删除一个已文档化的 statuscode 组合会破坏线上契约新增则不会。因此文档要求客户端在同一个状态类内对未知 code按默认分支处理。3.4 5xx 固定detail、4xx 保留细节防信息泄漏两条规则约束 problem body 能说什么5xx 的detail是每个 code 的固定字符串与底层错误无关。原因在problem.go的staticDetail与注释中讲得很清楚驱动与拨号错误经常内嵌 DSNgo-sql-driver 会渲染usertcp(127.0.0.1:PORT)/db查询错误可能携带 SQL 片段——一旦服务器以--allow-non-loopback绑定冗长的 5xx detail 就变成对网络对端的信息泄漏通道。真实错误进服务器日志由 5xx body 携带的request_id关联——那是客户端拿到唯一一条日志的把手。4xx 的detail保持具体因为它只是把调用者自己的输入反射回去。唯一的例外是 401呈现的凭据绝不能回显进 detail否则会落进客户端日志与代理链路。3.5 类型化冲突对采用客户端意味着什么设计文档明确建议过去靠子串匹配错误文本already assigned to、claimed by分类认领冲突的客户端应迁移到类型化的 409 code。assignee、issue_status这些扩展成员来自失败事务内部的一次读取而不是从哨兵消息里解析片段。dependency_exists携带existing_type/requested_typedependency_cycle在层级规则拒绝时携带issue_id/blocker_id/blocker_is_ancestor普通调度环时什么都不带——成员缺席本身就是区分信号。这些成员只能在拒绝发生的事务内部读到因为冲突的边可能只存在于已回滚的批次里。四、游标契约不透明、无生命周期、单一恢复、顺序敏感GET /v0/beads/issues用不透明的 keyset 游标分页GET /v0/beads/ready不分页ready 排序策略无法表达 keyset 谓词预期用法是快照 重新查询。四条规范性属性设计文档原文不透明Opaque客户端不得构造、解析或修改游标。编码是base64(小 JSON 对象)且带版本前缀v2.历史编码为v1.——它足够可读所以契约靠版本前缀而非模糊性来强制见 cursor.go 的encodeCursor。无生命周期No lifetime令牌只携带位置、该位置所处的顺序、私有编码版本——没有别的。服务器不为它保存任何状态不过期、重启不失效、不与签发它的连接绑定。唯一会让它失效的是编码变更而迄今唯一一次变更v1→v2新增 order 标签保留了所有在途v1令牌按created顺序继续可读因此没有一次进行中的遍历需要重启。单一恢复One recovery任何失败模式——版本错误、base64 不可解码、JSON 畸形、空位置、位置处于另一种顺序——都是同一个答案不带cursor重新开始分页。重发该值不可能成功。过滤器误用不可检测顺序ORDER误用可检测——这是整个设计的分界点。过滤器不进入令牌所以换一组过滤器复用旧游标不会被拒绝而是从旧位置按新过滤器静默续页、跳过本应排在前面的行。文档给出的义务是整个遍历期间逐字重复过滤器变更过滤器就开启新遍历。这是刻意的权衡把过滤器嵌进令牌会让令牌变成请求的第二份不透明副本可能与请求本身互相矛盾。顺序不是过滤器不享受同样待遇。sortcreated与sortpriority下同一个(时间, id)命名的是不同行的位置字节里没有任何信息能说明其意图——所以换sort重放令牌会得到invalid_cursor而非被重新解释。decodeCursor的实现正是把期望顺序作为参数传入而不是事后比较cursor.go让顺序不匹配在接缝处就不可表达而不是在某个调用点悄悄给出错误页面还回 200。只有具备可证明全序键的顺序才会被服务sort只接受created与priority因为每个值都是一份 keyset 契约——位置形状、严格后继谓词、索引。两者的键都是全序的priority、created_at非空id唯一所以等键段内的页边界可按id消解不丢行不重行。bd list --sort接受的其余七种顺序id的自然数字序、updated每次写入都变动、closed可空、status/title/type/assignee可变且无索引都没有这样的键客户端若需要其中之一仍按created顺序分页、自己排序收到的结果。priority本身可变而created_at不可变后果被明确陈述created顺序下只有新行相对遍历移动priority顺序下一次优先级更新也会移动已有行可能被看到两次或漏掉一次——游标钉住的是位置不是快照但不变的数据在两种顺序下都不会跳行或重行。GET /v0/beads/ready仍然发布词汇表更宽的sort且没有游标这并不矛盾没有游标时顺序只是显示决策不需要能够从中间恢复。五、环回姿态可选鉴权、无 TLS与三层硬约束5.1 鉴权开与关v0 有可选的 bearer 鉴权且无 TLS。在环回上信任模型就是环回边界本身——数据库已经依赖的同一边界——因此不带任何鉴权标志的bd serve与从前逐字节相同。--auth-token-file打开鉴权除GET /healthz外的每个操作包括报告仓库根目录、beads 目录与数据库名的GET /v0/beads/context都要求Authorization: Bearer token。文件每行一个令牌、每行都被接受运行期间持续重读全进程最多每秒一次stat(2)见 auth.go 的authReloadInterval因此轮换与吊销都在约一秒内生效、都无需重启。刻意没有--auth-token标志参数会出现在进程列表中对每个本地用户可读。环境变量BEADS_SERVE_TOKEN_FILE是回退见 serve.go。令牌是授予整个操作面的共享秘密不是身份、没有作用域因此永远不会让actor变成已认证主体。令牌以 SHA-256 摘要形式持有并常量时间比较auth.go进程不持原始凭据堆转储也不会泄露。401 的code: unauthenticateddetail是固定字符串且绝不回显所呈现的内容WWW-Authenticate: Bearer。检查发生在数据库信号量之前所以一次拒绝风暴的成本只是每人一次 SHA-256。5.2 绑定规则与三个标志--addr默认127.0.0.1:0主机必须是数字 IP 字面量DNS 名被拒绝ValidateBindAddr见 server.go。--allow-non-loopback是操作者决定默认绝不开启它现在要求--auth-token-file否则能到达地址本身就等于全部授权——每个能到达的对端都能完整读写与认领。--insecure-no-auth是我明确知道自己在干什么的可审计写法仅可搭配--allow-non-loopback与令牌文件互斥并会记录一条点明暴露范围的警告。两种非环回绑定在启动时都会打印不同措辞的警告——一个点名缺凭据一个点名缺 TLS。5.3 三层无条件约束Host 允许列表没有关闭开关。环回上的服务可被宿主机上任意浏览器访问而把自身名字重新解析为127.0.0.1的页面发出的请求会被浏览器视为同源CORS 拦不住。浏览器保留的是攻击者的主机名在Host头里——这就是被拒绝的东西。每个绑定都响应环回拼写与绑定地址本身通配绑定0.0.0.0、::没有单一配置地址因此响应任意数字 IP 字面量、仍拒绝外部 DNS 名重绑页面无法产生 IP 字面量的Host因为浏览器发送的是攻击者 URL 里的主机名。匹配基于解析后的地址所以被允许地址的每种拼写都被允许doc.go 记录了与设计文档字面的一处偏差通配绑定的语义。每个 body 都要求 JSON-only 内容类型。这是 CSRF 控制而非洁癖JSON 内容类型不是 CORS simple 的跨源写入必然触发本服务器从不批准的预检。接受text/plain或表单编码会让页面跳过预检、从宿主机任意浏览器驱动一次写入。模式相关的无界读取拒绝。limit0在两个列表操作上意味着无界与bd list --limit 0完全一致——除非在--allow-non-loopback下此时被 400invalid_argument/param: limit/reason: invalid_value拒绝。无界读取会把整个活动集及其 JSON 编码缓冲进一个共享进程绝不能暴露给任意网络对端。绑定模式刻意不在ContextResponse里宣传想要无界读取的客户端就请求它收到那个 400 后用显式 limit cursor重新分页——这是客户端侧修复绝不是重试。actor在 HTTP 请求上是审计轨迹的调用者自证来源不是已认证主体——即使强制 bearer 也是如此一个共享令牌把客户端放行到一切既不能证实也不能反驳请求发送的名字。这与 CLI 上actor一贯的含义相同任何本地进程都能传任意--actor。因此认领的 compare-and-set 是对并发认领的正确性围栏而非授权边界它保证两个竞速认领者不能双赢但不保证他们各自到底是谁。六、写路径三大契约无 hooks、无自动提交、每请求持久化6.1 无 hooks本面上的任何写入都不触发 hooksCLI 认领会跑on_updateHTTP 认领不会此后新增的任何写入也都不触发。理由写得很硬hook 是每次变更一个用户控制的子进程在并发服务器里是无界的延迟倍增器和关闭时的孤儿子进程而且基于工作目录的 hook 查找在一个不共享客户端工作目录的服务器进程里毫无意义。这是契约声明不是日后要补的缺口——需要 hook 副作用的客户端就通过 CLI 执行该变更。6.2 无自动提交包裹 CLI 调用的按命令自动提交、导出与推送维护在这里不运行。持久化是按请求的改变内容的写入在自己的事务内提交与现在的 proxied CLI 完全一致。两个对采用客户端值得说明的后果没有进程结束时的冲刷。服务器做过的事在响应写出时已经持久或者永远不会持久。幂等写入不产生提交。已经拿到想要结果的当事人再写一次当前持有者重新认领、已关闭 issue 再次关闭、已打开 issue 重新打开——compare-and-set 因无可改变而没匹配到任何行空提交消息让事务运行器跳过提交。因此轮询客户端不会每次调用凭空造出一个空存储提交。6.3 写吞吐的定标逻辑本面上每次变更都花费一次存储提交容量规划由此而来、而非由 HTTP 而来改变内容的写入必然提交其吞吐上限是存储的写路径提交串行化不是前面的请求管线HTTP 并发不会抬高这个上限。争用以可重试的 503 而非停顿呈现。事务运行器内部消耗一个串行化重试预算耗尽后产生busy带Retry-After: 5。这个延迟刻意不是一秒预算已横跨观测到的好几秒写争用一秒回城会招来一列重试每列都占着一个数据库槽位等待恰好在服务器最忙时饿死读。饱和以同一 code 更短延迟呈现。请求在有界等待内拿不到数据库槽位也是busy带Retry-After: 1因为槽位压力消退很快。负载卸除不引入新状态词汇——一个 code、两种延迟头部是唯一要服从的东西常量定义见 problem.go。幂等写入不产生提交所以轮询确认自己仍持有认领的客户端不消耗写吞吐。读受槽位约束不受提交约束。每个触碰数据库的 handler 持有固定数量在途槽位之一每个槽位钉住一条 SQL 连接。连接预算的算术见运行手册。wisps瞬态记录在 v0 上不可认领认领只分派到 issues 表wisp id 会得到 404。七、The Claim读路径共享的精确边界设计文档把这段声明从internal/httpapi/doc.go与issueops/reader.go原样搬来、逐句可查并且刻意不在本页加强它SHARED共享GET /v0/beads/ready、GET /v0/beads/issues、GET /v0/beads/issues/{id}都走issueops.Readerbd show --json的详情视图在两条路由上也是。这没说到面上的其他读——它们在兄弟角色上。bd list与bd ready不在该角色上共享的是请求类型、internal/workapi中由 golden 文件钉住的两个 builder以及workapi.FinishPagebd list在两种模式下的两条路由——除层级--parent树外bd ready仅在其 proxied 路由上。ENFORCED被强制depguard规则httpapi-transport-boundary从internal/httpapi的每个非测试文件拒绝internal/workapi所以 builder 在那里不可调用一条forbidigo规则在那里连命名types.IssueFilter或types.WorkFilter都禁止所以手写的过滤器也写不出来。两者都是目录级作用域、无逐文件例外——明天加入该包的每个文件从存在那一刻起就被覆盖。同一条 forbidigo 规则以默认拒绝 64 个具名例外覆盖cmd/bd所以实现bd list/bd show的文件不能写过滤器它们被拆分或改名后的新文件也不行除非新名字上了那份名单。NOT ENFORCED未强制规则禁止的是命名那些类型而非持有值——所以属性是那里没有过滤器被写出来不是那里的过滤器都来自 builder。测试文件对两条规则都豁免因为 oracle 要持有过滤器以便检查它们。bd ready的文件在 64 个例外之列。GET /healthz与GET /v0/beads/context不是 issue 查询、不在任何角色上。而且这一切都不是合并门禁规则在每次 PR 的make ci-pr-lint中运行、聚合进ci-gate任务但 main 分支除禁止删除、禁止非快进外没有分支保护红灯靠约定约束。关闭其余操作需要更多角色认领角色、解释角色而不是给读角色加更多方法——这是角色设计每个能力一个角色接口、一个 accessor的核心原则在 reader.go 的Reader接口三方法Ready/List/Get上清晰可见。八、工作区模式嵌入 Dolt 被永久拒绝其余 SQL 模式全部可服务8.1 唯一被永久拒绝的模式bd serve永久拒绝且只拒绝一种工作区模式嵌入式 Doltembedded Dolt。它的提交协议在 SQL 事务之外、在独立连接上运行因此本契约承诺的每请求原子性在那里会是谎言。这是后端本身的属性而非迄今只做了这么多所以拒绝是永久的——也因此没有、将来也不会有它的 unit-of-work provider。在哪里强制文档原文stated exactly因为它以前靠构造强制、现在不再如此httpapi.Listen曾经只接受 unit-of-work provider 或什么都不接受所以嵌入式支撑的服务器根本不可构造——provider 缺席本身就是拒绝。现在httpapi.Config也接受两个 issue 角色作为数据库源而嵌入式 store 发布了两个 accessor于是它变得可构造。门禁是cmd/bd/serve.go里的serveDatabaseSource它分类工作区并拒绝既是门禁又是接线决策一个函数内二者无法对同一工作区产生分歧。TestServeRefusalsPromiseNothingserve_test.go钉住它拒绝且它的消息不承诺任何东西TestServeRefusesAnEmbeddedWorkspaceEndToEndserve_registered_backend_test.go通过runServe驱动该拒绝TestServeNamesOneDatabaseSourcePerServerItBuildsserve_test.go钉住cmd/bd构建的每台服务器恰好命名一个完整数据库源且 roles-backed 的服务器只在咨询该分类之处被构建。未强制internal/httpapi不拒绝嵌入式支撑的服务器也不能——角色是接口检查它无法揭示背后后端的提交协议该层可做的每次检查都是被检查代码自身的自我声明。前置条件因此写在Config.Reader/Config.Claimer上每次调用各自原子且持久地提交而bd之外的调用者把嵌入式角色交给服务器会得到一台每请求原子性声明为假的服务器本仓库没有任何东西拦得住。8.2 其余模式与注册后端带 SQL 服务器支撑的每种模式都可服务proxied托管或外部、server、external-server 与 shared-server。后三种里根命令已经打开过一个 serve 从不使用的DoltStoreserve 用同一套连接设置构建自己的 unit-of-work provider。那个闲置 store 只对连接预算有意义。注册后端从根命令打开的 store 被服务下游发行版注册一个后端internal/storage/backends其门面是 store 而非 unit-of-work provider而PersistentPreRunE已通过与其他普通bd命令相同的backends.Lookup分派打开它。因此 serve 在此臂上什么都不创建它从那个 store 取下Config.Reader与Config.Claimer交给Listen。第二个句柄会翻倍连接池、并可能与持有排他工作区锁的后端自我冲突——单一创建路径就是要点。PersistentPostRunE在runServe返回即服务器完全排空之后关闭 store。角色取自hook 装饰器之下(*storage.HookFiringStore).Unwrap只剥一层绝不用storage.UnwrapStore——其下的遥测层必须存活。store 的 accessor 按设计分发其装饰器所以理所当然的store.IssueClaimer()返回一个每次认领都跑工作区on_update脚本的 claimer——正是本服务器文档声明不做的。Listen拒绝 hook-firing 角色而不是信任有人读懂了这段话checkDatabaseSource见 server.go。分类在任何 Dolt 模式信号之前咨询注册表因为 store 打开已经按此顺序解析注册工作区即使导出了BEADS_DOLT_SHARED_SERVER1也打开其注册 store。反着解析会在非 Dolt store 上构建 Dolt provider从与同一目录下 CLI 所达不同的数据库应答 HTTP。单一 store被当作属性而非形状钉住TestServeAnswersFromTheStoreTheRootCommandOpenedserve_store_identity_test.go给注册后端接线让每次打开都交回一台其 reader 以该次打开命名的 issue 应答的 store并从GET /v0/beads/ready读回这个名字——自己开了个句柄的 serve 会以store-2应答。端到端测试还通过注册表数打开次数、要求整个进程恰好一次。两者都需要——计数抓泄漏名字抓替换——因为第二个句柄否则不可见同样的读、同样的认领、同样的握手、同样干净的关闭。8.3 身份握手感知后端GET /v0/beads/context曾对注册工作区报告backenddolt、dolt_modeembedded、databasebeads——在自动化被告知信任的、用于服务器身份的唯一端点上完整描述了本命令拒绝服务的拓扑而旁边的启动行却正确地命名了注册后端。根因在共享投影里GetContextInfo把后端硬编码为dolt并无条件复制 Dolt 字段而两者都是缺省而非失败缺失的 dolt_mode 读作 embedded缺失的 dolt_database 读作 beads。修复在投影domain.ContextInfo.SetBackendIdentity而非bd serve位置就是要点bd context、bd context --json与本端点都通过domain.PublishedContext读取工作区身份正是为了让三者不能对同一工作区命名出不同的样子。在runServe里纠正值会让 HTTP 握手诚实、却让 CLI 在同一目录继续打印backend: dolt——重新引入该投影本要防止的漂移。硬编码曾有两份所以是一次策略函数而非一次编辑contextinfo 用例以及bd context的直接路由后者自己读配置文件以便在打不开数据库的降级状态下也能应答。两者各自携带Backend: configfile.BackendDolt它们同意的方式是说同一个谎——此前与真的一致无法区分。TestContextRoutesNameOneWorkspaceTheSameWaycontext_identity_test.go比较两条路由对同一工作区发布的内容。注册后端对两者报告空字符串这也是bd唯一能断言的后端的Open从工作区读它想要的任何东西bd不实现它、无从知道它落在哪个逻辑数据库上任何非空猜测都是说得更轻的同一个谎。两者在线上保持必填字符串——没有字段被改名、改类型或删除v0形状不变。8.4 严格只读被拒绝bd --readonly serve不绑定两条源上都不绑定且门禁在工作区解析之前运行所以答案不依赖拓扑。本命令构建的每台服务器发布同一操作集认领在内。它曾在每条源上以不同方式静默降级store 源上根命令通过backend.OpenReadOnly打开、serve 从该 store 取认领者于是服务器照常绑定、继续宣传issues.claim、对每次认领回一个不透明的 500issue 保持打开且未分配provider 源上serve 用工作区连接设置构建自己的 unit-of-work provider不携带只读姿态--readonly什么都没买到、每次认领照常落库。Proxied 模式两者都到不了根 pre-run 已为它拒绝严格只读。备选方案——从只读服务器的已宣传 capabilities 里去掉issues.claim——被作为线上变更否决capabilities是客户端调用前检查的文档化预检让一个操作的存在性依赖启动该服务器的进程上的标志会交给客户端一个它在连接前无法发现的东西。拒绝让已发布的面保持构建的属性并与bd在下一层回答同一问题的方式一致不能保证无变更访问的后端被拒之门外而不是照常打开。九、部署与运维速查源自 SERVE_RUNBOOK.md显式端口bd serve --addr 127.0.0.1:7777。默认127.0.0.1:0取临时端口适合临时与测试——但没有互斥两台 serve 对同一工作区以:0绑定会各占一个不同临时端口并行运行、无法枚举。固定端口上第二进程绑定失败——这是本命令唯一的互斥机制。并发 serve 无论哪种都数据安全认领在 SQL 服务器中仲裁不在 HTTP 进程里。流stdout 恰好一行、在绑定时输出bd serve: listening on http://127.0.0.1:7777——临时端口调用者读这一行就停。其余一切含请求日志走 stderr两者可分开重定向。信号bd serve前台运行对SIGHUP与 SIGINT、SIGTERM 一样优雅关闭。关闭前台bd serve的终端会停掉它而不是留下孤儿占着端口和连接池。鉴权示例bd serve --addr 127.0.0.1:7777 --auth-token-file /run/secrets/bd-tokens。/healthz是唯一豁免路由kubelet 探针没有凭据会 401 的存活端点就是永远重启的 pod就绪探针需要令牌。轮换 原子重写文件临时文件 renameKubernetes secret 挂载已经如此约一秒内生效。非环回示例bd serve --addr 0.0.0.0:7777 --auth-token-file /run/secrets/bd-tokens --allowed-host bd.internal.example。仍无 TLS需自备机密性service mesh 或可信网络边界。探针存活GET /healthz不碰数据库数据库挂死也保持绿——所以它是正确的存活探针、无用的就绪探针就绪GET /v0/beads/ready?limit1真实查询200 就绪503 活着但未就绪code区分db_unavailable与busy两者都带Retry-After。就绪探针建议超时 2–5 秒。检测卡死wedge数据库停止应答而进程健康时/healthz看不见。三个信号eventsemaphore_saturated等槽位 ≥1 秒outcomeacquired/abandoned、eventsemaphore_timeout等满 10 秒被 503busy卸除、eventconn_cap_saturated到达 64 连接上限边沿触发。连接预算每进程稳态约~22 条连接handler 池maxInflight 4 20条打开 / 16 条空闲server/external-server/shared-server 模式下根命令闲置DoltStore1–2 条。共享服务器max_connections需覆盖约22 × (bd serve 进程数)加其他指向同服务器的bd进程。provider 不暴露池旋钮时启动打eventpool_limits_unavailable并以无界池运行——务必检查该行。maxConns 64限制已接受的 TCP 连接maxWatchStreams 48限制GET /v0/beads/events:watch流刻意比连接上限低 16让 503 可送达。关闭与歧义认领SIGINT/SIGTERM/SIGHUP 后停止接受并排空至多20 秒排空刻意不取消在途 handler 上下文一次认领在其串行化重试预算 提交内必须被覆盖。排空超限则剩余连接被关闭并打eventshutdown_forced——这是客户端可见的歧义情形中途被杀死的认领可能已落库。恢复 以同一 actor 重新认领并读结果认领是 compare-and-set同 actor 重认领幂等若第一次成功第二次发现 actor 已持有、返回 200 且不写提交若别人中途赢了得到类型化 409already_claimed携带assignee——那是真实答案不是要重试的错误。绝不要从断连推断结果也绝不要回退到读 issue 然后猜。请求日志stderr 上一请求一行eventrequest request_id前缀-序号 op… status… code… duration_ms… sem_wait_ms… uow_ms… conns… remote_addr…。request_id回显进任何 problem body5xx 的detail固定所以它是对接eventrequest_error那条真实错误的唯一把手。值含空格///控制字符时加引号防字段伪造与终端 CSI 注入。其他事件startup含authnone|bearer (path)、auth_refusedreasonmissing|malformed|unknown_token、auth_reload_error、limits、panic、shutdown_*、events_watch_*等。启动即拒绝的常见错误由resolveServeConfig在打开数据库源或监听器之前统一抛出与工作区无关嵌入 Dolt 工作区bd serve requires a Dolt SQL server; this workspace uses embedded Dolt永久拒绝严格只读bd serve is unavailable under strict readonly--addr给了 DNS 名用127.0.0.1而非localhost非环回--addr缺--allow-non-loopback--allow-non-loopback缺令牌文件--insecure-no-auth配在环回上或与令牌文件同时出现令牌文件不可读/是目录/无令牌/大于 1048576 字节address already in use固定端口互斥按设计工作。十、给采用客户端的最小集成清单基于全文契约一个 v0 客户端应当先握手调用GET /v0/beads/context需要鉴权时带 bearer从capabilities判断操作级支持从 400unknown_parameter判断参数级支持读api_version与项目身份字段但不要分支于schema_version。只分派code对同状态类内未知 code 默认分支用类型化 409 替代旧的子串匹配分类reason区分参数版本偏差与值非法5xx 一律按Retry-After重试4xx 一律换请求而非重试。遵守游标纪律游标逐字保留过滤器、变更过滤器即开新遍历、换sort得到invalid_cursor后从零重启分页、created/priority之外的需求自排序优先使用快照-重新查询处理 ready。理解写路径语义写入按请求持久化、无 hooks、无进程结束冲刷轮询确认用幂等重写不消耗提交争用与饱和都是 503busy唯Retry-After是尊5 秒 vs 1 秒actor是自证来源认领 CAS 防并发双赢而非授权。探测就绪而非存活/healthz只证明进程活着数据库状态问GET /v0/beads/ready?limit1带令牌并用semaphore_saturated/semaphore_timeout等日志事件区分卡死与安静。延伸阅读设计文档的权威实现位于 routes.go路由表、problem.go错误映射与词汇表、cursor.go游标编码、server.go操作包络常量、Listen/Serve、数据库源检查与 auth.go令牌文件鉴权读角色契约见 reader.go运维细节以 SERVE_RUNBOOK.md 为准其上标注了每个数字都是internal/httpapi/server.go中的常量暂未开放为标志变成标志时线上契约不变。【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进