Goose 00036 is a renumbered copy of the vault tables. Degraded read stays off by default and only runs after authorization; tombstones and unpublished artifacts fail closed. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
5.2 KiB
Go
208 lines
5.2 KiB
Go
package docquery
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/docrevision"
|
|
)
|
|
|
|
const (
|
|
ConsistencyAligned = "aligned"
|
|
ConsistencyMissing = "missing"
|
|
ConsistencyDrift = "drift"
|
|
ConsistencyEmpty = "empty"
|
|
ConsistencyLedgerMissing = "ledger_missing"
|
|
|
|
MaxBatchGet = 100
|
|
)
|
|
|
|
var (
|
|
ErrClosed = errors.New("docquery: authorization failed")
|
|
ErrBatchLimit = errors.New("docquery: batch exceeds 100")
|
|
ErrCrossProject = errors.New("docquery: mixed project references")
|
|
ErrLedgerMissing = errors.New("docquery: ledger missing")
|
|
ErrFavNotEnabled = errors.New("docquery: FAV degraded read is disabled")
|
|
)
|
|
|
|
// Principal comes only from the authentication adapter. It must not carry
|
|
// caller-supplied role or duty overrides.
|
|
type Principal struct {
|
|
UserID int64
|
|
}
|
|
|
|
// DocRef identifies a document. User/role/duty must not be attached.
|
|
type DocRef struct {
|
|
DocID string
|
|
}
|
|
|
|
type ContentView struct {
|
|
DocID string `json:"doc_id"`
|
|
Version int `json:"version"`
|
|
ContentSHA256 string `json:"content_sha256"`
|
|
Markdown string `json:"markdown"`
|
|
IsTemplateOnly bool `json:"is_template_only"`
|
|
Consistency string `json:"consistency"`
|
|
RecordSHA256 string `json:"record_sha256,omitempty"`
|
|
FileSHA256 string `json:"file_sha256,omitempty"`
|
|
HeadSHA256 string `json:"head_sha256,omitempty"`
|
|
}
|
|
|
|
type Authorizer func(ctx context.Context, p Principal, rec *docgov.DocumentRecord) error
|
|
|
|
type Service struct {
|
|
Gov *docgov.GovernanceEngine
|
|
Rev *docrevision.Service
|
|
RepoRoot string
|
|
Authorize Authorizer
|
|
FavDegradedRead bool
|
|
Vault VaultReader
|
|
}
|
|
|
|
type VaultReader interface {
|
|
ReadVerified(ctx context.Context, projectID int64, logicalID string) (markdown string, sha string, err error)
|
|
}
|
|
|
|
func FavDegradedReadEnabled() bool {
|
|
switch strings.ToLower(strings.TrimSpace(os.Getenv("FAV_DEGRADED_READ"))) {
|
|
case "1", "true", "yes":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (s *Service) Get(ctx context.Context, p Principal, ref DocRef) (*ContentView, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, ErrClosed
|
|
}
|
|
if p.UserID == 0 {
|
|
return nil, ErrClosed
|
|
}
|
|
if s == nil || s.Gov == nil {
|
|
return nil, ErrClosed
|
|
}
|
|
rec, err := s.Gov.GetDocument(ctx, ref.DocID)
|
|
if err != nil || rec == nil {
|
|
return nil, ErrClosed
|
|
}
|
|
if s.Authorize != nil {
|
|
if err := s.Authorize(ctx, p, rec); err != nil {
|
|
return nil, ErrClosed
|
|
}
|
|
}
|
|
|
|
if s.Rev != nil {
|
|
md, sha, herr := s.Rev.HeadContent(ctx, ref.DocID)
|
|
if herr == nil && md != "" {
|
|
return s.viewFromLedger(ctx, rec, md, sha), nil
|
|
}
|
|
}
|
|
|
|
if s.FavDegradedRead && s.Vault != nil {
|
|
md, sha, verr := s.Vault.ReadVerified(ctx, rec.ProjectID, rec.DocID)
|
|
if verr != nil {
|
|
return nil, verr
|
|
}
|
|
return s.viewFromLedger(ctx, rec, md, sha), nil
|
|
}
|
|
if s.FavDegradedRead {
|
|
return nil, ErrFavNotEnabled
|
|
}
|
|
return &ContentView{
|
|
DocID: rec.DocID,
|
|
Version: rec.Version,
|
|
IsTemplateOnly: rec.IsTemplateOnly,
|
|
Consistency: ConsistencyLedgerMissing,
|
|
RecordSHA256: rec.ContentSHA256,
|
|
}, ErrLedgerMissing
|
|
}
|
|
|
|
func (s *Service) BatchGet(ctx context.Context, p Principal, refs []DocRef) ([]*ContentView, error) {
|
|
if len(refs) > MaxBatchGet {
|
|
return nil, ErrBatchLimit
|
|
}
|
|
if p.UserID == 0 {
|
|
return nil, ErrClosed
|
|
}
|
|
var projectID int64
|
|
seen := map[string]bool{}
|
|
out := make([]*ContentView, 0, len(refs))
|
|
for _, ref := range refs {
|
|
if seen[ref.DocID] {
|
|
continue
|
|
}
|
|
seen[ref.DocID] = true
|
|
rec, err := s.Gov.GetDocument(ctx, ref.DocID)
|
|
if err != nil || rec == nil {
|
|
return nil, ErrClosed
|
|
}
|
|
if projectID == 0 {
|
|
projectID = rec.ProjectID
|
|
} else if rec.ProjectID != projectID {
|
|
return nil, ErrCrossProject
|
|
}
|
|
view, err := s.Get(ctx, p, ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, view)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Service) viewFromLedger(ctx context.Context, rec *docgov.DocumentRecord, md, sha string) *ContentView {
|
|
view := &ContentView{
|
|
DocID: rec.DocID,
|
|
Version: rec.Version,
|
|
ContentSHA256: sha,
|
|
Markdown: md,
|
|
IsTemplateOnly: rec.IsTemplateOnly,
|
|
Consistency: ConsistencyAligned,
|
|
RecordSHA256: rec.ContentSHA256,
|
|
HeadSHA256: sha,
|
|
}
|
|
if s.Rev == nil {
|
|
return view
|
|
}
|
|
class, err := s.Rev.InspectProjection(ctx, rec.DocID, s.RepoRoot)
|
|
if err != nil {
|
|
view.Consistency = ConsistencyMissing
|
|
return view
|
|
}
|
|
view.FileSHA256 = class.RawSHA
|
|
switch class.Class {
|
|
case "", docrevision.ProjectionAligned, docrevision.ProjectionNoLedger:
|
|
view.Consistency = ConsistencyAligned
|
|
case docrevision.ProjectionMissing:
|
|
view.Consistency = ConsistencyMissing
|
|
case docrevision.ProjectionEmpty:
|
|
view.Consistency = ConsistencyEmpty
|
|
default:
|
|
view.Consistency = ConsistencyDrift
|
|
}
|
|
return view
|
|
}
|
|
|
|
func (v *ContentView) ToDocumentContent() *docgov.DocumentContent {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return &docgov.DocumentContent{
|
|
DocID: v.DocID,
|
|
Version: v.Version,
|
|
ContentSHA256: v.ContentSHA256,
|
|
Markdown: v.Markdown,
|
|
IsTemplateOnly: v.IsTemplateOnly,
|
|
Consistency: v.Consistency,
|
|
RecordSHA256: v.RecordSHA256,
|
|
FileSHA256: v.FileSHA256,
|
|
}
|
|
}
|