// Package store defines the persistence layer for CarrotAssistant. // // All persistent structured state lives here: admin accounts, applications, // model configurations, skill metadata, chat sessions and messages, and // audit logs. Skill *content* itself (the markdown file) lives on disk and // is managed by the skill package; this package only stores the metadata // needed to locate and govern that file (status, version, ownership). package store import ( "time" "gorm.io/datatypes" ) // AdminUser is an operator who can log in to the admin console. type AdminUser struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` Username string `gorm:"uniqueIndex;size:64;not null" json:"username"` PasswordHash string `gorm:"size:255;not null" json:"-"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // App is a target application (a forum, a resource site, ...). Each App owns // a set of Skills and exposes a Chat API guarded by its access token. type App struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` Slug string `gorm:"uniqueIndex;size:64;not null" json:"slug"` Name string `gorm:"size:128;not null" json:"name"` Description string `gorm:"size:512" json:"description"` TokenHash string `gorm:"size:255;not null" json:"-"` // SHA-256 hash of the access token; never store plaintext ModelConfigID *int64 `gorm:"index" json:"model_config_id,omitempty"` Status string `gorm:"size:32;not null;default:'active'" json:"status"` // active | disabled CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ModelConfig describes an OpenAI-compatible LLM endpoint. Any provider // speaking the OpenAI Chat Completions + tool-calling protocol (OpenAI, // DeepSeek, Moonshot, vLLM, Ollama, ...) can be wired in here. type ModelConfig struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` Name string `gorm:"uniqueIndex;size:64;not null" json:"name"` BaseURL string `gorm:"size:255;not null" json:"base_url"` APIKeyEncrypted string `gorm:"size:512;not null" json:"-"` // AES-GCM ciphertext, base64 APIKeyPreview string `gorm:"size:16" json:"api_key_preview"` // last 4 chars, display only DefaultModel string `gorm:"size:64;not null" json:"default_model"` SupportsTools bool `gorm:"not null;default:true" json:"supports_tools"` MaxTokens int `gorm:"default:0" json:"max_tokens"` // 0 = leave unset CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Skill is the metadata row for a skill markdown file. The markdown content // (system prompt + tool definitions) lives at FilePath; this row only governs // lifecycle (draft/published/archived), versioning, and ownership. type Skill struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` AppID int64 `gorm:"index;not null" json:"app_id"` Slug string `gorm:"size:64;not null" json:"slug"` // unique within app Name string `gorm:"size:128;not null" json:"name"` Description string `gorm:"size:512" json:"description"` FilePath string `gorm:"size:512;not null" json:"file_path"` // path under SkillsDir Version int `gorm:"not null;default:1" json:"version"` Status string `gorm:"size:32;not null;default:'draft'" json:"status"` // draft | published | archived Source string `gorm:"size:32" json:"source"` // openapi | markdown | source | manual PublishedAt *time.Time `json:"published_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Session is a chat conversation between an end-user and an App's assistant. type Session struct { ID string `gorm:"primaryKey;type:text" json:"id"` // UUID AppID int64 `gorm:"index;not null" json:"app_id"` Title string `gorm:"size:255" json:"title"` MessageCount int `gorm:"not null;default:0" json:"message_count"` // PendingConfirmation, when non-empty, holds a JSON blob describing a // mutating tool call awaiting the user's approval. The chat stream pauses // until POST /chat/confirm resolves it. Empty = no pending call. PendingConfirmation datatypes.JSON `json:"pending_confirmation,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Message is a single message within a Session. Role follows the OpenAI // convention: system | user | assistant | tool. ToolCalls stores the // assistant's tool call payload (JSON) when present. type Message struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` SessionID string `gorm:"index;not null" json:"session_id"` Role string `gorm:"size:32;not null" json:"role"` // system | user | assistant | tool Content string `gorm:"type:text" json:"content"` ToolCalls datatypes.JSON `json:"tool_calls,omitempty"` ToolCallID string `gorm:"size:64" json:"tool_call_id,omitempty"` // for role=tool, the call id it answers Tokens int `gorm:"default:0" json:"tokens"` CreatedAt time.Time `json:"created_at"` } // AuditLog records security-relevant actions for traceability. type AuditLog struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` AppID *int64 `gorm:"index" json:"app_id,omitempty"` SessionID *string `gorm:"index" json:"session_id,omitempty"` Actor string `gorm:"size:128" json:"actor"` // admin username, "system", app slug, ... Action string `gorm:"size:64;not null;index" json:"action"` Detail datatypes.JSON `json:"detail"` CreatedAt time.Time `gorm:"index" json:"created_at"` }