Files
knowledge-governance/internal/objectgateway/protocol.go
T
姚章浩 a55319d972
build / build (push) Failing after 2m29s
fix(objectgateway): 取消整桶独占前置并按维护停写更新
现象:共享桶部署下网关启动即报 gateway_auditor_key_required,残留过期
审计文件则每次 Execute 报 gateway_exclusive_writer_evidence_unverified;
用户已改为“停目标写入容器→更新→启动”的维护式更新。

根因:Gateway.Ready 强制 Isolation.Verify,main 强制 Ed25519 公钥,等于
把整桶独占证明当作发布前置;这与共享桶、沿用原 AK 的实际部署矛盾。

取舍:删除审计校验与相关 env 依赖,保留 journal、治理 PG 只读 authority、
plan/owner/attempt/epoch 绑定、intent-before-send、单次写与精确版本回滚。
create 仅在精确版本读回且仍为 current 时提交;发现外部 latest 只返回冲突,
不在 confirm 阶段删除(journal create intent 已提交,删除会变成无 delete
intent/receipt 的未记账副作用并让 intent 永久 held)。同 key 不自动合并。

证据:真实二进制无审计/残留审计两环境通过;kg-pgtest-writeset 一次性库 +
随机 MinIO 桶跑 14 个网关用例(含共享桶共存写者、同 key 既有对象拒绝、
精确回滚)全过;umask 022 下 make verify 本地必需项全 success。
2026-11-04 09:45:47 +08:00

108 lines
3.7 KiB
Go

// Package objectgateway implements non-preemptive object write ownership.
package objectgateway
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"strings"
)
const Protocol = "kg-object-gateway-v1"
var (
ErrHeld = errors.New("gateway_held_unresolved_intent")
ErrEpoch = errors.New("gateway_epoch_or_plan_mismatch")
ErrConflict = errors.New("gateway_identity_conflict")
ErrPersistence = errors.New("gateway_persistence_unavailable")
ErrAuthority = errors.New("gateway_control_authority_unavailable")
ErrAuthorityPrivileges = errors.New("gateway_control_reader_privileges_invalid")
ErrRejected = errors.New("gateway_create_rejected_existing_object")
)
type Grant struct {
Epoch int64 `json:"epoch"`
Owner string `json:"owner"`
Plan string `json:"plan"`
}
type Version struct {
ID string `json:"version_id"`
Owner string `json:"owner"`
SHA256 string `json:"sha256"`
Bytes int64 `json:"bytes"`
ETag string `json:"etag"`
}
type Request struct {
Grant Grant `json:"grant"`
Operation string `json:"operation"`
Key string `json:"key"`
Action string `json:"action"`
SHA256 string `json:"sha256"`
Bytes int64 `json:"bytes"`
CreatedBy string `json:"created_by,omitempty"`
Version Version `json:"version,omitempty"`
}
type Receipt struct {
Request Request `json:"request"`
State string `json:"state"`
Version Version `json:"version"`
}
// Backend never retries mutations. List is complete and scoped to exactly key.
// Not finding a PUT is never evidence that its request cannot still arrive.
type Backend interface {
Ready(context.Context) error
Observe(context.Context, string, string) (Version, bool, error)
List(context.Context, string) ([]Version, error)
Put(context.Context, string, []byte, string) (Version, error)
Delete(context.Context, string, Version) error
}
func Digest(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) }
func validHash(s string) bool {
b, e := hex.DecodeString(s)
return e == nil && len(b) == 32 && strings.ToLower(s) == s
}
func (g Grant) valid() bool { return g.Epoch > 0 && validHash(g.Owner) && validHash(g.Plan) }
func (r Request) validate() error {
if !r.Grant.valid() || !validHash(r.Operation) || r.Key == "" || len(r.Key) > 2048 || strings.ContainsAny(r.Key, "\x00\r\n") || strings.HasPrefix(r.Key, "/") || !validHash(r.SHA256) || r.Bytes < 0 {
return ErrConflict
}
if r.Action != "create" && r.Action != "delete" {
return ErrConflict
}
if r.Action == "create" && (r.CreatedBy != "" || r.Version != (Version{})) {
return ErrConflict
}
if r.Action == "delete" && (!validHash(r.CreatedBy) || !validVersion(r.Version) || r.Version.SHA256 != r.SHA256 || r.Version.Bytes != r.Bytes) {
return ErrConflict
}
return nil
}
func validVersion(v Version) bool {
return v.ID != "" && v.ID != "null" && validHash(v.Owner) && validHash(v.SHA256) && v.Bytes >= 0 && v.ETag != ""
}
// Claim binds backend ownership to the complete immutable gateway request.
func (r Request) Claim() string { return r.token() }
func (r Request) token() string { raw, _ := json.Marshal(r); return Digest(raw) }
func (r Receipt) valid() bool {
if r.Request.validate() != nil {
return false
}
switch r.State {
case "committed":
return r.Request.Action == "create" && r.Request.matches(r.Version)
case "rejected":
return r.Request.Action == "create" && r.Version == (Version{})
case "removed":
return r.Request.Action == "delete" && r.Version == r.Request.Version
}
return false
}
func (r Request) matches(v Version) bool {
return validVersion(v) && v.Owner == r.token() && v.Bytes == r.Bytes && v.SHA256 == r.SHA256
}