Files
knowledge-governance/internal/objectgateway/protocol.go
姚章浩 5eef13b615 feat(object-gateway): 统一对象写网关与不可变治理日志
方案A:所有对象写入经 loopback 网关,持久计划/grant/intent/逐版本 receipt 与
attempt→grant delivery 落在治理 PG;scope 由 provider+endpoint+bucket 决定,
同一物理桶只允许一个写域。版本 append 不等于 CAS,未锚定计划在首笔业务写前拒绝。

本提交只含网关与其适配层接线、协议文档;生产独占、凭据切换与桶设置未改动。
2026-11-01 11:35:05 +08:00

110 lines
3.8 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")
ErrIsolation = errors.New("gateway_exclusive_writer_evidence_unverified")
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
}
type IsolationVerifier interface{ Verify(context.Context) 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
}