Files
Agent-System/internal/docquery/service.go
weichenandCursor c7e3e99303 feat(artifactvault): extract audited FAV with union-deny reads
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>
2026-09-14 15:59:36 +00:00

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,
}
}