diff --git a/backend/internal/localreset/client.go b/backend/internal/localreset/client.go index adb884f..fedfd93 100644 --- a/backend/internal/localreset/client.go +++ b/backend/internal/localreset/client.go @@ -37,7 +37,7 @@ func Begin(ctx context.Context, conn net.Conn) (*Client, error) { client.stop = context.AfterFunc(ctx, func() { _ = conn.Close() }) var response Response if err := readFrame(client.reader, &response); err != nil || !validChallenge(response) || - len(response.Methods) == 0 || len(response.Methods) > 8 || response.RecoveryPage != nil || response.Inspection != nil { + len(response.Methods) == 0 || len(response.Methods) > 8 || response.RecoveryPage != nil || response.Inspection != nil || response.InspectionPart != nil { _ = client.Close() return nil, ErrProtocol } @@ -77,7 +77,7 @@ func (c *Client) exchange(request Request, stage int) error { _ = c.Close() return ErrProtocol } - if response.RecoveryAvailable || response.RecoveryPage != nil || response.Inspection != nil { + if response.RecoveryAvailable || response.RecoveryPage != nil || response.Inspection != nil || response.InspectionPart != nil { c.stage = -1 _ = c.Close() return ErrProtocol diff --git a/backend/internal/localreset/protocol.go b/backend/internal/localreset/protocol.go index 65e9e23..a102a21 100644 --- a/backend/internal/localreset/protocol.go +++ b/backend/internal/localreset/protocol.go @@ -59,6 +59,7 @@ type Response struct { RecoveryAvailable bool `json:"recovery_available,omitempty"` RecoveryPage *deployment.PendingRecoveryPage `json:"recovery_page,omitempty"` Inspection *deployment.RecoveryInspection `json:"inspection,omitempty"` + InspectionPart *inspectionPart `json:"inspection_part,omitempty"` } // Session 由主服务创建,身份验证及密码持久化仍由主服务负责。 diff --git a/backend/internal/localreset/recovery.go b/backend/internal/localreset/recovery.go index 330c2bf..f6763cd 100644 --- a/backend/internal/localreset/recovery.go +++ b/backend/internal/localreset/recovery.go @@ -81,7 +81,7 @@ func (c *Client) recoveryExchange(request Request) (Response, error) { return fail() } if request.Action == "recovery_list" { - if response.RecoveryPage == nil || response.Inspection != nil || len(response.RecoveryPage.Items) > deployment.RecoveryPageSize { + if response.RecoveryPage == nil || response.Inspection != nil || response.InspectionPart != nil || len(response.RecoveryPage.Items) > deployment.RecoveryPageSize { return fail() } previous := request.Cursor @@ -95,9 +95,17 @@ func (c *Client) recoveryExchange(request Request) (Response, error) { (len(response.RecoveryPage.Items) != deployment.RecoveryPageSize || cursor != previous) { return fail() } - } else if response.Inspection == nil || response.RecoveryPage != nil || response.Inspection.TransactionID != request.TransactionID || - len(response.Inspection.Files) != 9 || len(response.Inspection.Locks) != 3 || response.Inspection.ObservedAt.IsZero() { - return fail() + } else { + if response.InspectionPart != nil { + inspection, err := readInspectionParts(c.reader, response) + if err != nil { + return fail() + } + response.Inspection, response.InspectionPart = inspection, nil + } + if !validInspectionPayload(response.Inspection) || response.RecoveryPage != nil || response.Inspection.TransactionID != request.TransactionID { + return fail() + } } c.nonce, c.stage = response.Nonce, 3 return response, nil diff --git a/backend/internal/localreset/recovery_frames.go b/backend/internal/localreset/recovery_frames.go new file mode 100644 index 0000000..020e7f5 --- /dev/null +++ b/backend/internal/localreset/recovery_frames.go @@ -0,0 +1,116 @@ +package localreset + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "io" + + "git.pcux.cn/xuans/Scert/backend/internal/deployment" +) + +const ( + inspectionPartSize = 8 << 10 + maxInspectionParts = 48 + maxInspectionBytes = inspectionPartSize * maxInspectionParts + maxInspectionPathBytes = 4096 + len(".scert-backup-") + 160 +) + +// 单帧仍限制为 16 KiB;总量覆盖十二条最长路径经 JSON 六倍转义后的大小。 +// 分片不增加业务请求次数,不重复采样;任何缺片、乱序或超限均不返回部分核查结果。 +type inspectionPart struct { + Index int `json:"index"` + Total int `json:"total"` + Data []byte `json:"data"` +} + +func validInspectionPayload(value *deployment.RecoveryInspection) bool { + if value == nil || !validRecoveryID(value.TransactionID) || len(value.Files) != 9 || len(value.Locks) != 3 || value.ObservedAt.IsZero() { + return false + } + for _, file := range value.Files { + if len(file.Path) > maxInspectionPathBytes { + return false + } + } + for _, lock := range value.Locks { + if len(lock.Path) > maxInspectionPathBytes { + return false + } + } + return true +} + +func writeRecoveryResponse(ctx context.Context, writer io.Writer, response Response) error { + if err := ctx.Err(); err != nil { + return err + } + if response.Inspection == nil { + return writeFrame(writer, response) + } + if !validInspectionPayload(response.Inspection) || response.InspectionPart != nil || response.RecoveryPage != nil { + return ErrProtocol + } + encoded, err := json.Marshal(response) + if err != nil { + return ErrProtocol + } + if len(encoded)+1 <= MaxFrameSize { + return writeFrame(writer, response) + } + data, err := json.Marshal(response.Inspection) + if err != nil || len(data) > maxInspectionBytes { + return ErrProtocol + } + total := (len(data) + inspectionPartSize - 1) / inspectionPartSize + response.Inspection = nil + for index := 0; index < total; index++ { + if err := ctx.Err(); err != nil { + return err + } + start := index * inspectionPartSize + end := min(start+inspectionPartSize, len(data)) + response.InspectionPart = &inspectionPart{Index: index, Total: total, Data: data[start:end]} + if err := writeFrame(writer, response); err != nil { + return err + } + } + return nil +} + +func readInspectionParts(reader *bufio.Reader, first Response) (*deployment.RecoveryInspection, error) { + if first.InspectionPart == nil || first.InspectionPart.Total < 1 || first.InspectionPart.Total > maxInspectionParts { + return nil, ErrProtocol + } + total := first.InspectionPart.Total + data := make([]byte, 0, total*inspectionPartSize) + response := first + for index := 0; index < total; index++ { + if index > 0 { + response = Response{} + if err := readFrame(reader, &response); err != nil { + return nil, ErrProtocol + } + } + part := response.InspectionPart + if !validChallenge(response) || response.Nonce != first.Nonce || len(response.Methods) != 0 || response.RecoveryAvailable || + response.Inspection != nil || response.RecoveryPage != nil || part == nil || part.Index != index || part.Total != total || + len(part.Data) == 0 || len(part.Data) > inspectionPartSize || (index < total-1 && len(part.Data) != inspectionPartSize) { + return nil, ErrProtocol + } + data = append(data, part.Data...) + } + var inspection deployment.RecoveryInspection + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + if decoder.Decode(&inspection) != nil || !validInspectionPayload(&inspection) { + return nil, ErrProtocol + } + var extra any + if !errors.Is(decoder.Decode(&extra), io.EOF) { + return nil, ErrProtocol + } + return &inspection, nil +} diff --git a/backend/internal/localreset/recovery_frames_test.go b/backend/internal/localreset/recovery_frames_test.go new file mode 100644 index 0000000..03c3995 --- /dev/null +++ b/backend/internal/localreset/recovery_frames_test.go @@ -0,0 +1,215 @@ +package localreset + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net" + "reflect" + "strings" + "testing" + "time" + + "git.pcux.cn/xuans/Scert/backend/internal/deployment" +) + +func longInspectionFixture() deployment.RecoveryInspection { + value := deployment.RecoveryInspection{ + TransactionID: strings.Repeat("t", 160), ServerID: "server-1", + Phase: "recovery_required", Status: "recovery_required", ObservedAt: time.Now().UTC(), + } + for _, name := range []string{"certificate", "chain", "key"} { + path := "/" + strings.Repeat(strings.Repeat("<", 199)+"/", 20) + path += strings.Repeat("&", 4096-len(path)-len(name)) + name + for _, suffix := range []string{"", ".scert-tmp-" + value.TransactionID, ".scert-backup-" + value.TransactionID} { + value.Files = append(value.Files, deployment.RecoveryFileObservation{ + Path: path + suffix, Kind: "target", Exists: true, Digest: strings.Repeat("a", 64), Comparison: "unproven", + }) + } + value.Locks = append(value.Locks, deployment.RecoveryLockObservation{Path: path + ".scert-lock", Exists: true, Ownership: "unproven"}) + } + return value +} + +func TestRecoveryInspectionLongPathsRoundTrip(t *testing.T) { + inspection := longInspectionFixture() + encoded, err := json.Marshal(inspection) + if err != nil || len(encoded) <= 250<<10 || len(encoded) > maxInspectionBytes { + t.Fatalf("fixture does not exercise escaped path bound: %d %v", len(encoded), err) + } + session := &recoveryTestSession{inspection: &inspection} + _, conn, _ := recoveryConnection(t, session) + client, err := Begin(t.Context(), conn) + if err != nil { + t.Fatal(err) + } + defer client.Close() + before := client.nonce + result, err := client.InspectRecovery(inspection.TransactionID) + if err != nil || !reflect.DeepEqual(result, inspection) { + t.Fatalf("long observation changed or failed: %v", err) + } + if client.nonce == before || session.inspects.Load() != 1 { + t.Fatal("nonce unchanged or observation repeated") + } + if _, err := client.ListRecoveries(""); err != nil || session.lists.Load() != 1 { + t.Fatalf("next request failed: %v", err) + } +} + +func TestRecoveryInspectionFrameBounds(t *testing.T) { + inspection := longInspectionFixture() + response := Response{Version: Version, OK: true, Code: "CHALLENGE", Nonce: strings.Repeat("a", 64), Inspection: &inspection} + var output bytes.Buffer + if err := writeRecoveryResponse(t.Context(), &output, response); err != nil { + t.Fatal(err) + } + lines := bytes.Split(bytes.TrimSuffix(output.Bytes(), []byte{'\n'}), []byte{'\n'}) + if len(lines) < 2 || len(lines) > maxInspectionParts { + t.Fatalf("unexpected fragment count: %d", len(lines)) + } + for index, line := range lines { + var frame Response + if len(line)+1 > MaxFrameSize || json.Unmarshal(line, &frame) != nil || frame.InspectionPart == nil || + frame.InspectionPart.Index != index || frame.InspectionPart.Total != len(lines) || frame.Nonce != response.Nonce { + t.Fatal("frame exceeds limit or has invalid sequence") + } + } + inspection.Status = strings.Repeat("x", maxInspectionBytes) + output.Reset() + if err := writeRecoveryResponse(t.Context(), &output, response); !errors.Is(err, ErrProtocol) || output.Len() != 0 { + t.Fatal("oversized observation emitted partial frames") + } +} + +func TestRecoveryInspectionRejectsMalformedParts(t *testing.T) { + for _, scenario := range []string{"first-index", "many-parts", "zero-total", "negative-total", "large-part", "short-part", "missing-last", "index", "total", "nonce", "mixed", "methods", "capability", "empty-part", "missing-part", "status", "unknown-field", "trailing-json", "partial-observation", "wrong-id"} { + t.Run(scenario, func(t *testing.T) { + serverConn, conn := net.Pipe() + defer serverConn.Close() + defer conn.Close() + _ = serverConn.SetDeadline(time.Now().Add(2 * time.Second)) + done := make(chan struct{}) + go func() { + defer close(done) + defer serverConn.Close() + nonce := strings.Repeat("a", 64) + if writeFrame(serverConn, Response{Version: Version, OK: true, Code: "CHALLENGE", Nonce: nonce, + Methods: []Method{{ID: "email"}}, RecoveryAvailable: true}) != nil { + return + } + var request Request + if readFrame(bufio.NewReaderSize(serverConn, MaxFrameSize+1), &request) != nil { + return + } + inspection := longInspectionFixture() + if scenario == "partial-observation" { + inspection.Files = inspection.Files[:8] + } else if scenario == "wrong-id" { + inspection.TransactionID = "other" + } + data, _ := json.Marshal(inspection) + if scenario == "unknown-field" { + data = append(data[:len(data)-1], []byte(`,"unexpected":true}`)...) + } else if scenario == "trailing-json" { + data = append(data, []byte(" {}")...) + } + total := (len(data) + inspectionPartSize - 1) / inspectionPartSize + for index := 0; index < total; index++ { + if scenario == "missing-last" && index == total-1 { + return + } + part := &inspectionPart{Index: index, Total: total, Data: data[index*inspectionPartSize : min((index+1)*inspectionPartSize, len(data))]} + response := Response{Version: Version, OK: true, Code: "CHALLENGE", Nonce: strings.Repeat("b", 64), InspectionPart: part} + if index == 0 { + switch scenario { + case "first-index": + part.Index = 1 + case "many-parts": + part.Total = maxInspectionParts + 1 + case "zero-total": + part.Total = 0 + case "negative-total": + part.Total = -1 + case "large-part": + part.Data = make([]byte, inspectionPartSize+1) + case "short-part": + part.Data = part.Data[:100] + } + } + if index == 1 { + switch scenario { + case "index": + part.Index = 0 + case "total": + part.Total++ + case "nonce": + response.Nonce = strings.Repeat("c", 64) + case "mixed": + response.RecoveryPage = &deployment.PendingRecoveryPage{} + case "methods": + response.Methods = []Method{{ID: "email"}} + case "capability": + response.RecoveryAvailable = true + case "empty-part": + part.Data = nil + case "missing-part": + response.InspectionPart = nil + case "status": + response.OK = false + } + } + if writeFrame(serverConn, response) != nil { + return + } + } + }() + client, err := Begin(t.Context(), conn) + if err != nil { + t.Fatal(err) + } + defer client.Close() + result, err := client.InspectRecovery(strings.Repeat("t", 160)) + if err == nil || result.TransactionID != "" || len(result.Files) != 0 || client.stage != -1 { + t.Fatal("invalid fragments returned partial observation or reusable connection") + } + <-done + }) + } +} + +type cancelInspectionWriter struct { + output bytes.Buffer + cancel context.CancelFunc +} + +func (w *cancelInspectionWriter) Write(data []byte) (int, error) { + n, err := w.output.Write(data) + w.cancel() + return n, err +} + +func TestRecoveryInspectionCancellationStopsFragments(t *testing.T) { + inspection := longInspectionFixture() + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + writer := &cancelInspectionWriter{cancel: cancel} + err := writeRecoveryResponse(ctx, writer, Response{Version: Version, OK: true, Code: "CHALLENGE", Nonce: strings.Repeat("b", 64), Inspection: &inspection}) + if !errors.Is(err, context.Canceled) || bytes.Count(writer.output.Bytes(), []byte{'\n'}) != 1 { + t.Fatalf("cancel did not stop after first frame: %v", err) + } + reader := bufio.NewReaderSize(&writer.output, MaxFrameSize+1) + var first Response + if readFrame(reader, &first) != nil { + t.Fatal("first frame unreadable") + } + if result, err := readInspectionParts(reader, first); err == nil || result != nil { + t.Fatal("canceled fragments accepted") + } + if err := writeRecoveryResponse(ctx, io.Discard, Response{Inspection: &inspection}); !errors.Is(err, context.Canceled) { + t.Fatal("pre-canceled operation accepted") + } +} diff --git a/backend/internal/localreset/recovery_server.go b/backend/internal/localreset/recovery_server.go index 1c1baee..1df559a 100644 --- a/backend/internal/localreset/recovery_server.go +++ b/backend/internal/localreset/recovery_server.go @@ -33,7 +33,7 @@ func (s *Server) handleRecovery(ctx context.Context, conn net.Conn, reader *bufi _ = writeFrame(conn, Response{Version: Version, Code: "REJECTED"}) return } - if writeFrame(conn, response) != nil { + if writeRecoveryResponse(ctx, conn, response) != nil { return } request = Request{} diff --git a/backend/internal/localreset/recovery_test.go b/backend/internal/localreset/recovery_test.go index ffc834d..a6d46a9 100644 --- a/backend/internal/localreset/recovery_test.go +++ b/backend/internal/localreset/recovery_test.go @@ -15,10 +15,11 @@ import ( type recoveryTestSession struct { testSession - lists atomic.Int32 - inspects atomic.Int32 - fail bool - oversized bool + lists atomic.Int32 + inspects atomic.Int32 + fail bool + oversized bool + inspection *deployment.RecoveryInspection } func (*recoveryTestSession) RecoveryAvailable() bool { return true } @@ -28,6 +29,9 @@ func (s *recoveryTestSession) ListRecoveries(context.Context, string) (deploymen } func (s *recoveryTestSession) InspectRecovery(_ context.Context, id string) (deployment.RecoveryInspection, error) { s.inspects.Add(1) + if s.inspection != nil { + return *s.inspection, nil + } result := deployment.RecoveryInspection{TransactionID: id, ServerID: "s1", ObservedAt: time.Now(), Files: make([]deployment.RecoveryFileObservation, 9), Locks: make([]deployment.RecoveryLockObservation, 3)} if s.fail { diff --git a/doc/Linux生产流程测试记录.md b/doc/Linux生产流程测试记录.md index 7dbaf05..c52c66e 100644 --- a/doc/Linux生产流程测试记录.md +++ b/doc/Linux生产流程测试记录.md @@ -1,5 +1,17 @@ # Linux 生产流程测试记录 +## 2026年10月3日19:02 本机核查边界回归 + +第一台 `8.137.23.95` 隔离目录 `/var/tmp/scert-local-recovery-20261003-1828/`;不使用第二台运行本轮回归,不清理业务数据库或恢复现场。 + +- [x] `full.log`:数据库修复后的后端全量退出码零,耗时四分四十四秒。外部数据库及实机可选测试缺少环境配置时跳过,不属于生产验收证据。 +- [!] `race.log`:数据库、部署、本机协议、管理员终端四包通过,HTTP 依赖编译因内存限流达到二十分钟上限;总退出码一、单元 Result=timeout、MainPID=0,未当成测试通过。 +- [x] `frames-vet.log`:长路径有界分片版本的本机协议、管理员终端和部署三包竞态通过;SQLite 截止时间两组专项重复二十次通过;后端完整 `go vet -p 1 ./...` 通过;联合命令退出码零,耗时一分四十八秒。 +- [~] `final-full-http-race.log`:最新全量与 HTTP 单包竞态串行运行,结果待记录。隔离任务内存硬上限 900 MiB、CPU 75%,未调整生产服务配置或关闭校验。 +- [x] 第二台磁盘再次复核:根分区 29 GiB、已用 2.9 GiB、可用 26 GiB,使用率 10%,inode 3%;五项原业务服务 active,Nginx 配置检查通过。本轮没有扩大删除范围。 +- [ ] 新代码尚未通过正式安装器部署;三库首次 Web、系统发信、DNS/ACME、公共 HTTPS、证书推送与回滚仍需独立实测。 + + ## 2026年10月3日18:28 最新执行证据 本节优先于下方历史状态;第一台部署测试,第二台仅推送测试。源码隔离测试不替代清库后的正式安装与浏览器业务验收,未清理生产数据库或部署现场。 diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index 9295d26..2ab1e99 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -1,5 +1,16 @@ # Scert 开发任务清单 +## 2026年10月3日19:02 当前开发进度 + +本节优先于历史章节。第一台用于部署,第二台仅用于推送;仅处理 Linux。 + +- [x] SQLite 截止时间边界已修复,修复后全量通过;专项二十次重复通过。 +- [x] R2 长路径核查响应有界分片已实现,单帧限制不变;三包竞态和后端完整静态检查通过,等待正式包复审。 +- [!] 原 HTTP 竞态依赖编译超时,不能记为完成;最新全量和 HTTP 竞态正在串行验证。 +- [~] R3、R4 继续依据《部署事务人工处置设计》推进,独立记录、确认和持久化互斥尚未实现。 +- [ ] R5 正式候选包与真实生产流程未完成;不因只读功能通过而关闭整体门禁。 + + ## 2026年10月3日18:28 当前开发入口 本节优先于下方历史状态。最新确认范围:Linux;第一台部署 Scert,第二台仅作证书推送目标,Smail 使用 `web.cdis.cn`。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 08c33d2..b4af5ca 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1,5 +1,16 @@ # Scert 整改任务执行清单 +## 2026年10月3日19:02 当前执行状态 + +本节优先于历史章节。第一台部署、第二台推送,Linux 范围不变。 + +- [x] 数据库截止时间修复后的原生后端全量通过,相关专项重复二十次通过。 +- [x] R2 有界分片实现及异常传输回归通过;本机协议、管理员终端、部署三包竞态通过;完整 `go vet -p 1 ./...` 通过。 +- [!] 原五包竞态任务因 HTTP 依赖编译限流超时,整体失败,四包成功不得替代整体结论。 +- [~] 最新全量和 HTTP 竞态串行验证中;R3、R4 人工处置闭环继续开发。 +- [ ] 新版正式候选包、三库首次 Web、真实邮件、DNS/ACME、严格 HTTPS 和回滚恢复仍待完成。 + + ## 2026年10月3日18:28 当前执行入口 本节优先于下方历史状态。第一台 `8.137.23.95` 用于部署测试,第二台 `45.136.13.241` 仅用于证书推送测试;不增加双机主服务要求,不处理 Windows。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index c3dc18a..e212f57 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1,5 +1,18 @@ # Scert 整改、合并与优化清单 +## 2026年10月3日19:02 最新进度 + +本节为最新状态,后续章节仅保留历史过程。第一台用于部署测试,第二台仅用于推送测试,Smail 为 `web.cdis.cn`,只处理 Linux。 + +- [x] SQLite 截止时间修复后的原生 Linux 后端全量通过;截止时间确定性用例及执行路径专项重复二十次通过。 +- [x] R2 长路径响应改为有界分片:单帧仍为 16 KiB,数据片 8 KiB,最多四十八片;不截断、不重复采样,不返回部分结果。长路径转义、缺片、乱序、令牌变化、超限及取消回归通过。 +- [x] 最新本机协议、管理员终端、部署三包竞态及后端完整静态检查通过,等待最新全量与正式包复审。 +- [!] 旧五包竞态任务四包通过,HTTP 包依赖编译因资源限流达到二十分钟上限;整体退出码一,不能记为通过。 +- [~] 最新后端全量与 HTTP 单包竞态串行运行;仅调整隔离测试资源限额,未修改业务服务。 +- [ ] R3、R4 人工处置、独立审计与持久化互斥仍未完成;R5 正式候选包及三数据库、邮件、DNS、HTTPS、回滚等生产门禁仍未完成。 +- [~] 总体验收未通过,当前不具备最终上线承诺依据。 + + ## 2026年10月3日18:28 范围确认与最新任务入口 本节取代下方历史章节中的“当前”进度描述;后续结果继续按时间记录。用户已确认:第一台 `8.137.23.95` 用于 Scert 部署测试,第二台 `45.136.13.241` 仅用于证书推送测试;不以两台均部署 Scert 为验收要求。Smail 地址为 `web.cdis.cn`,仅处理 Linux。 diff --git a/doc/部署事务人工核查任务清单.md b/doc/部署事务人工核查任务清单.md index 05955fe..eb32fb2 100644 --- a/doc/部署事务人工核查任务清单.md +++ b/doc/部署事务人工核查任务清单.md @@ -1,5 +1,29 @@ # 部署事务人工核查任务清单 +## 2026年10月3日19:02 边界整改结果 + +本节优先于历史记录。 + +- [x] 长路径响应分片已实现,单帧 16 KiB、数据片 8 KiB、最多四十八片;保持一次采样,完整验证后才返回结果。 +- [x] 长路径六倍 JSON 转义、后续请求、二十类异常分片、总量超限及取消用例通过;最新本机协议、管理员终端和部署三包竞态通过。 +- [x] SQLite 截止时间专项重复二十次及后端完整静态检查通过。 +- [!] 旧五包竞态四包通过,HTTP 依赖编译达到运行上限;整体退出码一。 +- [~] 最新后端全量及 HTTP 竞态继续验证,R2 仍待完整和正式包复审。 +- [ ] R3、R4 独立人工处置及持久化审计未完成,R5 未部署本轮候选包,总体验收未通过。 + + +## 2026年10月3日18:52 本机核查响应边界整改 + +本节为最新执行入口,保留后续历史记录。需求确认:只处理 Linux,第一台用于部署、第二台用于推送;核查仍为只读,不增加公开接口,不改变历史事务。 + +- [x] 修改前检查点 `75b3b71` 已提交并推送,保存数据库截止时间修复及人工处置设计。 +- [x] 修复后原生 Linux 后端全量退出码零;数据库、部署、本机协议及管理员终端四包竞态已通过,HTTP 包仍在编译,不能记为五包通过。 +- [~] R2 边界整改:合法长路径可能使完整核查结果超过 16 KiB;保持单帧上限,采用有序、有总量上限的分片,仅采样一次,不截断路径,不暴露部分结果。 +- [ ] 覆盖长路径及 JSON 转义膨胀、丢片、乱序、随机令牌变化、混合载荷、超限、取消及后续请求;完成原生 Linux 回归与静态检查。 +- [ ] R3、R4 独立人工处置及审计尚未编码;不得将只读核查当作恢复闭环。 +- [ ] R5 正式候选包部署与生产流程复审未完成,总体验收仍未通过。 + + ## 2026年10月3日18:28 最新范围与执行状态 本节取代下方历史“当前”状态。第一台负责 Scert 部署测试,第二台仅负责证书推送测试;R5 不再要求双机部署 Scert 主服务,已有服务不擅自卸载。