Files
Agent-System/internal/server/vault.go
T
weichen a9c9a9182e refactor(server): remove 24 mirrored Runtime fields via nil-safe accessors; fix Shutdown-vs-background-loop DATA RACE and goose 00071 duplicate version
- Server no longer mirrors app.Runtime services (gov/svc/pwSvc/...): 574 call sites now use accessors in runtime_accessors.go
- backgroundLoop (mutex+sync.Once) stops sweeper/publisher/reminder/wsGC goroutines race-free (baseline 44161d2a reproduced DATA RACE; now green under -race)
- goose 00071 duplicate (dialogue merge 1da17a28 brought two 00071): context-conflict-adjudications renumbered to 00072; dev DB applied only through 00070 so zero data migration
- task_finalize tests build minimal app.Runtime; test injection via serverTestOverrides + setOverride* (explicit-nil markers for fail-closed cases)
2026-09-25 13:17:22 +00:00

186 lines
5.5 KiB
Go

package server
import (
"context"
"fmt"
"net/http"
"sync/atomic"
"time"
"chat-agent-system/internal/artifactvault"
"chat-agent-system/internal/config"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/replyexport"
"chat-agent-system/internal/taskrun"
"github.com/jackc/pgx/v5"
)
func (s *Server) initArtifactVault() {
if s == nil || s.cfg == nil {
return
}
env := s.cfg.Env
if env == "" {
env = "dev"
}
if s.rt != nil {
s.vaultReg = s.rt.ArtifactVaultRegistry()
} else {
s.vaultReg = artifactvault.NewMemoryRegistry()
}
s.vaultIngest = &artifactvault.Ingest{Env: env, Reg: s.vaultReg}
if s.cfg.VaultStateRoot == "" || s.cfg.VaultMasterKey == "" {
return
}
if !s.cfg.FAVPublisherEnabled {
return
}
if err := config.ValidateIndependentVault(s.cfg.VaultStateRoot, s.cfg.DataDir); err != nil {
return
}
ident := s.cfg.VaultStorageIdentity
if ident == "" {
ident = "vault-" + env
}
store, err := artifactvault.NewFilesystemStore(s.cfg.VaultStateRoot, ident, []byte(s.cfg.VaultMasterKey))
if err != nil {
return
}
s.vaultStore = store
s.vaultPub = &artifactvault.Publisher{Reg: s.vaultReg, Store: store}
}
func (s *Server) startVaultPublisher() {
if s == nil || s.vaultPub == nil || s.cfg == nil || !s.cfg.FAVPublisherEnabled {
return
}
stopCh, ok := s.vaultLoop.start()
if !ok {
return
}
go func() {
t := time.NewTicker(2 * time.Second)
defer t.Stop()
for {
select {
case <-stopCh:
return
case <-t.C:
_ = s.vaultPub.Drain(context.Background())
}
}
}()
atomic.StoreInt32(&s.vaultPublisherStarted, 1)
}
func (s *Server) vaultReady() (identityOK, connected, lagOK bool) {
if s == nil || s.vaultStore == nil {
return false, false, true
}
idOK, conn, _ := s.vaultStore.Ready()
lagOK = true
if s.vaultReg != nil {
pending, err := s.vaultReg.LoadRequested(context.Background())
lagOK = err == nil && len(pending) < 100
}
return idOK, conn, lagOK
}
func (s *Server) ingestAssistantReply(ctx context.Context, record *taskrun.RunRecord, content, terminalStatus string) {
if s == nil || s.vaultIngest == nil || record == nil || terminalStatus != "completed" {
return
}
_, cap := replyexport.EvaluatePolicy(replyexport.PolicyOptions{
Role: "assistant", Status: "completed", Content: content,
})
now := time.Now().UTC()
logical := fmt.Sprintf("msg:%d", record.AssistantMsgID)
eventID := record.SessionID + ":" + record.ClientTurnID
if eventID == ":" {
eventID = record.RunID
}
_ = s.vaultIngest.AssistantReply(ctx, record.ProjectID, logical, eventID, record.AssistantMsgID, record.RunID, string(cap.Policy), &now, []byte(content))
}
func (s *Server) wireFinalArtifactHooks() {
if s == nil || s.vaultIngest == nil {
return
}
if gov := s.gov(); gov != nil {
gov.OnFinalArtifact = func(ctx context.Context, doc *docgov.DocumentRecord, markdown []byte, snapshotID int64) {
if doc == nil {
return
}
_ = s.vaultIngest.Approved(ctx, doc.ProjectID, doc.DocID, snapshotID, doc.ApprovedAt, markdown)
}
gov.OnFinalArtifactTX = func(ctx context.Context, tx pgx.Tx, doc *docgov.DocumentRecord, markdown []byte, snapshotID int64) error {
if doc == nil {
return nil
}
return s.vaultIngest.ApprovedTX(ctx, tx, doc.ProjectID, doc.DocID, snapshotID, doc.ApprovedAt, markdown)
}
}
if rev := s.rev(); rev != nil {
rev.OnFinalArtifact = func(ctx context.Context, kind, docID string, projectID, snapshotID int64, eventID string, payload []byte, sealedAt *time.Time) {
switch kind {
case "agent_commit":
_ = s.vaultIngest.AgentCommit(ctx, projectID, docID, eventID, snapshotID, payload)
case "version_sealed":
_ = s.vaultIngest.SealedVersion(ctx, projectID, docID, eventID, snapshotID, sealedAt, payload)
}
}
rev.OnFinalArtifactTX = func(ctx context.Context, tx pgx.Tx, kind, docID string, projectID, snapshotID int64, eventID string, payload []byte, sealedAt *time.Time) error {
switch kind {
case "agent_commit":
return s.vaultIngest.AgentCommitTX(ctx, tx, projectID, docID, eventID, snapshotID, payload)
case "version_sealed":
return s.vaultIngest.SealedVersionTX(ctx, tx, projectID, docID, eventID, snapshotID, sealedAt, payload)
default:
return nil
}
}
}
if fd := s.freeDoc(); fd != nil {
fd.OnFinalArtifact = func(ctx context.Context, projectID int64, docID, eventID string, snapshotID int64, payload []byte) {
_ = s.vaultIngest.FreeformPublish(ctx, projectID, docID, eventID, snapshotID, payload)
}
}
}
func (s *Server) tryVaultDegraded(ctx context.Context, env, logical, projectKey string) ([]byte, string, error) {
if s == nil || s.vaultPub == nil || s.cfg == nil || !s.cfg.FAVDegradedRead {
return nil, "", fmt.Errorf("artifactvault: degraded read disabled")
}
return s.vaultPub.DegradedPayload(ctx, env, logical, projectKey)
}
func (s *Server) handleVaultStatus(w http.ResponseWriter, r *http.Request) {
_ = r
cfg := s.cfg
degraded := false
publisher := false
storeReady := false
if cfg != nil {
degraded = cfg.FAVDegradedRead
publisher = cfg.FAVPublisherEnabled
}
if s.vaultStore != nil {
storeReady = true
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"status": "ok",
"consumer": "fav",
"consumer_mode": "off",
"publisher_enabled": publisher,
"publisher_running": atomic.LoadInt32(&s.vaultPublisherStarted) == 1,
"degraded_read": degraded,
"store_configured": storeReady,
"subscriber_default": false,
"shadow_observe": true,
"would_stage": false,
"would_publish": false,
"live_not_opened": true,
})
}