本文是 push_policy=trunk 与 storage-only HTTP 的运维操作手册。用户可见的路径、分支、Tag 和推送规则见使用指南;N 分流、不变式、墓碑和路径索引的设计细节见 refactoring/trunk-push.md。配置键以 config/config.toml 为准。
范围:不接入 mega2 用户系统、不使用 Issue / Change List、不走评审门控的存储与分发部署。
push_policy默认为trunk,config/config.toml默认配置使用push_auth = "none"(仅限本机或受信网络);仍需 CL / 评审管线的部署须显式设置push_policy = "review",示例见config/config-review.toml。
该部署形态提供 Monorepo Git 服务;Agent Session Capture 是独立的可选 HTTP 接口,需单独启用和配置 ingest token,不会由 Git push 自动产生会话记录。配置条件与数据接口见 refactoring/agent-capture.md。
[monorepo]
push_policy = "trunk"
max_push_commits = 250 # trunk 链长;review 仍用 MAX_CL_CHAIN_COMMITS = 250
[git]
push_auth = "token" # 或 "none";缺省(省略)拒绝启动
ssh_receive_pack = false # storage-only 必须显式写 false,省略会拒绝启动
# [[git.push_tokens]] 见第 3 节CL merge 只经 MonoWriteQueue;[monorepo] 无 merge_writer 键(plan-20260910.md)。残留该键按未知字段拒绝启动。
[monorepo].object_format:sha1 为标准 Git;sha256 与 blake3 是 git-internal / Libra extension,不宣称与标准 Git 客户端互通(refactoring/protocol.md,plan/plan-20260907.md)。Git Object Format 与 LFS Digest 独立:Git=blake3/LFS=sha256 与 Git=sha256/LFS=blake3 可表达;LFS BLAKE3 业务面属于 DEFER-B3-LFS-01。
产品 API 写(POST /api/v1/create-entry、POST /api/v1/delete-entry、POST /api/v1/move-entry、POST /api/v1/edit/save)在 trunk 下经 git.push_auth 鉴权后,将对象写入存储并用 MonoWriteQueue 前进 path tip(与 git push 同 tip 权威;见 plan-20260904.md)。delete-entry / move-entry 在 is_directory=false 时可删 / 移文件(省略字段仍只针对目录;见 refactoring/directory-entry-api.md)。成功响应的 cl_link 为 null,不创建 mega_cl / refs/cl/*。写后同栈 git clone / git pull 可读到新内容。集成黑盒见 refactoring/integration.md 的 integration_api_write_trunk。monorepo tag 写(POST /api/v1/tags、DELETE /api/v1/tags/{name};plan-20260917 LB-04 起挂在 storage-only)同样经 git.push_auth 鉴权——create 以 path_context(缺省 /)、delete 以选择器 path(省略或空 = /):省略 path 的 delete 与 root / 缺省 path_context 的 create 需要能覆盖 / 的 token(paths 省略/空 = whole repo),?path=/project 的 delete 与非根 path_context 的 create 可用覆盖该 path 的 token——但只写 tag 元数据(refs/tags/*;注解 tag 另写 mega_tag.path),不经 MonoWriteQueue、不前进 path tip;GET /api/v1/tags/list 与 GET /api/v1/tags/{name} 不要求 Authorization;POST /api/v1/tags/list 回 405。
产品写的落点:写路径之上已有非根 main tip 时,在最深的那个 tip 上落地(与此前相同)。全新栈上(service init 之后只有根 / 有 tip)不需要先 clone 任何祖先路径:写路径合法时(首组件列在 root_dirs 中、不在 import_dir 之下),服务端先惰性物化其一级根(例如 /project,与 clone / ls-remote 触发的物化是同一原语),再在其上落地,落地 commit 的父即物化 commit。例外:import_dir 嵌套在某个根之下时(如 /third-party/vendor),该一级根(/third-party)是 import_dir 的严格祖先,服务端永不物化它(否则其下的 ImportRepo 挂载会被拒绝),该根下尚无 tip 的写返回 409 MONO_PATH_UNINITIALIZED,需先开通更深的非 import 路径。以下写入在落点解析之前返回 400 MONO_PATH_NOT_ALLOWED: …(错误契约见 errors.md 的 PathPolicyError 小节):根外路径(首组件不在 root_dirs 中,如 /vendor/x 或已存在的 /.cedar);在 / 下新建顶层目录,以及共同父为 / 的跨顶层 move / 顶层目录删除(产品写从不在 / 落地);import_dir 命名空间内、不经 ImportRepo 处理的操作数:import_dir 本身;从父目录删除 / 移动挂载叶子(path=/third-party、name=<repo>——不论该仓库是否存活,因为请求按父路径分派到 Monorepo);git_repo 行已不存在(清理之后)的叶子内部路径;以及移动的目的地(不论 main@/third-party 是否曾被物化——ImportRepo 只由 Git 推送创建)。存活 ImportRepo 内部的路径仍按 git_repo 分派给 ImportRepo 处理,行为不变(edit/save 可写;delete / move 返回既有 409;目的地是存活 ImportRepo 的 move 返回 409)。错误消息点名用户写的路径;只有跨顶层 move 与删除顶层目录(首组件是合法根、但落点只能是 /)点名 /。MONO_PATH_UNINITIALIZED 消息提到的开通 CLI / API 在后续版本提供(plan-20260923 FU-07 / FU-08)。同一落点 tip 上已有排队或执行中的写入时(全新栈上即同一一级根,例如同时写 /project/a 与 /project/b),第二个并发写仍按 ADR-TP-10 被拒(目前表现为 500,见 plan-20260923.md DEFER-FU-12),客户端应串行写或重试。
启动期 fail-closed(Config::validate / AppContext::new):
trunk+cedar.enforcement != "off"→ 拒绝。trunk+ 仍有 open CL → 拒绝。push_policy变更且push_queue有非终态行 → 拒绝(须排空/取消)。push_auth ∈ {token, none}⇒ 必须push_policy=trunk。push_policy=trunk⇒ 必须显式push_auth。- storage-only(显式
push_auth)⇒ 必须git.ssh_receive_pack = false(省略 ≠ 关闭)。 - 形态切换后执行索引水位重置(第 5 节)。
HTTP 表面:只读 preview + 产品写(create-entry / delete-entry / move-entry / edit/save;目录变更契约见 refactoring/directory-entry-api.md)+ monorepo tags(POST /tags、GET /tags/list、GET / DELETE /tags/{name};LB-04 / FT-04 / FT-06;POST /tags/list 为 405)+ Git smart HTTP + LFS(/info/lfs、/api/v1/lfs);不注册 CL / issue / reviewer / OAuth user 路由;OpenAPI(/api/openapi.json)列出 LFS 与上述写路径,CL/issue 为空。只读 blob/tree/blame 保留。
Trunk 没有 review 门控与 Cedar 判定:cedar.enforcement 必须为 off,授权快照不构建、不被消费。
保留 UN-16 的写入侧钩子(拒绝删除 refs/heads/main + notify)不构成授权保护。那是结构性保护(快照若将来开启仍以 main 的 /.mega_cedar.json 为源),不是评审或 Cedar。
仍然生效的访问控制:
push_auth=token:静态 token 常量时间查找;命中后身份为 token 名。paths前缀按组件边界授权(/project/foo不授权/project/foobar)。认证身份与 commit author 分离——author 是自声明 provenance,不参与判定。Git receive-pack、LFS 批/锁写与 产品 API 写共用该模型(见第 6 节与上文 API 写段)。无凭据 / 坏 token → HTTP 401;path 越权 → HTTP 403。push_auth=none:允许无凭据 API 写(requester=anonymous),仅适用于受控网络(第 3 节)。- 对象存储与 pack 收发仍按既有存储配置。
- Git 客户端 tag 仍禁止。
凭据经 SecretRef / 文件挂载注入,不要把明文写进提交的 config.toml。
[git]
push_auth = "token"
[[git.push_tokens]]
name = "agent-ci"
token = "${file:/run/secrets/mega2-push-token}"
paths = ["/project"] # 省略或空 = 全库push_auth=none 必须写在配置里(省略 ≠ none)。它旁路 git HTTP 的 token/OAuth 前置,只适用于受控内网、回环或 Unix socket 前置的部署。对公网暴露 none 等于匿名 receive-pack 以及匿名 LFS 上传(批/锁写与 receive-pack 同级)。启动会打可诊断警告;SSH receive-pack 在 storage-only 下仍关闭(第 4 节),因此「无凭据推送 / 无凭据 LFS 写」只可能出现在你显式打开的 HTTP 面上。
Storage-only(显式 push_auth)不暴露 SSH receive-pack。启动要求配置里写 ssh_receive_pack = false;省略该项会 fail-closed,不是默默关闭。需要推送时用 Git smart HTTP + token(或 none + 网络边界)。
ssh_receive_pack = false 不关闭 SSH clone/fetch/pull(upload-pack)。storage-only 读与 HTTP 共用 git.anonymous_access:
anonymous_access = true:SSHauth_none放行,无需 UserStorage 公钥、无需 password。push_auth = "none"且anonymous_access = false:SSH 读 fail-closed(无 password 通道;勿在 trunk+none 依赖 UserStorage)。push_auth = "token"且anonymous_access = false:SSHauth_password,password 字段 =[[git.push_tokens]]密文(与 HTTP Basic 只取 password、忽略 username 对齐)。客户端用SSH_ASKPASS/sshpass/ 定制GIT_SSH_COMMAND。storage-only 仍不提供 SSH receive-pack。
review 形态(省略 push_auth)仍走 UserStorage 公钥;默认匿名开时 不 成功放行 auth_none,以免 OpenSSH 跳过公钥推送。
| 方向 | 前置 | 索引 |
|---|---|---|
review → trunk |
无 open CL;排空 push_queue 非终态行;cedar.enforcement=off;显式 push_auth |
UPDATE blob_paths SET indexed_push_id = NULL(RESET INDEX WATERMARK) |
trunk → review |
同样排空非终态行 | 必须重置水位,否则 review 只写 IS NULL 行会使已有水位永久失效 |
trunk → review 不重建 roll-up 之前的 CL 历史。切回后新的推送重新走 CL 管线。
Trunk / storage-only 挂载 /info/lfs 与 /api/v1/lfs。LFS 批/锁写授权与 Git receive-pack 共用 git.push_auth(plan-20260909.md ADR-LF-01;supersede plan-20260905 TP-18「关闭 LFS」产品决策,不回改该卡历史验收):
push_auth |
LFS 读 | LFS 写(batch upload / locks) |
|---|---|---|
none |
匿名允许 | 匿名允许(与匿名 receive-pack 同级网络边界;见第 3 节警告) |
token |
anonymous_access 或有效 token |
有效 [[git.push_tokens]] 且 paths 覆盖 LfsRepoContext 路径 |
Object PUT/GET 仍走 batch 注册后的能力 URL,不逐请求鉴权。Review 形态(省略 push_auth)仍走 UserStorage mono access token。SSH git-lfs-transfer 不在 storage-only 交付范围;LFS 走 HTTP。
不再把 token-aware LFS 列为未决议题。多 token 轮换/审计/限流仍属 DEFER-TP-05(第 7 节),与 LFS 是否可用无关。
基础认证(单/多 token 查找、组件边界前缀、none 旁路)已随阶段 4.2a(TP-19/20)交付。Token 轮换、审计、限流等运维强化未规范,登记为 DEFER-TP-05(重启条件:trunk-push.md 阶段 5 补齐可执行规范)。
仓库根 docker/docker-compose-storage-only.yml 对照 IT 的 docker/docker-compose.test.yml,保留 Postgres / Redis / RustFS / mega2,挂载 config/config-storage-only.toml 与 /run/secrets/mega2-push-token(默认 secrets/mega2-push-token.local)。不含 website、mailpit、OAuth。
对象存储默认 RustFS(s3compatible)。默认启动不要加 --env-file;只有把 mega2 改成本地文件系统后端时,才需要 --env-file config/compose.env.storage-only.local(RustFS 容器仍会启动,仅切换 mega2 的 storage_type)。
# 默认 RustFS(s3compatible)——无需 --env-file
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml up -d --wait
# 空卷 bootstrap(可复制)
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml exec -T mega2 \
mega2 --config /etc/mega2/config.toml service init --yes
# 干净重跑(破坏性:删 named volume)
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml down -v
# 可选:mega2 改用本地文件系统(仅此情况加 --env-file)
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml \
--env-file config/compose.env.storage-only.local up -d --wait
# HTTP http://127.0.0.1:9000/ SSH ssh://git@127.0.0.1:2222/
# RustFS API/console: 127.0.0.1:29000 / 29001
# push token 默认: mega2-storage-only-local-dev-token-0001presigned URL 签名主机(plan-20261001 FIX-BB-03):mega2 在网内经 http://rustfs:9000 读写对象,compose 默认把 MEGA_OBJECT_STORAGE__S3__PUBLIC_ENDPOINT_URL 设为 http://127.0.0.1:29000,artifacts / LFS 的 presigned URL 按宿主发布的 RustFS 端口签名,宿主机客户端可以直接跟随(键语义见 orbit;网内 LFS 见 §8.1)。宿主侧往返检查(batch → presigned PUT → commit → 跟随 302 下载 → 比对 sha256,整轮限时 60 秒,可用 MEGA2_PRESIGN_CHECK_BUDGET 调整):
MEGA2_BASE_URL=http://127.0.0.1:9000 MEGA2_IT_SEED_TOKEN="$(cat secrets/mega2-push-token.local)" \
scripts/artifacts_presign_host_check.sh脚本逐步打印 presign-put: <status>、commit-status: <status>、sha256 match;只有上传 URL 与下载跳转目标都按 MEGA2_PRESIGN_ORIGIN(默认 http://127.0.0.1:29000)签名、presigned PUT 为 2xx、commit 为 ok、下载内容 sha256 一致时退出 0,缺少工具或 token 时退出 2;token 不回显,签名 URL 打印时去掉 query。
互通黑盒 runner interop-smoke(profile interop,plan-20261001;登记见 test-infra)不随默认 up 启动,需要时单独构建并启动:
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile interop up -d --wait --build interop-smoke容器内的环回中继让 127.0.0.1:9000(HTTP)、127.0.0.1:2222(SSH)、127.0.0.1:29000(RustFS)与宿主机地址一致。
opt-in case(需要切换 mega2 配置的 case)一律经宿主侧 helper 运行,helper 用 trap 保证无论成败都把 mega2 恢复为默认配置;切换后与恢复后都读取运行中 mega2 容器的 compose config hash 标签,分别与切换配置、默认配置的 hash 比对(plan-20261001 FIX-BB-07):
scripts/bb_optin_run.sh <none|gc|local|views> <script> '<case>' target/tmp/<log>
scripts/bb_optin_run.sh --selftest local target/tmp/<log> # 自测:桩命令退出 1views 模式叠加 docker/docker-compose-storage-only.views.yml,只接受 libra_view_smoke.sh;切栈前重建独立库 mega2_hp25_views 并清空 Redis 逻辑库 1。--selftest views 执行 reset、切栈、桩命令及默认栈恢复,不执行 prepare。
scripts/bb_optin_run.sh退出码 0:case 通过,且 mega2 已恢复默认配置(log 含bb-optin: restored mega2 (config hash match))。scripts/bb_optin_run.sh退出码 1:case 失败,或切换后运行中 mega2 容器的 compose config hash 标签不是切换配置的 hash(case 未运行);两种情况下 mega2 都已恢复默认配置。scripts/bb_optin_run.sh退出码 2:参数或前置错误(未知模式、env-file 缺失、脚本与模式不匹配、另一个 helper 持有栈锁),未切栈。scripts/bb_optin_run.sh退出码 3:恢复失败(运行中 mega2 的 config hash 不是默认配置的 hash,或 mega2 不健康),需人工执行docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml up -d --wait --force-recreate mega2。
Trunk / storage-only 协议冒烟使用 scripts/git_protocol_smoke_storage_only.sh,经 compose --profile smoke 的 git-smoke 容器内真实 git / git-lfs / ssh 调用已发布端口。这是 compose 黑盒,与 cargo CARGO_BIN_EXE 黑盒分层并存;不要用 libra 作协议客户端,也不要在 trunk 上跑 review/CL 语义的 scripts/git_protocol_smoke.sh。
产品 API 写 → Git 可见性 另用 scripts/api_write_smoke_storage_only.sh(curl + git;同样禁止 libra 客户端):
作用域注记(2026-10-02,plan-20261001 ADR-BB-03):本条约束 Git 协议 smoke 与 API 写 smoke 的客户端;Libra↔mega2 互通黑盒(
scripts/libra_smoke_storage_only.sh)以 libra 作被测客户端,见 plan-20261001。
TOKEN='mega2-storage-only-local-dev-token-0001'
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile smoke \
exec -T \
-e MEGA2_HTTP_REPO_URL="http://x:${TOKEN}@mega2:8000/" \
-e MEGA2_API_BASE=http://mega2:8000 \
-e MEGA2_IT_SEED_TOKEN="${TOKEN}" \
git-smoke bash /repo/scripts/api_write_smoke_storage_only.sh稳定 case:API create-entry then git clone sees file、API edit/save then git pull sees update、API write rejects unauthenticated;plan-20260917 LB-05 追加 delete-entry-git-visible、move-entry-git-visible、tags-list-create-delete、delete-entry-unauth-401;plan-20260918 FT-07 追加 delete-file-git-visible、move-file-git-visible、tags-get-list-create-delete、tags-path-get-delete、delete-file-unauth-401(文件删移后 git clone / git pull 工作树与 GET /tree 一致;匿名 GET list 200、POST list 405;?path= get/delete 隔离;未授权文件 delete 401)。单 case 用 MEGA2_SMOKE_CASE=<精确名>(未匹配退出 2),-h 列出全部 case 名。summary 须 0 failed——全量运行请在干净栈上(down -v 重建 + service init --yes)执行,或用 MEGA2_SMOKE_CASE 只选所需 case:AW-04 的 API create-entry then git clone sees file 用固定文件名,在同一栈上第二次全量运行会撞重名 500 而 failed=1(LB-05 / FT-07 的具名 case 用每次运行唯一的名字)。可被 plan-20260906 吸收为附加 case(见 docs/refactoring/test-infra.md)。矩阵行见 refactoring/integration.md 的 integration_api_write_trunk。
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile smoke \
exec -T \
-e MEGA2_HTTP_REPO_URL=http://mega2:8000/ \
git-smoke bash /repo/scripts/git_protocol_smoke_storage_only.sh单 case:MEGA2_SMOKE_CASE='HTTP ls-remote'(精确 case 名;未匹配退出非 0)。已注册只读 case:HTTP ls-remote、HTTP clone、HTTP fetch、HTTP protocol v2 fetch、HTTP shallow clone depth=1、HTTP protocol v2 ls-remote、HTTP protocol v2 blob:none clone。写 case(需 MEGA2_GIT_SMOKE_PUSH=1 + token / MEGA2_IT_SEED_TOKEN):HTTP trunk push——对 /project(非根 /;B0 拒根路径)断言 tip 前进;不以新建 refs/cl 为成功条件。根 URL 入参时脚本自动改写为 /project。另:HTTP reject Git-client tag push(tag push 非 0 且远端无残留 tag)。负例(默认 token 栈、无凭据 URL):HTTP reject unauthenticated push。
Token 写示例:
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile smoke \
exec -T \
-e MEGA2_HTTP_REPO_URL=http://mega2:8000/ \
-e MEGA2_IT_SEED_TOKEN=mega2-storage-only-local-dev-token-0001 \
-e MEGA2_GIT_SMOKE_PUSH=1 \
-e MEGA2_SMOKE_CASE='HTTP trunk push' \
git-smoke bash /repo/scripts/git_protocol_smoke_storage_only.shpush_auth=none 栈(ADR-SO-06):双 -f 覆盖挂载 config/config-storage-only.none.toml,并 --force-recreate mega2。opt-in case:HTTP trunk push (none)(无凭据 tip 前进)、HTTP reject Git-client tag push (none)(tag push 非 0 且远端无残留)。
docker compose -p mega2-trunk \
-f docker/docker-compose-storage-only.yml \
-f docker/docker-compose-storage-only.auth-none.yml \
up -d --wait --force-recreate mega2
docker compose -p mega2-trunk \
-f docker/docker-compose-storage-only.yml \
-f docker/docker-compose-storage-only.auth-none.yml \
--profile smoke \
exec -T \
-e MEGA2_HTTP_REPO_URL=http://mega2:8000/ \
-e MEGA2_GIT_SMOKE_PUSH=1 \
-e MEGA2_SMOKE_CASE='HTTP trunk push (none)' \
git-smoke bash /repo/scripts/git_protocol_smoke_storage_only.sh切回默认 token 样例:去掉第二个 -f,再 --force-recreate mega2。
LFS 网内 URL(ADR-SO-04):--env-file config/compose.env.storage-only.lfs-innetwork 把 MEGA_HTTP__PUBLIC_BASE_URL / MEGA_LFS__SSH__HTTP_URL 设为 http://mega2:8000,并设 MEGA_OBJECT_STORAGE__S3__PUBLIC_ENDPOINT_URL=http://rustfs:9000(presigned LFS URL 改按网内 RustFS 签名,plan-20261001 FIX-BB-03),供 git-smoke 容器内 LFS href 可达。opt-in case:HTTP LFS push and pull (trunk)(需 MEGA2_GIT_SMOKE_PUSH=1 + MEGA2_GIT_SMOKE_LFS=1 + token)。
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml \
--env-file config/compose.env.storage-only.lfs-innetwork \
up -d --wait --force-recreate mega2
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile smoke \
exec -T \
-e MEGA2_HTTP_REPO_URL=http://mega2:8000/ \
-e MEGA2_IT_SEED_TOKEN=mega2-storage-only-local-dev-token-0001 \
-e MEGA2_GIT_SMOKE_PUSH=1 \
-e MEGA2_GIT_SMOKE_LFS=1 \
-e MEGA2_SMOKE_CASE='HTTP LFS push and pull (trunk)' \
git-smoke bash /repo/scripts/git_protocol_smoke_storage_only.sh切回宿主机 LFS URL:去掉 --env-file,再 --force-recreate mega2。
SSH 只读(anonymous_access=true → auth_none,不依赖 UserStorage 公钥)。首次连接写入 known_hosts:
# Inside git-smoke (or any OpenSSH client on the compose network):
export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/mega2-smoke-known_hosts'
# Or omit GIT_SSH_COMMAND: scripts/git_protocol_smoke_storage_only.sh defaults the same
# StrictHostKeyChecking=accept-new + a workdir UserKnownHostsFile.docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile smoke \
exec -T \
-e MEGA2_SSH_REPO_URL=ssh://git@mega2:2222/ \
-e MEGA2_SMOKE_CASE='SSH ls-remote' \
git-smoke bash /repo/scripts/git_protocol_smoke_storage_only.sh已注册 SSH case:SSH ls-remote、SSH clone、SSH fetch、SSH protocol v2 fetch、SSH shallow clone depth=1、SSH protocol v2 ls-remote、SSH protocol v2 blob:none clone、SSH reject receive-pack(未设 MEGA2_SSH_REPO_URL 时后者 SKIP)。
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile smoke \
exec -T \
-e MEGA2_SSH_REPO_URL=ssh://git@mega2:2222/ \
-e MEGA2_SMOKE_CASE='SSH reject receive-pack' \
git-smoke bash /repo/scripts/git_protocol_smoke_storage_only.sh结果获取(stdout 为主;推荐宿主 tee):
mkdir -p target/tmp
LOG="target/tmp/so-smoke-$(date -u +%Y%m%dT%H%M%SZ)-${MEGA2_SMOKE_CASE:-all}.log"
set -o pipefail
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile smoke \
exec -T git-smoke bash /repo/scripts/git_protocol_smoke_storage_only.sh \
2>&1 | tee "$LOG"排障工作区(非 PASS 主证据):宿主 ${MEGA2_IT_GIT_WORKDIR:-/tmp/mega2-trunk-git}(挂载为容器 /work)。
与 -p mega2-it 的测试栈可并存(端口 25432 / 26379 / 9000 / 2222 / 29000)。生产凭据用 MEGA2_PUSH_TOKEN_FILE 覆盖,勿提交。
每次 trunk 推送后执行:
git fetch && git reset --hard origin/mainN = 1 时为 no-op;N > 1 时对齐到 squash tip。子路径 clone(例如 /project/foo),不要对 / 做根 clone 作为该形态的假设。
0.38.24 之前,产品写 API(create-entry、edit/save、delete-entry、move-entry,含 ImportRepo 的 edit/save)、review 形态的 CL merge 与旧式后代续接合成的未签名 commit 缺少头/体空行:git log --format=%s 显示空主题,git fsck(含 --strict)报 unterminatedHeader,开启 transfer.fsckObjects / fetch.fsckObjects 的客户端拒绝 clone / fetch 历史中含这类对象的路径。升级后新写入的 commit 已成帧;已有对象按 commit 身份不改写(plan-20260923.md ADR-FU-02 / DEFER-FU-07)。
unterminatedHeader 属于 Git 不允许降级的致命类检查(fetch.fsck.unterminatedHeader=ignore 会报 Cannot demote),需要严格校验的客户端改用按对象跳过的 skip list:
git -c transfer.fsckObjects=false -c fetch.fsckObjects=false clone <url> probe
LC_ALL=C git -C probe fsck 2>&1 | sed -n 's/^error in commit \([0-9a-f]*\): unterminatedHeader.*/\1/p' > mega2-fsck-skiplist.txt
git -c transfer.fsckObjects=true -c fetch.fsck.skipList="$PWD/mega2-fsck-skiplist.txt" clone <url> work本地 git fsck 同样可加 -c fsck.skipList=<同一文件>。skip list 只豁免列出的对象,其余对象仍按严格规则校验。
升级到 0.40.4 后的首次启动执行一次迁移 m20260923_000200_canonicalize_import_repo_paths:git_repo 中 repo_path 不是规范形式的行,即 TP-08 之前写入的旧别名(重复斜杠、尾斜杠、. 段,例如 /third-party//foo/),改写为规范路径(/third-party/foo),repo_id 及其 ref 与对象行不变。迁移按行本身判定,与 monorepo.import_dir 无关,非默认的 import 根同样覆盖;整个改写在一个事务内完成,重复执行无变化,无需配置。协议层的 TP-08 规范化保持不变,旧别名 URL 仍命中同一仓库(回归用例 import_repo_alias_*,plan-20260923.md FU-15)。
启动日志输出一行汇总:
canonicalized ImportRepo alias paths rewritten=<N> collisions=<M> invalid=<K>
| 计数 | 含义 |
|---|---|
rewritten |
已改写为规范路径的别名行数。 |
collisions |
规范路径已被另一行占用而保留原样的别名行数:同一仓库已分裂为两个 repo_id。每行另有一条 WARN,只含 repo_id 与规范路径。 |
invalid |
路径无法规范化(例如含 .. 段或 \)而保留原样的行数,每行另有一条 WARN,只含 repo_id。 |
两个计数都为 0 时无需任何操作。
collisions 不为 0 时,同一仓库已分裂为两个 repo_id(升级前即已如此,迁移不改变它):
- Git 请求(clone / fetch / push)一律按规范路径解析,命中占用规范路径的那一行;
- Web / API 按路径前缀查找仓库时取最长的已存路径:别名以
/或/.结尾时,规范路径之下的文件、tag、commit 的查询与写入仍落到别名那一行; - 后续版本的 ImportRepo 清理按规范路径精确查找仓库:不删除这类别名行,也不把它们算作子仓(计划 ADR-FU-10 第 3 条、
DEFER-FU-10)。
处理步骤:
-
用 WARN 中的
repo_id与规范路径对照两行及其 ref:SELECT id, repo_path, created_at, updated_at FROM git_repo WHERE id = <WARN 中的 repo_id> OR repo_path = '<WARN 中的规范路径>'; SELECT repo_id, ref_name, ref_git_id FROM import_refs WHERE repo_id IN (<两个 id>) ORDER BY ref_name, repo_id;
-
不要手工改写或删除两行中的任何一行:自动合并尚未提供(计划
DEFER-FU-10),请附上述查询结果提交 issue。别名行的数据不会被迁移删除。
invalid 不为 0 时,计入的行没有规范形式,因此也没有对应的规范行:协议层对这类路径的请求一律拒绝(TP-08 规范化失败),升级前后都不被服务,迁移不改动它们。按 WARN 中的 repo_id 查看:
SELECT id, repo_path, created_at, updated_at FROM git_repo WHERE id = <WARN 中的 repo_id>;
SELECT ref_name, ref_git_id FROM import_refs WHERE repo_id = <同一 id> ORDER BY ref_name;需要其中的内容时同样不要手工改写该行,请附查询结果提交 issue。
架构与端点事实源:refactoring/oci.md(plan-20260902)。本节只覆盖运维启用与 docker login 认证语义。
/v2 双重门:必须同时满足
- storage-only:配置显式
git.push_auth(token或none)——与第 3 节相同; [oci] enabled = true。
缺一则不挂载 /v2(裸 404)。enabled=true 且非 storage-only → 启动拒绝。config/config-storage-only.toml 样例已含启用段;review / 省略 push_auth 的形态不得打开本开关。
认证复用 [[git.push_tokens]](无独立 OCI token 服务):
# username 可为任意值;password = push token 密文
docker login <host> -u oci -p '<push-token>'
docker tag <local-image> <host>/<repo>:<tag> # repo 可为多段,如 team/app
docker push <host>/<repo>:<tag>
docker pull <host>/<repo>:<tag>HTTP 明文 registry(如本机 compose http://127.0.0.1:9000)需在客户端配置 insecure registry;生产应终止 TLS。
宿主机 live smoke:bash scripts/oci_smoke_storage_only.sh(默认 registry http://127.0.0.1:9000;token 见 MEGA2_OCI_SMOKE_TOKEN 或 secrets/mega2-push-token.local)。docker / 端点不可达时脚本 SKIP 并以退出码 0 结束。
| 面 | 行为 |
|---|---|
| 写(push / upload) | push_auth=token:Basic/Bearer 取 token,且 paths 必须覆盖 "/" + repo;否则 DENIED。push_auth=none:写全放行(同第 3 节受控网络前提)。 |
| 读(pull / ping) | 跟随 git.anonymous_access:开 → 无凭据可读;关 → 需有效 token。无效凭据恒 401(含 GET /v2/,保证 docker login 失败可呈现)。 |
| 跨仓 mount | 目标写授权 + 源读授权。 |
manifest/blob DELETE 路由存在但恒返回 OCI UNSUPPORTED(405)。_catalog / referrers 未实现。
plan-20261001 的 OCI 黑盒脚本 scripts/oci_client_smoke_storage_only.sh 在 interop-smoke 容器内运行(客户端为 oras 与 curl,不用 libra),经容器内环回中继访问 http://127.0.0.1:9000/v2/。先按 §8 启动栈与 interop-smoke,单 case 运行并把结果 tee 到本地(输出与退出码契约:==> / PASS: / FAIL: / SKIP:,末行 oci client smoke storage_only summary: …;0 通过,1 有失败,2 前置错误或 case 名未匹配):
mkdir -p target/tmp; set -o pipefail
CASE='OCI ping' # 换成要跑的 case 名
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile interop exec -T -e MEGA2_SMOKE_CASE="$CASE" interop-smoke bash /repo/scripts/oci_client_smoke_storage_only.sh 2>&1 | tee target/tmp/bb-oci-case.log已登记 case:OCI ping、OCI oras push and pull、OCI reject unauthenticated push、OCI chunked blob upload、OCI cross-repo blob mount、OCI manifest HEAD and conditional GET、OCI blob range、OCI tags list pagination、OCI image index manifest、OCI reject manifest with unknown blob、OCI delete unsupported、OCI catalog not implemented、OCI not mounted without oci section (none)。
不设 MEGA2_SMOKE_CASE 时整跑全部默认栈 case;opt-in case(auth-none)只经 scripts/bb_optin_run.sh none oci_client_smoke_storage_only.sh '<case>' <log> 运行。
plan-20261001 的 Artifacts 黑盒脚本 scripts/artifacts_smoke_storage_only.sh 在 interop-smoke 容器内运行(客户端为 curl,不用 libra),经容器内环回中继访问 http://127.0.0.1:9000/api/v1/repos/<repo>/artifacts/;每次运行都在带 RUN_ID 的 repo 与 namespace 下读写,可在脏卷上重跑。先按 §8 启动栈与 interop-smoke,整跑或单 case 运行并把结果 tee 到本地(输出与退出码契约:==> / PASS: / FAIL: / SKIP:,末行 artifacts smoke storage_only summary: …;0 通过,1 有失败,2 前置错误或 case 名未匹配):
mkdir -p target/tmp; set -o pipefail
# 整跑全部已登记的默认栈 case(opt-in case 记为 SKIP)
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile interop exec -T interop-smoke bash /repo/scripts/artifacts_smoke_storage_only.sh 2>&1 | tee target/tmp/bb-art-all.log
# 单 case:CASE 取下方「已登记 case」中的名字
CASE='<case>'
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile interop exec -T -e MEGA2_SMOKE_CASE="$CASE" interop-smoke bash /repo/scripts/artifacts_smoke_storage_only.sh 2>&1 | tee target/tmp/bb-art-case.log已登记 case:ART discovery、ART presigned upload and commit、ART server PUT upload and commit、ART presigned download、ART reject unauthenticated write、ART commit with missing objects、ART commit replay and conflict、ART list sets pagination、ART metadata filters、ART resolve file、ART set detail、ART repo isolation、ART input validation、ART conditional GET and HEAD、ART anonymous write (none)、ART GC removes unreferenced object、ART proxied download range (local)。
不设 MEGA2_SMOKE_CASE 时整跑全部默认栈 case;opt-in case(auth-none、GC、本地存储)只经 scripts/bb_optin_run.sh <none|gc|local> artifacts_smoke_storage_only.sh '<case>' <log> 运行。GC 模式使用 config/compose.env.storage-only.artifacts-gc(每 5 秒回收一次、宽限 1 秒,仅限实验室栈);默认栈经 compose 透传的 MEGA_ARTIFACTS_GC__* 取代码默认值(关闭)。
plan-20261001 的 scripts/libra_smoke_storage_only.sh 在 interop-smoke 容器内运行。脚本以隔离 HOME 调用镜像内的 Libra 客户端,先检查版本不低于 0.30.26,并用只读 git ls-remote 检查 mega2 HTTP 端点。客户端经容器内环回中继访问 http://127.0.0.1:9000/;先按 §8 启动默认栈与 interop-smoke。
mkdir -p target/tmp; set -o pipefail
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile interop exec -T interop-smoke bash /repo/scripts/libra_smoke_storage_only.sh 2>&1 | tee target/tmp/bb-libra-all.log
CASE='<case>'
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml --profile interop exec -T -e MEGA2_SMOKE_CASE="$CASE" interop-smoke bash /repo/scripts/libra_smoke_storage_only.sh 2>&1 | tee target/tmp/bb-libra-case.log已登记 case:LIBRA clone HTTP、LIBRA fetch HTTP、LIBRA ls-remote HTTP、LIBRA trunk push HTTP、LIBRA multi-commit push HTTP、LIBRA reject unauthenticated push、LIBRA reject tag push、LIBRA reject non-main branch push、LIBRA clone SSH、LIBRA reject SSH push、LIBRA LFS push and clone、LIBRA browser list root、LIBRA browser list at ref、LIBRA browser unknown path、LIBRA browser list text、LIBRA browser create dir、LIBRA browser delete dir、LIBRA browser move dir、LIBRA browser rename dir、LIBRA browser list tags、LIBRA browser tags page two、LIBRA browser create lightweight tag、LIBRA browser create annotated tag、LIBRA browser delete tag。HTTP clone case 在 /project 写入带 RUN_ID 的种子文件,并用只读 Git clone 对照 Libra 克隆的文件 SHA-256;fetch case 在克隆后新增服务端文件,以只读 Git ls-remote 对照 Libra 的 origin/main;ls-remote case 对照两个客户端读取的 refs/heads/main OID;trunk push case 通过 stdin 在隔离 HOME 中配置加密存储的环回 HTTP token,再用不含凭据的 origin 推送一个 Libra commit,由只读 Git 观察者验证 tip 与文件内容;multi-commit push case 一次推送三个连续 Libra commit,并由 Git 观察者核对三个文件内容;无凭据推送 case 使用全新 HOME 和 Libra 配置库,确认认证拒绝且 Git 观察者所见的远端 tip 不变;tag push case 使用隔离凭据推送唯一标注 tag,检查服务端明确拒绝且 Git 观察者未见远端 tag;非 main 推送 case 用本地 main 作为来源尝试写入唯一远端分支,检查服务端 trunk 拒绝且 Git 观察者未见远端分支;SSH clone case 用隔离 known_hosts 严格校验环回 SSH 主机,比较 Libra SSH clone 与只读 Git HTTP 观察者的 /project tip 和种子文件 SHA-256。当前 Libra SSH 客户端从 URL 路径去掉首个斜杠,因此 SSH URL 使用 ssh://git@127.0.0.1:2222//project,使服务端收到绝对路径 /project。为兼容已有 LFS 指针的仓库,该 case 先通过 SSH --no-checkout 拉取 Git 对象,再临时把 origin 指向同一 /project 的 HTTP LFS 端点检出工作区,核对后恢复 SSH origin。SSH push case 在已验证的 SSH 克隆中提交新文件,要求 Libra 推送非零退出、隔离 SSH stderr 含服务端关闭 receive-pack 的明确拒绝,且只读 Git 观察者看到的远端 tip 不变。LFS case 跟踪 *.bin,在隔离 clone 中经 commit -a 生成 1 MiB 对象并推送,以只读 Git 观察者确认远端 LFS pointer 的 OID 与大小,再用全新 Libra clone 核对 SHA-256;默认栈的 upload action 给出 Content-Length,RustFS presigned PUT 可直接使用。browser root case 在非 TTY 中比较 Libra JSON 的 items[].name 与 HTTP /api/v1/tree?path=/ 的名称集合。browser ref case 先创建根标签并记录 /project 历史列表,再在 main 写入唯一文件,确认 --ref <tag> 的列表保持不变而当前列表包含该文件;完成后尽力删除临时根标签。browser unknown-path case 注入仅供泄漏检测的哨兵环境 token,要求命令非零退出、stderr 为含 HTTP 404 的 LBR-NET-002 JSON、stdout 为空,并确认 stdout 与 stderr 都不含该 token。browser list-text case 先用 0600 header 文件在 /project 创建唯一目录,再在非 TTY 中要求 stdout 含精确 dir <NAME> 行、不是 JSON 且 stderr 为空,并以匿名 tree API 的 content_type=directory 核对同名目录。browser create-dir case 用 0600 token 文件执行一次 JSON 写操作,核对完整 target 与非空 receipt,再以不带 token 的匿名 tree API 确认同名目录及其类型。browser delete-dir case 用 curl 创建并匿名确认唯一目录,再用 0600 token 文件执行 JSON 删除操作,核对完整 target 与非空 receipt,并以匿名 tree API 确认同名条目消失。browser move-dir case 用 curl 创建并匿名确认唯一源目录与目标父目录,再用 0600 token 文件执行 JSON 移动操作,核对完整 from/to target 与非空 receipt,并从源、目标两侧匿名确认移动结果。browser rename-dir case 用 curl 创建并匿名确认旧名目录且新名不存在,再用 0600 token 文件执行 JSON 改名操作,核对共享父目录的完整 from/to target 与非空 receipt,并匿名确认旧名消失、新名目录出现。browser list-tags case 用 curl 创建唯一 annotated root tag,再分别通过无凭据 Libra JSON 与匿名 HTTP API 查询第一页两个 tag,逐序比较名称与总数,最后删除夹具 tag。browser tags-page-two case 用 curl 创建两个唯一 annotated root tag,再比较 Libra 与匿名 HTTP API 的第二页单条结果和总数,同时确认 HTTP 第一、二页名称不同,最后删除两个夹具 tag。browser create-lightweight-tag case 以只读 Git 记录 root HEAD,用 0600 token-file 经 Libra 创建轻量 root tag,再以匿名 tag GET 确认 tag_id 和 object_id 均为该 HEAD,随后删除夹具并扫描 token。browser create-annotated-tag case 以只读 Git 记录 root HEAD,用 0600 token-file 经 Libra 创建带 message 的 annotated root tag,再以匿名 tag GET 确认 message、object_id 与 HEAD 一致且 tag_id 不同,随后删除夹具并扫描 token。browser delete-tag case 经 curl 创建并匿名确认唯一 annotated root tag,再用 0600 token-file 经 Libra 删除,核对删除 receipt 和匿名 GET 404;失败时清理夹具。客户端工作目录运行后清理。点名尚未登记的 case 退出 2。
本节的三段 SQL 只用于停服窗口。执行前先停掉全部 mega2 进程,包括 HTTP 与 SSH 服务。这些 SQL 不获取视图锁或根链锁,在线执行会与 worker 的批事务交错;不停服的在线回收与 rebuild 仍由 DEFER-HP-04 承接。SQL 使用的连接串必须与 mega2 的 [database].db_url 相同,或者使用覆盖它的 MEGA_DATABASE__DB_URL,并保留其中的 search_path。表名没有 schema 限定,连接到其他 search_path 不会操作 mega2 实际使用的表。
把每段代码块保存到文件后,用下面的方式执行;必须带 ON_ERROR_STOP=1:
psql "<数据库连接串>" -v ON_ERROR_STOP=1 -f <文件>三个标记行是稳定接口,本节的用例和 HP-25 的 scripts/libra_view_smoke_setup.sh 都按标记提取 SQL。回收单个过滤器时,填写停止告警中 filter_id 字段的值。
-- view-ops: recycle-filter
BEGIN;
DELETE FROM mega_view_commit_map
WHERE filter_pk = (SELECT id FROM mega_view_filter WHERE filter_id = '<filter_id>');
DELETE FROM mega_view_object_ref
WHERE filter_pk = (SELECT id FROM mega_view_filter WHERE filter_id = '<filter_id>');
UPDATE mega_view_filter
SET projected_seq = 0,
ready_seq = NULL,
warming_since = NULL
WHERE filter_id = '<filter_id>';
COMMIT;-- view-ops: rebuild-all
BEGIN;
DELETE FROM mega_view_root_chain;
DELETE FROM mega_view_root_chain_scan;
DELETE FROM mega_view_commit_map;
DELETE FROM mega_view_object;
DELETE FROM mega_view_object_ref;
UPDATE mega_view_filter
SET projected_seq = 0,
ready_seq = NULL,
warming_since = NULL;
COMMIT;-- view-ops: clear-root-chain-scan
DELETE FROM mega_view_root_chain_scan;recycle-filter 与 rebuild-all 都有显式事务。COMMIT 之前的语句失败时,psql 会停止并断开连接,事务不会提交。recycle-filter 只回收目标过滤器的派生提交映射与对象引用,保留对象以供后续清扫;rebuild-all 清掉全部派生状态。两者把过滤器置为回收态:projected_seq = 0,且 ready_seq、warming_since 为 NULL。worker 只为活跃视图(ready_seq 或 warming_since 非空)执行 catch_up,因此回收态视图会等待重新预热。clear-root-chain-scan 只清暂存表,不改视图状态;下一次根链扩展会按当前的 main@/ 重新判定。
三段 SQL 都不用 TRUNCATE,不删除 mega_view_filter、mega_view 或 mega_view_register_log,也不改 last_access_at 与过滤器定义列。rebuild-all 是全表 DELETE,停服窗口随派生行数增长。本节记录停服运维与恢复语义;视图 Git URL 已支持只读 clone 与 fetch,用法见用户指南。
投影停止时,首个失败点会以 ERROR 事件给出。除 message 外,停止告警字段为 metric=view_projection_stops_total、filter_id、s、commit_id、reason;当 reason 是 row_absent 或 unparsable 时还带 tree_id。例如:metric=view_projection_stops_total s=7 reason=row_absent。按 reason 处理:
premise_check_failed:等待 L0 修复(DEP-HP-05)。在修复前重新预热仍会停在同一位置;若需释放冷启动名额,可用告警的 filter_id 执行 recycle-filter。row_absent:补回 tree_id 指向的mega_tree行;之后自动从 s 续追。unparsable:把 tree_id 指向的行恢复为原字节;之后自动续追。commit_row_missing:补回 commit_id 指向的mega_commit行;之后自动续追。
批前提不成立的信号是 metric=view_batch_premise_failures_total 的 ERROR 事件,字段为 filter_id、s0、tip、batch_size。它计入 view_batch_premise_failures_total,不计入 view_projection_stops_total,也不发停止告警。batch_size 受配置校验限制为 1..=10000;按根链表出现空洞处理,使用 rebuild-all。
根链首次判为不连续时会发出带 reason 与 commit_id 的 ERROR 事件,没有 metric 字段。证据是 mega_view_root_chain_scan 中 pos 最大的一行:若 main@/ 已恢复为链尾后代,使用 clear-root-chain-scan;否则使用 rebuild-all,并公告视图历史可能改变。
P0 不提供设计 §4.2 的根链审计 SQL:审计需要逐项复算 SHA-1,Postgres 没有内置 sha1,本仓没有 pgcrypto 迁移。带 review 形态历史的部署在启用视图前需要这项审计;P0 只能依靠停止告警发现首个失败点。完整审计由 DEFER-HP-04 的 mega2 view status 承接。
投影 worker 只随 HTTP 服务启动:使用 service http,或 service multi 中包含 HTTP 的副本。
只运行 service ssh 的进程不启动投影 worker,也不执行周期补偿;部署中至少一个副本必须运行
HTTP 服务。
worker_interval_secs 是可热加载的周期,也是缺少进程内信号时推送后视图额外滞后的上界,另加
一轮执行所需的时间。根链被判为不连续时 worker 跳过整轮补偿;按本节停服步骤恢复根链后,重启
HTTP 服务即可从已提交水位继续。
停服时 worker 丢弃正在执行的轮。已提交的根链段与投影批会保留,未提交的当前事务会回滚;服务 重启后从这些已提交水位继续追赶。
本节接口处于实验阶段,适用于已按配置说明启用 [views] 的 trunk、sha1 部署;此处不表示视图 Git URL 已可用。
向 POST /api/v1/views 发送 JSON 请求体 {"filter_spec":":/project/a","name":"agent/task-1"}。name 可省略。注册按 git.push_auth 鉴权;使用 token 时,push token 须覆盖过滤器的全部源路径。成功应答包含 filter_id、name、version 与 ready。带 ?wait=true 时,准入提交后等待就绪,最多 20 秒;返回 ready=false 时可轮询 GET /api/v1/views/{filter_id}。没有 wait=true 时注册立即返回。
GET /api/v1/views/{filter_id} 返回 filter_id、canonical_spec、src_paths、push_enabled、ready、projected_seq、lag_commits 七个字段。lag_commits 是 seq(main@/) 减去该过滤器的投影水位;main@/ 尚未进入根链时,也计入它与根链链尾之间尚未接入的提交。main@/ 缺失、回走不能在 views.max_append_walk 个提交内接上根链链尾,或遇到回滚、分叉、多父、缺失首父等不连续历史时为 null。
GET /api/v1/views?name=agent%2Ftask-1 按名字查询。名字含 / 时须百分号编码;省略 version 取最新版本,&version=1 可指定版本。响应只含 filter_id、name 与 version。
GET /api/v1/views/metrics 返回进程内原子计数与实时查询的 view_cold_start_slots_in_use。这些元数据端点不鉴权:视图不是路径级读权限边界(ADR-HP-05)。若 /api/v1 暴露给不可信网络,应在反向代理处对 /api/v1/views 限流;可热调低 views.max_append_walk 以限制每次滞后量查询的回走步数,代价是根链扩展的单次预算也降低。
停服执行 recycle-filter 或 rebuild-all 后,回收态视图有两个恢复出口:以同一 filter_spec 再次 POST /api/v1/views,或者由读者访问已启用的视图 URL 触发重新预热。再次注册计入该 token(或 anonymous)的注册速率,超限返回 429;读者访问不计速率。两个出口都占冷启动名额,并受 max_filters 约束。
名额不足时,注册返回 429;读者访问返回 503 与 Retry-After。释放名额,或热调高 max_filters、max_concurrent_cold_starts 后,可以再次访问。预热期间访问同样返回 503 与 Retry-After,就绪后恢复宣告。views.enabled 为 false 时访问返回 404,不触发预热。
recycle-filter 之后的视图历史与回收前相同;rebuild-all 之后的视图历史可能改变,需要公告。
先用当前工作树重建 mega2 镜像并启动 interop-smoke,命令见 §8。三个 case 逐一运行;每次 helper 都重建独立库、切到 views 配置、准备视图状态,并在退出时恢复默认栈:
scripts/bb_optin_run.sh views libra_view_smoke.sh '<case>' target/tmp/hp25-<case>.log<case> 分别取 view_http_clone_fetch、view_unready、view_l0_mismatch。setup 先 provision /project/hp25-view-smoke,克隆其初始提交,再用 git mktree 构造 ok/、unready/ 与 l0/ 的 tree 和根 tree,在初始提交之上推送。l0/ 含不自洽的 100664 条目,属于历史投影设计的 R2 限制;它只写入独立库,避免破坏默认栈的根仓库 clone。先注册并回收 hp25-unready,再注册两个就绪视图使名额满额,确保未就绪访问稳定得到 503 / 75。
SSH case 用 LIBRA_SSH_COMMAND 指向本次运行的包装脚本,脚本从隔离的 known_hosts 读取环回中继 host key,并保持 StrictHostKeyChecking=yes。在 DEP-HP-04 落地前,未就绪 SSH 用例只断言 status 75;L0 ERR 用例断言 remote reported an error: 前缀及重算的 tree id。
views 栈与默认栈共用 RustFS bucket mega2;独立库与 Redis 逻辑库 1 可在 helper 没有运行时手工清理:
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml exec -T postgres psql -U mega2 -d postgres -c 'DROP DATABASE IF EXISTS mega2_hp25_views WITH (FORCE)'
docker compose -p mega2-trunk -f docker/docker-compose-storage-only.yml exec -T redis redis-cli -n 1 FLUSHDB