- interfaces.go: 1449 → 437 lines (13 kernel interfaces from 33) - models.go: 1137 → 397 lines (17 kernel types from 64) - Deleted 28 store impl files per dialect (PG + SQLite) - Deleted 3 model files (git, memory, workspace) - Deleted 3 store iface files (project, memory, tree_types) - Rewrote stores.go constructors for both PG and SQLite - Stores struct: 20 fields (from 40+, includes separate iface stores) 66 files changed, ~19K lines removed. Build will break — expected, fixed in steps 4-5.
398 lines
14 KiB
Go
398 lines
14 KiB
Go
package models
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ── Base ────────────────────────────────────
|
|
|
|
type BaseModel struct {
|
|
ID string `json:"id" db:"id"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// ── Scope Constants ─────────────────────────
|
|
|
|
const (
|
|
ScopeGlobal = "global"
|
|
ScopeTeam = "team"
|
|
ScopePersonal = "personal"
|
|
)
|
|
|
|
type User struct {
|
|
BaseModel
|
|
Username string `json:"username" db:"username"`
|
|
Email string `json:"email" db:"email"`
|
|
PasswordHash string `json:"-" db:"password_hash"`
|
|
DisplayName string `json:"display_name,omitempty" db:"display_name"`
|
|
AvatarURL string `json:"avatar_url,omitempty" db:"avatar_url"`
|
|
Role string `json:"role" db:"role"`
|
|
IsActive bool `json:"is_active" db:"is_active"`
|
|
Settings JSONMap `json:"settings,omitempty" db:"settings"`
|
|
LastLoginAt *time.Time `json:"last_login_at,omitempty" db:"last_login_at"`
|
|
AuthSource string `json:"auth_source" db:"auth_source"`
|
|
ExternalID *string `json:"external_id,omitempty" db:"external_id"`
|
|
Handle string `json:"handle" db:"handle"`
|
|
}
|
|
|
|
// =========================================
|
|
// TEAMS
|
|
// =========================================
|
|
|
|
type Team struct {
|
|
BaseModel
|
|
Name string `json:"name" db:"name"`
|
|
Description string `json:"description,omitempty" db:"description"`
|
|
CreatedBy string `json:"created_by" db:"created_by"`
|
|
IsActive bool `json:"is_active" db:"is_active"`
|
|
Settings JSONMap `json:"settings,omitempty" db:"settings"`
|
|
MemberCount int `json:"member_count,omitempty"` // computed
|
|
MyRole string `json:"my_role,omitempty"` // computed, user context
|
|
}
|
|
|
|
type TeamMember struct {
|
|
ID string `json:"id" db:"id"`
|
|
TeamID string `json:"team_id" db:"team_id"`
|
|
UserID string `json:"user_id" db:"user_id"`
|
|
Role string `json:"role" db:"role"`
|
|
JoinedAt string `json:"joined_at" db:"joined_at"`
|
|
// Joined fields from users table
|
|
Email string `json:"email,omitempty"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
UserRole string `json:"user_role,omitempty"`
|
|
}
|
|
|
|
// PROVIDER CONFIGS (replaces APIConfig)
|
|
|
|
// HasKey returns true if an encrypted API key is stored.
|
|
func (p *ProviderConfig) HasKey() bool {
|
|
return len(p.APIKeyEnc) > 0
|
|
}
|
|
|
|
// MODEL CATALOG (replaces model_configs)
|
|
|
|
func (c ModelCapabilities) HasProviderData() bool {
|
|
return c.ToolCalling || c.Vision || c.Thinking || c.Reasoning ||
|
|
c.CodeOptimized || c.WebSearch || c.MaxContext > 0 || c.MaxOutputTokens > 0
|
|
}
|
|
|
|
// PERSONAS (replaces ModelPreset)
|
|
|
|
// =========================================
|
|
// GRANTS
|
|
// =========================================
|
|
|
|
type Grant struct {
|
|
ID string `json:"id" db:"id"`
|
|
PersonaID string `json:"persona_id" db:"persona_id"`
|
|
GrantType string `json:"grant_type" db:"grant_type"`
|
|
GrantRef string `json:"grant_ref" db:"grant_ref"`
|
|
Config JSONMap `json:"config,omitempty" db:"config"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
}
|
|
|
|
// PLATFORM POLICIES
|
|
|
|
var PolicyDefaults = map[string]string{
|
|
"allow_user_byok": "false",
|
|
"allow_user_personas": "false",
|
|
"allow_raw_model_access": "false",
|
|
"allow_registration": "true",
|
|
"default_user_active": "false",
|
|
"allow_team_providers": "true",
|
|
"default_model": "",
|
|
}
|
|
|
|
// USER MODEL SETTINGS
|
|
|
|
// HiddenEntry identifies a model+provider pair for bulk visibility operations.
|
|
// CompositeModelKey builds the composite key used for per-provider model preferences.
|
|
func CompositeModelKey(providerConfigID, modelID string) string {
|
|
return providerConfigID + ":" + modelID
|
|
}
|
|
|
|
// CHANNELS
|
|
|
|
// SESSION PARTICIPANTS (v0.24.3)
|
|
|
|
// SessionParticipant is an ephemeral identity for anonymous workflow
|
|
// channel visitors. Scoped to a single channel, no users row required.
|
|
// MESSAGES
|
|
|
|
// CHANNEL PARTICIPANTS, MODELS, CURSORS
|
|
|
|
// PERSONA GROUPS (v0.23.0)
|
|
|
|
// HandleFromName generates a URL-safe @mention handle from a display name.
|
|
// "Veronica Sharpe" → "veronica-sharpe"
|
|
func HandleFromName(name string) string {
|
|
h := strings.ToLower(strings.TrimSpace(name))
|
|
h = strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
|
return r
|
|
}
|
|
if r == ' ' || r == '_' {
|
|
return '-'
|
|
}
|
|
return -1
|
|
}, h)
|
|
// Collapse multiple hyphens
|
|
for strings.Contains(h, "--") {
|
|
h = strings.ReplaceAll(h, "--", "-")
|
|
}
|
|
h = strings.Trim(h, "-")
|
|
if len(h) > 50 {
|
|
h = h[:50]
|
|
}
|
|
return h
|
|
}
|
|
|
|
// ORGANIZATION
|
|
|
|
// ProjectPatch holds optional fields for updating a project.
|
|
// ProjectChannel represents a channel's membership in a project.
|
|
// ProjectKB represents a KB's association with a project.
|
|
// ProjectNote represents a note's association with a project.
|
|
// NOTES
|
|
|
|
// NoteLink represents a directed link extracted from [[wikilink]] syntax.
|
|
// ExportNoteLink is a note_link with source_note_id included (for export).
|
|
// NoteLinkResult represents a backlink — a note that links to a given note.
|
|
// NoteGraphNode is a lightweight note representation for graph display.
|
|
// NoteGraphEdge is a resolved link between two notes.
|
|
// NoteGraphDangling is an unresolved [[link]] reference.
|
|
// NoteGraph is the full graph topology for a user's notes.
|
|
// ATTACHMENTS
|
|
|
|
// FILES
|
|
|
|
// FileOrigin constants
|
|
// =========================================
|
|
// AUDIT LOG
|
|
// =========================================
|
|
|
|
type AuditEntry struct {
|
|
ID string `json:"id" db:"id"`
|
|
ActorID *string `json:"actor_id,omitempty" db:"actor_id"`
|
|
ActorName *string `json:"actor_name,omitempty" db:"-"` // resolved via JOIN, not a column
|
|
Action string `json:"action" db:"action"`
|
|
ResourceType string `json:"resource_type" db:"resource_type"`
|
|
ResourceID string `json:"resource_id,omitempty" db:"resource_id"`
|
|
Metadata JSONMap `json:"metadata,omitempty" db:"metadata"`
|
|
IPAddress string `json:"ip_address,omitempty" db:"ip_address"`
|
|
UserAgent string `json:"user_agent,omitempty" db:"user_agent"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
}
|
|
|
|
// USAGE TRACKING
|
|
|
|
// MODEL PRICING
|
|
|
|
// VIEW MODELS (computed, not stored)
|
|
|
|
// UserModel is the view model returned by the capability resolver.
|
|
// Combines catalog entries + Personas for the frontend.
|
|
// =========================================
|
|
// JSON HELPERS
|
|
// =========================================
|
|
|
|
// JSONMap scans from/to JSONB columns.
|
|
type JSONMap map[string]interface{}
|
|
|
|
func (m *JSONMap) Scan(src interface{}) error {
|
|
if src == nil {
|
|
*m = nil
|
|
return nil
|
|
}
|
|
var source []byte
|
|
switch v := src.(type) {
|
|
case []byte:
|
|
source = v
|
|
case string:
|
|
source = []byte(v)
|
|
default:
|
|
return nil
|
|
}
|
|
result := make(JSONMap)
|
|
if err := json.Unmarshal(source, &result); err != nil {
|
|
return err
|
|
}
|
|
*m = result
|
|
return nil
|
|
}
|
|
|
|
// Extension tier constants
|
|
type Extension struct {
|
|
ID string `json:"id" db:"id"`
|
|
ExtID string `json:"ext_id" db:"ext_id"` // manifest id
|
|
Name string `json:"name" db:"name"`
|
|
Version string `json:"version" db:"version"`
|
|
Tier string `json:"tier" db:"tier"`
|
|
Description string `json:"description" db:"description"`
|
|
Author string `json:"author" db:"author"`
|
|
Manifest json.RawMessage `json:"manifest" db:"manifest"`
|
|
IsSystem bool `json:"is_system" db:"is_system"`
|
|
IsEnabled bool `json:"is_enabled" db:"is_enabled"`
|
|
Scope string `json:"scope" db:"scope"`
|
|
TeamID *string `json:"team_id,omitempty" db:"team_id"`
|
|
InstalledBy *string `json:"installed_by,omitempty" db:"installed_by"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// ExtensionUserSettings stores per-user overrides for an extension.
|
|
type ExtensionUserSettings struct {
|
|
ExtensionID string `json:"extension_id" db:"extension_id"`
|
|
UserID string `json:"user_id" db:"user_id"`
|
|
Settings json.RawMessage `json:"settings" db:"settings"`
|
|
IsEnabled bool `json:"is_enabled" db:"is_enabled"`
|
|
}
|
|
|
|
// UserExtension combines extension info with user-specific settings for API responses.
|
|
type UserExtension struct {
|
|
Extension
|
|
UserEnabled *bool `json:"user_enabled,omitempty"`
|
|
UserSettings *json.RawMessage `json:"user_settings,omitempty"`
|
|
}
|
|
|
|
func NullString(s *string) sql.NullString {
|
|
if s == nil {
|
|
return sql.NullString{}
|
|
}
|
|
return sql.NullString{String: *s, Valid: true}
|
|
}
|
|
|
|
func NullFloat(f *float64) sql.NullFloat64 {
|
|
if f == nil {
|
|
return sql.NullFloat64{}
|
|
}
|
|
return sql.NullFloat64{Float64: *f, Valid: true}
|
|
}
|
|
|
|
func NullInt(i *int) sql.NullInt64 {
|
|
if i == nil {
|
|
return sql.NullInt64{}
|
|
}
|
|
return sql.NullInt64{Int64: int64(*i), Valid: true}
|
|
}
|
|
|
|
func StringPtr(s string) *string { return &s }
|
|
func Float64Ptr(f float64) *float64 { return &f }
|
|
func IntPtr(i int) *int { return &i }
|
|
func BoolPtr(b bool) *bool { return &b }
|
|
|
|
// =========================================
|
|
// GROUPS (v0.16.0)
|
|
// =========================================
|
|
|
|
// Group scope constants (reuses ScopeGlobal, ScopeTeam from above)
|
|
|
|
// ResourceType constants for resource_grants
|
|
const (
|
|
ResourceTypePersona = "persona"
|
|
ResourceTypeKnowledgeBase = "knowledge_base"
|
|
)
|
|
|
|
// GrantScope constants for resource_grants
|
|
const (
|
|
GrantScopeTeamOnly = "team_only"
|
|
GrantScopeGlobal = "global"
|
|
GrantScopeGroups = "groups"
|
|
)
|
|
|
|
// Group is an access-control list. Decouples resource visibility from teams.
|
|
type Group struct {
|
|
BaseModel
|
|
Name string `json:"name" db:"name"`
|
|
Description string `json:"description" db:"description"`
|
|
Scope string `json:"scope" db:"scope"` // global, team
|
|
TeamID *string `json:"team_id,omitempty" db:"team_id"`
|
|
CreatedBy *string `json:"created_by,omitempty" db:"created_by"`
|
|
Source string `json:"source" db:"source"` // manual (default), oidc, system
|
|
Permissions []string `json:"permissions" db:"permissions"`
|
|
TokenBudgetDaily *int64 `json:"token_budget_daily,omitempty" db:"token_budget_daily"`
|
|
TokenBudgetMonthly *int64 `json:"token_budget_monthly,omitempty" db:"token_budget_monthly"`
|
|
AllowedModels []string `json:"allowed_models,omitempty" db:"allowed_models"` // nil = unrestricted
|
|
MemberCount int `json:"member_count,omitempty"` // computed, not a DB column
|
|
}
|
|
|
|
// GroupPatch holds optional fields for updating a group.
|
|
type GroupPatch struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
Permissions *[]string `json:"permissions,omitempty"`
|
|
TokenBudgetDaily *int64 `json:"token_budget_daily,omitempty"`
|
|
TokenBudgetMonthly *int64 `json:"token_budget_monthly,omitempty"`
|
|
AllowedModels *[]string `json:"allowed_models,omitempty"` // &[]string{} = restrict to none; nil = no change
|
|
ClearBudgetDaily bool `json:"clear_budget_daily,omitempty"`
|
|
ClearBudgetMonthly bool `json:"clear_budget_monthly,omitempty"`
|
|
ClearAllowedModels bool `json:"clear_allowed_models,omitempty"`
|
|
}
|
|
|
|
// GroupMember links a user to a group.
|
|
type GroupMember struct {
|
|
ID string `json:"id" db:"id"`
|
|
GroupID string `json:"group_id" db:"group_id"`
|
|
UserID string `json:"user_id" db:"user_id"`
|
|
AddedBy string `json:"added_by" db:"added_by"`
|
|
AddedAt time.Time `json:"added_at" db:"added_at"`
|
|
// Joined fields from users table (for list responses)
|
|
Username string `json:"username,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
}
|
|
|
|
// ResourceGrant controls who can USE a resource (personas, KBs) via groups.
|
|
// One row per resource. Not to be confused with persona_grants (what a Persona can DO).
|
|
type ResourceGrant struct {
|
|
BaseModel
|
|
ResourceType string `json:"resource_type" db:"resource_type"`
|
|
ResourceID string `json:"resource_id" db:"resource_id"`
|
|
GrantScope string `json:"grant_scope" db:"grant_scope"`
|
|
GrantedGroups []string `json:"granted_groups" db:"granted_groups"` // UUID array
|
|
CreatedBy string `json:"created_by" db:"created_by"`
|
|
}
|
|
|
|
// KnowledgeBase is a named collection of documents with vector embeddings.
|
|
// PersonaKB binds a knowledge base to a persona.
|
|
// KBDocument is a single uploaded file within a knowledge base.
|
|
// KBChunk is a text segment from a document with its embedding vector.
|
|
// KBSearchResult is a single result from similarity search.
|
|
// ChannelKB represents a knowledge base linked to a channel.
|
|
// PROVIDER HEALTH (v0.22.0)
|
|
|
|
// ProviderStatus represents the derived health state.
|
|
|
|
// AvgLatencyMs returns the average latency, or 0 if no requests.
|
|
func (w *ProviderHealthWindow) AvgLatencyMs() int {
|
|
if w.RequestCount == 0 {
|
|
return 0
|
|
}
|
|
return int(w.TotalLatencyMs / int64(w.RequestCount))
|
|
}
|
|
|
|
// ErrorRate returns the error fraction, or 0 if no requests.
|
|
func (w *ProviderHealthWindow) ErrorRate() float64 {
|
|
if w.RequestCount == 0 {
|
|
return 0
|
|
}
|
|
return float64(w.ErrorCount) / float64(w.RequestCount)
|
|
}
|
|
|
|
// ProviderHealthSummary is the API response for a provider's current health.
|
|
// ToolHealthWindow tracks health of built-in tools (web_search, url_fetch, etc.)
|
|
// in hourly buckets, analogous to ProviderHealthWindow.
|
|
// ToolHealthSummary is the API response for a tool's health.
|
|
// CAPABILITY OVERRIDES (v0.22.0)
|
|
|
|
// CapabilityOverride is an admin correction for a model's capabilities.
|
|
// ROUTING POLICIES (v0.22.2)
|
|
|
|
// RoutingPolicy is one routing rule that controls how requests are
|
|
// dispatched to provider configs. Stored in the routing_policies table.
|