diff --git a/cmd/object-api-options-replication_test.go b/cmd/object-api-options-replication_test.go new file mode 100644 index 000000000..6ee25f4ac --- /dev/null +++ b/cmd/object-api-options-replication_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Feng Ruohang +// +// This file is part of Silo Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "encoding/base64" + "net/http" + "reflect" + "strconv" + "strings" + "testing" + "time" + + xhttp "github.com/minio/minio/internal/http" +) + +func TestPutOptsFromHeadersReplicationTimestamps(t *testing.T) { + stamp := time.Date(2026, 9, 15, 1, 2, 3, 123456789, time.UTC) + context := base64.StdEncoding.EncodeToString([]byte(`{"purpose":"tag-replication"}`)) + for _, encryption := range []struct { + name string + headers map[string]string + }{ + {name: "none"}, + {name: "SSE-S3", headers: map[string]string{xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionAES}}, + {name: "SSE-KMS", headers: map[string]string{xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionKMS}}, + {name: "SSE-KMS-context", headers: map[string]string{ + xhttp.AmzServerSideEncryption: xhttp.AmzEncryptionKMS, xhttp.AmzServerSideEncryptionKmsID: "tag-replication-key", + xhttp.AmzServerSideEncryptionKmsContext: context, + }}, + {name: "SSE-C", headers: ssecKeyHeaders([]byte("01234567890123456789012345678901"), false)}, + } { + t.Run(encryption.name, func(t *testing.T) { + for _, trusted := range []bool{false, true} { + t.Run("trusted="+strconv.FormatBool(trusted), func(t *testing.T) { + for _, tagging := range []struct { + name, header string + want time.Time + invalid bool + }{ + {name: "absent"}, + {name: "nanoseconds", header: stamp.Format(time.RFC3339Nano), want: stamp}, + {name: "offset-whitespace", header: " " + stamp.In(time.FixedZone("UTC+8", 8*60*60)).Format(time.RFC3339Nano) + " ", want: stamp}, + {name: "invalid", header: "not-a-timestamp", invalid: true}, + } { + t.Run(tagging.name, func(t *testing.T) { + for _, metadata := range []map[string]string{nil, {"x-amz-meta-test": "kept"}} { + hdr := make(http.Header) + wantEncryption := make(http.Header) + for key, value := range encryption.headers { + hdr.Set(key, value) + wantEncryption.Set(key, value) + } + hdr.Set(xhttp.MinIOSourceTaggingTimestamp, tagging.header) + hdr.Set(xhttp.MinIOSourceMTime, stamp.Add(-time.Hour).Format(time.RFC3339Nano)) + hdr.Set(xhttp.MinIOSourceObjectRetentionTimestamp, stamp.Add(-time.Minute).Format(time.RFC3339Nano)) + hdr.Set(xhttp.MinIOSourceObjectLegalHoldTimestamp, stamp.Add(-time.Second).Format(time.RFC3339Nano)) + hdr.Set(xhttp.MinIOSourceETag, "source-etag") + opts, err := putOptsFromHeaders(t.Context(), hdr, metadata, trusted) + if trusted && tagging.invalid { + if err == nil || !strings.Contains(err.Error(), xhttp.MinIOSourceTaggingTimestamp) { + t.Fatalf("malformed trusted timestamp: got %v", err) + } + continue + } + if err != nil { + t.Fatal(err) + } + wantTag, wantMTime, wantRetention, wantLegalhold, wantETag := time.Time{}, time.Time{}, time.Time{}, time.Time{}, "" + if trusted { + wantTag, wantMTime = tagging.want, stamp.Add(-time.Hour) + wantRetention, wantLegalhold, wantETag = stamp.Add(-time.Minute), stamp.Add(-time.Second), "source-etag" + } + if !opts.ReplicationSourceTaggingTimestamp.Equal(wantTag) { + t.Errorf("tag timestamp=%s, want %s", opts.ReplicationSourceTaggingTimestamp, wantTag) + } + if !opts.MTime.Equal(wantMTime) || !opts.ReplicationSourceRetentionTimestamp.Equal(wantRetention) || + !opts.ReplicationSourceLegalholdTimestamp.Equal(wantLegalhold) || opts.PreserveETag != wantETag || opts.ReplicationRequest != trusted { + t.Error("other source fields did not preserve the replication trust boundary") + } + if opts.UserDefined == nil || (metadata != nil && !reflect.DeepEqual(opts.UserDefined, metadata)) { + t.Errorf("metadata=%v, want nonnil map preserving %v", opts.UserDefined, metadata) + } + gotEncryption := make(http.Header) + if opts.ServerSideEncryption != nil { + opts.ServerSideEncryption.Marshal(gotEncryption) + } + if !reflect.DeepEqual(gotEncryption, wantEncryption) { + t.Errorf("SSE headers=%v, want %v", gotEncryption, wantEncryption) + } + } + }) + } + }) + } + }) + } +} diff --git a/cmd/object-api-options.go b/cmd/object-api-options.go index 4d3400508..930808eae 100644 --- a/cmd/object-api-options.go +++ b/cmd/object-api-options.go @@ -452,11 +452,11 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin MTime: mtime, PreserveETag: etag, ReplicationRequest: trustedReplication, - // The Object Lock timestamps order replicated retention and legal - // hold updates. Dropping them here would leave every update on an - // SSE-KMS destination unordered. + // These timestamps order replicated retention, legal hold and tagging + // updates on an SSE-KMS destination. ReplicationSourceLegalholdTimestamp: lholdtimestmp, ReplicationSourceRetentionTimestamp: retaintimestmp, + ReplicationSourceTaggingTimestamp: taggingtimestmp, } return op, nil } diff --git a/cmd/object-copy-replication-tagging_test.go b/cmd/object-copy-replication-tagging_test.go new file mode 100644 index 000000000..e187663d6 --- /dev/null +++ b/cmd/object-copy-replication-tagging_test.go @@ -0,0 +1,143 @@ +// Copyright (c) 2026 Feng Ruohang +// +// This file is part of Silo Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/crypto" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/kms" +) + +// TestAPICopyObjectReplicaTaggingTimestampUnderKMS covers signed replica COPY +// requests through encryption, metadata replacement and disk persistence. Both +// the single-disk and 16-disk fixtures are single-pool backends. +func TestAPICopyObjectReplicaTaggingTimestampUnderKMS(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPICopyObjectReplicaTaggingTimestampUnderKMS}) +} + +func testAPICopyObjectReplicaTaggingTimestampUnderKMS(obj ObjectLayer, instance, bucket string, router http.Handler, creds auth.Credentials, t *testing.T) { + // Ignore the host free-space percentage while retaining real disk I/O. + for _, pool := range obj.(*erasureServerPools).serverPools { + for _, set := range pool.sets { + original := set.getDisks + disks := append([]StorageAPI(nil), original()...) + for i := range disks { + disks[i] = tagTestCapacityDisk{StorageAPI: disks[i]} + } + set.getDisks = func() []StorageAPI { return disks } + defer func() { set.getDisks = original }() + } + } + oldKMS, oldAuto := GlobalKMS, globalAutoEncryption + GlobalKMS = kms.NewStub("replica-tags-key") + globalAutoEncryption = false + defer func() { GlobalKMS, globalAutoEncryption = oldKMS, oldAuto }() + if _, err := globalBucketMetadataSys.Update(t.Context(), bucket, bucketVersioningConfig, enabledBucketVersioningConfig); err != nil { + t.Fatal(err) + } + const tsKey = ReservedMetadataPrefixLower + TaggingTimestamp + stamp := time.Date(2026, 9, 15, 1, 0, 0, 123456789, time.UTC) + for _, mode := range []string{"none", "explicit-sse-s3", "explicit-kms", "auto-kms", "bucket-kms"} { + t.Run(instance+"/"+mode, func(t *testing.T) { + globalAutoEncryption = mode == "auto-kms" + if mode == "bucket-kms" { + sseXML := []byte(`aws:kmsreplica-tags-key`) + if _, err := globalBucketMetadataSys.Update(t.Context(), bucket, bucketSSEConfig, sseXML); err != nil { + t.Fatal(err) + } + } + const data = "encrypted replica copy remains readable" + oi, err := obj.PutObject(t.Context(), bucket, mode, mustGetPutObjReader(t, bytes.NewReader([]byte(data)), int64(len(data)), "", ""), ObjectOptions{ + Versioned: true, UserDefined: map[string]string{xhttp.AmzObjectTagging: "key=old", tsKey: stamp.Format(time.RFC3339Nano)}, + }) + if err != nil { + t.Fatal(err) + } + for _, event := range []struct { + name, tags, wantTags string + delta, wantDelta time.Duration + missingTimestamp bool + }{ + {"newer", "key=new", "key=new", 2, 2, false}, + {"stale", "key=stale", "key=new", 1, 2, false}, + {"duplicate", "key=new", "key=new", 2, 2, false}, + {"newer-again", "key=latest", "key=latest", 3, 3, false}, + {"missing-timestamp", "key=unordered", "key=latest", 0, 3, true}, + } { + headers := map[string]string{ + xhttp.AmzCopySource: "/" + bucket + "/" + mode + "?versionId=" + oi.VersionID, + xhttp.AmzMetadataDirective: "REPLACE", xhttp.AmzTagDirective: "REPLACE", + xhttp.AmzObjectTagging: event.tags, xhttp.MinIOSourceReplicationRequest: "true", + xhttp.AmzBucketReplicationStatus: "REPLICA", xhttp.MinIOSourceTaggingTimestamp: stamp.Add(event.delta).Format(time.RFC3339Nano), + xhttp.MinIOSourceMTime: oi.ModTime.Format(time.RFC3339Nano), xhttp.MinIOSourceETag: oi.ETag, + } + if event.missingTimestamp { + delete(headers, xhttp.MinIOSourceTaggingTimestamp) + } + if mode == "explicit-sse-s3" { + headers[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES + } + if mode == "explicit-kms" { + headers[xhttp.AmzServerSideEncryption] = "aws:kms" + headers[xhttp.AmzServerSideEncryptionKmsID] = "replica-tags-key" + } + req, err := newTestSignedRequestV4(http.MethodPut, "/"+bucket+"/"+mode+"?versionId="+oi.VersionID, 0, nil, creds.AccessKey, creds.SecretKey, headers) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("%s: COPY %d %s", event.name, w.Code, w.Body.String()) + } + got, err := obj.GetObjectInfo(t.Context(), bucket, mode, ObjectOptions{VersionID: oi.VersionID}) + if err != nil { + t.Fatal(err) + } + t.Logf("%s: tags=%q timestamp=%q kms=%v", event.name, got.UserTags, got.UserDefined[tsKey], crypto.S3KMS.IsEncrypted(got.UserDefined)) + if got.UserTags != event.wantTags || got.UserDefined[tsKey] != stamp.Add(event.wantDelta).Format(time.RFC3339Nano) { + t.Errorf("%s: incorrect persisted tags/timestamp", event.name) + } + wantKMS := mode == "explicit-kms" || mode == "auto-kms" || mode == "bucket-kms" + if crypto.S3KMS.IsEncrypted(got.UserDefined) != wantKMS || crypto.S3.IsEncrypted(got.UserDefined) != (mode == "explicit-sse-s3") { + t.Errorf("%s: unexpected destination encryption", event.name) + } + if got.VersionID != oi.VersionID { + t.Errorf("%s: version=%q, want %q", event.name, got.VersionID, oi.VersionID) + } + req, err = newTestSignedRequestV4(http.MethodGet, "/"+bucket+"/"+mode+"?versionId="+oi.VersionID, 0, nil, creds.AccessKey, creds.SecretKey, nil) + if err != nil { + t.Fatal(err) + } + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + if w.Code != http.StatusOK || w.Body.String() != data { + t.Fatalf("%s: GET %d %q", event.name, w.Code, w.Body.String()) + } + } + }) + } +} diff --git a/docs/investigations/r4/consensus.md b/docs/investigations/r4/consensus.md new file mode 100644 index 000000000..7fe4e4222 --- /dev/null +++ b/docs/investigations/r4/consensus.md @@ -0,0 +1,33 @@ +# R4 plan consensus and review disposition + +## Agreed version + +- Plan: [plan v1](plan-v1.md), SHA-256 `ad539f2071155de6955b583991684ed33c4bfe2e29660005840cdc97d7e1a754`. The frozen file remains unchanged. +- Baseline: `9ebe81c1b3611f9cc73e676b5b741c2be62c467a`. +- Actual reviewer: Claude Code 2.1.270, every assistant model in the review stream is `claude-opus-5`; explicit `--effort max`. +- Opus: **GO_WITH_NONBLOCKING_NOTES**, zero blockers; explicitly agrees that this exact plan can enter local implementation. [Unedited returned review](opus-v1-review.md), [machine-readable provenance](opus-v1.metadata.json). +- Codex: agrees that adding the already-parsed timestamp to the KMS literal fixes R4, and accepts the nonblocking dispositions below. **No blocking disagreement remains on plan v1.** No production source edits were made before this record was saved. +- The agreement permits the planned local implementation and tests; it is not implementation acceptance, a merge decision or production release approval. + +## Item-by-item disposition + +| Opus ID | Disposition | +|---|---| +| R4-01 | Accepted citation correction here, leaving the agreed hash frozen: `ReplicaLockReconcile` is at baseline `object-handlers.go:1847`; encryption merge is at `:1903`. | +| R4-02 | Accepted scope clarification: ErasureSD and Erasure16 are both single-pool local backends. KMS rewrites use PutObject under-lock reconciliation. Multi-pool and multi-site validation are optional and deferred to the wider integration gate. Test comments and the final report will identify this boundary. | +| R4-03 | Accepted wording clarification: source encryption alone does not request destination encryption. Source-only SSE-C copy headers do not prevent destination bucket/default auto-KMS from selecting KMS. The three destination trigger categories stay unchanged. | +| R4-04 | Accepted intent. The regression matrix uses identical expected mtime, ETag, trust and all three source timestamps across all encryption modes, giving field-by-field equivalence without constructing expected values through the production function. The temporary expanded baseline matrix fails only trusted valid KMS tag timestamps. | +| R4-05 | Accepted optional test within the existing scope: a signed KMS COPY with nonempty tags and no source tag timestamp must preserve the stored value/time. This adds evidence, not production behavior. | +| R4-06 | Registered as a separate unverified-impact finding: KMS construction also omits `ProxyHeaderSet`, `ProxyRequest`, `Speedtest` relative to `getDefaultOpts`. No R4 fix or correctness claim for those flags. Send the observation to the parent for separate triage; do not assign it to R5. | +| R4-07 | Accepted. Assertions target final disk state; the REPLACE handler rebuilds metadata, while final stored-tag rejection occurs under the storage write lock. HTTP 200 alone is not acceptance. | +| R4-08 | Resolved provenance uncertainty by Codex: SHA-256 recomputed before/after review, baseline identity and current GitHub main/PR query captured in `baseline-identity.txt`. History was inspected locally with `git blame` / `git show`. Opus's read-only tools did not independently recompute the hash or check GitHub; those facts remain attributed to the local commands. | + +## Raw evidence + +Directory: `/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/`. + +- `review-prompt-v1.md`, `opus-review-v1.jsonl`, `opus-review-v1.stderr.log`, `opus-review-v1.exit`. +- `baseline-identity.txt`, `r4_repro_test.go`, `overlay.json`, `baseline-repro.log`. +- `options_repro_test.go`, `options-overlay.json`, `baseline-options.log`. + +The stream includes an attempted Write to Claude's own plan file. Its tool was disabled; the reviewer returned the full result in text and did not edit production source. The successful result and actual assistant models are checked separately from rate-limit status and auxiliary-model usage. diff --git a/docs/investigations/r4/implementation-review.md b/docs/investigations/r4/implementation-review.md new file mode 100644 index 000000000..67f5e7bae --- /dev/null +++ b/docs/investigations/r4/implementation-review.md @@ -0,0 +1,39 @@ +已完成独立复核:读了三份改动文件在 HEAD 的实际内容、`putOptsFromHeaders` 全函数与两条返回路径、`CopyObjectHandler` 的默认加密与标签排序段、`reconcileStoredObjectTags` 的三处生产调用点,以及全部指定日志与证据文件。 + +# R4 实现复核结论 + +**Verdict: GO_WITH_NONBLOCKING_NOTES(0 阻断项)** + +- 复核 HEAD:`dbcf8dec589deb5d91e17d295cb70997635f5b55` +- 代码/测试 diff SHA-256(按提供值记录):`c8cd6648f8ecea835ec74a038cdeaa82acaa3f36250395f97ead3260dc2fc0a5` + - 本会话无 shell,未重算该哈希;改为逐行比对 diff 与工作树三份文件,内容完全一致(`cmd/object-api-options.go`、`cmd/object-api-options-replication_test.go`、`cmd/object-copy-replication-tagging_test.go`)。 + +## 核验到的事实 + +- 生产改动确为一个字段 + 相邻注释:`cmd/object-api-options.go:459` 的 `ReplicationSourceTaggingTimestamp: taggingtimestmp`,变量来自 `:419-425` 已解析值,与非 KMS 路径 `:473` 对齐。未动解析、信任判定、KMS key/context、返回结构。 +- 影响面封闭:全仓该字段唯一消费点是 `cmd/object-handlers.go:1820`(COPY 标签排序)。PUT/POST/multipart 虽同经 `putOptsFromReq`,但无消费者,故不可能回归——与 R4/R5 切分一致。 +- 三条 KMS 触发路径真实可达:`object-handlers.go:1428-1433` 在 `copyDstOpts`(`:1454`)之前套用目的端默认;`bucket-sse-config.go:139-151` 在 `nil 配置 + AutoEncrypt` 与桶默认 KMS 两种情况下都写入 `aws:kms`,因此 explicit / auto / bucket 三种模式均进入 KMS 分支。 +- 回归证明成立:`baseline-final.log` 用 `-overlay` 换回未修复 constructor,失败面精确为「trusted × 有效标签时间戳 × SSE-KMS / SSE-KMS-context」和 6 个 KMS COPY 子测试(`tags="key=old"`、`kms=true`、HTTP 200),`none`/`SSE-S3`/`SSE-C`/非 trusted 全通过。修复后 `focused.log:194-204` 全 PASS。 +- 测试确实覆盖被要求的维度:信任边界(trusted=false 时 mtime/ETag/三时间戳全归零)、错误路径(trusted + 畸形值必须报错且错误串含头名)、SSE 序列化回环(KMS keyID/context 原样还原)、磁盘终态(每事件 `obj.GetObjectInfo` 读真实盘)、版本一致性、签名 GET 明文可读。全局 `GlobalKMS`/`globalAutoEncryption`/`set.getDisks` 均 defer 还原。 +- `race` exit 0、`vet` 空输出、`golangci-lint` 0 issues,均记录了与 HEAD 一致的三文件哈希。 +- 未发现 `verification.md` / `verification.json` / `consensus.md` 中与日志矛盾的陈述。(评审者版本/模型/effort 这类 provenance 声明不在我可验证范围,未作背书。) + +## 发现清单 + +| ID | 内容 | 阻断 | +|---|---|---| +| IMPL-01 | 单字段修复正确且充分,位置、变量、注释与 `:473` 语义一致 | 否(确认项) | +| IMPL-02 | 基线失败/修复通过的判别力成立,对照组不误报 | 否(确认项) | +| IMPL-03 | KMS 字面量相对 `getDefaultOpts` 仍缺 `ProxyHeaderSet`/`ProxyRequest`/`Speedtest`(`object-api-options.go:40-44` vs `:449-460`)。R4 范围外,已登记为 R4-06 | 否,不设为新合并门槛 | +| IMPL-04 | `metadata-directive: REPLACE` 下 `getCpObjMetadataFromHeader`(`:1143-1156`)返回全新 map,故 `:1818` 的 `lastTaggingTimestamp` 为空、`:1822` 解析失败使 handler 侧比较恒「incoming 胜」;真正的 stale 拒绝发生在写锁内的 `reconcileStoredObjectTags`(`erasure-object.go:1312-1315`)。测试终态断言仍正确,文档 R4-07 已明示此分工 | 否(R5 上下文) | +| IMPL-05 | 测试卫生:`bucket-kms` 模式写入的 `bucketSSEConfig` 未还原,仅因它是最后一个 mode、且 `ExecObjectLayerAPITest` 每后端重建对象层并 `resetTestGlobals()` 才安全;后续若在其后追加 mode 会继承默认 KMS | 否 | +| IMPL-06 | `object-api-options-replication_test.go:35` 局部变量名 `context` 遮蔽标准包名(本文件未导入该包),纯观感 | 否 | +| IMPL-07 | `focused.log` exit 1 的唯一失败是既有 `TestAPICopyObjectReplicaRetentionRemovalUnderBucketKMS`(`replication-trust_test.go:1284`,"Storage reached its minimum free drive threshold"),属本机磁盘余量环境问题,非本次引入;容量 overlay 是测试专用、未提交。新增 COPY 测试自带 `tagTestCapacityDisk` 包装,不受该阈值影响 | 否 | + +**没有发现阻断性正确性问题。** 生产语义、存储格式、API 与既有排序规则均未改变,无任何既有测试断言旧(缺陷)行为。 + +## 合并适配性 + +`dbcf8dec5` 直接位于实时 main `9ebe81c1b` 之上,可快进合并。按仓库 CI 通过为前提,本实现适合合入 main。 + +*(我未运行任何测试,也未查询 GitHub;以上仅基于源码阅读与所提供日志。)* diff --git a/docs/investigations/r4/implementation-review.metadata.json b/docs/investigations/r4/implementation-review.metadata.json new file mode 100644 index 000000000..f81bef75e --- /dev/null +++ b/docs/investigations/r4/implementation-review.metadata.json @@ -0,0 +1,33 @@ +{ + "baseline": "9ebe81c1b3611f9cc73e676b5b741c2be62c467a", + "reviewed_head": "dbcf8dec589deb5d91e17d295cb70997635f5b55", + "requested_model": "claude-opus-5", + "requested_effort": "max", + "cli_version": "2.1.270", + "diff_sha256": "c8cd6648f8ecea835ec74a038cdeaa82acaa3f36250395f97ead3260dc2fc0a5", + "started_at": "2026-09-15T15:59:41.004963+00:00", + "status": "completed", + "command": "/opt/homebrew/bin/claude --print --model claude-opus-5 --effort max --safe-mode --permission-mode plan --tools Read,Grep,Glob --strict-mcp-config --no-session-persistence --add-dir /Users/vonng/tmp/silo-r4-evidence-20260915-a9cb --output-format stream-json --verbose", + "completed_at": "2026-09-15T16:03:52.331640+00:00", + "assistant_models": [ + "claude-opus-5" + ], + "observed_model": "claude-opus-5", + "verdict": "GO_WITH_NONBLOCKING_NOTES", + "blocking_findings": 0, + "session_id": "c09bc4fb-85f1-4af8-a1de-c453398e4a20", + "duration_ms": 161469, + "subtype": "success", + "is_error": false, + "used_tools": { + "Read": 20, + "Glob": 3, + "Grep": 14, + "ExitPlanMode": 1 + }, + "raw_stream": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/merge-review-1/opus.jsonl", + "stream_sha256": "875f617454b27e15ab44d9b777de89643ee352e0ccb948faad570fc546261acc", + "review_sha256": "962ff88d2ffcb75cd692ec17017411d624de80dc120f8dedc675e0fe25009335", + "prompt_sha256": "e1341c721161229431b943ba18d89b740e94470803c099b9ae3d597fd50544a4", + "review_extraction": "The substantive review is an earlier assistant text block; result.result only repeats CLI plan-mode merge limitations. Full raw stream and all assistant text are retained." +} diff --git a/docs/investigations/r4/merge-verification.json b/docs/investigations/r4/merge-verification.json new file mode 100644 index 000000000..51deb4a3e --- /dev/null +++ b/docs/investigations/r4/merge-verification.json @@ -0,0 +1,84 @@ +{ + "original_reviewed_head": "dbcf8dec589deb5d91e17d295cb70997635f5b55", + "dco_signed_equivalent_head": "03027727d1d1b97d8beb83ac55569ea9a83dab23", + "notice_equivalence": { + "cmd/object-api-options-replication_test.go": { + "before_sha256": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "after_sha256": "c21fc8889a079085d9a882499a1cbe868278a3517580651f3bed1102e2a6aef8", + "package_body_sha256": "096f143c0b0a068581f9bb892f35ded0d65b6b60ab711f043236d27fbf51ca33", + "body_unchanged": true + }, + "cmd/object-copy-replication-tagging_test.go": { + "before_sha256": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3", + "after_sha256": "73f066ed7258d430f078ecc90e551ece878bd3d4672bc762094d434ff8fec23d", + "package_body_sha256": "6b5173db2ded2d54055073c3259be208a4d7c8eac0367687082877f1fd3bef15", + "body_unchanged": true + } + }, + "checks": { + "verifiers": { + "command": [ + "make", + "verifiers", + "GOLANGCI=/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/merge-review-1/golangci-serial" + ], + "exit_code": 0, + "started_at": "2026-09-15T16:04:55.536605+00:00", + "finished_at": "2026-09-15T16:07:03.252820+00:00", + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2", + "GOFLAGS": "-p=2" + }, + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "c21fc8889a079085d9a882499a1cbe868278a3517580651f3bed1102e2a6aef8", + "cmd/object-copy-replication-tagging_test.go": "73f066ed7258d430f078ecc90e551ece878bd3d4672bc762094d434ff8fec23d" + }, + "log_sha256": "e42a5bb55f5c1ebfcf02cebebf6d82cf1ec5a2d74590cdf838deba16dd80bfdf" + }, + "build": { + "command": [ + "make", + "build" + ], + "exit_code": 0, + "started_at": "2026-09-15T16:07:03.253715+00:00", + "finished_at": "2026-09-15T16:07:35.594062+00:00", + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2", + "GOFLAGS": "-p=2" + }, + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "c21fc8889a079085d9a882499a1cbe868278a3517580651f3bed1102e2a6aef8", + "cmd/object-copy-replication-tagging_test.go": "73f066ed7258d430f078ecc90e551ece878bd3d4672bc762094d434ff8fec23d" + }, + "log_sha256": "6ba9b545236be964861749c72e7609edf12b8f470df30d1ede8fd62f497e629b" + }, + "binary-version": { + "command": [ + "./silo", + "--version" + ], + "exit_code": 0, + "started_at": "2026-09-15T16:07:35.594918+00:00", + "finished_at": "2026-09-15T16:07:37.616706+00:00", + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2", + "GOFLAGS": "-p=2" + }, + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "c21fc8889a079085d9a882499a1cbe868278a3517580651f3bed1102e2a6aef8", + "cmd/object-copy-replication-tagging_test.go": "73f066ed7258d430f078ecc90e551ece878bd3d4672bc762094d434ff8fec23d" + }, + "log_sha256": "36317d06b691593fe0d74f88d053a24485500c15fc2001e857f2fc6fa5ba752a" + } + }, + "binary_version": "silo version DEVELOPMENT.2026-09-15T16-03-52Z (commit-id=03027727d1d1b97d8beb83ac55569ea9a83dab23)\nRuntime: go1.27.1 darwin/arm64\nLicense: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html\nCopyright: 2015-2025 MinIO, Inc.\nModifications: Copyright 2025-2026 PGSTY\nSource compatibility: based on MinIO technology\n", + "raw_evidence_directory": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/merge-review-1", + "all_function_and_test_bodies_identical_to_opus_reviewed_version": true +} diff --git a/docs/investigations/r4/merge-verification.md b/docs/investigations/r4/merge-verification.md new file mode 100644 index 000000000..2cc353368 --- /dev/null +++ b/docs/investigations/r4/merge-verification.md @@ -0,0 +1,36 @@ +# R4 合并前复核 + +用户已明确追加授权:使用 Opus 5 max 核实最终实现,确认无误后合并 main。本轮授权取代此前只交付本地补丁的范围限制。 + +## 真实实现评审 + +- 独立新调用:Claude Code 2.1.270,`--model claude-opus-5 --effort max`。 +- 复核代码提交:`dbcf8dec589deb5d91e17d295cb70997635f5b55`;当时实时 main 与 fetch 结果均为 `9ebe81c1b3611f9cc73e676b5b741c2be62c467a`。 +- 实际 assistant 模型只有 `claude-opus-5`。结论 **GO_WITH_NONBLOCKING_NOTES,0 阻断项**,明确表示仓库 CI 通过后适合合入 main。 +- [原始实现评审正文](implementation-review.md)、[实际模型与输出哈希](implementation-review.metadata.json) 已保存。 +- 原始流、全部 assistant 正文与最终 result 位于 `/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/merge-review-1/`。实质评审出现在较早的 assistant 消息;最终 result 只重复 Claude 只读会话不能自行合并的工具限制,不是对修复结论的撤回。本任务由 Codex 按用户明确授权完成合并。 + +## 意见处置 + +| 条目 | 处置 | +|---|---| +| IMPL-01 / IMPL-02 | 确认单字段修复和基线失败/修复通过的测试判别力,无需追加修改。 | +| IMPL-03 | Proxy/Speedtest 选项遗漏已交父任务单独核验,维持范围外,不纳入 R4 合并。 | +| IMPL-04 | REPLACE 请求的旧标签拒绝由写锁内对账完成,测试断言真实落盘状态,已有文档准确说明。 | +| IMPL-05 | 当前测试固定以 bucket-kms 为最后一种模式,且每后端重新初始化;现有执行顺序安全。后续增添模式需同步隔离桶默认配置,本次保持已评审测试逻辑。 | +| IMPL-06 | 局部变量 context 命名建议为可选观感项,不改动已评审逻辑。 | +| IMPL-07 | 既有锁测试的磁盘余量限制及仅测试容量 overlay 已如实记录;新测试和 race 不使用生产代码 overlay。 | + +## 提交规范调整 + +按 `CONTRIBUTING.md` 补齐提交作者对应的 DCO sign-off,并将两个新原创测试文件的文件头改为 `Copyright (c) 2026 Feng Ruohang`,保留 AGPL-3.0-or-later。原有生产文件的继承声明保持原样。 + +生产函数和测试的 `package cmd` 之后内容与 Opus 审查版本逐字节相同。`merge-review-1/notice-equivalence.json` 记录了旧/新文件哈希及不变的代码正文哈希。原 `verification.json` 保留当时原始验证记录,不覆盖历史哈希;本轮 PR 的 CI 对最终提交重新验证。 + +## 合并门槛 + +`make verifiers` 已通过:全仓 lint 为 0 issues,生成文件检查通过,rebrand 兼容性清单未变化,交付/运行时标识检查和 entrypoint 参数兼容性测试通过。首次执行曾遇到其他任务持有 golangci-lint 进程锁;使用工具自带 `--allow-serial-runners` 串行等待后完成全部检查。可选 typos 工具未安装,由仓库 Makefile 按既有规则跳过。 + +`make build` 通过,已生成本地 `silo` 并成功执行 `./silo --version`。最终三个源文件哈希与本轮校验记录一致,详情见 [本轮验证清单](merge-verification.json)。 + +接下来由 PR CI 验证最终候选,并在合并前再次核对 main 和精确 PR head。CI 与合并事实以 GitHub PR 状态和本机原始合并证据为准,评审意见不等同于合并或发布。 diff --git a/docs/investigations/r4/opus-v1-review.md b/docs/investigations/r4/opus-v1-review.md new file mode 100644 index 000000000..070def5dd --- /dev/null +++ b/docs/investigations/r4/opus-v1-review.md @@ -0,0 +1,46 @@ +## R4 独立评审(Opus 5.0,只读复核) + +**计划**:plan v1 — `docs/investigations/r4/plan-v1.md` +**哈希(按任务给定)**:`ad539f2071155de6955b583991684ed33c4bfe2e29660005840cdc97d7e1a754` +**Baseline**:`9ebe81c1b3611f9cc73e676b5b741c2be62c467a` + +### 裁定:GO_WITH_NONBLOCKING_NOTES + +无阻断项。**我同意这份 exact plan(上述哈希)可以进入本地实现。** 下列 R4-01…R4-08 均为非阻断;其中 R4-02/04/05 的测试建议是**可选**的,不自动并入必做范围。 + +(说明:本会话 Write 工具被禁用,评审仅以正文返回,未写入任何文件,也未改动任何源码。) + +### 我实际核验到的关键事实(支撑"单字段补丁正确且充分") + +1. **缺陷确认**:`cmd/object-api-options.go:449-460` 的 KMS 字面量带了 MTime/PreserveETag/ReplicationRequest + 两个 Object Lock 时间戳,独缺 tagging;默认路径 `:473` 有。补丁片段中的变量名 `taggingtimestmp` 与 `:419` 完全一致,可直接编译;gofmt 对齐由更长的两个 Lock 键决定,不会扰动他行。 +2. **影响面封闭**:全仓 `ReplicationSourceTaggingTimestamp` 只在 `cmd/object-handlers.go:1820` 被读取(定义于 `object-api-interface.go:99`)。因此该字段对 PUT/分段路径天然无效果——既印证 R4/R5 的切分合理,也说明补丁不可能回归其他路径。 +3. **充分性的关键点(我重点查证的风险)**:`encMetadata` 只有在 SSE-C 轮换分支 `object-handlers.go:1648-1659` 才批量快照全部保留键,而该分支与 KMS options 分支互斥(目的端是 SSE-C 时 `crypto.S3KMS.IsRequested` 为假)。故 `:1903` 的 `maps.Copy(srcInfo.UserDefined, encMetadata)` **不会**覆盖 KMS COPY 新写入的 tags/时间戳 —— 单字段补丁在 R4 边界内充分。 +4. **三个触发点准确**:`bucket-sse-config.go:135-153`(显式请求优先 → nil 配置 + AutoEncrypt → KMS → bucket 默认 KMS 写 header+keyID;默认 AES 走 AES 分支),配合 `object-handlers.go:1428-1433` 仅在非联邦时套用目的端默认。 +5. **REPLACE 副本路径准确**:`reconcileStoredObjectTags`(`erasure-server-pool-consistency.go:232-243`)语义即"存量有效时间戳胜过缺失/更旧/相等的 incoming,并连同 tag 值一起还原"。KMS 目的端因 `isTargetEncrypted` 使 `metadataOnly=false`,实际落到 `erasure-server-pool.go:1499-1513`(`ReplicaLockReconcile` 经 `:1509` 透传)→ `erasure-object.go:1276-1316`,在 `cloneMSS`(:1324) 之前于写锁内完成对账;纯元数据路径走 `erasure-object.go:136-139`。计划同时引用 `:136` 与 `:1509`,判断正确。 +6. **证据可信**:`baseline-repro.log` 中 options 用例非 KMS 保留 `...123456789Z`、KMS 返回零值;COPY 用例 6/6(ErasureSD + Erasure16 × explicit/auto/bucket KMS)失败,且均为 200、`kms=true`、明文 GET 通过、tags 停在 `key=old`。即"请求成功、加密正常,但复制标签被静默丢弃",与计划表述一致,未夸大。 +7. **修复后推演**:newer/stale/duplicate/newer-again 在 handler(:1817-1833) 与写锁对账的双重排序下分别得到 new/new/new/latest,与测试期望吻合;旧发送端不带 `X-Minio-Source-Tagging-Timestamp` 时仍为零值 → 行为不变,兼容性主张成立。 + +### 问题清单 + +| ID | 阻断 | 内容与建议 | +|---|---|---| +| **R4-01** | 否 | 行号漂移:计划写的 `1851/1910`,实际是 `object-handlers.go:1847`(`ReplicaLockReconcile`)与 `:1903`(encMetadata merge)。建议更正引用。 | +| **R4-02** | 否(建议可选) | `ExecObjectLayerAPITest` 两种后端均为**单 pool**(`test-utils_test.go:216` `mustGetPoolEndpoints(0, ...)`),故 `erasure-server-pool.go:1443` 多池分支未被覆盖;且 KMS 目的端命中的是 PutObject 重写对账而非 `CopyObject:136`。建议在计划或测试注释中点明"单盘/16 盘均为单池";补多池覆盖**可选**,不必进必做范围。 | +| **R4-03** | 否 | 措辞:`crypto.Requested`(`internal/crypto/sse.go:74`)只检查**目的端** SSE 头,因此仅带 SSE-C *copy-source* 头的请求在 KMS 默认桶/自动加密下仍会进入 KMS 分支(归入触发点 2/3,枚举仍完整)。建议澄清 "source encryption alone…" 一句。 | +| **R4-04** | 否(**可选**) | 建议在 options 矩阵里加一条 KMS 分支 vs 默认分支的**逐字段等价断言**(MTime/PreserveETag/ReplicationRequest/三个复制时间戳)。这是阻止第三次复发最廉价的护栏(2021 漏、2026 补了两个 Lock 时间戳仍漏此项)。计划第 1 条已基本覆盖,此为结构化建议。 | +| **R4-05** | 否(**可选**) | 建议加一例"KMS 目的端 + 有 tags 但无 tagging 时间戳头 → 存量不变",把兼容性主张钉在 handler 层而不仅在 options 层。 | +| **R4-06** | 否(范围外,仅登记) | 同一 KMS 字面量相对 `getDefaultOpts`(`object-api-options.go:40-44`) 还遗漏 `ProxyHeaderSet/ProxyRequest/Speedtest`;`opts.Speedtest` 在 `erasure-object.go:1625` 被读取,全局自动加密下 speedtest PUT 会丢该标志。**不要在 R4 修**,且当前也不在 R5 声明范围内,建议单列条目登记。 | +| **R4-07** | 否 | REPLACE 时 `getCpObjMetadataFromHeader:1143-1156` 会重建 map,`lastTaggingTimestamp` 为空 → handler 对 stale 事件**恒接受**,真正的拒绝来自写锁内对账。因此回归测试必须断言**最终落盘状态**(现有复现已如此),不要改为断言 handler 层行为。 | +| **R4-08** | 否(不确定性) | 本会话无 shell,无法独立复算计划 SHA-256、验证 `c4373ef290 / b2dca43fda / cfefc049c` 历史归属与 PR #184/#187。可由 `shasum -a 256 docs/investigations/r4/plan-v1.md` 与 `git log -L` 输出消解;均不影响补丁正确性。`cfefc049c` 的 KMS context 编码修复实体(`:436-444` 的 `sdkContext`)仍在,补丁不触碰。 | + +### 对计划各主张的逐项裁定 + +- 单字段补丁**正确且对本 bounded issue 充分**:同意(依据 2/3/7)。 +- 三个目的端 KMS 触发点**描述准确**:同意(R4-03 仅措辞澄清)。 +- 当前 REPLACE 副本路径**描述准确**:同意(R4-01/02 属引用精度)。 +- 回归矩阵与存量状态说明**充分**:同意;存量部分"不自动回填、丢失源时间不可重建、并列/更旧事件不保证修复"的表述与 `reconcileStoredObjectTags` 实际语义一致。 +- 信任边界、错误行为、加密 key/context、tie 语义、兼容性:补丁均未触碰,维持不变。 + +### 交付 + +本轮为**计划共识**,非实现验收。我未作任何源码或文件修改;R4 可按 plan v1 在本地实施,实施后的差异与测试证据需另行验收。 diff --git a/docs/investigations/r4/opus-v1.metadata.json b/docs/investigations/r4/opus-v1.metadata.json new file mode 100644 index 000000000..76ffb154c --- /dev/null +++ b/docs/investigations/r4/opus-v1.metadata.json @@ -0,0 +1,36 @@ +{ + "baseline": "9ebe81c1b3611f9cc73e676b5b741c2be62c467a", + "plan": "docs/investigations/r4/plan-v1.md", + "plan_sha256": "ad539f2071155de6955b583991684ed33c4bfe2e29660005840cdc97d7e1a754", + "requested_model": "claude-opus-5", + "requested_effort": "max", + "cli_version": "2.1.270", + "started_at": "2026-09-15T15:45:46.438630+00:00", + "status": "completed", + "raw_output": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/opus-review-v1.jsonl", + "raw_stderr": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/opus-review-v1.stderr.log", + "command": "/opt/homebrew/bin/claude --print --model claude-opus-5 --effort max --safe-mode --permission-mode plan --tools Read,Grep,Glob --strict-mcp-config --no-session-persistence --add-dir /Users/vonng/tmp/silo-r4-evidence-20260915-a9cb --output-format stream-json --verbose", + "completed_at": "2026-09-15T15:49:48.150062+00:00", + "assistant_models": [ + "claude-opus-5" + ], + "observed_model": "claude-opus-5", + "verdict": "GO_WITH_NONBLOCKING_NOTES", + "blocking_findings": 0, + "result_subtype": "success", + "is_error": false, + "session_id": "63e14a68-8565-41fa-9746-3e405fb63e9f", + "duration_ms": 161784, + "num_turns": 35, + "used_tools": { + "Read": 18, + "Glob": 4, + "Grep": 11, + "Write": 1 + }, + "stream_sha256": "eb0918d8a6185b180dddcfc664a96682f05502ecf3b686b08a0547f09879d57d", + "review_sha256": "e1dc12dd99326ae432623ff8de201813e6e84e7ed16a5556c21f9c514d663676", + "prompt_sha256": "07c225beff1523e056c154b3a387cf1ae345def4b4d0173065b882140ac5abdf", + "tool_scope_note": "Read/Grep/Glob allowed. Claude attempted Write to its own plan; the tool was disabled and no file was written. git diff before consensus showed no production source changes.", + "auxiliary_model_note": "assistant_models records actual reviewing assistant messages. Auxiliary usage is distinct. --effort max is explicit in the command, not inferred from model usage." +} diff --git a/docs/investigations/r4/plan-v1.md b/docs/investigations/r4/plan-v1.md new file mode 100644 index 000000000..0ea961c66 --- /dev/null +++ b/docs/investigations/r4/plan-v1.md @@ -0,0 +1,65 @@ +# R4 plan v1: preserve the replicated tag timestamp for SSE-KMS + +## Baseline and ownership + +- Baseline: `9ebe81c1b3611f9cc73e676b5b741c2be62c467a`, verified against GitHub main on 2026-09-15. +- Branch: `codex/r4-kms-tag-timestamp`; worktree: `/Users/vonng/.codex/worktrees/a9cb/silo`. +- Live open PRs at inspection: #184 and #187, neither owns this options change. +- The worktree lacks the ignored `AGENTS.md`; the parent explicitly confirms `/Users/vonng/pgsty/silo/AGENTS.md` applies. Maintain the PGSTY product graph and inexpensive compatibility. +- R4 owns only the missing field in `cmd/object-api-options.go` and its regression tests. R5 owns DELETE/empty tags, PUT/multipart receiving, sender propagation and full receiver ordering. R4 will supply a standalone source patch to R5; neither task edits the other's worktree. + +## Proven defect and actual trigger + +`putOptsFromHeaders` parses the trusted source tag timestamp before selecting encryption. The SSE-KMS branch constructs and returns another `ObjectOptions` carrying mtime, ETag, replication trust and both Object Lock timestamps, but omits `ReplicationSourceTaggingTimestamp`. The normal path retains it. The parser accepts and preserves RFC3339 fractional seconds even though its layout is `time.RFC3339`; the reproduction uses nanoseconds. + +`CopyObjectHandler` applies local destination encryption configuration before `copyDstOpts` → `putOptsFromReq` → `putOpts` → `putOptsFromHeaders`. The omission is reached by: + +1. Explicit destination SSE-KMS request headers (with or without a key ID/context). +2. A destination bucket with default SSE-KMS, when the request has no explicit SSE choice. +3. Global automatic encryption with no bucket SSE override and no explicit SSE choice. + +Explicit AES256/SSE-C takes its existing branch; source encryption alone does not select the destination KMS branch. Remote federation skips local destination defaults. The relevant trigger is trusted metadata entering the destination KMS branch, not every SSE-KMS object or every tag operation. + +At `CopyObjectHandler`'s tag decision, a zero source timestamp skips the tag update. Current under-lock reconciliation can preserve the stored tag/timestamp when metadata REPLACE reconstructs the map with no timestamp. In the observed same-version replica COPY, the request succeeds, destination encryption is valid, and the old tags/timestamp remain. A missing field in the options layer is not itself proof of a content-read failure. + +PUT and multipart consumers' independent failure to persist a parsed tag timestamp remain R5's responsibility. R4 does not claim to fix all tag replication by correcting this constructor. + +## Source and reproduction evidence + +- `cmd/object-api-options.go`: trusted parsing at 383–426; KMS construction at 433–460; normal assignments at 469–475. +- `cmd/object-handlers.go`: destination default encryption at 1425–1435; `copyDstOpts` at 1454; tag timestamp consumption at 1807–1834; replica reconciliation enabled at 1851; encryption metadata merge at 1910. +- `internal/bucket/encryption/bucket-sse-config.go:135`: explicit request wins, absent config + auto encryption selects KMS, otherwise configured bucket algorithm/key ID applies. +- `cmd/erasure-server-pool-consistency.go:232`: stored valid timestamp wins over absent, older or equal incoming timestamp; writes preserve the stored tag value alongside its timestamp. +- `cmd/erasure-object.go:136` and `cmd/erasure-server-pool.go:1509`: same-version replica COPY reaches existing under-lock tag reconciliation, including object-data rewrites. +- History: the omission exists in `c4373ef290` (2021-09-18); `b2dca43fda` (2026-09-05) added the two Object Lock timestamps but not the tag timestamp. `cfefc049c` fixed KMS context encoding independently and must remain intact. +- Fresh temporary reproduction: `/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/r4_repro_test.go` and `baseline-repro.log` (overlay; no production edits). +- Command: `GOMAXPROCS=2 go test -p 2 -overlay /Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/overlay.json ./cmd -run '^TestReviewR4' -count=1 -timeout 5m -v`. +- Result: expected failure. Unencrypted and AES256 options preserve `2026-09-15T01:00:00.123456789Z`; KMS returns zero. Signed metadata REPLACE COPY on ErasureSD and Erasure (16 disks), across explicit/default/automatic KMS, returns 200 but retains `key=old` and the old timestamp for newer events. Actual encrypted metadata and plaintext GET roundtrips pass. Test deltas are 1–3 nanoseconds. +- These are in-process signed HTTP router and real local disk tests. `kms.NewStub` replaces the remote key service; the normal server encryption/decryption code still runs. Existing `tagTestCapacityDisk` avoids the host's free-space percentage threshold; it delegates all object data/metadata I/O to real test disks. + +## Proposed production change + +Add exactly this field to the existing KMS `ObjectOptions` literal: + +```go +ReplicationSourceTaggingTimestamp: taggingtimestmp, +``` + +Update the neighboring explanatory comment to include tagging alongside retention/legal hold. Do not refactor the common return paths, change parsing/fallback/equal-timestamp semantics, change encryption context encoding, modify trust decisions, add SDK dependencies, or change storage/wire format. Those changes are unnecessary to restore the missing existing contract. + +## Required validation after consensus + +1. Add an options regression matrix covering unencrypted, SSE-S3, SSE-KMS with no context, SSE-KMS with a context, and SSE-C. Validate trusted/untrusted requests, missing/valid/malformed tag timestamps, nanosecond and timezone/whitespace handling, all three replication timestamps, mtime/ETag/trust, nonnil metadata, and unchanged SSE header serialization (including KMS key/context). +2. Promote the temporary COPY reproduction into a named, isolated regression test. Use actual signed same-version metadata COPY with REPLACE metadata and tagging directives, on single-disk and 16-disk backends. For explicit, bucket-default and automatic SSE-KMS, check newer update, older delivery, duplicate replay, and a second newer update. Verify stored tags, exact timestamp, version ID, encryption kind and plaintext GET after each operation. Include an unencrypted/SSE-S3 control if the fixture can do so without expanding implementation scope. +3. Fail the final regression tests against unmodified baseline using an overlay. Then run them on the fixed source, alongside existing replication-trust/options and bucket-KMS Object Lock tests. Check `gofmt`, `git diff --check`, and `go vet ./cmd`. +4. Run the new focused tests under `-race`. Use `GOMAXPROCS=2` and `-p 2` while sibling tasks share the host. A one-field pure option fix does not justify concurrent full-repository suites in all five tasks; full Linux CI and multi-site validation remain separate delivery gates. +5. If a test exposes a separate handler/storage defect, report evidence and coordinate with R5. Do not broaden R4's production patch to make unrelated tests pass. + +## Compatibility, existing state, effort and delivery + +- Public API, header names, stored key names, KMS context/key handling and supported dependencies remain unchanged. Untrusted source headers stay ignored; malformed trusted timestamps continue to fail; absent timestamp remains zero. Existing non-KMS behavior remains unchanged. +- No automatic rewrite/backfill. Lost source tag times cannot be reconstructed from the receiver alone. Upgrading permits subsequent properly timestamped events to be consumed. Review source-of-truth and target state before any targeted resync; full historical convergence also depends on R5. Repeated events subject to existing timestamp/tie semantics are not a universal repair guarantee. +- The source fix can land independently; complete deletion/empty-tag and mixed-encryption convergence needs R5 plus its integration evidence. +- Expected effort: approximately 0.5–1 engineer-day including reproduction, review and local validation; key-service deployment, multi-site failures and existing-state remediation are separate. +- After actual Opus 5.0/max agreement on this exact plan hash, implement locally without another user permission prompt. Preserve raw review, assistant model identity, request effort, baseline and plan hash, issue-by-issue disposition and explicit consensus before source edits. +- Deliver a reviewable local diff, tests and evidence. No main merge, remote publication/release, deployment or existing-state rewrite is authorized by this plan. diff --git a/docs/investigations/r4/research.md b/docs/investigations/r4/research.md new file mode 100644 index 000000000..d6ca3c2ac --- /dev/null +++ b/docs/investigations/r4/research.md @@ -0,0 +1,18 @@ +# R4 research log + +## Verified baseline + +2026-09-15: local clean HEAD and GitHub main both `9ebe81c1b3611f9cc73e676b5b741c2be62c467a`. Branch created as `codex/r4-kms-tag-timestamp`. Live GitHub open PRs #184 (`6addf9eb916b5a4b837480cf534cd1efa5407d3c`) and #187 (`b8f2fdde41dff3dc3b8db669c1d42d30ca5c1d3d`) concern other tasks. Claude Code reports `2.1.270`; Go reports `go1.27.1 darwin/arm64`. + +## Coordination + +- Parent task: `01a0a5ab-ee43-7911-bddd-1aca6f8afcc8`. +- R5: `01a0a5b9-602d-7470-9882-4817cf5fdcd1`, `/Users/vonng/.codex/worktrees/77ad/silo`. +- Parent and R5 acknowledged the ownership boundary: R4 options constructor and nonempty KMS COPY tests; R5 producer/receiver ordering and empty values. R5 will consume R4's minimal patch for combined KMS acceptance. +- Initial conservative expectation separated metadata COPY from REPLACE ordering. Inspection of current `ReplicaLockReconcile` and `reconcileStoredObjectTags` shows that stored timestamps are also reconciled under the write lock for REPLACE. The temporary reproduction therefore uses REPLACE directly; the fix must demonstrate the actual sender-shaped path without changing the handler. + +## Baseline reproduction + +Temporary overlay test source and output are in `/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/`. The options and HTTP/disk reproductions fail for the expected missing timestamp. Every KMS HTTP request completed with 200; newer tags remained old; encrypted object metadata and subsequent ordinary plaintext GET succeeded. The result is narrower than claiming all KMS replication fails, and stronger than merely comparing options. + +The temporary source is not a production implementation. See [plan v1](plan-v1.md) for exact scope and required acceptance. The plan is frozen by SHA-256 before invoking real Opus. diff --git a/docs/investigations/r4/verification.json b/docs/investigations/r4/verification.json new file mode 100644 index 000000000..076c40107 --- /dev/null +++ b/docs/investigations/r4/verification.json @@ -0,0 +1,358 @@ +{ + "baseline": "9ebe81c1b3611f9cc73e676b5b741c2be62c467a", + "branch": "codex/r4-kms-tag-timestamp", + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "cmd/object-copy-replication-tagging_test.go": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3" + }, + "source_files_match_all_test_runs": true, + "checks": [ + { + "name": "baseline-final", + "command": [ + "go", + "test", + "-p", + "2", + "-overlay", + "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/baseline-final-overlay.json", + "./cmd", + "-run", + "^Test(PutOptsFromHeadersReplicationTimestamps|APICopyObjectReplicaTaggingTimestampUnderKMS)$", + "-count=1", + "-timeout=5m", + "-v" + ], + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2" + }, + "started_at": "2026-09-15T15:50:49.430860+00:00", + "finished_at": "2026-09-15T15:51:20.637630+00:00", + "exit_code": 1, + "log": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/baseline-final.log", + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "cmd/object-copy-replication-tagging_test.go": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3" + }, + "log_sha256": "f849c2082235213764e3db7a314d52af75c4859102478a1e8f837afcaa8f1ea8", + "overlay_sha256": "f4cbc16e4ffccaf63191de2e8476162876055796adb4c38cda2d8c569a3bbabc", + "overlay_sources": { + "/Users/vonng/.codex/worktrees/a9cb/silo/cmd/object-api-options.go": { + "path": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/object-api-options.baseline.go", + "sha256": "16a560d0990ae929393f682f22b32ecd2e7f4d9390484b03b54e176fcd00cff5" + } + }, + "assessment": "Expected baseline regression failure; KMS timestamp loss. Non-KMS controls pass." + }, + { + "name": "focused", + "command": [ + "go", + "test", + "-p", + "2", + "./cmd", + "-run", + "^Test(PutOptsFromHeadersReplicationTimestamps|APICopyObjectReplicaTaggingTimestampUnderKMS|ReplicationTrustControlsInternalOptionsAndEvents|GetAndValidateAttributesOpts.*|APICopyObjectReplicaRetentionRemovalUnderBucketKMS)$", + "-count=1", + "-timeout=5m", + "-v" + ], + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2" + }, + "started_at": "2026-09-15T15:51:20.638633+00:00", + "finished_at": "2026-09-15T15:51:48.382212+00:00", + "exit_code": 1, + "log": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/focused.log", + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "cmd/object-copy-replication-tagging_test.go": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3" + }, + "log_sha256": "9a64cbc46e9fd6186b1d3031034b720857851ce70b1466b1da5da6d1a52b76ba", + "assessment": "New tests and options/trust pass; pre-existing KMS lock fixture blocked by host disk free-space percentage." + }, + { + "name": "focused-capacity-adapted", + "command": [ + "go", + "test", + "-p", + "2", + "-overlay", + "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/capacity-overlay.json", + "./cmd", + "-run", + "^Test(PutOptsFromHeadersReplicationTimestamps|APICopyObjectReplicaTaggingTimestampUnderKMS|ReplicationTrustControlsInternalOptionsAndEvents|GetAndValidateAttributesOpts.*|APICopyObjectReplicaRetentionRemovalUnderBucketKMS)$", + "-count=1", + "-timeout=5m", + "-v" + ], + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2" + }, + "started_at": "2026-09-15T15:52:20.113475+00:00", + "finished_at": "2026-09-15T15:52:50.842773+00:00", + "exit_code": 0, + "log": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/focused-capacity-adapted.log", + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "cmd/object-copy-replication-tagging_test.go": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3" + }, + "log_sha256": "13f7aa54564a433bef4dddcf2c5ad1fe46fa03869527fb902a255ce4c3263bc9", + "overlay_sha256": "76a7e6fb364bdaaa3469b1dc79f9ea318059f287a4888f322639920c76dfe53a", + "overlay_sources": { + "/Users/vonng/.codex/worktrees/a9cb/silo/cmd/replication-trust_test.go": { + "path": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/replication-trust-capacity_test.go", + "sha256": "c81b526ae51983881bbf464199e6b90f074fa695300fa8ff005e427e4d3c8208" + } + }, + "assessment": "PASS" + }, + { + "name": "race", + "command": [ + "go", + "test", + "-p", + "2", + "-race", + "./cmd", + "-run", + "^Test(PutOptsFromHeadersReplicationTimestamps|APICopyObjectReplicaTaggingTimestampUnderKMS)$", + "-count=1", + "-timeout=5m", + "-v" + ], + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2" + }, + "started_at": "2026-09-15T15:52:50.843898+00:00", + "finished_at": "2026-09-15T15:53:43.789325+00:00", + "exit_code": 0, + "log": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/race.log", + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "cmd/object-copy-replication-tagging_test.go": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3" + }, + "log_sha256": "ae66a8c9e569a5c4b57ae56e75afc85c06a8b76f1567187519da0726605a4de4", + "assessment": "PASS" + }, + { + "name": "vet", + "command": [ + "go", + "vet", + "-p", + "2", + "./cmd" + ], + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2" + }, + "started_at": "2026-09-15T15:53:43.790197+00:00", + "finished_at": "2026-09-15T15:53:51.129773+00:00", + "exit_code": 0, + "log": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/vet.log", + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "cmd/object-copy-replication-tagging_test.go": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3" + }, + "log_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "assessment": "PASS" + }, + { + "name": "lint", + "command": [ + "/Users/vonng/pgsty/silo/.bin/golangci/v2.13.1/golangci-lint", + "run", + "--build-tags", + "kqueue", + "--timeout=10m", + "--config", + "./.golangci.yml", + "./cmd/..." + ], + "cwd": "/Users/vonng/.codex/worktrees/a9cb/silo", + "env_override": { + "GOMAXPROCS": "2" + }, + "started_at": "2026-09-15T15:53:51.130456+00:00", + "finished_at": "2026-09-15T15:55:39.114303+00:00", + "exit_code": 0, + "log": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/lint.log", + "source_sha256": { + "cmd/object-api-options.go": "25e2e9484fafd94d1b2c857b94373758e481893ad93fb1a063edf7746277accc", + "cmd/object-api-options-replication_test.go": "1ea2a060987e32a4c76fce96ee974df475944c2d6ab482a4893e33daf7bca849", + "cmd/object-copy-replication-tagging_test.go": "5437a77e68736b4ce69de9c777675251fef24b0352dfe30bd8a836fc7ee810e3" + }, + "log_sha256": "e92606b0bf483111dff0a120c315ea165821348f31365020e2468a0059095c47", + "assessment": "PASS" + } + ], + "format_checks": [ + { + "command": [ + "gofmt", + "-l", + "cmd/object-api-options.go", + "cmd/object-api-options-replication_test.go", + "cmd/object-copy-replication-tagging_test.go" + ], + "exit_code": 0, + "output": "" + }, + { + "command": [ + "git", + "diff", + "--check" + ], + "exit_code": 0, + "output": "" + } + ], + "evidence_directory": "/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb", + "evidence_files": { + "baseline-final-overlay.json": { + "size": 177, + "sha256": "f4cbc16e4ffccaf63191de2e8476162876055796adb4c38cda2d8c569a3bbabc" + }, + "baseline-final.json": { + "size": 1584, + "sha256": "5bf7be51e5a0eb41a40dc5fc5d3a1aba5df7733ad5dcb001f8d870a01c4233ba" + }, + "baseline-final.log": { + "size": 22493, + "sha256": "f849c2082235213764e3db7a314d52af75c4859102478a1e8f837afcaa8f1ea8" + }, + "baseline-identity.txt": { + "size": 1676, + "sha256": "9b21841e19a0cbb8ded18c2597488a527a27bedc65109d05d4ff598103073b68" + }, + "baseline-options.log": { + "size": 11395, + "sha256": "d692f0a4bc9c58e2ac0087afa356ddf48f86e1040ea8138d68ffc4d0992bf3cb" + }, + "baseline-repro.log": { + "size": 5693, + "sha256": "aa89b76f4723c6a3ce224faa7796403628d978a8707544bd97848b8887de2113" + }, + "capacity-fixture.diff": { + "size": 870, + "sha256": "8d01e0b0068441f37ecee37125b81424d1f30d7c4fb37d435ea0cfe2e4617e5e" + }, + "capacity-overlay.json": { + "size": 185, + "sha256": "76a7e6fb364bdaaa3469b1dc79f9ea318059f287a4888f322639920c76dfe53a" + }, + "final_copy_repro_test.go": { + "size": 5942, + "sha256": "8949e07d96d2949a79f5a9e83c7a7c0473733d77b407e9c51da477d4ab74f1a8" + }, + "focused-capacity-adapted.json": { + "size": 1661, + "sha256": "1a599b41caabfc5eb44db8d89c8b7e4f4f84f5f036d008369155fd337f65bdf9" + }, + "focused-capacity-adapted.log": { + "size": 20384, + "sha256": "13f7aa54564a433bef4dddcf2c5ad1fe46fa03869527fb902a255ce4c3263bc9" + }, + "focused.json": { + "size": 1253, + "sha256": "d9bb4979ea8aeaabb809cdc6e400a8673530bc83abf3dc2b2a06853a8523d0d9" + }, + "focused.log": { + "size": 20475, + "sha256": "9a64cbc46e9fd6186b1d3031034b720857851ce70b1466b1da5da6d1a52b76ba" + }, + "format-checks.json": { + "size": 352, + "sha256": "7569260900a799d5efdfb39db1f575ab1dadbbb04ace222e036968e66b6b59e7" + }, + "lint.json": { + "size": 991, + "sha256": "a7144713b069f470a94b1ebe6fca6683a4b866a891a2756e15f28c280666ca14" + }, + "lint.log": { + "size": 10, + "sha256": "e92606b0bf483111dff0a120c315ea165821348f31365020e2468a0059095c47" + }, + "object-api-options.baseline.go": { + "size": 16653, + "sha256": "16a560d0990ae929393f682f22b32ecd2e7f4d9390484b03b54e176fcd00cff5" + }, + "options-overlay.json": { + "size": 185, + "sha256": "5506b9c3b998b32f01c45af3cf01605eae9e4fb262c9ff3a6b0040abe719d4d9" + }, + "options_repro_test.go": { + "size": 4192, + "sha256": "1a57a47bdd370042fa0f0d2d90efe447abedee9b9ef48a938d4bed631d83ec0b" + }, + "opus-review-v1.exit": { + "size": 2, + "sha256": "9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa" + }, + "opus-review-v1.jsonl": { + "size": 410466, + "sha256": "eb0918d8a6185b180dddcfc664a96682f05502ecf3b686b08a0547f09879d57d" + }, + "opus-review-v1.stderr.log": { + "size": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "overlay.json": { + "size": 158, + "sha256": "95f7c3f7e206fe36731e6d7e4a48f90c7403f07ae8155c125c6b86d2f1c2d487" + }, + "r4-kms-tag-timestamp.patch": { + "size": 874, + "sha256": "2d4806d986bbd94ba4bc3951f3aeee48401ee1921c28ded0988fa09ca76ca26f" + }, + "r4_repro_test.go": { + "size": 5508, + "sha256": "9bcefb6da2416b577b58085485cad60f677c2265e9dfa84d02e465e1b203766b" + }, + "race.json": { + "size": 1026, + "sha256": "50b73e4acbc2426f3dcfadde78d0f0a86f10702345d2939a30204600bc750a13" + }, + "race.log": { + "size": 18566, + "sha256": "ae66a8c9e569a5c4b57ae56e75afc85c06a8b76f1567187519da0726605a4de4" + }, + "replication-trust-capacity_test.go": { + "size": 61271, + "sha256": "c81b526ae51983881bbf464199e6b90f074fa695300fa8ff005e427e4d3c8208" + }, + "review-prompt-v1.md": { + "size": 2770, + "sha256": "07c225beff1523e056c154b3a387cf1ae345def4b4d0173065b882140ac5abdf" + }, + "run-checks.py": { + "size": 2266, + "sha256": "ddecea5220bc9c286df18c0e9eca101f3d8ab731c307cd4acef937f3ecd11e65" + }, + "vet.json": { + "size": 853, + "sha256": "e0964444bc91640ed6cf78229050bad5b94af4210bdf44b11d1a848f1ee930a6" + }, + "vet.log": { + "size": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + "scope": "Darwin arm64; real signed HTTP + disk I/O; KMS service stub; single-pool single/16-disk fixtures; no remote CI, multi-site, release or deployment." +} diff --git a/docs/investigations/r4/verification.md b/docs/investigations/r4/verification.md new file mode 100644 index 000000000..6242ae644 --- /dev/null +++ b/docs/investigations/r4/verification.md @@ -0,0 +1,52 @@ +# R4 修复与本地验收 + +这是 2026-09-15 的本地验收快照。用户后续授权的实现级复核、提交规范调整与合并流程见 [合并前复核](merge-verification.md);以下原始测试记录及哈希保留当时状态。 + +## 结果 + +在 `putOptsFromHeaders` 的 SSE-KMS 选项构造中补齐 `ReplicationSourceTaggingTimestamp`。目的端使用显式 SSE-KMS、桶默认 KMS 或自动加密时,可信复制 COPY 现在能消费来源标签时间戳,并在现有存储锁内完成排序。 + +生产修改只有一个字段和相邻注释。API、存储格式、KMS key/context、信任判断和既有排序规则保持兼容。R5 的删除/空标签及 PUT/multipart 时间戳传播独立交付。 + +## 方案与 Opus 共识 + +- 基线:`9ebe81c1b3611f9cc73e676b5b741c2be62c467a`,已重新查询 GitHub main。 +- 分支:`codex/r4-kms-tag-timestamp`。 +- [冻结方案 v1](plan-v1.md):SHA-256 `ad539f2071155de6955b583991684ed33c4bfe2e29660005840cdc97d7e1a754`。 +- 真实评审为本机 Claude Code 2.1.270,实际 assistant 模型 `claude-opus-5`,显式 `--effort max`。结论 **GO_WITH_NONBLOCKING_NOTES,0 个阻断项**。 +- [逐条意见处置与双方共识](consensus.md)、[原始返回评审正文](opus-v1-review.md)、[模型与哈希记录](opus-v1.metadata.json) 已保存。先保存共识,再修改生产源码。 + +## 变更与测试 + +| 文件 | 内容 | +|---|---| +| `cmd/object-api-options.go` | 在 KMS 字面量中保留已解析的来源标签时间戳。 | +| `cmd/object-api-options-replication_test.go` | 无加密、SSE-S3、SSE-KMS、带 key/context 的 KMS、SSE-C;可信/非可信;缺失、有效、无效标签时间;纳秒、时区与空格;mtime/ETag/三个时间戳、metadata 与 SSE 序列化。 | +| `cmd/object-copy-replication-tagging_test.go` | 两种单池后端 × 五种目的端加密模式 × 五个有序事件,共 50 次签名 COPY 和 50 次普通 GET。每步检查最终标签、精确时间戳、对象版本、加密类型及明文内容。 | + +COPY 使用 `metadata=REPLACE`、`tagging=REPLACE` 和可信复制身份。事件为较新更新、乱序旧更新、重复事件、再次更新,以及不带来源标签时间戳的请求。更新间隔仅 1–3 纳秒,防止时间精度退化被秒级测试掩盖。无加密与 AES256 是对照;KMS 覆盖显式、桶默认和自动加密入口。 + +## 验证状态 + +| 检查 | 结果 | 证据文件 | +|---|---|---| +| 最终测试 + 未修复基线 constructor overlay | 预期失败;只有可信 KMS 有效标签时间戳及 KMS COPY 更新失败,对照通过 | `baseline-final.log/json` | +| 修复后最终新增测试与既有 trust/options 测试 | 通过;未使用生产源码 overlay | `focused.log` | +| 既有 KMS Object Lock 回归 | 首次受宿主机磁盘余量阈值阻挡;仅适配测试容量报告后,与上述定向测试一起通过 | `focused-capacity-adapted.log/json`、`capacity-fixture.diff` | +| 新增测试 `-race` | 通过 | `race.log/json` | +| `go vet -p 2 ./cmd` | 通过 | `vet.log/json` | +| 仓库配置的 golangci-lint,范围 `./cmd/...`、`kqueue` build tag | 通过,0 issues | `lint.log/json` | +| gofmt、git diff --check | 通过 | `format-checks.json` | + +原始日志和每条命令的运行记录位于 `/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/`。每份检查 JSON 都记录命令、退出码、时间和三个源码/测试文件的 SHA-256;最终交付已逐一确认文件哈希一致。[验证清单](verification.json) 另记录 overlay 的实际替换文件哈希,避免混淆基线与修复版执行代码。 + +测试使用真实签名 HTTP 路由、实际本地对象数据/元数据读写、服务器加解密代码;远程密钥服务由 `kms.NewStub` 代替。ErasureSD 与 16 盘 Erasure 均为单池。容量适配只使用已有 `tagTestCapacityDisk`,避免本机磁盘使用比例触发防写阈值,所有对象 I/O 仍由真实测试磁盘承担;未调整生产容量保护。 + +## 交付与剩余边界 + +- 本地实现和要求的定向验证均已完成,将源码、回归、研究、共识和验收记录作为一个本地提交交付。 +- R4 的独立生产补丁已提供给 R5:`/Users/vonng/tmp/silo-r4-evidence-20260915-a9cb/r4-kms-tag-timestamp.patch`,SHA-256 `2d4806d986bbd94ba4bc3951f3aeee48401ee1921c28ded0988fa09ca76ca26f`。 +- Opus 共识为方案级共识;本地测试结论来自实际运行,不把它记作 Opus 执行了测试。 +- 多池/多站点故障恢复、外部 KMS 服务、完整 Linux CI、主干合并、远端发布和部署尚未执行。 +- 没有改写存量。丢失的来源时间戳不能仅从接收端推导;后续重放/重同步须核对来源权威性及 R5 的全链路处理,不保证旧事件重放可以修复全部历史状态。 +- 另登记 KMS 字面量缺少 Proxy/Speedtest 标志的范围外观察,已交父任务单独核验,本次未扩大修复。