# Felis 生产就绪审计 — 2026-09-22(真机 E2E + 混沌) 分支:全部修复**逐 commit 直接推 `main`**(不再用功能分支)。remote = `git@github.com:FelisMC/Felis.git`(2026-09-23 起;旧 `MliroLirrorsIngenuity/Felis` 仅靠 GitHub 301 兜住)。环境:CentOS Stream 9 / aarch64 / k3s v1.36.4, IPv6-only 接入(`ssh -6 -i ~/.ssh/id_ed25519 root@fdb2:2c26:f4e4:0:21c:42ff:fede:69ec`), 面板经 `socat TCP6:443 → 127.0.0.1:30443` 中继(手动启动,重启 VM 后需重开)。 ## 已修复并验证(分支内) | # | 缺陷 | 证据 | 修复 | |---|------|------|------| | 1 | **失败恢复后重试被静默吞掉**:restore Job 固定名 + `ErrAlreadyExists` 一律当"幂等成功";失败 Job 占名 10 分钟(TTL),期间重试返回 202 `restoring` 但什么都不跑 | 真机:坏 ref 制造失败 → 立刻合法重试 → Job 原地不动、无新 Pod | `k8sjobs.go`:撞名时检查已完成(成功/失败)→ 删除+**等 finalizer 释放**(有界 10s)+ 重建;进行中仍幂等吸收。单测 3 个。**真机复验:重试 4s 完成恢复** | | 2 | **备份 Job 全部 FailedMount**:Job 在 minecraft 命名空间挂 `felis-config`,而 bootstrap 只在 felis 命名空间创建该 Secret | 事件:`MountVolume.SetUp failed: secret "felis-config" not found` | `felis setup` 用既有 `ensureSecretReplica` 把 felis-config(`felis.toml`) 复制到 minecraft;VM 上手工复制后备份成功(167MB 归档) | | 3 | RBAC 缺 `jobs:get/delete`(修复 #1 需要) | Role 检查 | `APIMinecraftRole` jobs → create/get/delete,测试同步更新 | | 4 | gofmt 9 文件漂移 + CI 无 gofmt 门禁;staticcheck 9 处 | 基线扫描 | 全部修复;CI 加 gofmt job | | 5 | 依赖漏洞:pgx v5.7.1(GO-2026-5004,可达 pgrepo.go)、x/net、x/text | govulncheck | 升 pgx v5.9.2 / x/net v0.55.0 / x/text v0.39.0 | | 16 | **`ConsumeLoginEmailOTP` PG 实现与接口契约漂移**:契约/`fakeRepo` 说「同 VerifyEmailOTP 生命周期(扣尝试/锁定/ErrOTPInvalid/ErrOTPLocked)」,PG 却是单条 UPDATE+`ErrNotFound` → 错码/重放/过期在 login 门、op-login finish、migration confirm 三个入口全部 500;且尝试次数永不累计、`otpMaxAttempts` 锁定失效 | 真机:login 门重放正确码 → **HTTP 500**;修复前错码不扣次。修复后复测:5 次错码 400 且 attempts=5(正确码因锁定也 400、码未消费)、新码可用、重放 400 | `pgrepo.go` 改置为 VerifyEmailOTP 同构事务(FOR UPDATE、先锁后比、mismatch 扣次、match 消费),无 users 写副作用。提交 `52549f7` | | 17 | **`ListPendingOpLogins` PG 少列**:接口注释承诺「joined to its staff username」,handler 输出 `username`/`created_at`,fake 正确填充;PG SQL 未 JOIN 也未取 `created_at` → 真机 pending 列表 username 为空、created_at 为 `0001-01-01` | 真机 internal `/op-login/pending` 响应 | `pgrepo.go` SQL 改为 JOIN users + 取 created_at。提交见分支 | | 18 | **绑定码并发兑换 500**:`RedeemPlayerBindCode` 无 `FOR UPDATE`(同文件 `VerifyLinkCode` 有),且裸 INSERT。6 路并发同码兑换 → 3×HTTP 500(`users_username_key` 唯一冲突)+ 1×400 + 2×200;跨码并发同 UUID 同样会撞 | 真机四组并发测试 + API 日志 `unmapped error ... duplicate key` | 同码:码行 `FOR UPDATE`(输家干净地 400 invalid_code);跨码:`INSERT users ... ON CONFLICT (username) DO NOTHING`+重读、`account_links ON CONFLICT (mc_uuid) DO NOTHING`(两路汇聚同一 user)。复测:同码×6=1×200+5×400、双码×2=2×200 同 ID、DB 干净、日志零 unmapped | | 19 | **reaper CronJob 渲染位置错误,永远无法调度**:`felis manifests` 把 CronJob 渲染到控制 ns,却引用 minecraft ns 的备份 PVC(Pod 不能跨 ns 挂 PVC:真机 `FailedScheduling: persistentvolumeclaim "felis-backups" not found`);修正 ns 后又发现 ServerAccount 也不能跨 ns 使用(`serviceaccount "felis-reaper" not found`)。而 reaper 的 Role/RoleBinding 本就在 minecraft ns | 真机三层取证(PVC/SA/调度)| CronJob 与 SA、RoleBinding subject 全部移到 `MinecraftNamespace`(提交 `e4f2cff`+`c839454`)。**修复后完整演练通过**(见下) | | 6 | **默认安装无备份能力 + 回收链路不可达/不可用**:(a) 没有任何环节渲染归档 PVC,`FELIS_BACKUP_PVC` 永远为空 → backup/restore 恒 503;(b) bootstrap 从不下传 reaper 三旗标 → 官方安装路径根本无法启用回收;(c) 即便启用,stock k3s 的 `/` 布局不存在,且 k3s storage root 是 `0700 root:root`、reaper pod 以 uid 1000 运行 → 真机 `lstat /worlds/…: permission denied`(fail-closed 跳过,但纯空转);(d) README 口径(“定时备份”)不实 | 真机全链路:默认渲染 PVC+env → 建服→marker→备份 202→jobs 端点 running→succeeded→改 marker→restore→读回原值 ✅;再建 resolvecheck 世界(20d idle,marker)→ CronJob 手动 Job:`evaluated=2 reaped=1`,归档含 marker、PVC+宿主目录回收、`world_backups` 得 `inactive_15d` 行、servers 行/CR 保留 ✅ | `platform/workloads.go` 渲染归档 PVC(minecraft ns、RWO 10Gi、默认 SC),`felis manifests --backup-pvc` 默认 `felis-backups`(`=` 空为显式关闭),bootstrap 统一下传 env/旗标并写 [archive] local_path;`resolveWorldDir` 新增精确 local-path 目录解析(读 PVC `spec.volumeName`,非 glob,杜绝陈旧 PV 目录顶替);ReaperRole 增 `pvc:get`;bootstrap 设 `FELIS_WORLDS_HOST_PATH` 时给 uid 1000 授 traverse(setfacl/o+x);README/故障手册改写。提交 `fd33fd0`+`2b87a5a` | | 8 | **磁盘打满灾难链**:DiskPressure → kubelet 驱逐控制面(无 PriorityClass 保护)→ 镜像被 GC(无外网、registry 空)→ 全部 ImagePullBackOff;释放后数分钟才恢复调度。恢复依赖人工 `docker save | k3s ctr images import -` | 三轮真机 drill(原缺陷复现 + 两轮修复验证):①自定义 1e6 类:游戏 pod 先走、控制面随后仍被驱逐(kubelet 日志逐条列出 ranked/evicted),随后镜像被 GC → ErrImagePull;②内建 `system-cluster-critical`(2e9):填盘至 1.7G free,kubelet 对 felis-api/operator/registry 全部报 **“cannot evict a critical pod”**,三者在整个 DiskPressure 期间保持 Running;游戏 pod(0)被驱逐;③释放磁盘后:`DiskPressure` 经 ~5 分钟(`eviction-pressure-transition-period`)转 False——即“恢复慢”的主因;被 GC 的游戏镜像按 runbook 重导入后 25s 恢复 ✅ | 控制面四类 pod 挂内建 `system-cluster-critical`(用户自定义类值上限 1e9,达不到 2e9 临界阈值;preemption 保留=管理面可调度,已记录权衡);troubleshooting §13b 固化事件链、5 分钟条件过渡与镜像恢复路径。提交见下 | | 9 | **升级策略 Recreate**:单副本 + Recreate:任何控制面升级=停机;坏升级(错 tag)中断约 95s 且需人工 `rollout undo`(无自动回滚) | 复核部署模板(Recreate + 单副本 + 无 leader election 的注释理由成立) | 不改策略(Recreate 是对无 leader election 的正确取舍),改为固化 runbook:troubleshooting §15 = 升级即重跑安装器;停机窗口=rollout 时长;坏镜像在安装器 180s 等待内以 `kubectl describe` 诊断呈现;回滚 `kubectl rollout undo`(镜像被 GC 时先按 §13b 重导入)。提交见下 | | 10 | **备份语义**:归档包含整个 /data(jar、libraries、cache),167MB;是否符合 world backup 定位待评估 | 真机 tar 清单(cache/、config/、eula.txt、server.properties、jar)+ 恢复语义(overlay+prune) | 评估结论:**整卷备份是正确语义**(回滚=整服状态回滚,含配置/插件),保留行为;文档化:troubleshooting §10「backup contains」+ OpenAPI/README 口径改为“整服数据卷”。提交见下 | | 20 | **验证邮箱唯一性只存在于注释里**:errors.go/repo.go 都声称迁移 0010 建了 `users_verified_email_unique` 部分唯一索引、`VerifyEmailOTP` 会返回 `ErrEmailTaken`;实际上**索引从未创建**、`ErrEmailTaken` 全仓库从未被返回 → 两个账号可同时验证一个邮箱,而登录门正是按 verified email 解析账号 → 同一邮箱的登录码归谁由数据库任意决定 | pgint 首跑即红:第二账号验证同邮箱返回 `nil`;`grep users_verified_email_unique internal/store/migrations/*.sql` 零命中。真机复验(auditfix17 邮箱 409 演练):同码二次 verify 仍 409(码未被消费) | 新增迁移 `0020_verified_email_unique.sql`(`lower(email) WHERE email_verified`);`VerifyEmailOTP` 在消费码**前**查「他人已验证」→ `ErrEmailTaken`(不消费码、不扣次数),并把并发唯一冲突映射为同一答案;handler 新增 409 `email_taken`。提交 `b6ef27c`;PG 契约测试基建 `2a55a0d`(`-tags pgint`,见 CONTRIBUTING) | | 21 | **admin 改邮箱不清 verified**:`UpdateUser` 写入新地址但保留 `email_verified=true` → 面板改错一个字符就能让登录码寄到别人邮箱(该 flag 正是邮箱登录解析/寄码的依据);fake 同错 | pgint 契约测试 | 改地址时同写 `email_verified = email_verified AND email IS NOT DISTINCT FROM 新值`(同值 no-op 保留证明;换值即清除);fake 同步。提交 `d1ec40f`。真机验证:PATCH→`f`、PATCH 回原值仍 `f`、玩家重验证→`t` | | 22 | **`role='owner'` 是死信**:迁移 0011 加了 owner 角色并把全部用户管理路由压在 `IsOwner()` 上,但**没有任何代码写过 `owner`**——breakGlass(`UpsertOwner`)、setup MC 绑定(`CompleteOwnerSetup`)、重置路径一律写 `admin` → 全新安装的整个 owner 层(用户列表/创建/编辑/禁用/删除/配额/会话)不可达;且新角色没进各处 staff 判定(op-login 只认 admin、玩家邮箱门只拒 admin、reclaim 保护与 AdminExists 只认 admin);面板一旦有 owner 行还可被降级/删除/禁用 | 真机:owner 会话加载 `/api/v1/users` 403;promote 后 200。op-login:修复前 owner start 中立不发码,修复后铸请求+finish 得 `role=owner` 会话 | `UpsertOwner`/`CompleteOwnerSetup` 改写 `role='owner'`(冲突臂重断言,即 0011 文档的 promote 路径);op-login 双端改 `staffRole`;玩家邮箱门改 `role != 'user'`;`IsProtectedAdminLink`/`AdminExists` 计入 owner;面板新增守卫:owner 行不可降级/删除/禁用(用户名/邮箱编辑仍可)。提交 `e0d2378` | | 23 | **配额门非原子 + storage 缓存被清零**:(a) audit #4:`QuotaCheck` 与 `ClaimServer` 两条语句,同一用户并发认领两台无主服可双双通过 `max_servers`(deferred-seams 曾把这条挂为“只能在真 PG 上关闭”);(b) 更隐蔽:PATCH 资源时 `UpdateServerResources(..., 0)` 把本不能改的 storage 缓存写 0,而缓存列是配额聚合的**唯一**输入 → 此后该服的 storage 维度在配额里凭空消失 | (a) 新增 pgint 并发测试:修复前两台全赢;修复后恰 1 赢 + 1 `ErrQuotaExceeded`,DB 只 1 行 owned;(b) hermetic 测试 `TestPatchServerPreservesStorageCache`(修复前 `resourceUpdates` 里 storage=0) | (a) 门槛进 `ClaimServer`:同一事务内 `pg_advisory_xact_lock(hashtext(user_id))` + 四维重查(与 `QuotaCheck` 共用 `quotaAllows` 防漂移)+ 行 `FOR UPDATE`,两个 claim handler 把 `ErrQuotaExceeded` 映射为与串行一致的 403;(b) resize 前读取现值并透传 storage。提交 `bb68fef`;deferred-seams 对应条目核销 | | 24 | **reaper 警告信从不真正投递**:`felis reaper` 从不装配任何 Warner(`r.Warner` 恒 nil),而 `maybeWarn` 对 nil warner / 投递失败一律照样 `MarkWarned` + `warned++` → 每位有主的服都在**无人收到提醒**的情况下 15 天后被静默回收,跑批日志还谎报"已警告 N 台"。红线⑤的"best-effort 不阻塞回收"被误读成了"失败也要记成已通知" | hermetic 单测 3 例(坏 notifier / nil warner → 不盖章、成功才盖章)。真机 drill(auditfix20):13d idle 实收 1 封(收件人=owner 的验证邮箱、subject 含 3d);重跑不重发;14.5d 补发 1d(elif 档位);SMTP 端口打坏 → 日志 `warn delivery failed; will retry next run` 且**不盖章**,恢复后补发;邮箱未验证 → `has no verified email` 不盖章;owner NULL → 静默跳过;边界 11d23h 不发 / 12d1m 发;全程 `world_backups` 保持 4 条不动(纯警告零备份副作用) | `mail.SendNotice` + `mailWarner`:查 owner 的 verified email → `SendNotice`;nil/失败不盖章、下次重试;reaper pod 模板加 optional `FELIS_SMTP_PASSWORD` env;`felis setup` 复制 felis-smtp 镜像并在「configure email」刷新 minecraft ns 的 felis-smtp+felis-config 镜像;docs/troubleshooting §10 更新。提交 `8e7c7bb` | | 25 | **idle auto-stop 从未触发(三层复合缺陷)**:条件齐备的空载服永远不停。① CRD status schema 未声明 `emptySince`,apiserver **pruning** 掉计时戳(`unknown field "status.emptySince"`),计时器每次读回都是 nil;② 即使戳幸存,Running 空载稳态**没有任何 watch 事件**(玩家进出不碰 CRD、RCON 只在 Reconcile 内探),盖章一次后 reconcile 链停摆,无人叫醒;③ auto-stop 用整对象 `Update` 写 spec 且 OperatorRole 从未有 `minecraftservers:patch/update` → 403 `cannot update resource`。三层任一都让功能永久失效,而单测(fake client 不剪 schema、手动驱动、无 RBAC)全部覆盖不到 | 真机逐层实锤:schema 修复后 `empty=2026-09-22T20:02:13Z` 首次成功持久化;静置 88s+ 无动作、operator 日志 90s 零行(②实锤);修复前日志 5 条 403(③实锤);全修后场景 1:超时戳触发即 Stopped;场景 2:起服 → 20:11:43 盖章 → **全程无干预** → 20:12:17 自动 Stopped;翻转 Running→Stopped→Running 收敛 Running;`idle=null` 后不再自停 | ① schema 补 `emptySince`(`c04a3f0`);② `reconcileRunning` 尾部返回 RequeueAfter——空载=到点精确唤醒、有人=30s 探针周期,+3 个单测(`1c89a5e`);③ auto-stop 改 merge-patch(防 status clobber,与 reaper 的 Stop 同型)+ OperatorRole 补 `patch` + rbac 测试锚点(`f650bf8`)。真机 auditfix22 全通;troubleshooting §11 增自检三连 | | 26 | **RCON Secret 被删 → 服务器永久锁死**:删掉 per-server RCON 密码 Secret 后,(a) operator 没有任何 watch 能看到删除(Running 稳态零事件),secret 一直不重建;(b) 一旦被任意事件带到 reconcile:新密码铸造成功,但运行中的 pod 仍持有旧密码、sts 模板无变化 → pod 永不重启 → 探针用新密码连旧密码 pod,**永久 `RconNotReachable`** 直至 300s `ReadinessTimeout`;恢复只有人工删 pod。`ensureRconSecret` 的注释却声称"heals on the next pass" | 真机:删 secret 后静置 60s 无重建、无日志;annotate 触发后 96s+ 持续 RconNotReachable(新密码 `91dd62…` vs pod 旧密码);删 pod 后 17s 恢复 Running——根因=密码漂移实锤 | ① `SetupWithManager` 增 `Owns(&corev1.Secret{})`(controller-owned,删除事件映射回 CR)——真机日志见 `source="kind source: *v1.Secret"`;② pod template 新增 `RconSecretAnnotation` = 当前密码 SHA-256 指纹(64-bit):secret 重建指纹变 → sts 自动 rollout 用上新密码,未重建则恒定不抖动;测试 2 例(跨 reconcile 稳定 / 重建必变)。提交 `56f3abd`。真机复测:删 secret 后**同秒**察觉+重建+换戳,31s 全自动恢复 Running | | 27 | **陈旧启动锚点 → 已恢复的服务器被误判 StartupTimeout + Provisioned 永久 False**:`status.startRequestedAt` 只在 Stop 时清,成功(markRunningReady)不清——一旦服务器曾经历一次长 Starting,其 300s 预算就悬在健康运行中;此后任意一次 pod 波动(rollout/崩溃)都会立即套用旧戳判 `Failed(StartupTimeout)`。且 `markFailed` 写入的 `ConditionProvisioned=False` 无人复位,恢复后仍永久挂着失败标记 | 真机:20:19 已恢复 Running 的 test-one,20:21 因一次 stamp 注入引发的 pod rollout 被标记 `Failed StartupTimeout`(锚点残留自 20:16);恢复后 status.conditions 里 `Provisioned=False … StartupTimeout` 持续存在 | `markRunningReady`:清 `StartRequestedAt`(每次启动/恢复尝试各有独立预算)+ 复位 `ConditionProvisioned=True`;测试 2 例(Ready 清锚点、Failed→Running 后 Provisioned 恢复)。提交 `82b5a60`。真机复测:清戳生效(`startRequestedAt: None`)、pod blip 删→33s 恢复全程无 Failed、锚点重新盖章后成功清除 | | 28 | **cfsetup 把 "policy already exists" 当成功吞掉 → fail-closed 保证可被旧策略顶替**:Cloudflare Access 的 `CreateAccessPolicy` 在收到 already-exists 时直接返回 nil;若该 app 上已有一条更宽松的旧策略(改 identity 后重跑、或此前手工配置),守卫 op.console 的仍是旧策略,而 Setup 报告成功——`validateFailClosed` 只校验过"我们构建的策略",从未校验证留在线上的那条 | 代码审查(集成侧无真实 CF 账号,无法真机):httptest 3 例复刻——已存在同名策略、缺席、POST 竞态。修复前第 1 例吞错返回 nil 且不发 PUT | 改为按名 upsert:lookup → PUT 覆盖守卫体 → 缺席才 POST(POST 撞 already-exists → 重查后 PUT,绝不吞)。`apiPost/apiPut` 共用一个 `apiWrite`。提交 `30857df` | | 29 | **"configure email" 的工作负载镜像复制从不生效**:`replicateSMTPToWorkloadNamespace` 把硬编码 `namespace: felis` 的 felis-smtp manifest 用 `-n minecraft apply` 发出——kubectl 拒绝 namespace 冲突(`the namespace from the provided object ... does not match`)→ 第一段直接 return err,**第二段 felis-config 的镜像复制根本不会执行**。于是任何"装好后再改 SMTP"的部署,reaper 的 pre-reap 警告永远拿不到新配置(这正是 8e7c7bb 添加该刷新要解决的事),且失败仅 warning 不中止 | 真机 kubectl 行为实验:`-n minecraft apply` 带 `namespace: felis` 的 manifest → `error: … does not match …`;修复后(manifest namespace=minecraft)→ `accepted-namespace=minecraft`(server dry-run);felis-config 渲染+apply 序列同为 accepted | `smtpSecretManifest(password, namespace)` 显式参数(控制面调用传 "felis",镜像传工作负载 ns);回归测试 `TestSMTPSecretManifestCarriesTargetNamespace`。提交 `ed722d5` | ## 待决策台账(未修) 前两日台账的 #7、#11–#15 已全部修复(本批核销,证据见下),不再挂在"未修"里: | # | 主题 | 状态 | |---|------|------| | 7 | 异步失败不可感知 | ✅ 修复:`GET /api/v1/servers/{name}/jobs`(`ff7c57c`,含 RBAC `jobs:list` 与 OpenAPI);真机 drill 用它看到 running→succeeded | | 11 | PG 断连报 401 | ✅ 修复:会话存储故障改为 503(`2a8f897`) | | 12 | ready 门滞后 | ✅ 修复:operator 启动期内每 2s 重探 RCON(`a2df2f2`) | | 13 | setup URL 截断 | ✅ 修复:TUI 折行(`abce381`) | | 14 | controller-runtime 日志噪音 | ✅ 修复:SetLogger 接 slog(`a415246`) | | 15 | reaper 启用未演练 | ✅ 已演练(见"第二日"节;CronJob 仍 `suspend=true` 防误删) | ## 可达性分级(#1–#79;#62 立案后剔除)——这些缺陷真实使用中到底谁能踩到 回应质疑"是不是全在测边界条件 / 只有内部 hook 才能触发": - **hook 的边界**:演练中使用的内部手段(读服务端日志取 OTP 码、service token 打内部面代 approve、本地 SMTP sink、tmux 驱动 TUI)只是**测试仪表**——代替"真实收邮件 / 键盘敲击 / 操作台点击";触发路径本身是公开路径,真实用户做同一动作走同一段代码。真正**本环境不可复现**的只有 #28(无真实 CF 账号,仅 httptest 复刻),另有 #4/#5/#14 属工具/卫生级(无运行时触发面)。 - **分级口径**:① 日常=普通使用或默认安装下即会踩到;② 运维=升级/重启/故障恢复/磁盘满/删资源/闲置回收等真实运维动作会踩到;③ 窗口=需要并发或特定状态时序,真实但概率低;④ 工具=仅工具判定/审查;决策=非缺陷(评估/演练项)。 | # | 真实触发路径(一句话) | 分级 | |---|------------------------|------| | 1 | 恢复失败后 10 分钟内重试(必然的恢复动作)→ 202 但 Job 不动 | ② | | 2 | 默认安装下第一次点「备份」→ Job FailedMount(felis-config 缺在 minecraft ns) | ① | | 3 | #1 修复所需 RBAC(单独不产生用户可见行为) | ② | | 4 | gofmt/staticcheck 漂移 + CI 门禁缺失 | ④ | | 5 | 依赖漏洞(govulncheck 判定可达):需特定输入 | ④ | | 6 | 默认安装即踩:备份/恢复恒 503、回收链无旗标不可启用 | ① | | 7 | 任何异步操作(备份/构建)想查结果:无端点(功能缺口) | ① | | 8 | 磁盘打满事故链(控制面被逐 → 镜像 GC → 全体 ImagePullBackOff) | ② | | 9 | 非缺陷:升级策略评估 → runbook | 决策 | | 10 | 非缺陷:备份语义评估 → 文档化 | 决策 | | 11 | PG 掉线期间任何会话校验 → 401 误导 | ② | | 12 | 每次起服:就绪感知滞后 | ① | | 13 | 首次安装向导:长 URL 截断 | ① | | 14 | operator 日志噪音(卫生项) | ④ | | 15 | 流程项:reaper 启用补演练 | 决策 | | 16 | 邮箱登录主路径:错码/重放 → 500、5 次锁定失效 | ① | | 17 | 面板 op-login pending 列表字段缺失 | ① | | 18 | 绑定码并发兑换(双击/双端即可)→ 部分 500 | ③ | | 19 | 按文档启用回收 → CronJob 永远无法调度 | ② | | 20 | 两账号验证同一邮箱 → 登录码归属不定(安全) | ③ | | 21 | 面板改任一用户邮箱 → verified 未清、码寄旧地址 | ① | | 22 | 全新安装 owner 层不可达(用户管理全路由) | ① | | 23 | (a) 并发认领两服可双双过配额;(b) PATCH 任意服资源 → storage 缓存清零 | ①(a=③) | | 24 | 有主服闲置 12–15d:警告信从不投递、静默回收 | ② | | 25 | 启用空载停机后等超时:三层复合缺陷永不生效 | ② | | 26 | 运维删/轮换 RCON Secret → 永久锁死至人工删 pod | ② | | 27 | 曾长启动的服 + 任意 pod 波动 → 误判 StartupTimeout | ② | | 28 | 重跑 cfsetup 且线上有旧策略 → fail-closed 被顶替;⚠无真实 CF 账号,仅 httptest | ④ | | 29 | 装后改 SMTP(configure email)→ 复制链从未生效 | ② | | 30 | 对不存在/刚被删的用户 id 操作 → 500 而非 404 | ③ | | 31 | 真实 owner 打开 /admin/builds 被 mock 残留误判 | ① | | 32 | 新浏览器首访 /admin/builds 播种两个假 404 | ①(轻) | | 33 | 禁用/已删账号重走邮箱登录门 → 可复活(安全) | ① | | 34 | 同族:死账号游戏内身份面仍存活 | ① | | 35 | 任何真实服务器保存过一次后:所有备份/fileread 必败 | ① | | 36 | 任何构建失败的提交者永远看不到结果 | ① | | 37 | 管理端 fleet:login/lobby 行全是死操作 | ①(轻) | | 38 | felis manifests 输出残留过期指导 | ②(文档) | | 39 | 断玻璃 reset owner 用非在位名 → 铸第二 owner 永不可清 | ② | | 40 | 断玻璃 add operator 撞名 → 裸 SQLSTATE | ② | | 41 | 断玻璃 Sync 选系统服 → 裸内部错误 | ② | | 42 | 对无世界盘服备份/恢复 → 202 后静默卡 30 分钟 | ② | | 43 | #42 配套:409 一刀切文案 | ②(轻) | | 44 | 重跑 `felis setup` 完成 s/c reconfigure → 状态框退回“Setup complete”(信息一致性) | ①(轻) | | 45 | 评审看不到将被执行的 recipe(执行的是上传 blob 里的 Dockerfile)→ 盲批 | ① | | 46 | 大层推送打死 registry(256Mi 模板被 OOM kill;实测 475MB 层)——用户构建/安装器入仓即触发 | ① | | 47 | Mac 打包树构建:`._*.sql` 旁文件混入 embed → `felis migrate` 全挂 | ② | | 48 | 安装器重跑:每镜像 start/stop docker 触发 systemd 限流 → 批量入仓中途断 | ② | | 49 | 安装器重跑:`[registry]`/`[archive]` 运维配置静默回退(构建/S3/reaper 行为回默认) | ② | | 50 | 配过 SMTP 的安装按文档重跑安装器 → 生成的注释块被 `[smtp]` carry 吞并并每次 +1(纯膨胀) | ② | | 51 | 改配置/轮换 DB 凭据后:工作负载 ns 的 `felis-config` 副本永不刷新 → backup/reaper 静默读旧配置 | ② | | 52 | 向导内 `s`/`c` 重配置后:工作负载 ns 的 `felis-config` 镜像停在上一次运行快照,直到下次 `felis setup`/安装器才收敛 | ② | | 53 | 默认安装或 `felis update` 的 felis-api 检查:坐标仍指向已迁移的旧仓库,现仅靠 GitHub 301 兜住——redirect 一退休,安装器默认 URL 与更新检查全挂 | ② | | 54 | 完成态安装上照 `felis update` 指引升级:`sudo felis setup` 只开配置控制台,任何组件都不会动(三处 run 指引 + trailer 全假) | ②(文档) | | 55 | 配置的源回 200 但档案字段不可用(马虎自建/三方 Yggdrasil):坏源在前 → 经梯子的登录被静默吞掉(零日志、后续源不被询问) | ② | | 56 | 给"还没进过服"的玩家加白名单/封禁/踢出(面板说成功、vanilla 实际拒绝)——任何新玩家第一次被管理就撞 | ① | | 57 | 控制台发任何命令后无反馈(回复被丢弃、pod 日志也不回显) | ① | | 58 | 对从未启动过的服务器点"文件"页(90s 卡死 → 误导性 504) | ① | | 59 | 装了 LuckPerms 的服打开 LP 页:读不到被报成"未分配任何权限"(假事实),且历史伪造 `[RCON]` 输出 | ① | | 60 | 新建服务器输入非法名/子域名(前端预检比后端弱)→ 对话框直出 Go 英文错误;账户验证撞已占邮箱同理 | ① | | 61 | 给"还没进过服"的玩家预授权限:面板提示"仍然会实际生效"(过度承诺),实际 LuckPerms 静默丢弃(未进服的名字无法解析) | ① | 统计:① 26 | ② 25 | ③ 3+#23(a) | ④ 4 | 决策 3 = 61。第三批构建链另有 3 处未编号修复(Job requests 超小节点上限 → 永远 Pending、Kaniko chown、Trivy DB egress 被锁)——均属 ①/②。 第四十批追加:#63(demo-up 单起点化——旧镜像臂能产出"起了但无处路由"的演示机;dev/demo 路径)②;#62 经立案复核**不成立**(全部安装路径都构建 felis-velocity.jar,证据见该批节),已剔除、不计入。 第四十一批追加:#64(三个 vendored `gradlew` 以 100644 提交、无可执行位——README 教的构建命令在全新 clone 上直接 `Permission denied`;无任何 CI/安装路径跑过这三个模块)①;#65(三个装载器 mod 的 `license` 仍写 MIT、Forge/NeoForge 的 `issueTrackerURL` 是 `example.invalid` 占位,与仓库 AGPL-3.0-only 相悖——fabric loader 启动时会打印该字段)②(低)。 第四十二批追加:#66(英文 README 缺中文版"使用方式"里的私有仓库安装 workaround 与重跑升级说明——英文读者照文档第一步即 404、无任何指引)①(轻)。 第四十三批追加:#67(build Job 从不设 `ttlSecondsAfterFinished`——每构建一次就永久留下一个完成 Job+Pod,完成 Pod 计入节点 pod 预算(stock k3s 110),构建量上来后新构建全 Pending)②(时间维度;随构建量从②滑向①)。 第四十三批追加(二):#68(构建 context 拉取单次尝试——控制面滚动/重启/驱逐恰好撞上构建窗口时,fetch initContainer 一次 `connection refused` 直接打成终态 `Failed`;`BackoffLimit=0` 无第二次 Pod,代价 = 人工重审重提)②(运维:升级/重启/故障恢复撞上正在进行的构建;构建量大时概率上升)。 第四十五批追加:#69(README 中英"审批通过后自动构建**并部署**"过度承诺——数据模型无目标服务器、部署实为"选用该镜像";① 轻);#70(kaniko 拉内建底座默认 HTTPS——`--insecure` 只覆盖推,任何 `FROM registry.felis.svc:5000/…` 构建必败 ①);#71(kaniko 以 drop-ALL 解包底座层,chown 必败——任何非 scratch 底座必败 ①);#72(trivy 扫 jar 必拉 Java DB、被 egress 锁拒绝——含 jar 即所有真实模组包的构建必败 ①);#73(bootstrap 测试在跑 k3s 的主机上必假失败 ④ 工具);#74(console 断连测试读写竞态 flaky ④ 工具)。 第四十六批追加:#75(提交上传面无上限——pending 无个数上限、无存储预算、create/upload 无节流;一个账号可无限堆积上下文刷爆 uploads PVC ①);#76(提交无撤回/管理员删除路径——提交者无法自救、运维无法回收占用 ①);#77(未完成引导的会话触发受保护操作 → 面板显示"无权执行此操作"而非送往 /setup;tracker #8 ①);#78(create-if-absent 使新增 CR 字段在已装机永不落地——lobby 无 RCON 故控制台死、玩家数恒 0;tracker #1 ②);#79(troubleshooting [INERT] 图例指向已不存在字段 ④ 文档)。 第四十七批追加:无新缺陷——首个稳定 tag `v0.1.0` 的发布链与 release 安装/升级通道全实弹(release 二进制下载 → 无 checkout 全量安装 2m17s 零 fail/零 warn → `felis update` 双向报告;证据见该批节)。 ## 已验证事实(正向清单) - 安装→hook 发码→Owner 绑定→passkey(虚拟认证器)→面板管理员全链路 ✅ - 建服(POST /servers)→ 唤醒(operator 拉 StatefulSet pod)→ RCON `list` → SSE 控制台 → 停止 ✅ - 文件编辑:列目录/读/写(wire 为 base64)/256KiB 413/路径穿越 5 变体全拦截/运行中 409 ✅ - **备份→篡改→恢复数据演练**:v1→备份→v2→恢复→读回 v1 ✅(G2 数据可恢复) - **毒档案 fail-closed**:穿越/绝对路径/符号链接条目 → `archive entry escapes target` 退出码 1,零写入 ✅ - 混沌:PG 掉线(healthz 仍 200、恢复后连接池自愈)、API pod 击杀(~2s 中断)、整机重启(32s 回归、会话/CRD/停止态全保留)✅ - `felis update` 报告(k3s/velocity 有更新、私有仓库 404 优雅处理)✅ - 账户全套真机 E2E:绑定码新玩家注册(幂等/并发见 #18)、邮箱验证(onboarding 门)、邮箱 OTP 登录(错码扣次/5 次锁定后正确码也 400、重放 400、staff 账号 403 拒绝)、Passkey 注册+discoverable 登录+邮箱优先登录(虚拟认证器;Chrome 要求 `Page.bringToFront` 才能过 focus 检查)、登出吊销会话(旧 cookie 401) - op-login 全状态机(start→status→approve→finish;早 finish 不烧码、错码扣次且请求保留、重放/重复批准/非管理员批准/未知 handle 全部按契约返回)✅ - 并发:OTP 风暴 8 路 = 1×202 + 7×429 且仅铸 1 码;绑定码并发(同码/双码)见 #18 ✅ - **reaper 全链路真机演练**(修复 #19 后):绑定挂载假世界 → `felis reaper` 一 pass:归档 tar 落盘(内容含标记文件)、`world_backups` 行 `inactive_15d`+90d 过期、**PVC 删除且宿主目录回收**、servers 行保留且 activity 时钟重置(红线②)、CRD 保留;第二 pass:伪造过期归档被驱逐(`expired=1`,文件删、行转 `deleted`),合法归档未动;`evaluated=2 reaped=1` 只动到期的世界 ✅ - **S3 存储向导全链**(第三十二批):错误凭据预检零副作用、正例落地(Secret + 两 toml + API 滚 + 状态行)、上传 → MinIO 桶 → 内部面取件、UI 回滚归位、控制面/工作负载镜像对齐 ✅ - **NetworkPolicy 栅栏真机**(第三十五批):pod 源"身份×端口"矩阵全对(允许行与拒绝行同源对照)、转发型外部源白名单闭环(拒→放→拒,ipset 与 spec 双幂等)、host/ClusterIP 路径全通、Mac→NodePort 面板 200;两条归因注记见该批 ✅ ### 本轮新增真机证据(第二日) - **默认安装的备份闭环**:apply 新 bundle → 归档 PVC 自动创建(WaitForFirstConsumer,被首个 backup Job 拉绑定)→ `POST /backup` 202 → `GET /jobs` running→succeeded → 漂移 marker → `restore-backup` → 读回备份前内容 ✅ - **回收闭环(真实 k3s 布局 + 权限)**:resolvecheck(20d idle)→ CronJob 派 Job:`evaluated=2 reaped=1`;归档含 marker、PVC+宿主目录回收、`world_backups` 记 `inactive_15d`;期间先经历 `lstat /worlds/…: permission denied`(k3s storage root 0700)→ 修 ACL/root 权限后通过 ✅ - **磁盘压力三连 drill**:①1e6 自定义类:游戏 pod 先走、控制面随后仍被逐(kubelet ranked/evicted 日志)→ 镜像 GC → ErrImagePull;②改内建 `system-cluster-critical` 后:`cannot evict a critical pod` × felis-api/operator/registry,全部保持 Running;③恢复阶段实测 `DiskPressure` 条件 ~5min(eviction-pressure-transition-period)转 False,被 GC 的游戏镜像按 runbook 重导入后 25s 恢复 ✅ - **混沌**:SIGKILL k3s 主进程 → kubectl 与 felis-api ready 均在 **10s** 内恢复(api pod 本身未被重启)✅;内存压力(2.6G tmpfs 满写):无 OOM kill、无人被逐(swap 5.9G 吸收 + 内核回收),控制面不受影响 ✅ - **构建链路定位(当时未修)**:Kaniko/Trivy 默认镜像是外部的 → 已加 `[registry] kaniko_image/trivy_image/build_cpu_limit/build_mem_limit` 覆写;上下文跨 ns 不可挂载(PVC 不能跨 ns)是设计级缺口,s3 lane 也缺凭据注入 ✅(证据:`internal/submit/blobstore.go:40`、`cmd/felis/api.go` contextBase 分支、jobspec 无 volumes/env)→ **第三批已修复,见下** ### 本轮新增真机证据(第三批:探针 + 构建链路全通) - **控制面探针(`0c8e29b`)**:felis-api / felis-operator / registry 三个 Deployment 此前**完全没有探针**。修复后真机验证:api、operator `/readyz`+`/healthz`(internal 8081),registry `/v2/`(含命名端口解析);三个 Pod `Ready=true`、`restartCount=0`、无 `Unhealthy` 事件;operator 新增 `--health-probe-bind-address`(8081,与 metrics 8080 分离,零检查也 404 → 已注册 ping) - **构建链路全通(`f79e5eb` + `02fd2de`)**: - 传输:context_ref 改为 internal-face URL;`felis fetch-context` initContainer 走 service token(对象来自 `felis-build` 里按 `bootstrap`/`felis setup` 同款 Secret 复制机制落地的 `felis-service-token`;netpol 只放行控制 ns:8081)+ zip-slip 安全解包到限容 emptyDir;Kaniko `--context=/context` 只读挂载 - **真机 E2E**:玩家 OTP 登录 → 建 submission → 上传 gzip context(marker `felis-e2e-build-marker`)→ Owner approve → Job:fetch ✅ → Kaniko 构建并 push `registry.felis.svc:5000/user-uploads/:latest` ✅ → Trivy 扫描(内建镜像 DB)✅ → Job `Complete`;**从 registry 拉回镜像校验 `/hello.txt` 内容 = 上传的 marker** ✅ - 演练中真机抓到并修复 3 个单测看不到的缺陷:① Job requests=limits(2C/4Gi)在 4C/5.5G starter 上**永远 Pending**(`FailedScheduling/Insufficient memory`)→ requests 改为地板值(250m/512Mi,不超上限);② Kaniko 重拷 Dockerfile 时 chown/chmod 到源文件 owner(distroless uid 65532)在 drop-ALL 能力下失败(`copying dockerfile: chown … operation not permitted`)→ fetch 容器以 root 解包(= Kaniko 自身 uid);③ Trivy DB 默认 `mirror.gcr.io` 正被 egress 锁拒绝(fail-closed)→ 新配置 `[registry] trivy_db_repository` + 文档 §8e 固化镜像配方(`docker pull/tag/push aquasec/trivy-db:2` 进内建 registry;`--insecure` 已覆盖明文 HTTP) ### 本轮新增真机证据(第四批:PG 契约测试 + 验证邮箱唯一性 + owner 角色) - **PG 契约测试首跑抓到 #20**:`internal/pgint` 对着真实 PG(`felis_pgint`,drop schema + 重放迁移)跑会话/OTP/op-login/绑定码/submission/build 全契约;`TestOnboardingEmailOTPRejectsTakenEmail` 当场变红 → 挖出「索引从未创建 + `ErrEmailTaken` 从未返回」。 - **迁移 0020 真机应用**:`felis migrate up` → `applied 1 migration(s): [20]`;`schema_migrations` max=20;`pg_indexes` 出现 `users_verified_email_unique`。 - **邮箱唯一性真机 409 演练(auditfix17)**:owner 已验地址被玩家 onboarding 流程二次验证 → 第一次 409 `email_taken`;**同码重放仍是 409**(证明码未被消费,符合契约;若被消费会是 400)。 - **admin 改邮箱清 verified 真机演练(auditfix18)**:PATCH player.test → `player.test+edited@example.net` 后 `email_verified=f`;PATCH 回原地址仍 `f`;玩家走 onboarding 重验证 → `t`。 - **owner 角色真机闭环(auditfix18)**:owner 行按 0011 文档升级路径置 `role='owner'` → `GET /api/v1/users` 200(修复前 403);owner op-login start 铸请求+发码、finish 得 `role=owner` 会话;PATCH owner role / DELETE owner / DISABLE owner 全部 403;玩家邮箱登录回归 200。 - **配额原子门(第五批,`bb68fef`,auditfix19 已上线)**:pgint 并发实证(真实 PG 上两路并发认领:恰 1 赢 + 1 `ErrQuotaExceeded`;修复前两路全赢);`docs/deferred-seams.md` 的 audit #4 条目核销。 ### 本轮新增真机证据(第六批:reaper 警告链路端到端演练,auditfix20) - **演练装置**:独立 hostNetwork Pod(`felis:auditfix20`,SA/卷结构复刻 live CronJob)+ 宿主本地零依赖 SMTP sink(捕获完整 RFC5322 原文)+ 专用 `felis-config-drill` Secret(SMTP 指向 `127.0.0.1:1025`);场景服 `warntest`(独立 CRD,desiredState=Stopped,不占资源),期间 operator 暂停防 reconcile。 - **投递**:13d idle → 恰 1 封,`To: player.test@example.net`(owner 的**验证**邮箱)、subject 含 `3d`、正文含 server/remaining;`warned_3d_at` 盖章、`warned_1d_at` 留空;`world_backups` 保持 4 条(纯警告零备份)。 - **去重与档位**:重跑 0 封(已盖章不重发);14.5d idle → 补发第 2 封 = `1d` 档(elif 顺序正确,不是重复 3d),`warned_1d_at` 盖章。 - **失败语义(三连)**:SMTP 端口打坏 → 日志 `warn delivery failed; will retry next run`(err=`connection refused`)、**不盖章**,恢复端口后同一 run 补发成功;邮箱 `email_verified=false` → `resolve owner email: … has no verified email`、不盖章,恢复后补发;owner NULL → 静默跳过(无日志无警告)。 - **边界**:idle=11d23h(< 12d 阈值)不发不盖章;idle=12d1m 发。`http_code` 冒烟:升级 auditfix20 后 panel 200 / op-login 200。 - **环境还原**:warntest CRD+行删除、drill Secret/Pod 删除、operator 恢复;`servers` 表回到 resolvecheck+test-one,CRD 回到 lobby/login/resolvecheck/test-one。 ### 本轮新增真机证据(第七批:idle auto-stop 三层修复,auditfix21/22) - **发现路径**:operator 深挖时先怀疑"计时器无驱动",真机实验立刻抓到 pruning(操作日志 `unknown field "status.emptySince"`)+ 触发后 88s 无动作 + RBAC 403,三层各自独立、各自足以致死。 - **场景 1(超时点唤醒 + 写权限)**:EmptySince 已超时 11 分钟 → annotate 触发一次 → 秒级内 desiredState=Stopped、sts 0/replicas、EmptySince 清、phase=Stopped。 - **场景 2(完整自驱,决定性)**:desired=Running → pod 起 → 20:11:43 盖章 → 静置无干预 → **20:12:17 自动 Stopped**(30s 到点后 ~4s 完成 stop+scaledown+markStopped)。 - **翻转混沌**:3 秒内 Running→Stopped→Running,最终收敛 Running ready(期间 409 竞争为控制器正常噪音,controller-runtime 重试自愈)。 - **收尾**:`spec.idle` 删除后再无自停;test-one 回 Stopped、空 sts、无 EmptySince;VM 镜像 auditfix22 = `f650bf8`。 ### 本轮新增真机证据(第八批:operator 自愈深挖,auditfix23/24) - **RCON Secret 删除自愈(`56f3abd`)**:修复前——删 secret 静置 60s 零察觉、触发后 96s+ 永久 `RconNotReachable`、仅人工删 pod 可恢复;修复后——删 secret **同秒**(20:22:16)察觉(Secret watch 日志)→ 新密码 + stamp 换值(`3b24164f…`→`c465b627…`)→ rollout → **20:22:47 全自动恢复 Running**(31s)。 - **陈旧锚点(`82b5a60`)**:修复前——已恢复服务器因锚点残留被 `Failed(StartupTimeout)`、`Provisioned=False` 永挂;修复后——Ready 即清锚(`startRequestedAt: None`)、`Provisioned: True`;pod blip 演练:删除→20:24:48 重锚→20:25:19 恢复→锚清空,**全程无 Failed**。 - **StatefulSet 误删自愈**:删除 sts → **秒级**重建(PVC `world-test-one-0` 保持 Bound,Retain 策略)、29s 后 Running、stamp 保持 `c465b627…`(未触发多余 rollout)。 - **收尾**:期间 7 条 operator ERROR 全为 CR/sts 409 竞争噪音(自愈);test-one 回 Stopped。 ### 本轮新增真机证据(第九批:未演练端点地毯覆盖 — 迁移 / access / window / credentials) - **account/migrate 四步状态机全链路(首个端到端演练,0 缺陷)**:造 user2 目标账户(绑定码 `FDTGKYAQ`)→ player.test claim test-one → 内部 start(未联动 UUID=404 not_linked;源=201 initiated)→ `passkey_required 409`(有 passkey 强制强因子)→ **passkey credentials list + DELETE 204**(顺带覆盖管理端点;删后 OTP 门自动解除)→ OTP start 202 / 错码 400 / 真码 confirmed → issue-code(self=400 / missing=400 / 真目标=201)→ redeem 负例×2(源持码=400;target 错码=403 setup_required——**onboarding 门正确拦截未验证账户**;验证邮箱后重试)→ **redeem 成功**(小写+空格码兼容,`servers_moved:1`)。 - **retire 断言全对**:servers.owner→user2;migration=redeemed(含时间戳);源 disabled+软删;源活跃 session=0;源旧 cookie 401;double-redeem 400;源再加迁移 409 account_retired。**环境复原**:源复活 + 新 OTP 会话、test-one 归还无主。 - **access 玩家管理全组(0 缺陷)**:players/whitelist/banlist 三个读 projector 解析正确(`There are 0 of a max of 20 players online: `→[]);kick/permission/group/luckperms-info 调用全通(demo 服无 LuckPerms → 原样透传 `Unknown or incomplete command`,API 不掩饰);输入负例 4 连 400(bad player/bad action/bad node/bad world);`whitelist add/remove` 在空档案服上得到 vanilla `That player does not exist`(fail-closed egress 无法解析 Mojang profile——**vanilla 约束非 API 缺陷**,命令已如实送达);手写 whitelist.json + `command whitelist reload` 验证成功路径解析(`players:["E2E_Tester"]`);**audit 7 条 access.\* 全记录**。 - **updates/window**:unset→null;PUT 正例+读回;半设 400;倒置 400;clear→null 全对。 - **/fleet**:4 服全量(含运行中 lobby/login 的 endpoint 地址)一次读全。 - **bootstrap-assets crd**:嵌入式资产含 `emptySince`(auditfix24 二进制核对)。 ### 本轮新增真机证据(第十批:configure email 复制链修复,auditfix25) - **发现路径**:复核 `8e7c7bb` 新增的 replicate 逻辑时怀疑 manifest/-n namespace 冲突 → kubectl 真机实验裁决(hardcoded `namespace: felis` 版直接 `error: … does not match …`,证实第一段必错、第二段被跳过)。 - **修复后行为验证**:两条命令序列(felis-smtp manifest 携带目标 ns + `felis-config` create--dry-run|apply)均被 server 接受且落点 `minecraft`。 - **live 收敛项(随下次 `felis install/setup` 重跑)**:live CronJob 仍是旧模板(无 `FELIS_SMTP_PASSWORD` env);`felis-smtp`/镜像两 ns 均未创建(SMTP 未配置,属正常);与 operator Role 手动补 patch 同批处理。 - **部署**:`felis:auditfix25` = `ed722d5`(api/operator/reaper 三处 set;api/operator rollout 完成,panel 200)。 ### 本轮新增真机证据(第十一批:submission 全路由负例矩阵 + 内部面取件 + owner 会话重铸) - **session 重铸路径(hook)**:owner 旧 cookie 过期(401)→ 临时插 `account_links`(owner↔假 UUID)→ op-login 三件套:start 202 → 日志取码 → 内部面 `op-login/{id}/approve`(hook,经 service token)→ status `approved:true` → finish 200 `role=owner` → op 域新 cookie 生效(`/fleet` 200);**演习后已删除假链接行**。顺带复证:approve 失败时 status=false、finish 400 `op_login_invalid`(码不烧,复用同码二次 finish 成功)。 - **submission 车道负例矩阵(23/23 PASS,0 缺陷)**:create 未知字段/空名/非法字符/超长 → 全 400;非 gzip 上传 400、未知 id 404;跨用户上传 → **404 不可见**(非 403);玩家打 admin 三路由(queue/approve/reject)全 403;reject 空理由/1001 字/夹带字段 → 400,成功 200(`reject_reason`/`reviewed_by`=验主邮箱/`status=rejected` 全对);重复 reject 409、reject 后 approve 409;**无 blob approve → 400 且行保持 pending_review(未 stranded)**;玩家列表严格只见自己。 - **内部面取件**:`GET /api/v1/internal/submissions/{id}/context`(service token)→ 200 + gzip magic `1f8b` + 解包内容 = 上传 marker;无 token 401;未知 id 404。 - **演习清理**:E2E-Lane-\* 8 行已删、假链接已删;队列只留历史 "E2E *"(approved)行。 - 备注:port-forward 再次因 API pod 重建悬死(空响应)→ 按速查重启即恢复;本轮无需新镜像(纯验证)。 ### 本轮新增真机证据(第十二批:users 管理面全量矩阵 → 抓到并修复 #30) - **/users 管理面矩阵(修复前,39 PASS / 2 NOTE)**:create/get/patch/list 权限与负例全对(未知字段/非法用户名/owner 角色/未知 id → 400/404;owner 保护 403 复证);sessions 单条吊销→旧 cookie 立即 401→重登恢复、revoke-all 同效(重登两次含 61s 冷却等待,全绿);passkeys 无凭证 200 no-op;links 幂等/跨用户 409/非法 auth_source 400/解链 404 全对;disable/enable/delete/double-delete 全对。 - **缺陷 #30(两处 FK 500 + 一处假 200)**:`PUT /users/{unknown}/quotas` 与 `POST /users/{unknown}/links` 触碰 user_id 外键 → **500 internal**(契约要求 404);`GET /users/{unknown}/quotas` 回**零值视图(=unlimited)**,像 id 存在。修复:`PGRepo.requireLiveUser`(live 行 + `deleted_at IS NULL`)守卫三个方法,`handleGetQuotas`/`handleLinkAccount` 补 ErrNotFound→404 映射;单测 `TestAdminSubresourcesRequireLiveUser`(fake 同步 liveUserExists 契约)+ pgint 真库契约(ghost→ErrNotFound、live 对照、软删后→ErrNotFound)。commit `1918da2`。 - **auditfix26 真机复验**:ghost 三连 → **404 not_found**;活用户对照 → PUT/GET quotas 200(读回 2)、POST link 200、delete 200;panel 200,api/operator 滚动完成(新 pod 24s Ready)。 - 契约备注(非缺陷):软删用户 `GET /users/{id}` 仍 200(带 `disabled=true` + `deleted_at`,列表已过滤);`DELETE .../sessions/deadbeef` 幂等 200;revoke-all 对未知用户 200 no-op。 ### 本轮新增真机证据(第十三批:CLI / 镜像面 / 直接构建 / 面板 CDP 全路由 + #31/#32) - **CLI 面 19/19(VM,`felis-auditfix26`)**:`version`/`-h`/无参=exit2/未知命令=exit2;`manifests` 缺 `--velocity-cidr`、缺 `--felis-image`、坏 CIDR、坏 NodePort 全 fail-loud exit2,正例渲染 25 文档且 `--worlds-host-path` 分支出 CronJob+三条前置警示——整包 `kubectl apply --dry-run=server` **全部 `configured`**;`apply` 缺 `-f`/文件不存在/坏 JSON 各自 exit2/1/1;`migrate up` 二次幂等(`database already up to date`);`bootstrap-assets crd`(含 `emptySince`)与 `game-stack`(tar)正常;`update` 正常。 - **镜像面负例 + builds 负例**:`/images` 列表 200、空体 400、合法 201、重复幂等 201、无 `?ref` 400、删除 **204**(契约;我的 200 预期作废)、再删 404、坏 ref 400;`/images/build/unknown` GET/cancel 均 404;空体提交 400;玩家打 `/images*` 全 403。 - **`/images/build` 正路径(直接构建)**:借遗留 submission blob 作 context,`FROM scratch` 内联 Dockerfile → **202 → 12s succeeded**;`/logs` SSE 实收 Kaniko 流;真机 registry `tags/list` 出现 `e2e/direct-probe:latest`(镜像确实被推入)。留档:该 drill 镜像保留在 registry。 - **`auth/options`**:未知邮箱 → `{"methods":[]}`;玩家 → `["email_otp"]`(无 passkey 时);坏邮箱 400。 - **`PATCH /servers/{name}`**:空体/storage/空 policy/未知字段/坏名 → 400;未知服 404;displayName 设置+还原 200。**契约备注**:`displayName:""` 因 `omitempty`+merge-null 语义 = **清除该字段**(本次 drill 先误读为设空串,比对 patch 前 fleet 快照确认 test-one 原无 displayName,无数据损失)。 - **面板 CDP 全路由巡检(owner 会话,14 路由)**:全部有标题/有内容/无崩溃;唯二发现 = #31/#32(见下);`luckperms` 页对 stopped 服 409 → **优雅降级**为「服务器已休眠 + 启动」态(browser 级 409 日志属正常资源日志,非缺陷)。玩家会话 4 路由零错误且**导航分级正确**(不见管理/平台组);未登录 `/login` 渲染正常(`/me` 401 为预期探测)、`/account` 未登录重定向 `/login`。 - **缺陷 #31(面板 mock 残留·owner 判定)**:`ImageBuildPage` 用 `email==="owner@mock.felis.local" || startsWith("owner@")` 猜 owner——真实 owner(`felis-owner@example.com`)**不被识别**(只能看 approved 提交),而任何 `owner@x` 邮箱都冒充 owner。改为消费 TierProvider 服务端 `isOwner`。commit `6907961`。 - **缺陷 #32(面板 mock 残留·假构建种子)**:首次访问 `/admin/builds` 向 localStorage 播种 `["bld-1","bld-2"]` → 每个新浏览器打两个必然 404 的请求(页面注释自认“in production will 404”)。移除播种。同 commit。 - **auditfix27 复验**:清掉该 localStorage 键后重访 → **只发 `/me`**、零错误、空态正确;面板 111 单测 + typecheck 全绿。 - **passkey 全流程复验(CDP 虚拟认证器,面板 UI 实操)**:注册(对话框输入→仪式→列表出现 `E2E-Key`)→ API logout → 邮箱优先 passkey 登录(`/me` 回 `player.test@example.net user`、落回 `/`)→ `DELETE credentials/{id}` 204 → 列表清空(环境复原)。 - **环境**:api/operator/reaper 镜像 = `felis:auditfix28`(= `6907961`);面板 200。**教训**:`pkill -f "port-forward …"` 会匹配到**执行该命令的 ssh 自身 cmdline**(命令里同时含明文 `port-forward svc/…`)→ 自杀式断连;改为 `ss -ltnp` 取 PID kill,另起一条 ssh 启动转发。 ### 本轮新增真机证据(第十四批:#33 死账号复活漏洞 —— 全登录门闭环 + 资产切断) - **发现路径**:users 管理面演习收尾核对时发现软删用户仍留 `account_links`/`quotas` 孤儿行 → 顺藤摸瓜确认 `UserByEmail` 与 `SessionUser` 均不过滤 `disabled`/`deleted_at`。 - **红证据(auditfix27,真机)**:禁用用户的邮箱门重登录 `verify=200`、`/me=200`(**禁用锁死可绕过**);**已删号**用户 `start=202`(真实发码)、`verify=200`、`/me=200`(**删号可复活**)。 - **修复(`5853589`,7 文件 / +306 行)**:① `UserByEmail` 只解析 live 账号(覆盖 email/passkey 邮箱优先/op-login/auth options 全部前置门);② `SessionUser` 同样过滤(皮带:任何门铸出的死号会话都立即失效);③ `RedeemPlayerBindCode` 两个解析臂拒绝死账号(新哨兵 `ErrPlayerAccountRetired` → 403 `account_retired`,**不消费码**,可逆重试);④ `DeleteUser` 事务内 `DELETE webauthn_credentials` + `account_links`(凭证与 `UNIQUE(mc_uuid)` 占用不再外泄);⑤ discoverable passkey resolve 补 liveness 检查。单测 `TestDeadAccountsCannotLogInOrKeepSessions` + pgint `TestDeadAccountsAreLockedOutInPG`(含"删后新铸会话不验证"与资产清点)。 - **绿证据(auditfix28,真机)**:红期为死号铸出的会话 → **401**(皮带生效);死号 `start=202` 中立且 **90s 日志窗口 0 条码**、`verify=400 invalid_code`;禁用中 `start` 中立(日志码数 1→1 不变)→ `verify=400`;**恢复启用后 `verify=200` + `/me=200`(可逆)**;bind 门死账号 → **403 `account_retired`** 且码保留 `count=1`(retryable);删号后 `account_links`/`webauthn_credentials` 行数 = 0。 - **演习清理**:红演练遗留(2 个测试账号的 quotas 行、1 条旧链接、2 条死号会话)已清除;`/fleet` 与 panel 冒烟保持 200。 ### 本轮新增真机证据(第十五批:#34 死账号同族收尾 —— in-game 身份解析与链接接管,auditfix29) - **发现路径**:#33 修完 Web 登录门后做同族复查——**in-game 面是 UUID 驱动、不走 Web 会话,因此 #33 的会话皮带盖不住它**。两处:① `UserByMCUUID`(claim / menu / wake 授权 / op-login vouch / QR link-status 五处共享解析)直接读 `account_links`、不过滤 users 的 liveness;② `VerifyLinkCode` 对「holder 已死」的接管语义未定义——一刀切 409 会把迁移退役这类真实场景锁死。 - **修复(`bb9798e`,5 文件 / +182−7)**:① `UserByMCUUID` JOIN users 过滤 `disabled`+`deleted_at`——死账号的链接在游戏面**读作未链接**(claim 412、wake 落回 policy 门、vouch 403、status `linked:false`),绝不作为遗留身份存活;② `VerifyLinkCode`:**软删** holder 的链接可由新账号凭新 mint 码**接管**(账号已亡,码证明调用者仍持有该 UUID),**禁用** holder 仍 409(接管= 绕过锁死,不允许)、任何失败都不消费码。fake 单测 `TestLinkVerifyTakesOverDeletedLinkOnly`;pgint `TestVerifyLinkCodeTakesOverDeletedLink` + `TestDeadAccountsAreLockedOutInPG` 增「禁用链接无资格 / 复启用恢复」断言。 - **绿证据(auditfix29 真机,三面六点)**: 1. `link/status`:活 `{"linked":true}` → 禁用 `{"linked":false}` → 恢复 `{"linked":true}`; 2. `claim`:活链 404(身份已解析、ghost 服不存在)→ 禁用 **412 not_linked** → 恢复 404;未知 UUID 对照 412; 3. **接管正例**:临时软删 holder(`de49df52`)→ 新 mint 码 `X3G3RZZM` 由 player.test verify → **200 linked:true**;链接行迁移到 player.test(`auth_source=mojang`)、码被消费(`account_link_codes` 0 行);演练后 holder 与链接全部还原; 4. **接管负例**:临时禁用 holder(user2)→ 新码 `X5W94S42` verify → **409 already_linked**;同码重试仍 409(**不消费**);恢复启用后**同码**由 holder verify → **200**(码保留、可逆); 5. **wake 授权**:活 owner **202**(真实启动)→ 禁用 **403 forbidden** → 恢复 **202**;test-one 已停回 Stopped 且所有权释放; 6. **op-login vouch**:禁用 owner UUID → **403 not_admin**(与未链接/非 staff 同一个拒绝面);活 owner UUID → 404 op_login_not_found(vouch 已解析、只是请求不存在)。 - **环境刷新(部署态)**:热升级遗留 `FELIS_IMAGE=felis:auditfix16`(job 侧执行器 backup/restore/fileedit/forwarding-init 引用的镜像)→ 已 `set env` api/operator 双 Deployment 至 `felis:auditfix29` 并滚动完成;api/operator/reaper 三镜像一致 = auditfix29;port-forward 重启后内部面绿;panel 200、op 面 `/me` 200。 - **运维备注**:`account_link_codes` 会积累过期行("失败不消费"的另一面),巡检顺手 `DELETE FROM account_link_codes WHERE expires_at < now()`。 ### 本轮新增真机证据(第十六批:内部面残面地毯补测 —— ready / join-event / status / reclaim / blacklist / backup 负例) - **背景**:对内部面做覆盖盘点后发现五个端点此前无真机证据:`ready`、`join-event`、`status`、`player/reclaim`、`player/blacklist`,以及 `internal/servers/{name}/backup` 的负例臂。本批全部补测(auditfix29),**0 新缺陷**。 - **`ready`**:test-one → **204**(advisory 语义);坏名 → 400。 - **`join-event`**(临时 UUID `3333…`):首次 **204**、重复 **204**(幂等);DB 实锤:`test-one.last_active_at` 15:02→05:53(重置)、`server_allowlist` 恰 1 行;缺 uuid → 400;未知服 → 404。演习后 allowlist 行删除、`last_active_at` 复原。 - **`status`**:test-one → 200 全字段(phase/desiredState/endpointMode=fallback/…);未知服 → 404。 - **`player/reclaim`**:新建(临时 UUID `4444…`)→ 200 `hold_expires_at`=+30d;**幂等重试返回同一时间戳**(首窗保留,与存储行 21:53:51.726017Z 完全一致);缺字段 → 400;**protected-admin 负例**:临时插 owner↔thirdparty 链接 → **409 protected_admin**(不 bar、不 stash)。清理后 `username_blacklist`/`player_data_holds`/`server_allowlist`/临时链接 = 0/0/0/0。 - **`player/blacklist`**:bar 后命中 `{"blacklisted":true}`;陌生 UUID → `false`。 - **`internal backup` 负例**:未知服 → 404;坏名 → 400;保留名(login)→ 400 `bad_name`(在入队前拒绝,RWO 闸门单测已覆盖)。 - **探针**:`/healthz` 200、`/readyz` `{"status":"ready"}`。 ### 本轮新增真机证据(第十七批:hasJoined 多源会话校验器 —— 假 Yggdrasil 全链路 + 三方身份重写/改名) - **背景**:`hasJoined`(velocity 指向的 vanilla sessionserver 协议面,`handleHasJoined`)此前零真机覆盖。本批用「VM 主机假 Yggdrasil + 临时将 `[[auth_source]]` 换向」的方式把正/负路径全部打通。 - **装置**:主机 python 假源(`:18099`,按 username 分流 `ftok`/`ftnotch`/`ftbadname`/`ftdown`/其余 204);`felis-config` 的 littleskin 源临时改指 `http://10.42.0.1:18099/fake`(tag=`faketest`/prefix=`FT`),api 滚动后逐项打靶。**演练后配置已还原 littleskin、装置已清理**。 - **结果(全绿,0 缺陷)**: 1. **三方身份重写**:`ftok` → 200 `{"id":"74409c3bbae93acabd2176e517b4f2a0",…}`,与本地按 `uuid.NewMD5(felisAuthNS, "faketest:native-123")` 的预算值**逐位一致**; 2. **保费名冲突改名**:`ftnotch` → `47c5527a18b03fe5a49ec50c11154dcd` + `name="FT_Notch"`(Mojang 实查 Notch=200 premium);`ftok` 的 `FtPlayer` 恰也是真实 Mojang 名(`640c1672…`)→ `FT_FtPlayer`,改名按设计触发(非保费名保持原名); 3. **敌意插件名拒绝**:假源返回 `§4admin` → **204**(不落 proxy 玩家列表); 4. **源故障不静默**:假源 500 → **503**(velocity 报 auth servers down),对照未知玩家 → 204; 5. **形状负例**:缺参 / 超长参 → 204;声明 body → **400 + `Connection: close`**(防 drain 挂连接)。 - **顺带覆盖**:`isPremiumName` 的 Mojang 实查(`api.mojang.com`,超时/错误 fail-closed=改名)——404→非保费、200→保费判别实测成立。 ### 本轮新增真机证据(第十八批:缺陷 #35 —— world 执行器 uid 1000 读不了游戏服写的世界 + 面板备份模块补全) - **发现路径**:给备份页新增「立即备份 + 最近操作」后做第一次真机 drill——面板链路全对(按钮/成功消息/运行态→终态、零坏请求),**但 backup Job 真的失败了**:`Job has reached the specified backoff limit`,pod 日志 `felis backup: archive: backup: tar walk: open /world/world/level.dat: permission denied`。 - **根因(缺陷 #35,跨模块)**:世界卷的属主是**游戏镜像自己的 UID**(我们发布的 Paper 镜像都是 root),而 Paper 保存 `level.dat` 用的是 **0600**(Files.createTempFile 默认权限)→ 固定 uid 1000 的 **backup / restore / fileedit / reaper** 四类执行器:读不了(归档 `permission denied`)、也覆盖不了(restore 写不进 600-root 的 level.dat)。此前 drill 侥幸全绿,是因为当时的世界文件由 uid-1000 工具(restore/夹具)写的;**服务器真实启动保存过一次之后**,所有备份从此必死。测试全部是 shape 断言(无集群),这条只能真机抓。 - **修复(`2010961`)**:四类执行器统一改**以 root 运行**(`runAsNonRoot:false`,省略 fsGroup 防误 chgrp),容器保持除 `DAC_OVERRIDE` 外 drop-ALL——与 operator `init-forwarding` 容器的既定先例同源("只有 root 能可靠读写这些文件");DAC_OVERRIDE 兜住"游戏镜像是非 root UID"的任意镜像场景。四份 shape 测试同步改断言。troubleshooting §10「Permissions」段落重写(uid-1000 + setfacl 时代结束)。 - **绿证据(auditfix31,真机四联 drill)**: 1. **backup**(面板 CDP 实操,红→绿同场景):点击「立即备份」→ `进行中` → **`成功`**(同页面 reload 后仍在);Job pod `runAsUser:0` + DAC_OVERRIDE;新归档 `test-one-1790115428472416736.tar.gz` 实测含 `world/level.dat`(471B)与 server.properties,共 499 条。 2. **restore**:`POST restore-backup`(bk-9df0…)→ 202 → Job **Succeeded**(root 写路径过关),日志 `restored from … into /world`。 3. **fileedit**:`GET /servers/test-one/file?path=world/level.dat` → **200**(base64-gzip 内容 628B)——修复前该请求必然 permission denied。 4. **reaper**(从 live CronJob 派生一次性 Job + 复刻钻取世界:1Gi PV/PVC + `world/level.dat` 512B **0600 root** + marker + 20d idle 行):pod **root+DAC_OVERRIDE**;`world reaped server=reapdrill2 …`、`evaluated=3 reaped=1`;归档 711B 含 marker 与 level.dat;PVC 删除;`world_backups` 得 `inactive_15d` 行;servers 行/CRD 保留(红线②)。**钻取现场全部清理**(job/CRD/行/PV/宿主目录/临时归档)。 - **面板补全(`97a64c8`)**:备份页新增「立即备份」(仅 Stopped 可用,409 原文呈现)与「最近操作」卡(GET `/servers/{name}/jobs`,running 每 5s 自刷新,failed 显示 Job 失败文本——正是它把上面这次失败暴露出来的)。面板 113 单测 + typecheck 全绿。 - **部署注意**:backup/restore/fileedit 的 Job spec 是 api **运行时渲染**,随镜像即生效;**reaper CronJob 的 pod 模板是安装期静态渲染**——本轮已按新形状热补丁 live 对象(root + DAC_OVERRIDE),`felis install/setup` 重渲染时收敛。 ### 本轮新增真机证据(第十九批:面板补齐 owner 解绑通行密钥入口) - **背景**:`DELETE /users/{id}/passkeys`(owner-tier 凭据补救:密钥丢失/被盗时切断登录脚架,且不锁死账号——邮箱码/游戏内审批仍可用)后端早已实现并审计,但面板无入口,owner 只能靠 API。属"功能缺口"而非缺陷。 - **修复(`11ac4f5`)**:用户详情页危险操作区新增「解绑通行密钥」行 + 确认对话框(`api.unbindUserPasskeys` 客户端方法 + 双语 i18n + wire-shape 测试)。 - **绿证据(auditfix32,CDP 真机全链)**:player.test 注册虚拟认证器密钥 `E2E-Unbind` → `/auth/options` 从 `["email_otp"]` 变为 `["passkey","email_otp"]` → owner 在 `/admin/users/` 点「解绑通行密钥」→ 对话框 → 确认 → **凭据列表清空、options 回落 `["email_otp"]`**(passkey 门确实关闭);审计 `user.unbind_passkeys` 落账(同批还可见 `account.passkey.registered`/`backup.create`/`backup.restore`/`reap_world` 各审计行)。面板 114 单测全绿。 ### 本轮新增真机证据(第二十批:缺陷 #36 —— 提交者看不到构建结果) - **缺陷 #36(提交/构建模块·结果不可见)**:`/me/submissions` 只回审核状态;构建的成功/失败(含失败原因)只有 admin-tier `/images/build/{id}` 看得到 → **提交者永远不知道自己的包构建死了**。修复 `72c4aa3`:两个列表路由(玩家 `/me/submissions` + 管理 `/submissions`)对 `build_id` 非空的行附 `build_status`/`build_error`,来源是只读 `Builder.Get`(**绝不调 Sync**——状态推进归 15s reconcile 循环,列表渲染不碰集群);构建行已消失(ErrNotFound)→ 字段省略;其他存储错误照常 500,绝不静默吞。OpenAPI 的 Submission schema 同步。 - **绿证据(auditfix33,真机双例)**:① 失败例(上下文 Dockerfile `COPY does-not-exist`)→ 提交 → owner approve → `/me/submissions` 返回 `build_status:"failed"` + `build_error:"build job failed or scan found a CRITICAL CVE"`;② 成功例(`FROM scratch`+LABEL)→ `build_status:"succeeded"`。面板 CDP(玩家会话):展开行显示「构建状态」徽章(构建失败/构建成功)+ 失败原因 + build_id,全程零 4xx。**演习残留已清**(2 行 submission+build、2 个 context blob、1 条 whitelist 条目;`sub-bf7dc1…` 那条是更早 E2E 遗留,未动)。 ### 本轮新增真机证据(第二十一批:面板文件编辑器补齐 —— 缺口而非缺陷) - **背景**:`GET /files`、`GET/PUT /file` 后端早已全绿(可读写 `level.dat`),但面板无入口——「一行 server.properties 写错导致起不来」的修复路径只有 API。属功能缺口。 - **修复(`0a36b3f`)**:新增 `/servers/:name/files` 页:面包屑目录浏览、编辑器对话框([]byte ↔ base64 编解码)、二进制文件打开即只读(NUL/非 UTF-8 拒绝 round-trip)、>256KiB 禁用保存;**停服门前置**(世界卷 RWO,未停服时整页显示「服务器正在运行」+ 停止动作,而不是让每个调用 409);控制台右侧新增门口卡。i18n `files` 命名空间(en/zh)+ 3 条 wire-shape 测试。 - **绿证据(auditfix34,CDP owner 全链)**:根目录 → `world/` 导航;`felis-e2e-marker.txt`(原 `v1\n`)打开 → 追加 → 保存「已保存 …」→ **API 读回一致** → 重开一致 → 还原原始字节 → 落盘复核一致(零残留);`level.dat` 打开为只读 + 二进制提示;控制台门口卡存在;`audit_logs` 两条 `file.write`(edit/restore 各一);全程零 API 4xx/5xx。面板 117 单测 + typecheck 全绿。 ### 本轮新增真机证据(第二十二批:缺陷 #37 —— 系统服务在面板里全是死操作) - **缺陷 #37(面板·fleet 死操作)**:`login`/`lobby` 是平台自建系统服务、名字在保留名单里,于是**每个 per-server 路由都用 `ValidateServerName` 拒绝**(400 `bad_name`)——但管理端 fleet 表格给这两行渲染 认领/停止/唤醒/控制台,全是死操作(控制台链接点进去也是一页 `invalid server name: reserved`)。修复 `2f90851`:`naming.IsSystemServer` 作为唯一事实源;fleet 行附 `system:true`;面板把这两行渲染为「系统服务」纯标签(owner 列 + 操作列),不再给任何动作。玩家侧不受影响(`/me/servers` 本就不含系统服务)。 - **绿证据(auditfix35,真机)**:`GET /fleet`(owner)→ `lobby system:true`、`login system:true`、`test-one/resolvecheck` 无 flag;面板 CDP:两行「系统服务」、**0 按钮 0 链接**;`test-one` 行照常 认领/控制台、无系统标记;零 4xx。Go 侧新增 `TestIsSystemServer` + `TestFleetAdminRead` 的 system 子测试。 ### 本轮新增真机证据(第二十三批:缺陷 #38 + 多节点回收缺口 —— reaper 提示过期 / 钉节点) - **缺陷 #38(CLI·提示过期)**:`felis manifests` 渲染 reaper 时的 stderr 提示还在教“uid 1000 需要 `setfacl -m u:1000:x` 才能遍历存储根”——#35 之后 reaper 已改为 **root + DAC_OVERRIDE**,这条指导已失效且会误导运维(照做无害但白做,真问题被掩盖)。同批落 **多节点回收缺口**(结论第 5 条):新增 `--reaper-node`,reaper CronJob 的 pod 渲染 `nodeSelector kubernetes.io/hostname=`;多节点集群必须钉在存世界的节点,否则可能调度到 hostPath 为空的节点。`--reaper-node` 无 `--worlds-host-path` 时 fail-loud exit 2。修复 `daf7602`(双测:`TestReaperCronJob_NodePin`、`TestManifestsReaperNodePin`)。 - **绿证据(auditfix36,真机 render + dry-run + 收敛 diff)**: 1. **固定渲染**:`felis manifests --felis-image felis:auditfix36 --velocity-cidr 10.211.55.6/32 --panel-node-port 30443 --worlds-host-path /var/lib/rancher/k3s/storage --archive-local-path /var/lib/felis/archives --reaper-node localhost.localdomain` → exit 0;bundle 内 `kubernetes.io/hostname: localhost.localdomain`;stderr **0 处** uid 1000 / setfacl,改为「已钉到节点 …」; 2. **负例**:`--reaper-node` 无 `--worlds-host-path` → exit 2 + 原文;不传 node 的渲染 stderr 仍完整保留「NO nodeSelector … 多节点必须传 --reaper-node」警示,bundle 内 0 个 selector; 3. **`kubectl apply --dry-run=server -f -`**:整包 **全部 configured**(含新 nodeSelector 的 CronJob); 4. **再安装收敛性 diff**(`kubectl diff`,本批新增的收敛证据):与 live 对比只剩 **一个对象**(reaper CronJob)两处实质增量——`+env FELIS_SMTP_PASSWORD`(热升级期未补的模板字段)与本次显式传入的 `+nodeSelector`;其余 24 份文档(api/operator Deployment、RBAC、NetworkPolicy、PVC、registry)**零差异**——即“热补丁过的 live 对象”与“当前代码重渲染”已收敛(reaper 安全上下文 root+DAC_OVERRIDE 两侧一致,无 diff)。 ### 本轮新增真机证据(第二十四批:S3 上传通道真机演练 —— 此前只有单测的暗路径) - **背景**:`internal/submit/s3store.go`(S3ContextStore)此前只有单测,本装是 local 路径(`user_uploads_context = "/var/lib/felis/uploads"`)从未激活。本批用「VM 宿主 MinIO(quay.io 镜像,:9000)+ 临时把 felis-config 切到 `s3://felis-user-uploads` + `[registry.s3] endpoint=http://10.211.55.6:9000` + 创建 `felis-uploads-s3` Secret」把整条通道打通,**全程 0 缺陷**、演练后还原并逐项复核。 - **绿证据(auditfix36,真机全链)**: 1. 切 S3 后 api 滚动启动 **无** “S3 user-uploads store not configured” 告警(凭据解析成功);pod 内 busybox 探针实测可达 `http://10.211.55.6:9000/minio/health/live`(rc=0); 2. 玩家提交 + 上传 → **200**,对象实测落桶:`sub-12c953e12b0cd144/context.tar.gz`(197B,mc ls 实见); 3. owner approve → 构建 Job `build-bld-1790118222183394193` **status.succeeded=1**(fetch-context 从内部面流式取件 = api 自 S3 读回成功);`/me/submissions` 的 `build_status` 收敛为 `succeeded`; 4. **回滚**:felis-config 还原(sha256 与演练前备份**逐字节一致**)、删除 `felis-uploads-s3`、api 滚动;再演练一次本地路径:新提交上传 **200** 且 blob 实测落在 uploads PVC(context.tar.gz 197B);启动日志仅剩 smtp/jwks 两条既有提示; 5. **清理**:2 行 submission + 1 行 build 删除、回滚演练 blob 删除、S3 构建产物从 whitelist 摘除(204)、MinIO 容器 + 两个镜像移除;`/fleet` 200。 - **遗留观察(非缺陷)**:registry 里保留本次推送的 `user-uploads/sub-12c953e12b0cd144:latest` 层数据(与早前 direct-probe 同类,filesystem registry 无删除接口);`felis setup` 的「S3 存储」向导屏本身未演练(列入 TUI 逐屏待办)。 ### 本轮新增真机证据(第二十五批:breakGlass 控制台逐屏全量 + 备份/恢复门禁 —— 缺陷 #39–#43) - **背景**:breakGlass 此前只验过 Owner 首装与 Add Operator happy path。本批把菜单四操作(Owner reset / Add Operator / Halt / Sync)+ 首装(bootstrap)分支全部逐屏真机走完,并打穿 Sync/恢复背后的 API 门禁;共抓 5 个缺陷、全部修复复验。驱动方式:VM tmux(`remain-on-exit on` 才能读回 alt-screen 撕掉后的 durable summary)。 - **先落的正向证据(无缺陷)**:Halt——`test-one` Running → 选中 → 卡「is stopping」→ CRD `desiredState=Stopped`、pod 收敛消失、审计 `break_glass.halt`;面板 `wake`/`stop` 两个恢复杠杆均 202(复验后还原)。Sync 正路径——test-one(Stopped)→ 卡 backup started → Job 6s Complete → 归档落 `felis-backups` PVC(167MB)→ `world_backups` 行 `present` → `/api/v1/backups` 可见。Sync 负路径——对 Running 选 → 友好 409 卡(不误烧冷却)。 - **缺陷 #39(owner 席位可被静默复制,且不可清理)**:恢复模式下用非在位席位名做「reset」→ `UpsertOwner` insert 臂**铸出第二个 owner 行**、原席位继续存活;面板对任何 owner 行都删/降/禁 403 → 永久无法收敛回单席。修复 `55d515d`:`provisionOwner` 先查在位席位(新 `PGRepo.OwnerUsername`),非席位名 → `ownerSeatTakenError`(`Is api.ErrConflict` → TUI 路由回表单并**指名**应输入的用户名);bootstrap(无席位)与同席位名复位原样。真机双验:新名被拒(表单原位显示 `an Owner already exists as "08595879-…" — enter that username to reset the Owner`);改席位名复位成功(owner 恒 1 行、id/邮箱不变);面板删除保护同步实证(对新 owner 行 DELETE → 403)。 - **缺陷 #40(operator 撞名 = 裸 SQLSTATE,「换名重试」分支在真机从未生效)**:`InsertOperator` 冲突返回原始驱动错误(23505),TUI 却按 `api.ErrConflict` 判定「可恢复、换名重试」——fake 与 PG 漂移。真机复现:输入已存在用户名 → 控制台 exit 1 + 裸错误。修复 `55d515d`:`isUniqueViolation → ErrConflict` + pgint 契约断言。复验:撞名**回到表单**提示换名 → `drill-op-2` 成功(审计 `break_glass.operator_create` 落账,演练行已清)。 - **缺陷 #41(Sync picker 死选项)**:picker 列出 login/lobby,而备份 API 对它们**永远失败**(保留名 + 无 servers 行);真机选 lobby → 裸内部错误 + exit 1。修复 `ac3a557`:`backupPickable` 过滤系统服(halt picker 保留它们,断玻璃完整权力)。 - **缺陷 #42(缺失世界盘 → 202 后静默卡死 30 分钟)**:对「从未启动/已被回收」的服备份或恢复:202 → Job → Pod `persistentvolumeclaim "world--0" not found` **Pending 至 deadline**,全程零失败记录。真机用已回收的 `resolvecheck` 复现(留证后删除)。修复 `508a1c0`:`Cluster.WorldVolumeExists`(直接 Get 与 Job 挂载**同名**的 PVC)+ 两 handler 409 `no_world_volume`("start it once to create it, then retry")。**修复首跑翻出配套 RBAC 洞**:felis-api SA 无 `persistentvolumeclaims:get`(403 被吞成 500「internal error」)→ `APIMinecraftRole` 补 get-only 规则 + rbac 测试锚点。复验(auditfix38):resolvecheck 两面 409 + 友好文案;test-one 照常 202 → Job 10s Complete → 新行落库。 - **缺陷 #43(409 一刀切文案)**:TUI 把所有 409 当停服门 → 世界盘拒绝会展示错误原因。修复 `ac3a557`:按 body 的 `error.code` 分流(无 code 的旧体仍按停服门)。复验:无盘 pick 显示 API 原文;停服门文案不变。 - **同步完成**:① bootstrap 分支 scratch 库演练(`migrate up` 20 迁移 → 无菜单/无认证直接铸 owner;`local_auth_enabled=true`;审计 `break_glass.bootstrap`;exit 0;库/hba 规则/临时配置即测即清);② 台面收敛:reaper CronJob `suspend=true/auditfix36/无 pin` → `suspend=false / felis:auditfix38 / nodeSelector=localhost.localdomain`;③ 镜像升级 `auditfix37→38`(api/operator + `FELIS_IMAGE`)。 - **流程修正(教训)**:pgint 一度误用**本机 Docker Desktop**(启动 daemon + 临时 PG 容器)——已完全清理(容器/镜像删除、daemon 退出),并改为**经 ssh 隧道用 VM 的 postgres** 运行(`ssh -L 15433:127.0.0.1:5432` → `postgres://felis:***@localhost:15433/felis_pgint?sslmode=disable`)。勿再在本机跑容器。 ### 本轮新增真机证据(第二十六批:`felis setup` 重跑向导逐屏 + 构建链 pin 回验 —— 缺陷 #44) - **范围**:本机已装机,故覆盖"重跑状态屏 + c/s/e 三条 reconfigure 流";首装屏(postgres/owner/connect/edge/storage/smtp/mc-bind/migration/preflight/summary)此前各批已有定点真机证据(安装闭环 / 第二~三批 / 第九批 / 第二十五批),本轮不重复。 - **逐屏走查(auditfix38→39,VM tmux)**: 1. 重跑 → 直落状态屏「✓ Felis is already set up.」(owner/connect 不触碰;host bootstrap 已就绪跳过)✓ 2. `c` → 三选一 chooser(Local / Cloudflare+Access / Reverse proxy + 警示语)渲染 ✓,esc 无损返回。 3. `s` → chooser 预选当前后端;S3 分支表单(Endpoint/Bucket/Region/AK/SK + 提示)渲染 ✓。**观察:reconfigure 非只读**——选「Local disk」即 apply(写 /etc 两文件 + 重渲染 felis-config + 滚 API);从 S3 表单 esc 退回会把选择重置为 Local 预选。 4. `e` → SMTP 表单渲染 ✓;esc 直接回状态屏、零副作用(felis-smtp 未创建)✓。 - **缺陷 #44(CLI·重跑框脱落)**:重跑后完成 `s`/`c` reconfigure,落回首装 summary「✓ Setup complete.」——丢了 alreadySetUp 框(smtp 路径有专门分支,storage/connect 漏)。修复 `abb5910`(`showSummary` 透传 `m.result.alreadySetUp`)+ 回归测试 `TestRootReconfigureStorageKeepsStatusFraming`。真机复验(auditfix39):storage reconfigure 完成 → 「✓ Felis is already set up.」+ storage recap ✓。 - **演练事故(自曝;环境 drift,非产品缺陷)**:首次 storage 走查意外触发 Local apply——它从 `/etc/felis/felis.pod.toml` 重渲染 Secret,而构建链 pin 值当初**只热补在 live Secret、不在 /etc 文件** → 重渲染清空 pin(放任则下次构建死在 `:latest` 拉取 + trivy DB egress)。当场修复:pin 值写回 `/etc/felis/felis.host.toml` + `/etc/felis/felis.pod.toml` → 重渲染 Secret → 滚 API;再做第二次 storage apply,重渲染后 pin 仍在(drift 修复耐久)。 - **构建链回验(direct build,真机)**:Job 规格实证 `kaniko=gcr.io/kaniko-project/executor:v1.24.0`、`trivy=aquasec/trivy:0.74.0`、`--db-repository registry.felis.svc:5000/mirror/trivy-db:2`(registry 仍有 `mirror/trivy-db`);`POST /images/build`(context=遗留 `sub-bf7dc18…` blob,内部面取件)→ 202 → **succeeded**;registry `e2e/pins-check` 落位;whitelist 条目已摘除(204)、Job 已清。 - **文档修正(`ae6e925`)**:§8e 原「编辑 felis.toml 后重启 felis-api」不完整(API 挂的是 Secret)→ 改为「写进 /etc 两文件 → 重渲染 Secret → roll」,并写明三种无效/易损做法(只 restart / 只改 host 文件 / 只热补 live Secret——后者会在下次 reconfigure 被冲掉)。 - 收尾:api 1/1、panel 200、tmux 全清。 ### 本轮新增真机证据(第二十七批:告警模块落地 —— 内部面 /metrics + 规则集 + 真实构建失败实弹演练) - **范围**:把"指标 → 规则 → 告警"链路从零补到可交付:API 内部面 `/metrics`(`94f71ee`)、`deploy/alerts/` 规则与 promtool 单测(`43df08b`)、真机实弹演练(本批)。 - **/metrics(`94f71ee`)**:`felis_image_build_failures_total` 此前只在进程内存里、无任何 scrape 出口。修复:internal face(8081)新增 `GET /metrics`(服务面 Public 路由,语义同 /healthz);单测断言外部面 404。真机:port-forward `svc/felis-api-internal 18081:8081` → 200 且含 `felis_image_build_failures_total`;operator `:8080` 提供 `felis_servers_total` / `felis_start_duration_seconds_*`。 - **规则集(`43df08b`)**:`deploy/alerts/felis-alerts.yaml`(plain Prometheus)5 条——构建失败 increase>0 / 起服 p90>300s / 磁盘可用<15% / DiskPressure / 内存可用<10%;`felis-prometheusrule.yaml` 为 prometheus-operator twin(脚本比对两文件 groups 一致);`felis-alerts_test.yml` 为 promtool 单测。VM 上 promtool 3.14.0 实跑:`check rules` + `test rules` 双 SUCCESS。 - **实弹演练(真实构建失败 → pending → firing)**: 1. 打包含 `COPY does-not-exist` 的 Dockerfile 上传到 uploads PVC `sub-alertdrill` → `POST /images/build`(`e2e/alert-drill2:latest`); 2. Kaniko `failed to get fileinfo for /context/does-not-exist` → Job Failed、build 行 `failed`; 3. 真实 Prometheus(宿主 `:19090`)scrape `127.0.0.1:18081`(api) 与 `:18080`(operator) 双 target up;`felis_image_build_failures_total{job="felis-api"}=1`; 4. `FelisImageBuildFailures` pending(activeAt 08:28:14Z)→ **08:33:14Z 准时 firing**(`for: 5m` 精确到期),labels/annotations 完整。 - **清尾(残留全清)**:whitelist `e2e/alert-drill` 摘除(204);`sub-alertdrill` 目录、`/tmp/drillctx`、两个演练 Job、tmux `prom`/`fwd`/`alertpoll`、`/root/prom-drill`(promtool+prometheus 二进制)全删;DB `%alert-drill%` 行删除(whitelist/build 复核 count=0);宿主无残留监听/进程。演练期间 live api 进程内计数器=1(重启归零,属演练事实)。 - **可达性追加**:#44 定级 ①(轻)——已装机环境重跑 `felis setup` 完成 storage/connect reconfigure 即触发。 ### 本轮新增真机证据(第二十八批:缺陷 #45 —— 审核门"盲批":评审看不到将被执行的 recipe) - **缺陷 #45(构建 lane·审核语义)**:被执行的 Dockerfile 永远来自**上传上下文压缩包根目录的 `Dockerfile`**(`build/jobspec.go` 钉死 `--dockerfile=Dockerfile`),API 的 `dockerfile` 字段**仅审计存档**(`submit.auditDockerfile`、`build.Request` 注释均已声明)——但审核者没有任何路径能看到它:`GET /api/v1/submissions` 不含 blob 内容、面板只显示 `context_ref` 文本、内部面取件路由是 service-token(评审用不了)→ "人工审核是门禁"事实上是**盲批**。同批口径缺口:`POST /api/v1/images/build` 的 `dockerfile` 字段在 OpenAPI 里无任何说明(易被当成"将被执行"),面板表单也把该框呈现为"Dockerfile 内容 *"。 - **修复(`168a375`)**: - 新增 admin-tier `GET /api/v1/submissions/{id}/context`:评审下载与构建 Pod 同源同字节的 `context.tar.gz`;`Content-Disposition: attachment` + `nosniff`(攻击者提供的归档只下载、不渲染);审计 `submission.context.download`(actor=评审者、target=submission id)。 - 内部面取件 handler 共享 `openSubmissionContext`/`streamSubmissionContext`(行为不变,原测试锁定)。 - OpenAPI:新路由 + `/api/v1/images/build` 字段描述补全(明说"执行的是 context 根目录的 Dockerfile;`dockerfile` 仅审计")。 - 面板:SubmissionsPage 展开区新增「下载上下文」按钮(spinner/错误呈现,i18n en/zh);ImageBuildPage 表单补审计说明行。 - 测试:`TestAdminSubmissionContextRoute`(流式/404/503)+ admin-only 矩阵加该路由 + OpenAPI parity 强制文档;面板 117 单测 + 构建、go vet/go test 全绿。 - **真机验证(auditfix41 已部署;owner 会话经 op-login + 内部面代 approve 重铸)**: - admin 下载 `sub-bf7dc18e9dd97dc2` → **200**,`attachment; filename="context.tar.gz"`、`application/gzip`、`nosniff`;sha256 `205496f2…` **与 uploads PVC blob 逐字节一致**; - 内部面(Bearer=felis-service-token)同 blob → 200 + 同 sha256(重构未破坏构建取件路径);无 token → 401; - 有效玩家会话(console host 隔离,仅 adminOnly 生效)→ **403**;匿名 → 401;不存在 id(admin)→ 404; - 审计落账:`audit_logs` = `felis-owner@example.com | submission.context.download | sub-bf7dc18e9dd97dc2`; - 面板产物:服务端 index.html 引用新构建 `index-CwFSKpgW.js`,bundle 内含新按钮逻辑(grep 命中 3 处)。 - **可达性追加**:#45 定级 ①——每一次真实的"用户提交 → 管理员审核"都会踩到(审核者此前无法查看将被执行的内容)。 - **hook 链补齐(可复用)**:staff 账号走邮件登录门会被设计拒绝(refuse staff)→ owner 会话铸法:`op-login/start`(email=felis-owner@example.com)→ VM 日志 grep `email-otp` 取码(no-Mailer fallback)→ 内部面 `op-login/{id}/approve`(Bearer=felis-service-token;body `approver_uuid`=owner 的 mc_uuid)→ `op-login/finish`(curl -c 存 cookie)。 ### 本轮新增真机证据(第二十九批:镜像耐久落地 —— registry 托管 + 回环拉取路径;#46–#49) - **背景(结论清单第 2 条)**:磁盘压力演练证明 kubelet 会 GC 掉"当前无人使用"的镜像 → ImagePullBackOff,恢复依赖人工重导入。本批让镜像自愈:自建镜像全部托管进内建 registry,节点侧 pull 经回环 hostPort(节点 containerd 到 Service VIP 是死路,实测 "Empty reply")。 - **平台侧 `a9b275a`**:registry 容器端口加 `hostPort 127.0.0.1:5000`;同提交修 **#46** —— registry 独立资源模板(1 CPU / 2Gi):旧模板 256Mi 在实测推 475MB 层时被 OOM kill(dmesg `oom-kill … registry, oom_score_adj=989`,上传中断),2Gi 下同一推送 2 秒完成。 - **安装器侧 `13d64e0` + 文档 `fa0e8d7`**:自建镜像规范 ref = `registry.felis.svc:5000/felis/{felis,limbo,lobby,paper}:demo`;import 进 containerd 就用该名(首启命中本地,免 registry round-trip),`deploy_bundle` 之后统一 `push_images_to_registry` 入仓(推 `127.0.0.1:5000`;registry 只认主机名之后的路径 —— 推/拉落点一致)。`configure_registry_mirror` 写 `registries.yaml`(`registry.felis.svc:5000 → http://127.0.0.1:5000`),内容不变不重启 k3s;`import_registry_image` 预缓存 registry:2(重跑走跳过分支)。迁移 **0021** 把 recommended 白名单重指到 registry ref(线上实查两行已落)。 - **真机三次重跑**(`/opt/felis/src` = 765a892 快照;`FELIS_SKIP_FETCH=1 FELIS_INSTALL_MODE=full FELIS_IMAGE=registry.felis.svc:5000/felis/felis:auditfix42`): - run1 失败 = **#47**:Mac tar 的 `._*` 旁文件混入构建上下文,`._0004_*.sql` 被 //go:embed → 新二进制的 `felis migrate` 报 `non-numeric version "."`,安装器停在 run_migrations。修复 `5fa8b74`(.dockerignore 排除 `._*`/`.DS_Store`)。复验方式:故意在暂存树留 `._zz_probe_junk.sql` → 重建后 `migrations applied`(过滤生效)。 - run2 失败 = **#48**:每个镜像一对 `systemctl start/stop docker` 触发 systemd 限流(`Start request repeated too quickly / start-limit-hit`),第 4 个镜像(paper)未入仓。修复 `c7e585e`(整批一次 start/stop,单测锚定)。run3 零 `[fail]`:4 镜像全部入仓(push digest ×4 实收)。 - run3 收敛实查:api/operator/reaper = `registry.felis.svc:5000/felis/felis:auditfix42`(registry ref 首滚命中本地 import);registry 模板 1 CPU/2Gi 生效;reaper CronJob 同步换 ref 且 nodeSelector 保留;login/lobby Running 于 registry ref;迁移 applied。 - **GC 演练(本批验收本体)**: - A 控制面:`ctr images rm …/felis/felis:auditfix42` + `ctr content prune references` → 本地 ref 消失 → `rollout restart felis-api` → 事件 `Pulling` → `Pulled … Successfully pulled image … in 25ms`,ref 恢复、pod Running。 - B 游戏:rm `…/felis/lobby:demo` → 删 `lobby-0` → `Successfully pulled … in 10ms … Image size: 182357358 bytes`,Running。 - #46 复验:整轮重建 + 4 推送期间 dmesg `oom-kill` 计数不变(仍 2,历史)。 - **构建 lane 复验**:`POST /images/build`(context=遗留 `sub-bf7dc18…`;ref `registry.felis.svc:5000/e2e/durable-check2:latest`)→ 202 → succeeded;registry `e2e/durable-check2` tags 落位;Job 事件见 felis/kaniko/trivy 三镜像就绪;白名单条目摘除(204)、Job 清。 - **#49(同一条升级路径的第二类静默回退)**:`write_felis_toml` 重写整表([registry] 仅 url+build_namespace、[archive] 仅 store+local_path)→ §8e 的 kaniko/trivy pin、构建上限、uploads 后端、[registry.s3]、reaper 的 retention/warn_before/max_local_bytes 在重跑时全部丢失(S3 安装切回 local、构建回退被 egress 拒绝的上游 executor)。修复 `765a892`+`b8e554d`(沿用 [smtp]/[[auth_source]] 的 carry 模式;url/build_namespace/store/local_path 保持安装器所有)。真机复验:预置 `retention = "30d"`,run3 后 host toml / pod toml / felis-config Secret 三层都在,且构建 lane 直接用 carry 的配置跑通(上条)。 - **可达性**:#46 ①(用户构建大层或安装器入仓即触发;实测 475MB 层);#47 ②(Mac 打包树构建路径,真机踩中);#48 ②(安装器重跑,真机踩中);#49 ②(升级=重跑安装器,真机踩中)。 - **口径/遗留**:`demo-up.sh` 未改(dev/demo 路径,本地 tag 导入维持原状);kaniko/trivy 编译默认值未动(§8e 改为"镜像进 registry"配方);registry 2Gi 为渲染常量(暂未开 flag);drill 残留:registry 里 `e2e/*` 小镜像留档,`durable-check2` 白名单条目已摘。 ### 本轮新增真机证据(第三十批:缺陷 #50 —— `[smtp]` carry 吞掉下一节的注释块,每次重跑 +1) - **发现路径**:为 `felis setup` 首装连续走查做前置盘点时读 live 配置——`/etc/felis/felis.host.toml` 已经堆了 **3 份**、`felis.pod.toml` **4 份**重复的 Yggdrasil 注释块(同一段文案逐次叠加);用脚本自带的提取器实测:一次重跑会把 3 份全部当作 `[smtp]` 内容带走,再叠一份模板注释 → **每次重跑 +1、无上界**(host/pod 增速不同步,现场 3 vs 4 即历史残迹)。 - **根因**:`persisted_smtp_block` 打印"`[smtp]` 到下一个 section header 之间"的**所有行**;generated 注释块正好落在这段区间里 → 被吞并。同文件的自称"cached on first call"缓存因写方是命令替换(子 shell)从未生效,pod 写实际上重复抽取刚被重写的 host,加剧了两文件的不同步。纯注释膨胀、无功能损失,但属 #49 同族 carry 语义缺陷(把不属于自己的内容也带走了)。 - **修复 `4d3c85f`**:carry 改为白名单(section header + 键行),与 #49 的 `[registry]`/`[archive]` 提取同型;删掉失效缓存说明。`deploy/bootstrap_test.sh` 新增用例:配置值被携带 / 不吞注释行 / 不越节 / 写回后二次抽取**字节稳定**(幂等)。 - **真机验证(auditfix43,两次连续全量重跑)**: - run1(`bootstrap-auditfix43.log`):零 `[fail]`;host 注释块 **3→1**、pod **4→1**;与 run 前快照 diff 恰为 21/31 行(全部是被删掉的重复注释)——值零漂移(kaniko/trivy pin、`trivy_db_repository`、uploads local、`[registry.s3]` 空、`retention="30d"`、auth_source 原样、空 `[smtp]` 节保留;两文件 db host 仍分别为 127.0.0.1 / 10.211.55.6)。 - run2(`bootstrap-auditfix43b.log`,紧接再跑):零 `[fail]`,仍 **1/1** —— 收敛证明(旧代码此处会 1→2 继续增长)。 - 部署同步:api/operator = `registry.felis.svc:5000/felis/felis:auditfix43`;reaper CronJob 同 ref 且 nodeSelector 保留;registry `felis/felis` tags = auditfix41/42/43;panel 200;host 二进制已从新镜像提取。 - **可达性**:#50 ②——配置过 email(存在 `[smtp]` 节)的安装,按文档升级=重跑安装器即触发;危害=配置注释无限膨胀(每次 +1),无功能损失。 ### 本轮新增真机证据(第三十一批:首装连续走查 + 缺陷 #51 —— 工作负载 `felis-config` 副本永不刷新) **一、`felis setup` 首装单次连续走查(队列第 1 项,完成;0 缺陷)** - **装置**:scratch 库 `felis_scratch`(新建 + `felis migrate up` 21 条迁移)+ scratch 配置(真实 pod toml 副本,仅换库名;root 0600);hook 直插 `account_link_codes` 一枚绑定码(等价 `/link` 内网端点写入,代替"进服拿码");tmux 驱动 `felis setup -config`;k8s 只读复用(登录门 Ready 等待通过)。 - **连续走查(一条会话走完)**:Preflight(自动:PG✓/迁移 21 applied/面板✓)→ **MC 绑定**(输码 → working →「✓ Owner account is ready.」+ 一次性 setup URL)→ **连接 chooser**(Local)→ **存储 chooser**(Local → working → ~20s 后「✓ Local storage configured.」,含 Secret 重渲染 + API rollout)→ **首装 Summary**(「✓ Setup complete.」+ owner/setup URL/access/storage/panel 卡片 + c/s/e 提示)→ **轨道回顾**(← 依次只读 recap Storage→Connection→Owner→Preflight,→/esc 回到前台)→ Enter 退出 → stdout 汇总(`Owner account … provisioned (passwordless)`、`Recorded as "root"`、setup URL、Admin console)→ **EXIT=0**。 - **结果**:移动端/文案/切换全部符合设计,**0 新缺陷**;scratch 库侧复核:owner(role=owner) 1 行、绑定码已消费(0)、setup_tokens 1、`local_auth_enabled=true`。 - **副作用与还原(如实记录)**:存储 apply 会把 `/etc/felis` 两文件重写为 Go encoder 形态(无注释、含空值键如 `[archive.s3]`,**值零漂移**:kaniko/trivy pin、uploads local、retention 30d、auth_source 原样),并重渲染控制面 Secret + 滚 API——这是该向导的既定行为;演练后按 pre 快照整文件还原(sha256 逐字节一致),两 ns Secret 重渲染复核一致,scratch 库/配置/hba 行/tmux 全部清理。 **二、缺陷 #51(工作负载 `felis-config` 副本永不刷新)** - **发现路径**:上条还原核对时发现 minecraft ns 的 `felis-config` 是**旧形态**(encoder 式)而 felis ns 已是模板式 → 挖出 `ensureSecretReplica` 的「绝不覆盖既有副本」(凭据语义:防冲掉手工轮换值)把 **felis-config 也纳入只建不更**,而 bootstrap 只 apply 控制 ns。后果:改配置后(DB 凭据轮换、[archive] 保留策略调整、root domain 等)backup/restore/fileedit Job 与 reaper 永远读旧副本 → 静默失效(如备份认证失败)。 - **红证据(auditfix43,真机)**:向 minecraft 副本注入 `# drill-51-stale-marker` → 运行 `felis setup` → 输出 `- config (minecraft ns): skipped (already exists)`;副本 sha `5ec2ff6e…` 保持,控制面 `e4791fe1…` 不同(陈旧坐实)。 - **修复 `328e570`**:① setup 侧:`ensureSecretReplica` 增 `refreshExisting`(仅 felis-config 传 true)——源缺失降级 skip、内容一致 skip(`already current`)、不同则原地 Update;凭据类保持 create-if-absent;新增 `updated` 结果与「refreshed from the control namespace」文案。② bootstrap 侧:新增 `apply_felis_config_secrets()`,每次运行同时 apply 控制 ns + 工作负载 ns 两份(同渲染自最新 pod toml)。单测:Go 新增 4 例(陈旧刷新/一致跳过/空键补写/源缺失降级);`bootstrap_test.sh` 新增 4 断言(两 ns apply、同一 pod toml 渲染、恰 2 次 apply)。 - **绿证据(auditfix44,真机)**:A) 安装器重跑(标记仍在副本中)→ 日志出现 `secret/felis-config configured`(工作负载 ns 被刷新)→ 副本 sha 与控制面一致、标记 0;B) 再注入标记 → 运行**新** `felis setup` → 输出 `- config (minecraft ns): refreshed from the control namespace`、凭据仍 `skipped (already exists)`、副本 sha 一致、`EXIT=0`。部署=auditfix44(api/operator/reaper),panel 200,host 二进制随镜像 `docker cp` 刷新。 - **可达性**:#51 ②——升级=重跑安装器、或改完配置跑 setup 即触发;旧行为下 backup/reaper 静默使用旧配置(DB 轮换后备份全挂)。 **三、环境修复(非产品)**:现场 pg_hba 缺 `felis_pgint` 规则(按文档走 ssh 隧道跑 pgint 会 ident 失败)——补回 `host felis_pgint felis 127.0.0.1/32 scram-sha-256` 并复测连接成功。 ### 本轮新增真机证据(第三十二批:S3 存储向导逐屏走查收尾 + 缺陷 #52 —— 向导 apply 不刷新工作负载 `felis-config` 镜像) **一、S3 存储向导屏走查(队列第 2 项,完成;0 功能缺陷,走查自身暴露镜像缺口 → #52)** - **装置**:VM 宿主 MinIO 容器(`quay.io/minio/minio`,`felis`/`felis-drill-9000`,:9000)+ bucket `felis-wizard-uploads`;tmux 驱动 `felis setup` 重跑向导;行动前先留 preS3 快照(两 toml + 两 ns Secret + sha256)。 - **负例**:错误凭据 → `✗ Could not save storage settings.` + `submit: s3 credentials rejected: The Access Key Id you provided does not exist`(`CheckS3Access` 预检先于一切写入);**零副作用**(无 Secret、两 toml sha 不变);`esc` 返回编辑时已填值保留(密钥掩码)✓ - **正例**:修正凭据 → ~10s working → `✓ Object storage configured.` → Enter → 状态屏 `storage s3://felis-wizard-uploads · http://10.211.55.6:9000` ✓ - **落地核对**:`felis-uploads-s3` Secret(access_key_id/secret_access_key);两 toml `user_uploads_context` + `[registry.s3]` endpoint/refs;API 滚动;启动日志仅既有的 smtp/jwks 警告 ✓ - **功能链(batch24 同款)**:player.test 邮箱 OTP 登录(hook 取码)→ `POST /api/v1/me/submissions` 201 → context 上传 200 → MinIO 桶实见 `sub-853a4e2ba4ba6443/context.tar.gz` → 内部面取回 200 + tar 内容正确 ✓ - **UI 回滚(`s` → Local)**:两 toml 归位(`/var/lib/felis/uploads`、`[registry.s3]` 归空)、控制面 Secret 更新、API 滚毕;与 preS3 快照的差异仅「s3 字段回环 + encoder 形态」(注释丢失属该向导既定行为)——**值零漂移** ✓ - **观察(不计缺陷)**:切回 Local 后 `felis-uploads-s3` Secret 残留(演练按清理流程删除;是否自动清理属产品取舍)。 **二、缺陷 #52(向导内 apply 只刷新控制面,工作负载镜像滞后到下一次运行)** - **发现路径**:回滚后按计划复核「两 ns 重渲染」——minecraft 副本仍为 S3 内容(`4fb80ed8…`),而控制面与两 toml 已 local(`df206074…`)。 - **红证据(auditfix44)**:① S3 方向:S3 apply(20:18:44)后副本停在 local 内容(20:22:53 快照 = `e4791fe1…`)达 4 分钟;② Local 方向:回滚 apply(20:23:41)后副本停在 S3 内容。副本 managedFields 两笔写入(12:13:58Z `kubectl-client-side-apply` = 安装器 rerun 的双 ns apply;12:23:06Z manager `felis` = 下一次 setup 启动的 refresh)都不是 apply 时刻——**apply 本身不碰副本**。 - **根因**:`applyFelisConfigSecret`(storage/connection/edge 三条 apply 的公共出口)只 apply 控制 ns;镜像刷新只存在于 `felis setup` 启动(#51)与安装器。email 路径早有显式镜像刷新,storage/connection 是漏网的两条。 - **修复 `de7fb2c`**:镜像刷新移入 `applyFelisConfigSecret`(best-effort + stderr 警告;控制面-only 安装无工作负载 ns 时降级不阻塞);smtp helper 去掉重复块。门禁:`gofmt`/`go vet`/`go test ./...`/`bootstrap_test.sh` 全绿。 - **绿证据(`v0.0.0+fix52`,宿主二进制 sha `0bd49467…` 已装 `/usr/local/bin/felis`,旧版留 `/root/felis-auditfix44.bin`;真机双向)**:Run1 从 Local `s`→S3:apply 后 `control = mirror = podtoml = 4fb80ed8…`(S3 渲染;旧代码此刻镜像会停在 local);Run2 `s`→Local:`control = mirror = podtoml = df206074…`;副本 managedFields 写者 = `kubectl-client-side-apply` @ 12:33:40Z / 12:34:59Z(正是 apply 时刻)。Run1 启动块另见 `config (minecraft ns): refreshed from the control namespace`(#51 机制照常先收敛一次旧账)。 - **可达性**:#52 ②——任何 `s`/`c` 重配置即触发;危害等级低(镜像消费者 backup/restore/fileedit/reaper 当前不读被改动字段——`UserUploadsContext` 仅 `api.go` 消费——但「配置动了、镜像没动」正是 #51 要消灭的静默滞后类,且与 email 路径的既定行为不一致)。 **三、清理与还原**:MinIO 容器/卷/两镜像、`felis-uploads-s3` Secret、DB 行(`image_submissions` `sub-853a4e2ba4ba6443`)、/tmp 残留(player-cookies/drill-ctx/svc-tok 等)全清;docker 停;收尾 `control = mirror = df206074`(ALIGNED)、API 滚毕 Running、panel/healthz 200 ✓。 ### 本轮新增真机证据(第三十三批:`/updates` 维护窗口 API+UI 走查全绿;#53 跟随仓库迁移;#54 update 升级指引全假) **零、仓库迁移(背景)**:remote 已改 `git@github.com:FelisMC/Felis.git`(本批两个修复随 `2c6739a`、`397a400` 直推 main)。带 token 实测:旧 `MliroLirrorsIngenuity/Felis` 路径 301(GitHub rename redirect)、新路径 200——旧坐标当前仍能工作但全靠 redirect。新仓库**尚无 stable release**(`releases/latest` 带 token 也 404):felis-api 更新检查的 404 属发布流程事实,非代码缺陷。 **一、`/updates` 维护窗口 API 走查(完成;全绿)** - 读:GET 未设置 → `{"not_before":null,"not_after":null}`。 - 负例全按预期拒绝:半设 / 倒序 / 相等 / 坏 JSON / 未知字段 → 400;缺 `Content-Type` → 415。 - 正例:写入 → 回读一致 → **API pod 重启后仍在**(已落 DB,非内存态)。 - 鉴权:无 cookie → 401;player 会话 → 403(新铸 player 会话,留 `/tmp/player-cookies.txt`)。 - 收尾:DB 回到 `{null,null}`。 **二、`/updates` 面板 UI 走查(CDP,完成;全绿零 console 错误)** - 状态流转逐屏:生效中 → 过期 → 计划 → 未设置(截图 `/tmp/updates-{1..5}-*.png`)。 - 倒序提交 → 校验文案正确;清除后表单清空 + 「未设置」提示。 - 核账:DB 收尾 `{null,null}`;审计 `updates.window_set` 恰 3 笔(20:46:18 / :20 / :21)。 - 驱动:`/tmp/cdp-updates2.js`(bun + 原生 WebSocket;须 `Network.setCookie` 注入 owner 会话,否则新标签页 401 跳登录)。 - 观察(不计缺陷):窗口自身存取已验证;「窗口被 runner 消费」的端到端链路(Applier/Notifier)仍属 INTEGRATION-ONLY 设计(deferred-seams),不要当缺陷重复修。 **三、#53(旧仓库坐标残留)—— 提交 `2c6739a`** - 红(`v0.0.0+fix52` 实机):`felis update` → `github: MliroLirrorsIngenuity/Felis releases/latest returned HTTP 404 — …`。 - 绿(`v0.0.0+fix53` 实机):同一命令 → `github: FelisMC/Felis releases/latest returned HTTP 404 — …`。 - 范围:`internal/updater/topology.go`、`deploy/bootstrap.sh`(默认 `FELIS_REPO_URL` + 两处 UA)、README ×2、两个测试夹具(6 文件 9 处);门禁四件套全绿。 - 可达性:②——默认安装 / 每次 `felis update` 都读该坐标;危害在 redirect 退休时兑现(安装与更新一起挂)。 **四、#54(`felis update` 的 apply 指引在既有安装上全是错的)—— 提交 `397a400`** - 红(`v0.0.0+fix52` 实机文本):`--panel --force` → `run: sudo felis setup` + “felis setup is idempotent and re-runs the installer…” + felis-api「例外」块。 - 决定性证据(完成态安装):`felis setup` 跑 8s 退出,`/opt/felis/velocity/velocity.jar` mtime/hash 不变、零 bootstrap 输出;同刻安装器重跑日志有 `resolving the newest Velocity 3.5.1 build`。代码侧 `shouldRunHostBootstrapBeforeConfig` 仅在 4 个 marker 不全时进 bootstrap——既有安装上 `felis setup` 只开配置 TUI。 - 绿(`v0.0.0+fix54` 实机;宿主二进制已换 `/usr/local/bin/felis`,fix52 留档 `/root/felis-fix52.bin` sha `0bd49467…`): - `--panel --force` / `--velocity --force` → `run: curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash` + 单条 trailer(channel 未持久化 caveat、私有仓库 token'd form、`felis setup is not this path`); - `--mc` 无命令无 trailer;`--all` trailer 恰一次;`--force`/`LatestKnown` 语义不变。 - 文案同步:`docs/troubleshooting.md §15` 删掉 “or `sudo felis setup`”、补 channel caveat;测试改为 `TestApplyGuidancePointsEveryComponentAtTheInstaller`(钉安装器路径 + 禁 `run: sudo felis setup`)。 - 可达性:②(文档/指引;同 #38 型)。 ### 本轮新增真机证据(第三十四批:`felis nano` 全链走查 60/60;缺陷 #55 —— 坏源静默挡住验证梯子) **零、装置(全部在 VM,不进产品环境)**:`/srv/nanotest/nano-stub.py`=可控 Yggdrasil 源,路径即行为——`/ok/`、`/okid//`、`/props/`、`/badname/`(tooshort/toolong/badchar/section/noname)、`/none`=204、`/boom`=500、`/redir`=302、`/garbage`、`/emptyid`、`/slow/`;跑在 127.0.0.1:9900,临时单元 `nano-stub`,请求日志 `/srv/nanotest/stub.log`。驱动脚本:`nano-matrix.sh`(HTTP 矩阵 S1–S7)、`nano-service.sh`(systemd 层)、`nano-config.sh`(配置校验)、`nano-fw.sh`(firewalld);`nano-harness.sh` 从 HEAD 版 `bootstrap.sh` 提取产品函数在 scratch 路径跑(`felis-nano-test` 改名件,不碰产品单元)。 **一、HTTP 矩阵 S1–S7(60 项断言)** - 红(修复前 `v0.0.0+audit-nano1`):`SUMMARY pass=47 fail=13`——13 红全在 #55 语义内:5 个坏名用例「应 503+日志、实得静默 204」(状态+日志 ×2=10),failover 3(坏源在前登录不落第二源 ×2、无日志 ×1)。留档 `matrix-red.r2.log`。 - 绿(fix55,同装置重跑):`SUMMARY pass=60 fail=0`。留档 `matrix-fix55.r2.log` 与 `matrix-fix55/`(更早原跑在 `matrix/`)。 - 覆盖面:S1 输入校验(缺参/超长→204、POST→405、未知路径→404、带体 GET→400、HEAD→204、400 带 `Connection: close`)、S2 三方登录全链(前缀 + felisAuthNS UUID 确定性、ip/serverId 转发保真)、S3 premium 撞名改名(`LS_`)与免费名不动、S4 失败模式(不可达/500/302/garbage/204 → 跳过、记日志、503)、S5 多源优先级 + 坏源后 failover、S6 日志纪律(长 URI 截断、控制字节转义)、S7 properties 转发。 **二、systemd 层(`nano-service.sh`,真 unit + 真 DynamicUser;重跑落盘 `nano-service.r2.log`,0 FAIL)**:装服务→起服务;重跑语义(配置字节不动、`resolve_nano_listen` 从 unit 读回端点);drain(stop 期间 in-flight 登录照样答完,实测 stop 等待 5335ms);SIGKILL 后自动重启;端点迁移 8081→8099 后新址应答、旧址关闭;三种启动文案(loopback「Bound to loopback」/ CIDR「admits」/ 无 CIDR「WARNING」)。 **三、配置层(`nano-config.sh`,LoadNano 16/16,落盘 `nano-config.r2.log`)**:未知键、空/重复/保留 tag、colon、空白、坏 prefix、重复 prefix(大小写不敏感)、非 http scheme、URL 带 query、明文公网 http、缺文件、`[server] listen` 忽略警告、Mojang-only 可服务。 **四、firewalld(`nano-fw.sh`,3/3,落盘 `nano-fw.r2.log`)**:关旧「开全源」8081/tcp、仅对 CIDR 开 rich rule、loopback 不开洞、无 CIDR 警告、快照清理还原。 **五、缺陷 #55(提交 `9dad61f`)** - 红(fix54 实机):配置源回 200 但档案字段不可用(三方源名字进不了 MC 字符集;身份源 UUID 不解析)→ 静默 204、零日志;且筛查在 `handleHasJoined` 直接 `return` → [坏源, 好源] 顺序下 204,好源根本没被询问。 - 根因:两个 200-后筛查在 handler 层(200 已赢下梯子之后),而同类坏答案(200 无档案/非 200/不可达)在 `resolveHasJoined` 里是「记日志 + skip + failed」——一致性缺口。 - 修复:筛查移入 `resolveHasJoined`(身份源 `uuid.Parse`、三方源 `mcUsernameRe`)→ 命中记日志(`unusable profile name` / `unparseable profile id`)+ `failed=true` + `continue`;handler 守卫保留为最后防线(注释更新)。单测 +3(含 `log.Writer()` 捕获断言)。 - 可达性:#55 ②——需要配置了一个「回 200 但字段不可用」的源(马虎的自建/三方 Yggdrasil);坏源在前时经梯子的登录全部被吞(静默、零日志),安全侧(坏名不落玩家列表)保留。 **六、观察(不计缺陷)**:premium 冷查询 fail-closed 抖动——api.mojang.com 从 VM 首查偶发接近 2s 超时 → 偶按「疑似 premium」给免费名加前缀(`FelisNanoStub1` → `LS_FelisNanoStub`);符合 `isPremiumName` 既定取舍(误加前缀=外观代价,误放行=抢名),记观察不修。 ### 本轮新增真机证据(第三十五批:NetworkPolicy 真机强制矩阵全绿 —— 端到端白名单闭环 + felis-velocity 刷新存活验证) **零、装置与现场**:三张策略 live 于 `minecraft` ns(`felis-default-deny-ingress` / `felis-allow-rcon-from-control-plane` / `felis-allow-game-from-velocity`;apply 于 09-22T05:37:24Z,与渲染收敛 diff 零漂移);k3s 参数无 `--disable-network-policy`,**强制执行实测生效**;kube-router 机制实证:per-pod `KUBE-POD-FW-*` 链、未标记流量 `REJECT --reject-with icmp-port-unreachable`、每链首条 `--src-type LOCAL -j ACCEPT`(本节点豁免)、ipBlock 落 ipset `KUBE-SRC-*`(白名单成员可直接查)。探测法:**nsenter 进真实 pod 网络命名空间(源 IP=真实 pod IP)+ netns/veth 合成「转发型外部源」10.99.0.2(模拟 velocity 在另一台机器)**;全程不改产品代码。 **一、pod 源矩阵(源=真实 pod netns)** - api(felis,命中 RCON 白名单标签)→ lobby:25575 = **OPEN**;同源 → lobby:25565 = **REFUSED**(端口级区分 ✓) - registry(felis,非匹配)→ lobby:25575/25565 = **REFUSED**;同 pod → api:8081 = OPEN(对照:无策略命名空间不受限) - coredns(kube-system)→ login:25565 = **REFUSED** - 收尾复跑全矩阵与首轮逐行一致(`np-matrix-run1/2.log`)。 **二、host(=velocity 同机侧)**:→ login/lobby 的 ClusterIP 与 podIP :25565 **全 OPEN**(velocity 注册的后端路径实际可用);→ api-internal:8081 OPEN;→ lobby:25575 亦 OPEN——归因:kube-router 每 pod 链的 `--src-type LOCAL -j ACCEPT`(**本节点流量豁免,kube-router 设计行为**,kubelet 探针等依赖它;netpol 语义无法对节点自身收口,节点 root 本在 TCB 内)。**注记①:node-local 豁免。** **三、转发型外部源(最严苛模拟)** - 基线:netns(10.99.0.2) → 全部服务器端口(podIP 与 ClusterIP、25565/25575)= REFUSED,包级可见 netpol 的 icmp-port-unreachable(`td3.log`)。 - **白名单闭环**:临时把 10.99.0.0/24 加入 allow-game → ipset 即时生效(members:`10.99.0.0/24` + `10.211.55.6`)→ **lobby-svc:25565 与 login-svc:25565 = OPEN**(ClusterIP=velocity 实际拨号形态);25575 仍 REFUSED(端口维度不破)→ 回滚 → spec 哈希逐字节一致(md5 `e6b07440…`)、ipset 复原、复测全部 REFUSED。 - 层间归因:firewalld 规则含 `ct status dnat accept`(**DNAT 后的服务流量放行**;非 DNAT 转发走 forward policy 的 `admin-prohibited` 拒绝)——受支持拓扑(velocity 同机=LOCAL、ClusterIP、Mac→NodePort 面板 200)全部实测可用;「外部未经服务直连 podIP」不属于任何产品流。**注记②:firewalld 只放 DNAT 服务流。** - 装置保养:veth 未归区时 firewalld 会拒其转发属装置噪声(已归因);public 区临时挂载已摘、netns/veth 已删、策略零残留(spec diff 为空)。 **四、felis-velocity 刷新循环存活(顺带验证)**:20:18–20:42 的 `server list refresh failed` 全部落在 #52 演练的 API rollout 窗口(成功不打日志属设计);tcpdump 450s 窗口抓到 52 条 `GET /api/v1/servers` 载荷行(成对出现=双抓包点看到同一请求,折算约 26 次 ≈ 每 15–17s 一次,与 `REGISTRATION_REFRESH=15s` 常量吻合)及对应 200 响应;"keeping current registrations" 为设计降级。非缺陷。 **五、留档(VM `/srv/npdrill/`)**:`np-matrix.sh`+`np-matrix-run1/2.log`、`np-netns.sh`、`netns-probe.py`、`np-cidr-test.sh/.log`、`spec-before/after.json`、`td3.log`(包级归因)、`tcpdump-8081.log`(刷新存活)、`iptables-save.txt` 与 `nft-rules.txt`(现场快照)。 ### 本轮新增真机证据(第三十六批:面板错误文案全量本地化(#60)+ Run4c 管理面写操作复核 0 缺陷) **零、现场**:三个缺陷批次(#56–#59)已按「一缺陷一 commit」推 main 并部署 `auditfix59`;本批完成 #60 后部署 `auditfix60`(api/operator/reaper 三处 + 宿主 CLI,`felis version` = v0.0.0+fix60)。面板门禁:typecheck 0 错、vitest 117/117、vite build 通过(注意项目测试是 `bun run test`=vitest run;裸 `bun test` 是 Bun 内置 runner,解析不了 `@/` 别名,41 个 fail 系假象,勿误报)。 **一、Run4c 管理面写操作复核(4 段全真机,最终 0 缺陷;3 处为测试脚本自身误报,均已自纠)** 1. **镜像添加+删除**:添加 `registry.felis.svc:5000/e2e/probe:1` → 列表出现、API 落库(`added_at=14:50:53Z`)✅。删除初测"未生效"系脚本用 `tr` 找行——该表是 `div` 网格,`tr` 选择器命中 0 → 从未点到按钮。换 `div[class*=grid-cols-12] → button[title]` 重测:confirm("确定删除此镜像吗?")自动接受 → 页面 1ms 内刷新、**API 侧同 ref 从 5 条降至 4 条**——删除通道无缺陷。 2. **构建负例**:外部 registry(`docker.io/...`)+ 不存在 context → 对话框红字拒绝 `build: invalid request: image reference … must target the internal registry "registry.felis.svc:5000"`(截图 `run4c-3`)。脚本"未捕获"系其正则先命中了侧边栏"镜像"二字——误报。 3. **创建重复用户**:对话框内正确显示 **"该用户名已被使用。"**、对话框保持打开(截图 `run6-1`)。此前 run4c 的"dialog-closed"系脚本点错页面(进到了用户详情页);且该轮实际是一次**正例**:真管理员的 username 是 UUID 串(`08595879-…`,role=`owner` 是角色名),字面用户名 `owner` 当时空闲、创建确实成功——测试件已删(DELETE→200,列表 7→6)。 4. **创建重复子域名**:正确拒绝 **"该子域名已被占用。"**(截图 `run4c-5`)✅ **二、缺陷 #60(提交 `c9af548`):37 个用户可达错误码显示生英文** - 发现路径:run4c 复核 `email_taken` 时做了一次系统性对照——后端 `newError` 共 **71 个错误码**,面板 `humanizeError` 只映射 **23 个**;其余走 `default` 分支直接透传 `err.message`(多为 Go 包装嵌套的英文,如 `invalid server name: naming: invalid server name "BadName": must match ^[a-z0-9-]{3,32}$`)。 - 三个代表真机验证(修复前→修复后): - `bad_name`:新建服务器"名称"填 `BadName`(前端只查非空)→ 前:对话框直出上述嵌套英文;后:**"服务器名称不合法:需为 3–32 位小写字母、数字或连字符,且不能使用保留名。"**(`run8-1`) - `bad_subdomain`:子域名填 `ab`(前端 RE 允许 1–2 字符,后端要求 ≥3)→ 后:**"子域名不合法:…"**(`run8-2`) - `email_taken`:DB hook 造"他人已验证邮箱" → 账户页真发码(no-mailer 日志取码)→ verify 409 → 后:**"该邮箱已在其他账户上完成验证;请直接用该邮箱登录,或换一个地址。"**(`run9-1`) - 修复:api.ts +37 case、zh/en errors.json 各 +37 键(纯映射,无逻辑变更)。**剩 11 个故意不补**:`bad_request`/`conflict`/`internal`/`panic`/`not_found`(通用兜底)、`forbidden`/`not_admin`/`unauthorized`(状态码分支已在 default 覆盖)、`not_ready`(内部面)、`setup_required`(bootstrap 期 SPA 流控信号,403 文案可用)、`unsupported_media_type`(CSRF 门,面板永不可达)。 - 遗留观察(不计缺陷):构建负例的英文前缀 `build: invalid request:` 仍会透传——该错误无独立码、message 即最终文案;对管理员可读,记观察。 **三、观察(不计缺陷)** - UserDetailPage 对 owner/对自己都显示删除按钮;真的点了会得 403 + 具体 message("the owner account cannot be deleted from the panel"),但前端 403 分支统一显示"你无权执行此操作"——笼统但不算错,记观察。 - 字面用户名 `owner` 可注册(用户名无保留名单;角色由服务端管理、无提权路径),记观察。 **四、留档(Mac)**:脚本 `/tmp/cdp-run5.js`(镜像删除复测)、`run6.js`(重复用户复测)、`run7/run8.js`(bad_name/bad_subdomain 前后对照)、`run9b.js`(email_taken,内置 ssh 取码);截图 `run5-1`/`run6-1`/`run7-1/2`/`run8-1/2`/`run9-1`。VM `/opt/felis/src` = `c9af548` 快照。 ### 本轮新增真机证据(第三十七批:管理面交互收尾 0 缺陷 + 缺陷 #61 —— LuckPerms 写操作的过度承诺) **零、现场**:`auditfix61`(api/operator/reaper 三处 + 宿主 CLI)。面板门禁 typecheck/vitest 117/build 全绿后部署。 **一、管理面交互收尾(3 项全真机,0 缺陷)** 1. **submissions approve/reject**:player 会话(邮箱 OTP 铸造,no-mailer 日志取码)现场造两条 pending(`sub-489eda47fe4bc12d` / `sub-b1dcc4b4a2cc5f16`,各上传 tar.gz context)→ 面板「通过」→ 状态变「审核通过」且**自动构建 `bld-1790176562566802097` 到 succeeded**(点击到构建完成全链闭环);「驳回」→ 对话框必填原因 → 状态变「已拒绝」。全程 UI 零报错。 2. **用户会话撤销**:player.test 铸 2 条新会话(共 4 条)→ UI「单独撤销」首条 → **精确生效**(cookie5 → 401、cookie4 → 200、列表 4→3);「全部撤销」→ 提示「所有会话已撤销。」、列表清空、cookie4 也 401。 3. **ServerCard 启动交互**:面板点 test-one「启动」→ `Starting` → `Running/ready`。 **二、缺陷 #61(提交 `b4ef42d`):LuckPerms 页对写操作"过度承诺"** - 发现路径:LP 页写操作交互测试(给 `E2E_Tester` 写权限)→ 历史卡显示绿色 success +「(服务器未返回输出)」→ **落盘取证发现根本没写入**。 - 真机实验矩阵(test-one,LP 5.5.85 / H2 存储): | 操作 | RCON 回包 | 落盘(`lp export` 实测) | |---|---|---| | 面板 UI:`E2E_Tester permission set e2e.ui.write.test` | 空 | ❌ | | 面板控制台:`E2E_Tester … set probe.test true` | 空 | ❌ | | 面板控制台:` … set probe2.test true` | 空 | ✅ | | **独立 Python RCON 客户端**(绕开 felis):`E2E_Tester … set probe3.test` | 空 | ❌ | | 独立客户端:` … set probe4.test` | "Another command…"(异步提示) | ✅ | - 结论:**felis 只如实转发空回包;名字写入的静默失败是 LuckPerms 自身行为**(独立客户端 1:1 复现)。 - 根因(LP config.yml 官方注释背书):`use-server-uuid-cache: false`(LP 默认)→ "commands using a player's username will not work **unless the player has joined since LuckPerms was first installed**"——**未进过服的玩家名永远无法解析**(与有无外网无关)。 - 面板问题:旧文案承诺"授予与撤销操作仍然会实际生效"(#59 遗留半句)→ 对"给还没来过的玩家预授权"场景是不成立的承诺。 - 修复(纯文案,双语):`luckperms_no_reply` → "……按玩家名的操作只对「自 LuckPerms 安装以来进过本服」的玩家可靠,对没进过服的玩家名可能静默不生效";`luckperms_no_output` → "(服务器未返回输出,无法确认结果)"。真机复验(`run16b`):读提示与写占位均按新文案显示。 - 可达性:#61 ①——给未进服的玩家预授权是日常动作,此前面板显示绿色成功构成误导。 - 局限(记观察):读提示块只在"读取为空"时渲染;已有部分玩家数据的服上不会露出这段说明(后续增强候选:常驻说明或写前对未解析名字的提示)。 **三、观察(不计缺陷)** - LP 命令是**串行异步**执行:快速连发第二条会得到 `§7[§b§lL§3§lP§7]§r §7Another command is being executed, waiting for it to finish...`(带颜色代码原文,面板如实显示);`lp export` 回包时有时无(响应与执行解耦)。felis 层无责。 - submissions 徽章(`submissions.json`:"审核通过/已拒绝")与过滤标签(`admin.json`:"已通过/已驳回")两套词,语义均可,记观察。 - 控制台对空回包命令只显示 echo、无占位提示(终端风格);LP 页有占位文案。记观察。 **四、留档(Mac)**:`/tmp/cdp-run10.js`(approve/reject)、`run11a/11b.js`(会话撤销)、`run12a.js`(启动交互)、`run13/14/15/15b/16/16b.js`(LP 全链)。VM 独立探针:`/tmp/rcon_probe.py`、`/tmp/rcon_one.py`(+ `/tmp/rcon_pw.txt`);LP 导出样本在 `/data/plugins/LuckPerms/luckperms-2026-09-23-15-*.json.gz`。测试数据:两条 pending 提交(一 approved 一 rejected,构建 succeeded/failed 各一)。VM `/opt/felis/src` = `b4ef42d` 快照。 ### 本轮新增真机证据(第三十八批:#56–#59 证据回填 + 服务器详情/运维面收尾 0 缺陷) **零、现场**:`auditfix61`(api/operator/reaper 三处 + 宿主 CLI,`felis version` = v0.0.0+fix61)。本批两部分:把 #56–#59 四项修复的真机证据落节(此前只存在于 commit message),并把队列里剩余的服务器详情页/运维面交互复跑做完(0 缺陷)。 **一、缺陷 #56–#59 证据回填(部署 `auditfix59`;四项均 = 修复前真机触发 + 修复后复验)** 1. **#56(`0790f8d`)玩家管理操作把 RCON 回包扔掉、只报 canned 成功。** 触发路径:给"还没进过服"的玩家加白名单/封禁——vanilla 对没见过的名字回 `That player does not exist` 并拒绝,而旧面板无论如何都显示成功。修复后回包逐字上屏:`whitelist add E2E_Bad` → `Added E2E_Bad to the whitelist`(磁盘同步真写入);负例 `NoSuchPlayerXYZ` → `That player does not exist`(不再伪装成功);静默服务器回退本地化文案。 2. **#57(`70c988e`)控制台命令的回复无处显示。** 触发路径:控制台发任何命令——`sendCommand` 拿得到 RCON 回包但被丢弃、pod 日志也不回显命令输出,等于零反馈。修复后 echo + 回包以终端样式渲染在提示符上方(实测 `list`)。 3. **#58(`4d4cdd6`)无世界盘服务器的文件页 90s 卡死。** 触发路径:对从未启动/已回收的服点"文件"页——旧行为建 Job → Pod `FailedScheduling (pvc not found)` Pending 到 90s 超时 → 误导性 504 `files_timeout`。修复:file 路由补上与 backup/restore 同款 `WorldVolumeExists` 门 → 快速 409 `no_world_volume` + 面板双语文案。本批现场复查:`resolvecheck`(无 world PVC)→ 409 `no_world_volume`,0.03s;`test-one`(有盘)→ 200(~2s)。 4. **#59(`a2ff2a1`)LuckPerms 页把"读不到"报成"没有"。** 触发路径:打开装了 LP 的服的管理页——LP 5.5.85 的 `lp` 命令 RCON 回包全为空(独立 RCON 客户端 1:1 复现),读投影永远为空,旧页面却断言"没有父组/没有显式节点"(假事实)、写历史伪造 `[RCON]` 行。修复:原始回包随 rosters 同款披露渲染;空回包显式提示、不再假断言;历史占位不再伪造输出。 **二、服务器详情页交互复跑(5 项全真机,0 缺陷)** 1. **ServerCard 停止**:test-one `Stopping` → `Stopped`(启动在第三十七批;收尾复查 `desiredState: Stopped`)。 2. **ServerFiles 写流程**:编辑 motd → 保存「已保存」→ 重开读回一致(`motd=Felis E2E files drill`)→ 还原默认 `A Minecraft Server`(收尾复查确认)。 3. **备份/恢复**:立即备份 → Job `succeeded`;恢复 → 确认对话框(破坏性警告原文)→ Job `restore-test-one` `Complete`(5s)→ UI「恢复 27秒钟前 成功」→ 唤醒 `Running/ready`(恢复后的世界可加载)。 4. **白名单**:加/负例/移除全链复跑(证据见 #56)——终态磁盘只剩 `E2E_Tester`(88 字节)。 5. **封禁/解封**:封禁(内联确认)→ `Banned E2E_Bad: Banned by an operator.` + 磁盘写入;解封 → `Unbanned E2E_Bad`;`banned-players.json` 终态 `[]`。 **三、运维面复查(3 项,0 缺陷)** 1. **`/admin/updates` 复跑**(队列"深挖"项):设置窗口 → 「维护窗口更新成功。」+ 状态卡更新;负例 end` → 回落 `A Felis server`(非 felis 子域不被劫持)。 - 登录边界:`LoginStart`(含现代协议 UUID 字段)→ 服务端首包 `0x01 EncryptionRequest` ⇒ 边缘 online-mode 强制成立。 - 加载面:`Loaded plugin felis-link 0.2.0`(共 5 插件)。 - 口径:真实账号进服(limbo 门 → 菜单 → 转服)仍按"进服跳过"决定不演练;以上为不依赖客户端的最大真机面。(首次探测因探针漏发 UUID 字段被静默关闭——探针缺陷,非服务端问题;补齐后一次通过。) **五、观察(不计缺陷)** - ViaVersion 自报有 5.12.0(当前 5.11.0):bootstrap 的 pin 是 FL-007 实测版本,属刻意,不随提示升级。 - limbo 插件编译期一条 deprecated API 提示(`FelisLimboPlugin` 用/覆写已弃用 API):门禁下可见、不阻塞,留观察。 ### 本轮新增真机证据(第四十一批:装载器 mod 层收口 —— #64 exec 位、#65 元数据、编译+起服双门禁) **零、现场**:控制面与面板不动;产出三个 commit:`f6048f2`(#64 修复)、`c2fe6a6`(#65 元数据)、`aa5abfa`(`plugins/test-mods.sh` + CI `mods` 作业 + release 双门禁 + README 记录)——这是最后一块"从未被任何自动化碰过"的模块面(fabric / forge / neoforge 三个装载器 mod)。 **一、#64(`f6048f2`):三个 vendored `gradlew` 缺可执行位——文档正路第一步即失败** - 红证据:`git ls-tree origin/main` 三个 `gradlew` 全为 `100644`(blob `b9bb139f…`);`plugins/README.md` 第 212–214 行原文教 `plugins/fabric/gradlew -p plugins/fabric build`(forge/neoforge 同式);VM 全新 clone 实跑 → `bash: line 1: ./gradlew: Permission denied`,exit 126。 - 为何从未暴露:无 CI、无安装路径,README 是唯一入口——直到本批第一次真跑才现形。 - 修复:`chmod +x` + `git update-index --chmod=+x`(mode 100644→100755 ×3)。 - 可达性:①(全新用户照文档走的第一步)。 **二、#65(`c2fe6a6`):mod 元数据与仓库 LICENSE 相悖** - 三处 `license = "MIT"`(`fabric.mod.json`、forge/neoforge 的 `mods.toml`)vs 仓库 `README.md:73` 的 `AGPL-3.0-only` + LICENSE 全文。时间线:mods 2026-06-26 加入(`93f143f`)、LICENSE 2026-07-12 才落地(`037eb24`)——陈旧残留。 - 另两处 `issueTrackerURL = "https://example.invalid/felis"`(占位域名)→ `https://github.com/FelisMC/Felis/issues`。 - 用户面:fabric loader 启动打印 license 字段;`mods.toml` 被两个 loader 解析。 - 可达性:②(低;元数据/合规面,非功能)。 **三、双门禁落地(`aa5abfa`)** - `plugins/test-mods.sh`:JDK 17 下依次跑三个模块的 vendored wrapper(`./gradlew --no-daemon build`);java 大版本非 17 直接 fail-loud(三模块目标是 Java-17 的 Minecraft 线)。 - `ci.yml` 新增 `mods` 作业(temurin 17 + `gradle/actions/setup-gradle@v4`,版本由各模块 wrapper 自管);`release.yml` 发布前加两道 Java 门禁(JDK21 `plugins/test.sh` + JDK17 `plugins/test-mods.sh`)。 - README:Status 更新为 compile + boot 双验证;Building 段补 limbo `-PlimboVersion=` 要求与两个门禁说明。 - CI:run `35892544563`(`aa5abfa`)——`mods` 作业首跑即绿,五作业全过(shell / mods / panel / go / plugins 5/5)。 **四、三个模块的真机编译 + 起服 E2E(本批核心证据)** - 编译(VM 容器 `eclipse-temurin:17-jdk` + 持久 gradle 缓存):三模块 `BUILD SUCCESSFUL`(首跑 4–5 分钟级/模块),产物 `felis-{fabric,forge,neoforge}-0.1.0.jar` = 29276 / 28855 / 28562 B;留档 `/root/mods-build-b41.log`。 - 起服(三个真实专用服,控制台直驱 `/link`,随后 `linkx` 未知命令对照、`stop` 正常停服): - **fabric** 1.20.1 + loader 0.19.5 + fabric-api:`Felis link ready; /link is registered.` → `Done (10.670s)!` → `/link 只能由玩家执行 / /link can only be run by a player.` → `Unknown or incomplete command`(linkx)→ `Stopping server`;`EXIT_fabric_boot=0`。 - **forge** 1.20.1-47.3.0:`Felis link ready; /link will be registered.`(modloading-worker)→ `Done (11.751s)!` → 同四段证据;`EXIT_forge_boot=0`。 - **neoforge** 20.4.251(1.20.4):`Done (16.094s)!` → 同四段证据;`EXIT_neoforge_boot=0`。 - 口径:三服均为"装好 mod 后真启动"的场景,同时验证 loader 真实解析修改后的元数据(license=AGPL、tracker URL)后正常装载;完整取码链(玩家在游戏内 `/link`)按"进服跳过"决定不演练,装载/注册/拒绝面已全部真机成立。 - 留档:`/root/mods-e2e-b41.log`、`/root/mods-e2e-b41-resume{2}.log`、`/opt/felis/mods-e2e/`(496M,三服目录保留);收尾态:docker `inactive`、k3s/felis-velocity `active`、磁盘 17G free。 **五、演习装置自身两次修正(非产品缺陷,如实记录)** - 控制台驱动脚本 `/root/modserver-drive.sh` 初版把 FIFO 写端先开(`exec 3> console`)→ 自我死锁(内核 `wait_for_partner`、`State: S`),java 从未启动(无 `server.out` 实证);改 `exec 3<> console`(RDWR 打开不阻塞)后三服全通。 - forge 安装器首跑:`libraries.minecraft.net` 连接失败 → `com.google.code.findbugs:jsr305:3.0.2` 下载失败、`There was an error during installation`、无 `run.sh`;原样重试即 `The server installed successfully`(VM 网络抖动,非 forge/产品问题)。 **六、观察(不计缺陷)** - 无新增。 ### 本轮新增真机证据(第四十二批:覆盖面对账 + 文档/工具收尾 —— #66 README_EN、§28 图对齐、sync.sh 本地修复) **零、现场**:本批不碰产品代码;对"所有模块均已演练"的结论做**独立对账**(枚举仓库全部模块/交付物 × 台账覆盖),只挖到文档与本地工具层面的残留。commits:`62c5a8a`(#66 README_EN)、`62e8c87`(§28 序列图对齐)。 **一、覆盖面对账(本批核心,逐项)** - 仓库卫生:`git ls-files` 全量扫描无 `.DS_Store`/`*.log`/`node_modules`/构建产物误入库;顶层 `felis` 二进制(83MB)被 `.gitignore` 正确忽略;旧仓库名 `MliroLirrorsIngenuity` 残留仅存在于台账(历史记录)与本地未跟踪二进制。 - 未竟工作标记:`TODO|FIXME|XXX|HACK` 全仓(go/ts/sh/md/yml)仅 1 处命中——`internal/api/api_test.go:263` 的 `context.TODO()`(合法测试写法),无未完成实现标记。 - CLI 派发面:`cmd/felis/run.go` 的 usage ↔ dispatch 表有双向测试强约束(`run_test.go`,含显式 `undocumentedCommands` 允许表:`bootstrap-assets`/`init-forwarding` 等 in-Pod 入口),机制已核实成立。 - CI 覆盖:`shell` 作业按 shebang 对**全部 tracked `*.sh`** 做语法检查(`git ls-files '*.sh'` 逐文件 `bash -n`/`sh -n`)并运行 `deploy/bootstrap_test.sh`;`plugins`/`mods` 各自作业。无"存在但从无入口运行"的脚本。 - `internal/*` 22 个包逐一对账:0 提及者(`internal/apis`)为 CRD 类型包(被 40+ 站点引用、随 operator/manifests 演练间接全覆盖);`deploy/{limbo,lobby,paper}` 为三张游戏镜像定义,随每次起服演练执行。真盲区=无。 - `panel` 全部 17 条页面路由 × 既有批次(CDP 巡检、深挖、写流程)覆盖;`scripts/` 三脚本为**本地私有**(`.git/info/exclude`,不入库)。 - `docs/` 四件:troubleshooting(多批引用)、openapi(含 parity 强制测试)、deferred-seams(内容为现行状态,含 CLOSED/verified-live 注记,抽查与代码一致)、sequence-diagrams(发现漂移 → 修复,见第三节)。 **二、#66(`62c5a8a`):README_EN 与中文 README 漂移** - 红证据:`README.md`(中文)"使用方式"含**必需**的私有仓库 workaround(仓库私有 → 裸 raw 命令 404;token 经 `curl --config -` 由 stdin 下发、不进 argv;`sudo -E` 传给安装器)与"重跑=升级 felis-api、通道不继承(跟 main 需 `FELIS_VERSION_BOOTSTRAP=dev`)"两段;`README_EN.md` 是 `README.md:6` 链接的英文入口,这两段**完全没有** → 英文读者照文档第一步就 404 且无指引。 - 修复:两段译入 EN(保留原命令与语义;`grep` 校验 token/config/dev 三关键串在位)。可达性:①(轻)——英文首装路径第一步。 **三、§28 序列图对齐(`62e8c87`)** - 漂移点(两处,与修复后代码实读对照):① Claim Transaction 图注仍是 audit #4 前形态(`SELECT EXISTS` + 事务外 pre-check)→ 更新为现行实现:`pg_advisory_xact_lock(user_id)` + 行 `FOR UPDATE` + **事务内**四维配额门(图上 `QuotaAvailable` 只是 fast path);② Link Flow 更新为:取码 `FOR UPDATE`、同用户重验幂等、退役(soft-deleted)账户链接被当场接管——409 仅对"其他活跃用户"。均以 `internal/api/pgrepo.go` 实读 + handler 映射核对。 - 惯例依据:该文件有维护史(`676407d docs(diagrams): align §28 sequence diagrams with implemented routes`)。 **四、本地工具修复(私有脚本,不入库):`scripts/sync.sh` 排除 `plugins/` 与 `go:embed` 冲突** - 红证据(本机复现):以 sync.sh 原排除清单 rsync 一个 fresh 目标 → `go build` 立即失败:`bootstrap_asset.go:30:12: pattern plugins/limbo/build.gradle: no matching files found`(`bootstrap_asset.go` embed 了 plugins/{limbo,paper,velocity,shared} 源码;排除清单成文于 embed 引入之前)。存量目标靠 rsync 对排除路径的"保护"而看似正常,fresh 目标必炸。 - 修复:排除项从 `plugins/` 改为 `plugins/*/build|.gradle|bin/`(与 `.gitignore` 同义);`watch.sh` 同步去掉 plugins 的忽略。验证:新清单 rsync fresh 目标 → `go build` 绿(83.7MB 二进制产出);两脚本 `bash -n` 通过。因脚本被 `.git/info/exclude` 私有,修复留在工作机、无 commit。 **五、release.yml 静态复核(该流水线从未执行过——仓库当前无任何 tag/release)** - 三处输入/输出契约实核:① 发布资产名 `felis-linux-amd64/arm64` ↔ `deploy/bootstrap.sh` 的 `asset="felis-linux-${arch}"` 及收敛检查 `felis ${FELIS_REF}`(版本首行契约两侧一致);② `out/linux_amd64/usr/local/bin/felis` ↔ Dockerfile 最终层 `COPY /out/felis /usr/local/bin/felis`;③ 版本断言 `felis ${GITHUB_REF_NAME}` ↔ `cmdVersion` 的 `felis %s` 首行。 - 唯一无法在本机完全复刻的 `file(1)` 机器类型断言:用交叉编译的 linux/arm64 二进制实测输出 `ELF 64-bit LSB executable, ARM aarch64, …`,断言串 `ARM aarch64` 匹配(BSD/Ubuntu file 同源 magic)。 - 结论:整条流水线未跑属"尚未切 tag"的发布流程事实(第四十批已注记);切首个 tag 前无待修项。 **六、观察(不计缺陷)** - `internal/api/handlers_account.go:169` 的 reclaim 选择仍为 Java/Velocity 侧 CODE-ONLY(deferred-seams 已记 accepting)——维持。 ### 本轮新增真机证据(第四十三批:构建 Job 收尸 #67 + #68 上下文拉取重试 + 生命周期/故障注入压测) **零、现场**:控制面从 auditfix61 升到 **auditfix62**(含 #67;自建镜像推入内建 registry,api/operator/reaper 三处滚动,`felis version` = `v0.0.0+fix62`)。本批证据 = 6 轮起停循环 + 3 处故障注入 + PG 断连语义 + 并发风暴 + 持续轮询。 **一、#67(`2755e41`):build Job 从不回收——每构建一个、完成 Pod 永久堆积** - 红证据(真机):`felis-build` 里 5 个完成 Job/Pod 最长 26h 无人回收;全仓 TTL 对照——fileedit 2m / backup 10m / restore 10m / reaper CronJob 3+3 历史,**唯独 build 没有 `ttlSecondsAfterFinished`**;且 `build.go:69` 的注释写着 "(e.g. GC'd); treated as failed"——预期的 GC 从未存在。危害:完成 Pod 计入节点 pod 预算(stock k3s 110),构建量一上来先把节点塞满,新构建全部 Pending;etcd/磁盘同步膨胀。 - 修复:`buildJobTTL = 7 * 24h`(终态后计时;日志路由是失败分诊面故取长窗;`Sync` 对终态幂等、`JobUnknown` 只影响非终态 → 删除后的唯一代价是日志 404)。单测 `TestBuildJobIsReapedAfterCompletion`。 - 真机验证(auditfix62):① 真实路径触发构建(owner 会话 → `POST /api/v1/images/build`,借遗留 blob 作 context)→ 新 Job `ttlSecondsAfterFinished=604800`、构建 25s `succeeded`;② 对旧 Job 打 `ttl=30s` → 40s 内 Job+Pod 被 TTL 控制器收走(机制实证);③ 旧堆积 5 个里 1 个已收、4 个留存对照。 **二、生命周期 ×6 + 三处故障注入(operator / api / postgresql)** - 每轮:CR `desiredState` Running → 等 Running → Stopped → 等 pod 消失。结果:**6/6 全收敛**——Running 23–29s(注入轮与无注入轮无差)、Stopped 3s、pod 每轮如期消失;收尾 conditions 无 Failed 残留(Ready=False / RconReached=False = Stopped 正常;Provisioned=True)。 - 注入 1(cycle 2,删 operator pod @t+2s):Running 仍 23s 达成(新 operator 立即接管 reconcile)。 - 注入 2(cycle 4,`systemctl restart postgresql`):CR 路径无感;另做定点验证:PG 停机窗口 `/me` = **503**(非 401,#11 语义保持)、`/healthz` = 200(存活探针独立)、内部 status = 200(纯集群读);PG 恢复后 `/me` = 200。 - 注入 3(cycle 5,删 api pod @t+2s):内部轮询出现 2 次 `http=000`(约 6s 窗口)后自愈;Running 28s 达成。 - 观察(不计缺陷):高频翻杆期间 operator 报 9 条 `Operation cannot be fulfilled ... object has been modified`(乐观锁冲突 → 重排队自愈),目标时间无差;controller-runtime 正常重试语义,生产低频操作下更罕见。 **三、并发风暴 + 拒绝面 + 持续轮询** - 20 路并发 internal status + 10 路 `/me`:30/30 全 200。 - 无主 + ownerOnly 服 wake:403 `forbidden`(正确拒绝面,非 5xx)。 - 持续轮询(status + healthz + me,5s 一轮 × 300 轮 ≈ 25 分钟,900 样本):收盘 `LONG POLL DONE fails=27`——27 个失败样本**全部**落在三个自导演练窗口内(02:24:15/20 红证据杀 api〔6〕、02:29:48/53 auditfix63 滚动升级〔6〕、02:30:53–02:31:13 #68 确定性复现 scale api→0〔15〕),窗口外 873 样本全 200、零自发失败;每个窗口在动作结束后 ≤1 个探测周期(~5s)内恢复。 **四、留档与收尾** - 留档:`/root/soak43.sh` + `soak43.log`、`/root/poll43-long.sh` + `poll43-long.log`、`/root/mint-owner-43.sh`(owner 会话重铸)、`/root/felis-image-build43.log`(镜像构建)、`/tmp/owner-jar43.txt`(owner cookie);镜像 `10.43.182.43:5000/felis/felis:auditfix62` 已入 registry(`e2e/ttl-probe:latest` = 本批 drill 镜像,保留)。 - VM 状态:docker 用毕已停(inactive)、k3s/velocity active;控制面三处 = auditfix62。 **五、#68(`b76d0ac`):构建 context 拉取对控制面重启零容忍——一次拒连即终态失败** - 红证据(真机):删 api pod 后触发构建 `bld-1790187851749257160` → Job `Failed`;pod 时间线 `18:24:12Z` 起、context-fetch `18:24:13Z` exit 1,日志原文 `dial tcp 10.43.237.249:8081: connect: connection refused`;Kaniko 从未启动(PodInitializing);新 api pod 11s 后就绪、同 blob 前后各一次构建均 25s succeeded——纯粹"单次尝试"造成的无谓失败。 - 修复(`b76d0ac`):`fetchContextWithRetry`——传输错误/5xx 重试至 45s 窗口(3s 间隔),4xx 快速失败(是答案不是抖动);URL 校验提前为 usage error(2);保留原错误文案前缀。窗口/间隔为 vars(测试可缩窗);4 例单测:5xx 后恢复、拒连后恢复、404 不重试、窗口耗尽(全绿,含 `-race`)。 - 真机验证(auditfix63,确定性优先,不复刻竞态):`scale api→0` → 以复刻 Job 直建(由红证据 Job 派生:换名 `build-retry68`、剥 controller 标签与 selector、镜像改 auditfix63,复用 `felis-service-token` Secret)→ fetch 连续 8 次 `connection refused; retrying`(实捕日志)→ `scale api→1` → fetch 恢复、Kaniko 构建推送、trivy 干净 → **Job Complete**(`18:30:52Z→18:31:24Z`,32s);同形场景对照旧版 = 终态 Failed。正常路径回归:api 在线直构 `bld-1790188306964162960` → `succeeded`(10s),fetch 日志**零重试行**(重试不引入正常路径开销)。 - 附带发现(运维):本批升级沿用 `set image` 后 `FELIS_IMAGE` env 仍停 `auditfix61`(两代落后)——构建 Job 的 fetch 容器实际一直在用旧镜像;已把 api/operator 的 `FELIS_IMAGE` 与 api/operator/reaper 三处镜像全部对齐 `auditfix63`。真实安装器路径随 manifests 重渲注入该 env,手工升级须成对改(升级清单事项)。 ### 本轮新增真机证据(第四十四批:modpack 规模上下文全链 + 并发构建) **零、现场**:控制面 = auditfix63(含 #67/#68;api/operator `FELIS_IMAGE` 同版本),VM docker 停。本批目标 = 把"真实模组包大小"这条链压到生产尺度:此前所有提交/构建演练的上下文都是 KB 级。 **一、200 MiB 上下文全链(零缺陷)** - 构造:5×40 MiB `/dev/urandom` 载荷 + Dockerfile(随机数据不可压缩 = 最坏情形),tar.gz = 209,750,275 B。 - 上传(真实 app 路由):`POST /api/v1/me/submissions/{id}/context`(owner 会话)→ 200,**0.44s / ~481 MB/s**;宿主核对 PVC 落盘 `sub-cc160b3ad19080f9/context.tar.gz`。服务端读写超时设计(ReadTimeout/WriteTimeout 有意不设、仅 ReadHeaderTimeout)与流式落盘(io.Copy → cappedReader)均按预期,无内存尖峰。 - 批准 → 构建(`bld-1790189227767686191`):fetch init **1s**(200MB 内部面拉取 + 解压,无重试行)、kaniko **4s**(unpack/COPY/snapshot/push gzip 层 **209,738,853 B**)、trivy **4s**(0 findings);Job Complete,全链 13s(18:47:07Z→18:47:20Z)。registry manifest 实查层大小一致(推送非虚)。 - 覆盖点随验:提交构建 Job `ttlSecondsAfterFinished=604800`(#67 对真实提交路同样生效);`/me/submissions` `build_status=succeeded`(#36 面在 200MB 规模下仍成立)。 - 资源采样(2s 粒度,构建太快可能漏尖峰,标注局限):无 OOM——api 17.9MB / kaniko 33.4MB / registry 34.1MB / trivy 35.9MB;emptyDir+快照为临时占用,随 Job TTL 回收。 - 1 GiB 帽的单测已有覆盖(`internal/submit/submit_test.go` oversize→ErrInvalid);帽下真机 = 本条(帽上真机不划算,不做)。 **二、并发构建 ×3(零缺陷)** - 三路同时 `POST /api/v1/images/build`(各自 ref)→ 202×3;三个 Job 均 **11s Complete**,无串扰:每个 Job destination 与 tag 一一对应(conc-b44-1/2/3 各落位)、`ttl=604800` 各自在。 **三、磁盘运维注记(非缺陷)** - 连续镜像构建(docker build)把 VM 根盘从 11G free 压到 4.3G;`docker builder prune -af` 一键回收 **6.5G**(回 11G free)。即每轮全量重建约需 5–7G 构建缓存空间;生产主机根盘建议 ≥40G 并定期 prune(如需可在部署文档补一行,本批未改码)。 - 留档:`/root/bigctx.tar.gz`(200MiB 上下文原件)、`/root/bigctx/`、`/root/stats44.log`、`/root/stats44-build.log`;提交 `sub-cc160b3ad19080f9`(blob 与 `user-uploads` 镜像保留作尺寸样本);镜像 `e2e/conc-b44-{1,2,3}:latest`。 ### 本轮新增真机证据(第四十五批:模组构建链三连修 #70/#71/#72 + 提交→装服全链闭环 + 文档/测试修 #69/#73/#74) **零、现场**:控制面 auditfix63→66(每修一版真机重演);本批目标 = 真实用户会写的那种模组包 Dockerfile(`FROM <平台底座>` + 内容层)。此前所有构建演练都是 `FROM scratch`——用户真实形态的第一步从未跑过,本批连撞三个缺陷。 **一、#70(`ac403b9`):kaniko 拉底座缺 pull 侧 insecure 标志** - 红证据(真机):提交 `FROM registry.felis.svc:5000/felis/paper:demo`(`bld-1790189685537480076`)→ kaniko `Retrieving image manifest …` 后即败:`Get "https://registry.felis.svc:5000/v2/": http: server gave HTTP response to HTTPS client`。根因:Job 只给了 `--insecure --skip-tls-verify`——kaniko v1.24 `--help` 实测二者均只覆盖 **push**,拉取默认 HTTPS,撞上明文 registry。 - 修复:补 `--insecure-pull` / `--skip-tls-verify-pull`(对称补齐;构建命名空间 egress 本就只许 DNS/内建 registry,不扩大可达面)。单测断言两 flag 在参。 - 真机复验:下一次构建 `Retrieving image manifest` 不再报错,进入解包阶段(随即暴露 #71)。 **二、#71(`6e47730`):kaniko 解包底座层被 drop-ALL 卡死** - 红证据(真机):`error building image: error building stage: failed to get filesystem from image: chown /etc/gshadow: operation not permitted`——kaniko 以 root 解包 tar 层要把文件 chown 到层里记录的属主(root:shadow 等),drop-ALL 后 CAP_CHOWN/CAP_FOWNER/DAC_OVERRIDE 全无。`FROM scratch` 全用 COPY(属主= kaniko 自己)所以从未暴露;**任何真实底座**必触。 - 修复:仅 kaniko 容器补回最小能力集 `CHOWN + DAC_OVERRIDE + FOWNER`(fetch/trivy 保持 drop-ALL 基线;单测断言"恰好三枚"防漂移)。 - 真机复验:解包通过、COPY、推送成功(进入 trivy 阶段,撞上 #72)。 **三、#72(`b14bacb`):trivy 扫 jar 必拉 Java DB——egress 锁下必败** - 红证据(真机):kaniko 全绿后 trivy `FATAL … Unable to initialize the Java DB … failed to download artifact from mirror.gcr.io/aquasec/trivy-java-db:1: connection refused`。Java DB 按需下载:镜像一有 jar 就触发——模组包 = jar 集合,故所有真实用户构建都会倒在扫描门;旧构建全 scratch(无 jar)从未触发。 - 修复:新增 `[registry] trivy_java_db_repository`(→ `--java-db-repository`,与 vuln-DB 旋钮对称);installer carry 白名单收录;§8e 配方补 Java DB 镜像步骤;deferred-seams 更新。 - 真机复验(第 4 次尝试,auditfix66):Job spec 实测含 `--db-repository registry.felis.svc:5000/mirror/trivy-db:2 --java-db-repository registry.felis.svc:5000/mirror/trivy-java-db:1`;扫描 `ubuntu 26.04` + `paper/paper.jar` + `pebble` 全 0 漏洞;**Job Complete**(全链 27s,`bld-1790190788310114143`)。 **四、capstone:提交 → 装服 → 起服,全链闭环(零缺陷)** - 第 4 次尝试产物 `user-uploads/sub-c006cbd633317ccb:latest`(= `felis/paper:demo` + 用户 marker,经完整提交管道构建): - 白名单自动收录(`source=built`)→ 产品路由 `PATCH /api/v1/servers/test-one {"image": …}` **200**(`image_not_whitelisted` 门通过); - `POST /servers/test-one/wake` 202 → pod `test-one-0` Running 1/1(25s 内); - `kubectl -n minecraft exec test-one-0 -- cat /felis-probe-marker.txt` → **`felis-probe44-ok`**(用户构建上下文的内容确在运行中的服务器里); - 服务器日志 `Done (4.678s)!`(paper 完整启动);RCON 线程应答平台就绪探针; - 收尾:stop 202、pod 消失、镜像回滚 `felis/paper:demo`、desiredState=Stopped。 - 口径:这是"玩家上传模组包 → 服主审批 → 自动构建 → 选用为该服镜像 → 起服"的机制全链(真实客户端进服仍按决定跳过)。 **五、#69(`5cbfa89`):README 过度承诺"自动部署"** - 复核(读全):提交数据模型无目标服务器字段;approve→build→白名单是唯一自动化;部署 = 服务器编辑框选镜像(产品路由与 UI 均在);面板文案本身只承诺"自动触发安全构建"。 - 修复:README 中英两行改为"自动构建;产物进入镜像白名单,可直接选用为服务器镜像完成部署"。可达性 ①(轻)。 **六、#73(`2961beb`):bootstrap 测试在跑 k3s 的主机上必假失败(工具级)** - 红证据:VM(真实 k3s 主机)跑 `bootstrap_test.sh`(**原版同现**)→ `FAIL a missing worlds root is warned about`:用例拿真实路径 `/var/lib/rancher/k3s/storage` 期望 WARN,而该目录在"安装器真正要服务的机器"上必然存在 → 假红;CI 从未见到(runner 无此路径)。 - 修复:warn 情形改用保证不存在的路径;并把 `trivy_db_repository` / `trivy_java_db_repository` carry 断言补齐。VM 复跑 **140 PASS / 0 FAIL**。 **七、#74(`96aa817`):CI flaky——console 断连测试读写竞态(工具级)** - 红证据:run `35907662213`(纯 README 提交)go 作业红:`TestServerConsoleDisconnectTeardown: expected the first event before disconnect, got ""`;`gh run rerun --failed` 即绿 → 竞态确认。 - 根因:测试只等"源读到第一行"就 cancel,"中继把事件写入响应"尚未发生;cancel 落进缝里时 body 为空。 - 修复:加 `firstDataWriter` 信号(写完成后再 cancel,写→读有 happens-before);本地 `-count=60` 与 `-race ×5` 全绿;CI 复跑 `5cbfa89` = success。 **八、运维注记(非缺陷)** - 升级滚动撞 kubelet **ephemeral-storage 压力**:auditfix64 滚动时新 api/operator pod `Pending 4m46s`(事件 `untolerated taint(s)`),02:58:12 kubelet eviction manager 回收后自动调度成功。诱因 = 构建把根盘压到 89%(docker 构建缓存 + kaniko/emptyDir 临时层);`docker builder prune -af` 两清共回收 ~11G,余 11G free。生产清单:根盘 ≥40G + 升级前清构建缓存(与批 44 注记合并)。 - 中间失败构建留下的 registry 镜像(`user-uploads/sub-83f6…`、`sub-171e…` = 已推未过门;`sub-c006…` = capstone 产物)留档;测试服 `test-one` 已回 `felis/paper:demo` + Stopped;docker 停。 ### 本轮新增真机证据(第四十六批:上传面 #75/#76 真机闭环 + tracker #8/#1 收口 #77/#78 + 文案 #79 + 发布链 rc smoke) **零、现场**:代码三条 commit 先落(`43699b4` #77 面板跳转、`c57daaf` #78 converge、`b9ebc87` #79 INERT 文案),随后按 `b9ebc87` 重建镜像 = `registry.felis.svc:5000/felis/felis:auditfix77`(`v0.0.0+fix77`;宿主源 `10.43.182.43:5000`,docker build → push),控制面 api/operator + minecraft ns `felis-reaper` CronJob + api/operator 的 `FELIS_IMAGE` 四处成对齐;`/opt/felis/src` = b9ebc87 快照(上一版 `src.bak46`);宿主 drill 二进制 `/root/felis-fix77.bin`(Mac 侧 `GOOS=linux GOARCH=arm64` 交叉编译,`-X main.version=v0.0.0+fix77`)。 **一、#75 真机闭环(配额/限流,owner 会话,auditfix77)**——留档 `/root/probe77/create-lane.log`、`lane2.log`: - **create 失败不烧窗口**:坏 JSON → 400,**同一秒**合法 create → 201(`sub-811427aa7dcf6807`)——`release` 路径生效; - **create 冷却**:同用户 30s 内第二条 → 429 `submission_cooldown`; - **pending 上限**:攒到 5 条 pending → 第 6 条 → 403 `submission_quota_exceeded`; - **upload 失败不烧窗口**:对已审核行上传 → 409 `already_reviewed`,**同一秒**对 pending 行上传 → 200; - **upload 冷却**:15s 内第二次 → 429 `submission_cooldown`;等 16s → 200; - **存储预算**:把 S2 的 blob 稀疏 `truncate` 到让 owner 已存字节 = 2GiB−100B(`blocks=8`,不占盘)→ 上传 → 403 `submission_quota_exceeded`;管理员删除 S2(行 + 目录双清)后 → 同一上传 → 200(预算即时释放)。 - 口径:预算按 blob 的实际占用聚合(`Blobs.Size` 遍历汇总,正是"用久了就超"的同一条读取路径);稀疏垫付只是把"已经存了 2GiB 的用户"这一状态合成出来。 **二、#76 真机闭环(撤回 + 管理员删除)**——留档 `/root/probe77/lane3.log`: - 撤回自己 pending → 200;DB 行 = 0、uploads 目录 = gone;重复撤回 → 404;对已审核 → 409 `already_reviewed`;对他人 id → 404 `not_found`(owner 判定先于状态判定,不泄露他人行状态); - 管理员删除(pending)→ 200;重复 → 404;行与 blob 双清(lane2 的 S2 同证:`s2 rows=0 dir=gone`); - 面板侧 CDP:**撤回两步确认**(展开行 → 「撤回提交」→「确认撤回」→ 行消失)与 **admin 队列每行两步删除**(trash →「确认删除」→ 行消失、计数回落)双双走过;截图 `/tmp/withdraw-{1,2}-*.png`、`/tmp/admdelete-{1,2}-*.png`(Mac),脚本 `/tmp/cdp-withdraw76.js`、`/tmp/cdp-admdelete76.js`。 - 真机注记(非缺陷,分级 ZT 的对照实证):admin 面只在 `op.console.` 主机可用——同一 owner 会话在玩家面板主机上 `is_admin=false`(admin 路由 403),在 `op.console.` 上 `is_admin=true`(200);面板按要求显示「无权访问」而不是假装能点。 **三、#77(tracker #8,`43699b4`)真机闭环:锁定会话 → /setup** - 铸一个未完成引导会话(SQL:`sha256('drill-locked-77')` → `sessions`,用户 `3f2c1b0a-…`,`email_verified=f`、无 passkey)→ `GET /me` 200、`GET /me/submissions` → 403 `setup_required`(后端本就正确); - CDP 用该 cookie 打开 `/submissions`:页面请求 `/me/submissions` 收 403(code=`setup_required`)→ **自动跳转 `/setup`**(`location.href` 实测)→ 向导渲染「初始化你的账户 / 第一步 · 填写邮箱」(截图 `/tmp/setup8-redirect.png`;网络事件 = 200/403(/setup/status 200) 序列在脚本输出里)。 - 顺带确认:Dashboard 首屏三请求(`/me`、`/me/servers`、`account/link/start`)都是 `SetupAllowed`——所以补丁前用户是在"点受保护操作"时才撞到那句误报「无权执行此操作」。 **四、#78(tracker #1,`c57daaf`)真机演练:`felis converge`** - 现场先剥后补:`kubectl patch` 清空 lobby `spec.rcon` 与 login `spec.startup.healthHTTPPort`(复刻老装机 CR 缺后加字段的状态)→ `/root/felis-fix77.bin converge` → 两条全部填回(`rcon{enabled:true,secretRef:lobby-rcon/password}`、`healthHTTPPort:8080`);**第二次运行 → 双双 `already converged`**(幂等);operator 侧滚动收尾,login/lobby 回 Running/Ready(日志 `converge-{1,2}.log`、`crs-before.yaml`)。 - 语义边界(单测):非零值一律不碰(运维自换的 secretRef 生还)、缺席 CR 只提示(创建仍归 setup)、占名非系统角色 CR 拒绝收敛、未配镜像跳过。 **五、#79(`b9ebc87`,文档)**:troubleshooting 的 `[INERT]` 图例仍写「§12 列着唯一仍适用的字段」,而唯一候选 `spec.storage.retainOnDelete` 早已移除、§12 自述「每个字段都有 controller 读」。改为「今天没有字段处于该状态」并指向 §13 的移除记录。 **六、发布链 rc smoke(tag `v0.1.0-rc1` → release run `35949621233`)**: **六、发布链 rc smoke(tag `v0.1.0-rc1` → release run `35949621233`):首次全绿** - run 步骤实况:go vet/test ✅ → `plugins/test.sh` ✅ → `plugins/test-mods.sh` ✅(**release.yml 史上第一次真正跑这三关**)→ buildx 双架构构建 ✅ → **stamp 校验** ✅(`felis v0.1.0-rc1` + arm64 ELF 断言)→ publish ✅; - 产物释出:`felis-linux-amd64`(61,378,722 B)与 `felis-linux-arm64`(57,344,162 B)双资产,release 标记 `prerelease=true`; - **prerelease 语义复核**:`GET /repos/FelisMC/Felis/releases/latest` → **404**(RC 没有顶掉 latest——正是 release.yml 注释里防的那件事);bootstrap 默认通道在无 stable 时按设计给出显式指引后 die、`felis update` 的 404 报错可读——两个行为本批均实测; - 产物级复验(目标架构实机):arm64 资产 scp 到 VM 执行 → `felis v0.1.0-rc1`(`go1.26.8 linux/arm64`),且直接可用:`/root/felis-rc1.bin converge` 打现集群 → 双服 `already converged`; - 留档:`/root/felis-rc1.bin`;asset 副本 Mac `/tmp/rc1b/felis-linux-arm64`。 - 剩余(产物决定,未代拍板):仓库仍无 **stable** release → 新装走默认 release 通道会以指引性报错 die(提示改 dev 或等 stable);切首个稳定 tag(如 `v0.1.0`)即让默认通道与 `felis update` 真正上线,建议维护者择时执行。 **七、运维注记(非缺陷)** - pg_hba 的 `host felis_pgint felis 127.0.0.1/32 scram-sha-256` 行**再次丢失**(批 31 补过一次)——补回并 reload 后 pgint 才能连。重装/动过 PG 后先查这条(已写进速查)。 - 升级域注记:#75/#76 的 pgint 新断言(`CountPendingSubmissionsBy`、`DeletePendingSubmission`/`DeleteSubmission` CAS)首次上真 PG:**17/17 全绿**。 - 根盘:镜像构建后 83% → `docker builder prune -af` 回收 3.5G,docker 停回 inactive。 - CI:`43699b4` success;`c57daaf` 被后一 commit 的并发策略取消(同分支 cancel-in-progress),其树被 `b9ebc87` 的 success 完整覆盖;`b9ebc87` success。 - tracker 收编:**关** #10/#20/#21/#22(#20→`d9246dd`、#21→`f5a76cf`、#22→`3f2b28d`、#10→`23792d6`,均附证据评论)+ **关** #1/#8(本轮实现并真机验证);**注记**(保持打开作老装机待办)#2/#3/#4;**留存** #9/#12/#13/#15(真增强,超出生产可用主干,未动)。 ### 本轮新增真机证据(第四十七批:首个稳定版 `v0.1.0` 发布链全实弹 + release 通道无 checkout 全量安装 + `felis update` 双向报告) **零、现场**:切首个稳定 tag `v0.1.0` → `b9c97ff`(annotated),release run `35950722509` 全绿,产物 `felis-linux-amd64` 61,378,722 B / `felis-linux-arm64` 57,344,162 B;`GET /releases/latest` 现解析到 `v0.1.0`(`prerelease=false`),`v0.1.0-rc1` 保持 Pre-release。VM 侧事件前快照 `/root/probe77/pre77/`(host toml + deploys + crs + `hostbin.sha`=`106c4c9e…`);引导函数副本 `/root/probe77/bootstrap-funcs.sh`(删 `main "$@"` 行、可 source)与完整版 `/root/probe77/bootstrap-full.sh`(sha256 `89d0181d…` = 仓库 `deploy/bootstrap.sh` 逐字节)。 **一、Stage 1 — release 二进制下载(真实 github_api + asset + 原子安装)**: - `resolve_install_ref`(release 通道)→ `REF=v0.1.0`; - `download_release_binary`:宿主二进制 `felis v0.0.0+gunknown`(sha `106c4c9e…`)→ `felis v0.1.0`(sha `a64f32c5…`,57,344,162 B 与 arm64 资产一致); - 复跑收敛:`host binary is already v0.1.0; skipping the download`(零 API、零下载)。留档 `stage1.log`、`stage1-release-download.sh`。 **二、Stage 2 — 无 checkout 全量安装(真实 `curl|bash` 形态;本批主线)**: - 配方:`systemctl stop felis-velocity` → `mv /opt/felis/src src.bak47`(全程无 checkout)→ `bash < bootstrap-full.sh`(stdin 形态),`FELIS_IMAGE=…/felis/felis:v0.1.0`、`FELIS_WORLDS_HOST_PATH=/var/lib/rancher/k3s/storage`(对齐在册 reaper 形态); - 结果:**rc=0、2m17s、`[fail]`=0、零 `[warn]`**(留档 `stage2.log`、`stage2-run.sh`)。 - 关键路径逐条为真:`use_release_binary` 命中 → 跳过下载(`HAVE_PREBUILT_BINARY=1`)→ `build_image_from_binary`(宿主二进制裹 distroless 镜像并导入 k3s)→ **`game_stack_source` 走 `unpacking the embedded game-stack sources (no checkout on this host)`**(二进制内嵌 tar 解包)→ limbo/lobby/paper 构建(Paper 26.3-38)→ 四镜像全 mirror 进内部 registry(实测 tags:`felis` 增 `v0.1.0`、limbo/lobby/paper = `demo`)→ api/operator 收敛到 `felis:v0.1.0` 且 rollout 全绿 → **minecraft ns `felis-reaper` CronJob 模板同步为 `felis:v0.1.0`** → 既有系统服 login/lobby 滚到新镜像 Running。 - 数据面复验:`users=16`、`servers=2`(resolvecheck/test-one)不变;面板 `https 200`;`/etc/felis/felis.host.toml` 与事件前快照**逐字节一致**(手改保留承诺实测);`bootstrap.done` 更新至 `03:32:18Z`。 - 收尾:`src.bak47` 复原为 `/opt/felis/src`;docker 停回 inactive。 **三、Stage 3 — `felis update` 报告(双向对照)**: - `v0.1.0-rc1` 二进制:`felis-api v0.1.0-rc1 -> v0.1.0 update available (notify)`;velocity `3.5.1 -> 4.2.0 (notify)`;附 apply 命令(重跑安装器 + 私仓 token 指引文案); - 现装 `v0.1.0` 二进制:`felis-api v0.1.0 up to date`——锤实「felis-api 已装版本 = 运行中二进制自身版本」(panel 内嵌,无独立版本可探); - velocity minor(3.5.x→4.2.0)按设计走 notify;installer 只取 pinned minor 的最新 build。 **四、运维注记(非缺陷)** - pg_hba `felis_pgint` 行又被重装清掉(第三轮)——**根因定位**:前两轮的补回位置落在 `# BEGIN/END FELIS MANAGED HBA` **块内**,重写段的 skip 对块内照删;本轮改为插在 `# END FELIS MANAGED HBA` 之后(块外,清理条件 `$2==felis` 不命中),下轮重装可复检;速查已注明正确插入位。 - 控制面镜像引用:`auditfix77` → `felis:v0.1.0`(release 二进制包裹);回退面 = 旧 `auditfix77` 仍在宿主 docker 与 registry;registry 的 `v0.1.0` tag 保留为 kubelet 回拉源。 - 演练后 VM 复位:docker inactive、k3s/felis-velocity active、test-one 仍 Stopped。 **五、结论**:stable release 通道端到端闭合——「tag → CI 双资产 → `/releases/latest` → 无 checkout 全量安装(embedded 资产 + registry mirror)→ 控制面/游戏栈全绿 → `felis update` 报告」全链实弹,无新缺陷。 ## 结论:离"生产可用"还差什么(按优先级) 1. ~~构建链路的上下文通道~~ ✅ **已修**(`f79e5eb`/`02fd2de`,真机全链路含拉回校验;Trivy DB 需按 §8e 镜像一次)。 2. ~~镜像耐久~~ ✅ **已完成**(第二十九批:`a9b275a`/`13d64e0`/`fa0e8d7` + `c7e585e`;三次真机重跑 + GC 两演练——控制面与游戏镜像被 GC 后自动回拉;同批修复并复验 #46/#47/#48/#49)。 3. ~~PG 级契约测试~~ ✅ **已落地**(`2a55a0d`):`internal/pgint`(`-tags pgint`,需 `FELIS_TEST_PG_URL` 指向名字含 `pgint` 的库,harness 会 drop schema + 重放真实迁移)已覆盖会话/OTP/op-login/绑定码/submission/build/owner 角色;**首跑即抓到 #20**(索引与 ErrEmailTaken 从未存在)。运行方式见 CONTRIBUTING.md。 4. ~~面板把 /jobs 显示出来~~ ✅ **已完成**(`97a64c8`,备份页「最近操作」卡,正是它把 #35 暴露出来的)。 5. ~~多节点回收~~ ✅ **已修**(`daf7602`:`--reaper-node` → CronJob pod `kubernetes.io/hostname` nodeSelector,真机 render/dry-run/收敛 diff 三连;单节点部署不传即维持原状)。附带核销缺陷 #38(渲染提示里过期的 uid-1000/setfacl 指导)。 6. ~~告警~~ ✅ **已完成**(`94f71ee` + `43df08b` + 第二十七批实弹演练:真实构建失败 → pending → 08:33:14Z firing;规则随 `deploy/alerts/` 交付)。**2026-09-24 补齐无 Prometheus 的告警面**(`d17524c`):主机 `felis-watchdog.timer` 每 2 分钟巡检控制面/登录门/系统服/失败服/Job/reaper/节点/PG/代理/DB 备份/磁盘/内存,按持续时长门限给平台所有者发邮件;operator 增加 `felis_server_phase`、`felis_build_info`、卡死存活探针,规则新增 `felis.platform.rules`/`felis.jobs.rules`(promtool 22 条全过)。真机:felis-api 缩到 0 → 5 分钟后告警邮件落地("Felis 严重告警…1 项异常"),恢复 10 分钟后收到恢复邮件。 7. ~~玩家可见的构建结果~~ ✅ **已修**(`72c4aa3`,缺陷 #36:列表路由附 `build_status`/`build_error`,面板「我的提交」展开行呈现,真机双例验证)。 8. ~~面板文件编辑器入口~~ ✅ **已补**(`0a36b3f`,缺口补齐,真机 CDP 全链)。 9. ~~fleet 系统服务死操作~~ ✅ **已修**(`2f90851`,缺陷 #37)。 10. ~~S3 上传通道演练 + 存储向导逐屏~~ ✅ **已演练**(第二十四批:上传通道全链、0 缺陷;第三十二批:向导屏逐屏 + 上传取件 + UI 回滚,0 功能缺陷并修 #52)。 11. ~~breakGlass 控制台逐屏~~ ✅ **已演练**(第二十五批:菜单 4 操作 + bootstrap 分支,缺陷 #39–#43 全修全验)。~~`felis setup` 全屏向导逐屏~~ ✅ **重跑侧已逐屏**(第二十六批),**首装侧连续走查也已完成**(第三十一批:scratch 库单次连续走完全部屏幕 + 轨道回顾,0 缺陷)。 12. ~~`felis nano` 全链~~ ✅ **已走查**(第三十四批:HTTP 矩阵红 13→绿 60/60;systemd/配置/firewalld 三层重跑全绿;同批修复 #55——坏源静默挡住验证梯子)。 13. ~~NetworkPolicy 真机强制矩阵~~ ✅ **已演练**(第三十五批:pod 源端口级矩阵全绿、转发型外部源「拒→放→拒」白名单闭环、host/ClusterIP/NodePort 路径全通;两条已归因注记——node-local 流量豁免、firewalld 仅放 DNAT 服务流)。 14. ~~面板错误文案全量本地化~~ ✅ **已修**(第三十六批:#60,37 个用户可达码补齐双语映射,三类真机验证;管理面写操作 Run4c 复核 0 缺陷)。 15. ~~管理面交互级收尾(submissions / 会话撤销 / 启动)~~ ✅ **已演练**(第三十七批:点击→构建→succeeded 全链、单条/全部撤销精确验证;同批修 #61——LP 写承诺按 LP 官方语义收窄,真机复验)。 16. ~~缺陷 #56–#59(玩家管理回包 / 控制台回显 / 文件页 fail-fast / LP 诚实化)~~ ✅ **已修并归档**(第三十八批回填真机证据:修复前触发 + 修复后复验,四项全绿)。 17. ~~服务器详情与运维面收尾~~ ✅ **已演练**(第三十八批:ServerFiles 写流程、备份/恢复全链、白名单/封禁闭环、ServerCard 停止、`/admin/updates` 复跑、metrics、CLI 核销——0 缺陷)。 18. ~~reaper 多节点实机~~ ✅ **已演练**(第三十九批:VM 克隆双节点 k3s——pin 命中持盘节点并跑通、错位 pin fail-closed Pending、装置全回收;`felis-backups` PVC 的 volume node affinity 构成第二层保险)。 19. ~~Java 插件层自动化缺口~~ ✅ **已补**(第四十批:`plugins/test.sh` + CI `plugins` 作业——三个手工测试与三个装机 jar 全进门禁,CI 4/4 绿;首跑抓出两处"没跑过"的错误:InviteCardTest 编译类路径缺 examination-api、limbo `+` 版本不可解析。同批关闭 demo-up 单起点化 #63、并复核剔除 #62)。 20. ~~装载器 mod 层(fabric/forge/neoforge)~~ ✅ **已收口**(第四十一批:#64 三个 `gradlew` 补可执行位、#65 元数据对齐 AGPL-3.0-only;`plugins/test-mods.sh` + CI `mods` 作业 + release 双门禁;三真实专用服起服 E2E——mod 装载、`/link` 注册、控制台拒绝全绿。CI run `35892544563` = 5/5,`mods` 作业首跑即绿)。 21. ~~全仓覆盖面对账 + 文档收尾~~ ✅ **已完成**(第四十二批:22 个 internal 包 × 面板 17 条页面路由 × 全部 tracked 脚本 × 仓库卫生逐项对账,无盲区;#66 README_EN 修复;§28 两张序列图对齐现行实现;本地 `scripts/sync.sh` 排除清单与 `go:embed` 冲突——修复+复现验证〔脚本私有,不入库〕)。 22. ~~构建层资源回收~~ ✅ **已修**(第四十三批:#67 build Job TTL——完成 Job/Pod 不再无限堆积;真机:新构建 Job `ttl=604800` + 旧 Job 打 30s TTL 实测被 TTL 控制器收走。同批:6 轮生命周期循环 + operator/api/pg 三处注入全绿、PG 503 语义复验、30 路并发全 200)。 23. ~~构建上下文拉取的短暂断连~~ ✅ **已修**(第四十三批追加:#68——fetch-context 有界重试〔45s 窗口 / 3s 间隔;4xx 不重试〕;真机:api 停机期连续 8 次拒连全部重试、恢复后构建 32s Complete;正常路径无重试开销。附:演练升级暴露的 `FELIS_IMAGE` 两代错位已对齐 auditfix63)。 24. ~~modpack 规模上下文全链 + 并发构建~~ ✅ **已演练**(第四十四批:200MiB 上下文上传 0.44s → 批准 → 构建全链 13s、三路并发构建全绿;TTL 与玩家可见面在规模下复验,零缺陷。见该批节)。 25. ~~内建底座构建(`FROM registry.felis.svc:5000/…`)~~ ✅ **已修并真机闭环**(第四十五批三连修:#70 拉取缺 `--insecure-pull`、#71 drop-ALL 卡解包、#72 trivy Java DB 未镜像——"真实模组包"形态的三个必踩点;修复后 paper 底座构建 27s Complete、`paper.jar` 扫描 0 漏洞)。 26. ~~提交 → 装服 → 起服 闭环~~ ✅ **已演练**(第四十五批 capstone:构建产物 PATCH 为服务器镜像〔白名单门通过〕→ wake → Running 1/1 → 用户 marker 可读 + `Done (4.678s)!`;test-one 已复原为 paper:demo/Stopped)。 27. ~~文档与测试工具面~~ ✅ **已修**(第四十五批:#69 README 部署承诺对齐实现;#73 bootstrap 测试密闭化〔VM 140 PASS〕;#74 console 断连测试消抖;CI 全绿)。 28. ~~提交上传面的上限与生命周期~~ ✅ **已修并真机闭环**(第四十六批:#75 per-user create/upload 冷却〔429〕、pending ≤5〔403〕、2GiB 存储预算〔403〕;#76 撤回〔owner+pending CAS〕与管理员删除〔行+blob 双清〕;面板两步确认 CDP 全绿)。 29. ~~未完成引导的导航(tracker #8)~~ ✅ **已修并真机闭环**(第四十六批 #77:`403 setup_required` → 自动 `/setup`;CDP 复验)。 30. ~~已装机系统服的新增 CR 字段(tracker #1)~~ ✅ **已修并真机闭环**(第四十六批 #78:`sudo felis converge`——零值才填、非零不覆写;剥字段→填回→幂等三连真机过)。 31. ~~troubleshooting `[INERT]` 图例~~ ✅ **已修**(第四十六批 #79,文档级)。 32. ~~稳定版发布与 release 安装/升级通道~~ ✅ **已实证**(第四十七批:tag `v0.1.0`(`b9c97ff`)+ release run `35950722509` 双资产、`/releases/latest` 解析到 v0.1.0;无 checkout 全量安装 2m17s / 零 fail / 零 warn、registry mirror 四镜像、reaper CronJob 对齐 `felis:v0.1.0`;`felis update` 双向报告〔rc1→v0.1.0 notify / v0.1.0 up to date〕)。 33. ~~世界归档与数据库备份只在本机~~ ✅ **已修并真机闭环**(2026-09-24:`felis offsite` + `felis-offsite.timer` 每小时把世界归档与 DB bundle 以 AES-256-GCM 分段加密推到 S3 兼容桶,`world_backups.offsite_at` 记账〔迁移 0024〕;配了 `[offsite]` 后 reaper 只在归档的异地副本确认后才删 PVC;watchdog 12 小时无成功同步即告警;安装器 `FELIS_OFFSITE_*` 生成密钥并在收尾横幅要求离机保存。真机(MinIO):首次同步 8 世界 1.2 GiB + 12 bundle、两条"记录在册但盘上已无"的历史归档如实列出;`fetch-db latest` sha256 与本机一致、错误密钥拒绝且不留半成品;`fetch-worlds` 取回被挪走的归档 sha256 一致;reaper 演练〔20 天空闲世界〕第一轮 `awaiting_offsite=1` 且 PVC 保留 → 同步后第二轮 `reaped=1`、PVC 删除、所有权释放;watchdog 把 status 改成 35 小时前 → `[warning] offsite`。演练装置已清理)。 ## 剩余待演练队列(截至第四十三批) - ~~reaper 多节点实机~~ ✅ 已完结(第三十九批:临时克隆第二节点组双节点 k3s 实机——pin 命中持盘节点并跑通、错位 pin fail-closed Pending;装置已回收)。 - ~~面板交互级收尾~~ ✅ 已完结(第三十六–三十八批:管理面写操作、submissions/会话、服务器详情页全链,0 缺陷)。 - ~~`demo-up.sh`~~ ✅ 已完结(第四十批:单起点化 #63——bootstrap 装机 + 三项硬校验 + `felis setup` 交棒;T1/T2/T3 真机全绿)。 - ~~Java 插件层自动化~~ ✅ 已补(第四十批:`plugins/test.sh` + CI `plugins` 作业;三个手工测试与三个装机 jar 首进 CI)。 - ~~装载器 mod 层(fabric/forge/neoforge)~~ ✅ 已收口(第四十一批:#64/#65 修复 + `plugins/test-mods.sh`/CI `mods`/release 双门禁 + 三真实专用服起服 E2E)。 - ~~全仓覆盖面对账~~ ✅ 已复核(第四十二批:模块×台账逐项对账无盲区;批次产出为文档/本地工具层面的修复,见该批节)。 - ~~长稳/故障注入(首轮)~~ ✅ 已完成(第四十三批:6 轮起停循环 + 3 处注入 + PG 断连 503 + 30 路并发 + 25 分钟持续轮询;见该批节)。 - ~~modpack 规模上下文 + 并发构建~~ ✅ 已完成(第四十四批:200MiB 上下文全链 13s + 并发 ×3 全绿;见该批节)。 - ~~提交 → 装服 全链(含平台底座 FROM)~~ ✅ 已完成(第四十五批:三连修 #70/#71/#72 + capstone 提交→装服→起服;见该批节)。 **队列现已清空。**(真实游戏客户端进服已按决定跳过,用内部 mint/approve hook 链替代;不依赖客户端的最大真机面见第四十批节 + 第四十一批节〔装载器三服起服〕;第四十二批为覆盖面对账复核。) ## 系统性观察 - **PGRepo 与接口契约/fake 漂移**(#16/#17/#18):`fakeRepo` 与接口注释是对的、PG 实现在细节上落后,单测全绿也发现不了。建议后续引入 PG 级契约测试(testcontainers 或针对关键写路径的集成测试),重点覆盖「接口注释承诺了字段/错误码/生命周期」的方法。 - 部署知识:交叉编译必须先把 `panel/dist` 拷进 `internal/panel/static` 再 `go build`,否则镜像内 SPA 缺失(页面显示 “assets were not built”)。本批镜像为 `felis:auditfix7`。 ## 复现入口速查 - 面板会话 cookie:`/tmp/owner-cookies2.txt`(2026-09-23 铸;auditfix42 部署后仍有效;staff 邮件门被设计拒绝 → 走第二十八批的 op-login + 内部面 approve hook 链);API 助手:`/tmp/fcurl.sh` - 测试服:`test-one`(minecraft ns,stopped);合法备份 `bk-47ee2e7e96e5a4ca9d0e51b805518bac` - CDP 调试口:Mac `127.0.0.1:9333`(独立 Chrome,profile `/tmp/felis-chrome2`);WebAuthn 虚拟认证器需在**同一 CDP 会话**内完成仪式,且先 `Page.bringToFront`(否则 NotAllowedError: page does not have focus) - 已部署到 VM:控制面(api/operator/reaper + `FELIS_IMAGE`)= `registry.felis.svc:5000/felis/felis:auditfix61`(含至 #61 的全部修复;镜像托管于内建 registry);系统服/推荐 ref = `registry.felis.svc:5000/felis/{limbo,lobby,paper}:demo`;源码快照 `/opt/felis/src`(`b4ef42d`;上一版在 `/opt/felis/src.bak`,更早 `src.old43`);host 二进制 = `/usr/local/bin/felis` = `v0.0.0+fix61`(含 #53–#61;旧件留档 `/root/felis-fix54.bin`(sha `a0b29153…`)、`/root/felis-fix52.bin`、`/root/felis-auditfix44.bin`);**docker 守护进程在第三十七批构建后已停(用前 `sudo systemctl start docker`)**;**world 执行器以 root+DAC_OVERRIDE 运行**;reaper CronJob(minecraft ns)`suspend=false / …auditfix61 / nodeSelector=localhost.localdomain`;迁移 `schema_migrations` max=21;SMTP 密码 env 待「configure email」刷新时落地 - 安装器重跑配方(第三十批复用;先同步 `/opt/felis/src` 再跑):`cd /root && FELIS_SKIP_FETCH=1 FELIS_INSTALL_MODE=full FELIS_IMAGE=registry.felis.svc:5000/felis/felis: FELIS_WORLDS_HOST_PATH=/var/lib/rancher/k3s/storage nohup bash /opt/felis/src/deploy/bootstrap.sh > /root/bootstrap-.log 2>&1 &`;完成后 `grep -c '\[fail\]'` 应为 0 - 第三十一批 drill 留档:`/root/preTUI43/`(首装走查前快照:两 toml + 两 ns Secret + sha256、走查后 Secret 快照)、`/root/pre51-replica.toml`/`post51-replica.toml`/`post51b-replica.toml`(#51 红/绿副本证据);走查残留(scratch 库、scratch 配置、hba 行、tmux 会话)均已清理 - 第三十二批 drill 留档:`/root/preS3/`(S3 走查前快照:两 toml + 两 ns Secret + sha256 + deploys.txt)、`/root/s3wiz/`(posts3 快照、postroll-sha256、fix52-red.txt、fix52-green.txt)、`/root/felis-fix52`(含 #52 的宿主二进制);MinIO 容器+卷+两镜像、`felis-uploads-s3` Secret、DB 行(`image_submissions` `sub-853a4e2ba4ba6443`)、/tmp 残留均已清理,docker 已停 - 第三十三批 drill 留档(VM):`/tmp/felis-fix53`、`/tmp/felis-fix54`(宿主二进制;sha `9afd141d…` / `a0b29153…`)、`/root/felis-fix52.bin`;`/tmp/player-cookies.txt`(player 会话);面板 CDP 驱动脚本 `/tmp/cdp-updates2.js`(Mac 侧) - 第三十四批 drill 留档(VM):`/srv/nanotest/`(nano 装置:`nano-stub.py` + 5 驱动脚本 + `matrix/` 红原跑 / `matrix-fix55/` 绿原跑)与四层重跑落盘 `matrix-red.r2.log`(`pass=47 fail=13`)/ `matrix-fix55.r2.log`(`pass=60 fail=0`)/ `nano-service.r2.log` / `nano-config.r2.log` / `nano-fw.r2.log`;`nano-stub` 临时单元仍在跑(收尾 `systemctl stop nano-stub`);宿主二进制 `/usr/local/bin/felis-nano-test` = fix55 改名件(sha `2c8c34fd…`,与 `/usr/local/bin/felis` 同物) - 第三十五批 drill 留档(VM):`/srv/npdrill/`(netpol 装置全套:`np-matrix.sh` + `np-matrix-run1/2.log`、`np-netns.sh`、`netns-probe.py`、`np-cidr-test.sh/.log`、`spec-before/after.json`、`td3.log`(包级归因)、`tcpdump-8081.log`(刷新存活)、`iptables-save.txt` 与 `nft-rules.txt`);netns/veth、firewalld 临时挂载、策略改动全部已清理/复原(spec md5 `e6b07440…` 前后一致) - pgint 正确跑法(走 VM 的 PG,勿在本机起容器):Mac 侧短隧道 `ssh -6 -i ~/.ssh/id_ed25519 -N -L 15433:127.0.0.1:5432 root@…` → `FELIS_TEST_PG_URL='postgres://felis:@localhost:15433/felis_pgint?sslmode=disable' go test -tags pgint ./internal/pgint/ -count=1`(pg_hba 需 `host felis_pgint felis 127.0.0.1/32 scram-sha-256`;第三十一批把现场丢失的这条补回;隧道用完即 kill) - breakGlass TUI 驱动法:VM tmux `new-session -d -s bg -x 160 -y 45 "/root/felis-auditfixNN breakGlass"` + `set-window-option -t bg remain-on-exit on`(否则退出摘要读不到);send-keys/capture-pane 驱动;退出码 1 = 操作失败(卡上会显示原因) - 构建链路 drill 现成条件:`felis-build` 里有 `felis-service-token`(Secret 复制);`felis-config` 的 kaniko/trivy pin 指向内建 registry 的 mirror 路径(`registry.felis.svc:5000/mirror/...`,见 §8e)——本批后安装器重跑会 **carry** 这些 pin(不再被写盘冲掉;#49 复验点);直构一行:`POST /api/v1/images/build`(context=`http://felis-api-internal.felis.svc.cluster.local:8081/api/v1/internal/submissions/sub-bf7dc18e9dd97dc2/context`) - registry 工具:`curl -s http://127.0.0.1:5000/v2/_catalog`、`/v2//tags/list`;**取 manifest 必须带 `Accept:`(OCI index/manifest list;缺 Accept 的 GET 会 404,别误判没推上)**;GC 演练配方:`k3s ctr images rm `(要清到 blob 级再加 `k3s ctr content prune references`)→ 删 pod / `rollout restart` → `kubectl describe` 看 `Pulled … in …ms` - setup 重跑向导(第二十六批):状态屏 `c/s/e` 三流已走查;**reconfigure 非只读**——storage 选 Local / 提交 S3 即 apply + 滚 API(幂等),email 表单 esc 无副作用;从 S3 表单 esc 退回会重置为 Local 预选;存储屏(第三十二批补充):chooser 预选当前值(↑↓ 切换),Local 直接 enter 提交、S3 五字段 `enter next`/末字段 `enter submit`,提交后 10–40s(含 API rollout status 180s 上限) - VM 内部面:`k3s kubectl -n felis port-forward svc/felis-api-internal 18081:8081`(Pod 重建后转发会悬死,需重启);reaper CronJob(minecraft ns)已应用且已收敛(`suspend=false`) - 第三十八批收尾复查:docker `inactive`;控制面三处 = `auditfix61`;`/opt/felis/src` = `b4ef42d` tarball 展开(无 `.git`);reaper `suspend=false` + `nodeSelector=localhost.localdomain`;test-one `Stopped`;`resolvecheck` 文件接口 409 `no_world_volume`(0.03s) - 第三十九批 drill(已回收):临时 VM `felis-node2`(`prlctl clone --linked` 克隆 node1;node2 停用 felis-velocity/k3s-server/docker 后以 agent 加入)——流程与两向对照见该批节;node1 为克隆经历过一次正常重启(组件全回归、docker 停回 `inactive`);收尾 job/节点对象/VM 全部删除,集群回到单节点 - 第四十批工具与留档:VM `/tmp/mcprobe.py`(纯 socket MC 探针:`python3 /tmp/mcprobe.py status .`;`login . ` 打登录首包);插件门禁跑法 = `docker run --rm -v /opt/felis/src:/src:z -w /src gradle:8.14-jdk21 bash plugins/test.sh`(或宿主 `bash plugins/test.sh`,需 JDK 21 + Gradle 8.14;日志 `/root/plugins-test-b40.log`);T3(demo-up 默认臂全量重跑)日志 `/root/demo-up-b40.log`;`/opt/felis/src` = `c59b387` 快照(上一版 `src.bak40`;此后全量重跑用 `sudo bash /opt/felis/src/deploy/demo-up.sh` 即可,SKIP 开关见脚本头);CI run `35888009965`(含新 `plugins` 作业首跑绿) - 第四十三批工具与留档:控制面三处 = `registry.felis.svc:5000/felis/felis:auditfix62`(含 #67;镜像源 `10.43.182.43:5000/felis/felis:auditfix62`,宿主 docker build 后 `docker push 10.43.182.43:5000/...`);`/opt/felis/src` = `2755e41` 快照(上一版 `src.bak42`);owner 会话重铸 = `bash /root/mint-owner-43.sh`(cookie 落 `/tmp/owner-jar43.txt`);直构触发(含 TTL 检查)= `curl -sk -b /tmp/owner-jar43.txt -X POST https://127.0.0.1:30443/api/v1/images/build -H 'Content-Type: application/json' -d '{"image_ref":"registry.felis.svc:5000/e2e/ttl-probe:latest","dockerfile":"FROM scratch\nLABEL felis=x\n","context_ref":"http://felis-api-internal.felis.svc.cluster.local:8081/api/v1/internal/submissions/sub-bf7dc18e9dd97dc2/context"}'` → `kubectl -n felis-build get job build-bld- -o jsonpath='{.spec.ttlSecondsAfterFinished}'` 应为 `604800`;压测编排 `/root/soak43.sh`(日志 `soak43.log`,含 6 轮循环 + 3 注入)、长轮询 `/root/poll43-long.sh`(日志 `poll43-long.log`);镜像构建日志 `/root/felis-image-build43.log`。注意:`lobby`/`login` 是保留名,internal per-server 路由(status/wake 等)对它们**按设计**返回 400,勿作缺陷误报。 - 第四十三批追加(#68)留档:控制面 api/operator/reaper 镜像 + api/operator `FELIS_IMAGE` = `registry.felis.svc:5000/felis/felis:auditfix63`(`felis version` = `v0.0.0+fix63`;升级时 `set image` 与 `FELIS_IMAGE` env 必须成对改,否则构建 Job 的 fetch 容器会悄悄用旧镜像);`/opt/felis/src` = `b76d0ac` 快照(上一版 `src.bak43` = `2755e41`);复刻 Job `/root/job68-new.json`(由红证据 Job `build-bld-1790187851749257160` 派生:换名 `build-retry68`、剥 controller uid 标签与 selector、镜像改 auditfix63、复用 `felis-service-token` Secret);重演验证法:`kubectl -n felis scale deploy/felis-api --replicas=0` → `kubectl -n felis-build create -f /root/job68-new.json` → `kubectl -n felis-build logs -c context-fetch`(应见 `connection refused; retrying`)→ `scale --replicas=1` → Job 收敛 `Complete`;对照:api 在线直构成功且 fetch 日志无重试行。 - 第四十四批工具与留档:200MiB 上下文构造 = 5×40MiB `head -c 41943040 /dev/urandom` + `Dockerfile`(`FROM scratch` + `COPY payload /payload`),`tar -czf /root/bigctx.tar.gz Dockerfile payload`;上传 = `curl -sk -b /tmp/owner-jar43.txt -X POST -H "Content-Type: application/gzip" --data-binary @/root/bigctx.tar.gz https://127.0.0.1:30443/api/v1/me/submissions//context`;采样 = `k3s crictl stats`(此版 **无 `--no-trunc`**,列解析取 `$2=NAME $4=MEM`);docker 缓存清理 = `systemctl start docker && docker builder prune -af && systemctl stop docker`;留档 `/root/stats44.log`、`/root/stats44-build.log`、提交 `sub-cc160b3ad19080f9`、镜像 `e2e/conc-b44-{1,2,3}`。 - 第四十五批工具与留档:从平台底座构建的配方 = 提交上下文含 `Dockerfile`(`FROM registry.felis.svc:5000/felis/paper:demo` + `COPY marker.txt /felis-probe-marker.txt`);装服验证 = `PATCH /api/v1/servers/test-one {"image":"registry.felis.svc:5000/user-uploads/:latest"}` → `POST /servers/test-one/wake` → `kubectl -n minecraft exec test-one-0 -- cat /felis-probe-marker.txt`;trivy 双 DB 镜像 = `mirror/trivy-db:2` + `mirror/trivy-java-db:1`(Java DB digest `5766dfbb…`;两键在 `/etc/felis/felis.{host,pod}.toml` 与 felis-config Secret 双副本);`bootstrap_test.sh` 需在 Linux 跑(VM 留档 `/root/btest72/`,应 140 PASS);本批提交样本 `sub-{b45d416f4af811ef(无镜像),83f677e41acd80c3,171e8f967f0de3bc,c006cbd633317ccb,cc160b3ad19080f9}`;留档 `/root/probe44/`、`/root/probe44.tar.gz`、`/root/probe44*.sid`。 - 第四十六批工具与留档:控制面三处 = `registry.felis.svc:5000/felis/felis:auditfix77`(宿主 docker 源 `10.43.182.43:5000/felis/felis:auditfix77`,`v0.0.0+fix77`;api/operator + minecraft ns `felis-reaper` CronJob + api/operator `FELIS_IMAGE` 四处成对改);`/opt/felis/src` = `b9ebc87` 快照(上一版 `src.bak46`);宿主 drill 二进制 `/root/felis-fix77.bin`(Mac 侧 `CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=v0.0.0+fix77" ./cmd/felis`;scp 到 IPv6 主机时主机位必须写 `root@[fdb2:…]` 方括号);#75/#76/#77/#78 留档 `/root/probe77/`(`create-lane.log`、`lane2.log`、`lane3.log`〔含玩家会话矩阵 + 分级 ZT 对照 + curl 7.76 在 `-H Host:` 下**不发 jar cookie**的坑——host 路由 drill 用显式 `-H "Cookie: felis_session=…"`〕、`converge-1.log`/`converge-2.log`、`crs-before.yaml`、各步请求/响应 json);面板 CDP 截图(Mac):`/tmp/setup8-redirect.png`、`/tmp/withdraw-{1,2}-*.png`、`/tmp/admdelete-{1,2}-*.png`,脚本 `/tmp/cdp-setup8.js`、`/tmp/cdp-withdraw76.js`、`/tmp/cdp-admdelete76.js`;锁定会话铸法 = `sha256('drill-locked-77')` 直插 `sessions`(用户 `3f2c1b0a-…`);玩家会话铸法 = `user2@example.net` 邮件 OTP(码在 felis-api 日志 `no Mailer configured` 行;`edge-dis2` 是软删死账号,登录门按设计排除);pgint 前置:`pg_hba` 需 `host felis_pgint felis 127.0.0.1/32 scram-sha-256`(本批第二次丢失并补回);发布链 smoke = tag `v0.1.0-rc1`(release run `35949621233`),prerelease 不移动 `/releases/latest`——无 stable 时 bootstrap 默认通道给出显式指引后 die、`felis update` 404 报错可读;首个稳定 tag 是产物决定(未代拍板)。 - 第四十七批工具与留档:stable tag `v0.1.0` = `b9c97ff`(annotated;release run `35950722509`;资产 amd64 61,378,722 B / arm64 57,344,162 B;`/releases/latest` = v0.1.0、rc1 保持 prerelease)。VM `/root/probe77/`:`stage1-release-download.sh`+`stage1.log`(REF=v0.1.0;host bin sha `106c4c9e…`→`a64f32c5…`)、`stage2-run.sh`+`stage2.log`(无 checkout 全量安装 rc=0/2m17s/零 fail warn;"unpacking the embedded game-stack sources" 行)、`bootstrap-funcs.sh`(可 source 副本)、`bootstrap-full.sh`(完整版,sha `89d0181d…`)、`pre77/`(事件前快照);Stage 3 = `/root/felis-rc1.bin update --all`(rc1→v0.1.0)对照现装 `felis update --all`(v0.1.0 up to date);"无 checkout 主机"演练配方 = 停 felis-velocity → `mv /opt/felis/src src.bak47` → `bash < bootstrap-full.sh` → 复原 mv;pg_hba `felis_pgint` 行**必须插在 `# END FELIS MANAGED HBA` 之后**(块内会在重装时被重写段删除——三轮同根因;本轮已按块外补回)。