- 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 (baseline44161d2areproduced DATA RACE; now green under -race) - goose 00071 duplicate (dialogue merge1da17a28brought 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)
76 lines
2.4 KiB
Go
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
|
|
}
|
|
}
|