Files
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

76 lines
2.4 KiB
Go

package server
import (
"context"
"log/slog"
"os"
"strings"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/contracts/vocabulary"
)
// Free-lane intents: any project member may invoke without stage/duty gates.
// Controlled-document *write* actions stay on duty + stage + claim paths.
var freeLaneIntentKeys = map[string]struct{}{
"conversation.chat": {},
"conversation.help": {},
"conversation.unsupported": {},
"conversation.decline": {},
"document.status.query": {},
"project.advisory": {},
"project.impact.analyze": {},
"project.fact.supplement": {},
"regulation.query": {},
"case.query": {},
"permission.query": {},
"document.reject_reason.query": {},
"run.status.query": {},
"workflow.status.query": {},
}
// IsFreeLaneIntent reports whether the action key is member-level free collaboration.
func IsFreeLaneIntent(actionKey string) bool {
_, ok := freeLaneIntentKeys[strings.TrimSpace(actionKey)]
return ok
}
// allowFreeLaneChatInitiation always allows initiating chat once project membership
// is established. Stage ABAC still gates reading other stages' history / controlled
// content via allowActiveStageChatAccess / filterWorkflowDocsByStageAccess.
func (s *Server) allowFreeLaneChatInitiation(projectID, userID int64) bool {
if projectID <= 0 || userID <= 0 {
return false
}
return true
}
// maybeAutoClaimOnView records an author view (and claim when non-template) after a
// successful controlled-content read. Failures are logged and never block the read.
func (s *Server) maybeAutoClaimOnView(ctx context.Context, user *auth.UserAccount, docID string) {
if s == nil || s.gov() == nil || user == nil || docID == "" {
return
}
if !autoClaimOnViewEnabled() {
return
}
role := s.resolveDocGovRole(ctx, docID, user)
if role != vocabulary.RoleAuthor {
return
}
if _, err := s.gov().RecordDocumentViewed(ctx, docID, user.ID, role, user.Name); err != nil {
slog.Warn("docgov_auto_claim_on_view_failed", "doc_id", docID, "user_id", user.ID, "error", err)
}
}
// autoClaimOnViewEnabled defaults true. Set DOCUMENT_AUTO_CLAIM_ON_VIEW=0|false|off to disable.
func autoClaimOnViewEnabled() bool {
v := strings.TrimSpace(strings.ToLower(os.Getenv("DOCUMENT_AUTO_CLAIM_ON_VIEW")))
switch v {
case "0", "false", "off", "no":
return false
default:
return true
}
}