WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,947 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// retrieverEngineMapping maps RETRIEVE_DRIVER values to retriever engine configurations
|
||||
var retrieverEngineMapping = map[string][]RetrieverEngineParams{
|
||||
"postgres": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: PostgresRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: PostgresRetrieverEngineType},
|
||||
},
|
||||
"elasticsearch_v7": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: ElasticsearchRetrieverEngineType},
|
||||
},
|
||||
"elasticsearch_v8": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: ElasticsearchRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: ElasticsearchRetrieverEngineType},
|
||||
},
|
||||
"qdrant": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: QdrantRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: QdrantRetrieverEngineType},
|
||||
},
|
||||
"milvus": {
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: MilvusRetrieverEngineType},
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: MilvusRetrieverEngineType},
|
||||
},
|
||||
"weaviate": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: WeaviateRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: WeaviateRetrieverEngineType},
|
||||
},
|
||||
"doris": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: DorisRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: DorisRetrieverEngineType},
|
||||
},
|
||||
"sqlite": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: SQLiteRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: SQLiteRetrieverEngineType},
|
||||
},
|
||||
"tencent_vectordb": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: TencentVectorDBRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: TencentVectorDBRetrieverEngineType},
|
||||
},
|
||||
"opensearch": {
|
||||
{RetrieverType: KeywordsRetrieverType, RetrieverEngineType: OpenSearchRetrieverEngineType},
|
||||
{RetrieverType: VectorRetrieverType, RetrieverEngineType: OpenSearchRetrieverEngineType},
|
||||
},
|
||||
}
|
||||
|
||||
// GetRetrieverEngineMapping returns the retriever engine mapping
|
||||
// This allows other packages to access the driver capabilities
|
||||
func GetRetrieverEngineMapping() map[string][]RetrieverEngineParams {
|
||||
return retrieverEngineMapping
|
||||
}
|
||||
|
||||
// GetDefaultRetrieverEngines returns the default retriever engines based on RETRIEVE_DRIVER env
|
||||
func GetDefaultRetrieverEngines() []RetrieverEngineParams {
|
||||
result := []RetrieverEngineParams{}
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, driver := range strings.Split(os.Getenv("RETRIEVE_DRIVER"), ",") {
|
||||
driver = strings.TrimSpace(driver)
|
||||
if params, ok := retrieverEngineMapping[driver]; ok {
|
||||
for _, p := range params {
|
||||
key := string(p.RetrieverType) + ":" + string(p.RetrieverEngineType)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Tenant represents the tenant
|
||||
type Tenant struct {
|
||||
// ID
|
||||
ID uint64 `yaml:"id" json:"id" gorm:"primaryKey"`
|
||||
// Name
|
||||
Name string `yaml:"name" json:"name"`
|
||||
// Description
|
||||
Description string `yaml:"description" json:"description"`
|
||||
// Status
|
||||
Status string `yaml:"status" json:"status" gorm:"default:'active'"`
|
||||
// Retriever engines
|
||||
RetrieverEngines RetrieverEngines `yaml:"retriever_engines" json:"retriever_engines" gorm:"type:json"`
|
||||
// Business
|
||||
Business string `yaml:"business" json:"business"`
|
||||
// Storage quota (Bytes), default is 10GB, including vector, original file, text, index, etc.
|
||||
StorageQuota int64 `yaml:"storage_quota" json:"storage_quota" gorm:"default:10737418240"`
|
||||
// Storage used (Bytes)
|
||||
StorageUsed int64 `yaml:"storage_used" json:"storage_used" gorm:"default:0"`
|
||||
// Global Context configuration for this workspace (default for all sessions)
|
||||
ContextConfig *ContextConfig `yaml:"context_config" json:"context_config" gorm:"type:jsonb"`
|
||||
// Global WebSearch configuration for this workspace
|
||||
WebSearchConfig *WebSearchConfig `yaml:"web_search_config" json:"web_search_config" gorm:"type:jsonb"`
|
||||
// Parser engine config overrides (MinerU endpoint, API key, etc.). Used when parsing documents; overrides env.
|
||||
ParserEngineConfig *ParserEngineConfig `yaml:"parser_engine_config" json:"parser_engine_config" gorm:"type:jsonb"`
|
||||
// Credentials config: third-party provider credentials (e.g. WeKnoraCloud AppID/AppSecret)
|
||||
Credentials *CredentialsConfig `yaml:"credentials" json:"credentials" gorm:"type:jsonb"`
|
||||
// Storage engine config: parameters for Local, MinIO, COS. Used for document/file storage and docreader.
|
||||
StorageEngineConfig *StorageEngineConfig `yaml:"storage_engine_config" json:"storage_engine_config" gorm:"type:jsonb"`
|
||||
// DefaultStorageBackendID is the workspace default concrete storage instance.
|
||||
DefaultStorageBackendID *string `yaml:"default_storage_backend_id" json:"default_storage_backend_id,omitempty" gorm:"column:default_storage_backend_id;type:varchar(36)"`
|
||||
// Chat history config: knowledge base configuration for indexing and searching chat messages via vector search
|
||||
ChatHistoryConfig *ChatHistoryConfig `yaml:"chat_history_config" json:"chat_history_config" gorm:"type:jsonb"`
|
||||
// Retrieval config: global search/retrieval parameters shared by knowledge search and message search
|
||||
RetrievalConfig *RetrievalConfig `yaml:"retrieval_config" json:"retrieval_config" gorm:"type:jsonb"`
|
||||
// Memory config: workspace switch for cross-session long-term memory
|
||||
MemoryConfig *MemoryConfig `yaml:"memory_config" json:"memory_config" gorm:"type:jsonb"`
|
||||
// API principal config: controls how X-API-Key requests map to terminal principals.
|
||||
APIPrincipalConfig *APIPrincipalConfig `yaml:"api_principal_config" json:"-" gorm:"type:jsonb"`
|
||||
// Creation time
|
||||
CreatedAt time.Time `yaml:"created_at" json:"created_at"`
|
||||
// Last updated time
|
||||
UpdatedAt time.Time `yaml:"updated_at" json:"updated_at"`
|
||||
// Deletion time
|
||||
DeletedAt gorm.DeletedAt `yaml:"deleted_at" json:"deleted_at" gorm:"index"`
|
||||
}
|
||||
|
||||
// RetrieverEngines represents the retriever engines for a tenant
|
||||
type RetrieverEngines struct {
|
||||
Engines []RetrieverEngineParams `yaml:"engines" json:"engines" gorm:"type:json"`
|
||||
}
|
||||
|
||||
// GetEffectiveEngines returns the tenant's engines if configured, otherwise returns system defaults
|
||||
func (t *Tenant) GetEffectiveEngines() []RetrieverEngineParams {
|
||||
if len(t.RetrieverEngines.Engines) > 0 {
|
||||
return t.RetrieverEngines.Engines
|
||||
}
|
||||
return GetDefaultRetrieverEngines()
|
||||
}
|
||||
|
||||
// BeforeCreate is a hook function that is called before creating a tenant
|
||||
func (t *Tenant) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.RetrieverEngines.Engines == nil {
|
||||
t.RetrieverEngines.Engines = []RetrieverEngineParams{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface, used to convert RetrieverEngines to database value
|
||||
func (c RetrieverEngines) Value() (driver.Value, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface, used to convert database value to RetrieverEngines.
|
||||
// It supports both the legacy bare-array format (e.g. [{...}, {...}]) and the current
|
||||
// object-wrapped format (e.g. {"engines": [{...}, {...}]}).
|
||||
func (c *RetrieverEngines) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try the current object format first: {"engines": [...]}
|
||||
if err := json.Unmarshal(b, c); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: legacy bare-array format: [{...}, {...}]
|
||||
var engines []RetrieverEngineParams
|
||||
if err := json.Unmarshal(b, &engines); err != nil {
|
||||
return fmt.Errorf("retriever_engines: cannot unmarshal as object or array: %w", err)
|
||||
}
|
||||
c.Engines = engines
|
||||
return nil
|
||||
}
|
||||
|
||||
// CredentialsConfig holds third-party provider credentials at the tenant level.
|
||||
// Stored as a single JSONB column; each provider is a nested object so new
|
||||
// providers can be added without schema changes.
|
||||
type CredentialsConfig struct {
|
||||
WeKnoraCloud *WeKnoraCloudCredentials `json:"weknoracloud,omitempty"`
|
||||
}
|
||||
|
||||
// WeKnoraCloudCredentials stores WeKnoraCloud AppID and AppSecret.
|
||||
// AppSecret is AES-256 encrypted before persisting to database.
|
||||
type WeKnoraCloudCredentials struct {
|
||||
AppID string `json:"app_id"`
|
||||
AppSecret string `json:"app_secret"`
|
||||
}
|
||||
|
||||
type APIPrincipalMode string
|
||||
|
||||
const (
|
||||
APIPrincipalModeTenant APIPrincipalMode = "tenant"
|
||||
APIPrincipalModeDirect APIPrincipalMode = "direct_header"
|
||||
APIPrincipalModeSignedToken APIPrincipalMode = "signed_token"
|
||||
)
|
||||
|
||||
// APIPrincipalConfig controls how tenant API-key requests map to terminal
|
||||
// principals. Direct header mode is low-assurance and should only be used for
|
||||
// trusted server-to-server calls; signed-token mode verifies the user claim.
|
||||
type APIPrincipalConfig struct {
|
||||
Mode APIPrincipalMode `json:"mode"`
|
||||
DirectHeaderName string `json:"direct_header_name,omitempty"`
|
||||
SignedTokenHeaderName string `json:"signed_token_header_name,omitempty"`
|
||||
// RequireDirectHeader, when true in direct_header mode, rejects API-key
|
||||
// requests that omit the configured user-id header instead of falling
|
||||
// back to the tenant-level principal.
|
||||
RequireDirectHeader bool `json:"require_direct_header,omitempty"`
|
||||
HMACSecret string `json:"hmac_secret,omitempty"`
|
||||
}
|
||||
|
||||
func (c *APIPrincipalConfig) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
cp := *c
|
||||
if cp.HMACSecret != "" {
|
||||
if key := utils.GetAESKey(); key != nil {
|
||||
if encrypted, err := utils.EncryptAESGCM(cp.HMACSecret, key); err == nil {
|
||||
cp.HMACSecret = encrypted
|
||||
}
|
||||
}
|
||||
}
|
||||
return json.Marshal(&cp)
|
||||
}
|
||||
|
||||
func (c *APIPrincipalConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(b, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if plain, ok := utils.DecryptStoredSecretLenient(c.HMACSecret); ok {
|
||||
c.HMACSecret = plain
|
||||
} else {
|
||||
log.Printf("[crypto] tenant api_principal_config.hmac_secret: decrypt failed (SYSTEM_AES_KEY missing/rotated?), treating as unconfigured")
|
||||
c.HMACSecret = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWeKnoraCloud returns the WeKnoraCloud credentials, or nil if not configured.
|
||||
func (c *CredentialsConfig) GetWeKnoraCloud() *WeKnoraCloudCredentials {
|
||||
if c == nil || c.WeKnoraCloud == nil {
|
||||
return nil
|
||||
}
|
||||
if c.WeKnoraCloud.AppID == "" || c.WeKnoraCloud.AppSecret == "" {
|
||||
return nil
|
||||
}
|
||||
return c.WeKnoraCloud
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface for CredentialsConfig
|
||||
func (c *CredentialsConfig) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
cp := *c
|
||||
if cp.WeKnoraCloud != nil && cp.WeKnoraCloud.AppSecret != "" {
|
||||
if key := utils.GetAESKey(); key != nil {
|
||||
if encrypted, err := utils.EncryptAESGCM(cp.WeKnoraCloud.AppSecret, key); err == nil {
|
||||
cp.WeKnoraCloud = &WeKnoraCloudCredentials{AppID: cp.WeKnoraCloud.AppID, AppSecret: encrypted}
|
||||
}
|
||||
}
|
||||
}
|
||||
return json.Marshal(cp)
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface for CredentialsConfig
|
||||
func (c *CredentialsConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(b, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.WeKnoraCloud != nil {
|
||||
if plain, ok := utils.DecryptStoredSecretLenient(c.WeKnoraCloud.AppSecret); ok {
|
||||
c.WeKnoraCloud.AppSecret = plain
|
||||
} else {
|
||||
log.Printf("[crypto] tenant credentials we_knora_cloud.app_secret: decrypt failed (SYSTEM_AES_KEY missing/rotated?), treating as unconfigured")
|
||||
c.WeKnoraCloud.AppSecret = ""
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParserEngineConfig holds tenant-level overrides for document parser engines (e.g. MinerU endpoint, API key).
|
||||
// These values take precedence over environment variables when parsing documents.
|
||||
type ParserEngineConfig struct {
|
||||
// ChatParserEngineRules selects parser engines for session-scoped chat
|
||||
// documents. Knowledge bases keep their own rules in ChunkingConfig.
|
||||
ChatParserEngineRules []ParserEngineRule `json:"chat_parser_engine_rules,omitempty"`
|
||||
MinerUEndpoint string `json:"mineru_endpoint"` // MinerU 自建服务端点
|
||||
MinerUAPIKey string `json:"mineru_api_key"` // MinerU 云 API Key
|
||||
|
||||
// MinerU 自建解析参数
|
||||
MinerUModel string `json:"mineru_model,omitempty"` // backend: pipeline, vlm-*, hybrid-*
|
||||
MinerUVLMServerURL string `json:"mineru_vlm_server_url,omitempty"` // vLLM 服务器地址 (vlm-http-client / hybrid-http-client)
|
||||
MinerUEnableFormula *bool `json:"mineru_enable_formula,omitempty"`
|
||||
MinerUEnableTable *bool `json:"mineru_enable_table,omitempty"`
|
||||
MinerUParseMethod string `json:"mineru_parse_method,omitempty"`
|
||||
// MinerUEnableOCR is retained for compatibility with configurations saved
|
||||
// before parse_method supported auto/ocr/txt.
|
||||
MinerUEnableOCR *bool `json:"mineru_enable_ocr,omitempty"`
|
||||
MinerULanguage string `json:"mineru_language,omitempty"`
|
||||
|
||||
// MinerU 云 API 解析参数
|
||||
MinerUCloudModel string `json:"mineru_cloud_model,omitempty"` // model_version: pipeline, vlm, MinerU-HTML
|
||||
MinerUCloudEnableFormula *bool `json:"mineru_cloud_enable_formula,omitempty"`
|
||||
MinerUCloudEnableTable *bool `json:"mineru_cloud_enable_table,omitempty"`
|
||||
MinerUCloudEnableOCR *bool `json:"mineru_cloud_enable_ocr,omitempty"`
|
||||
MinerUCloudLanguage string `json:"mineru_cloud_language,omitempty"`
|
||||
|
||||
// OpenDataLoader PDF (docreader engine); hybrid requires opendataloader-pdf-hybrid service.
|
||||
ODLHybrid string `json:"odl_hybrid,omitempty"` // off (default), docling-fast, hancom-ai
|
||||
ODLHybridURL string `json:"odl_hybrid_url,omitempty"` // e.g. http://odl-hybrid:5002
|
||||
ODLHybridMode string `json:"odl_hybrid_mode,omitempty"` // auto, full
|
||||
ODLHybridFallback *bool `json:"odl_hybrid_fallback,omitempty"`
|
||||
ODLMarkdownWithHTML *bool `json:"odl_markdown_with_html,omitempty"`
|
||||
|
||||
// PaddleOCR-VL self-hosted pipeline service (full /layout-parsing API).
|
||||
PaddleOCRVLEndpoint string `json:"paddleocr_vl_endpoint,omitempty"` // e.g. http://paddleocr-vl:8080
|
||||
PaddleOCRVLUseSealRecognition *bool `json:"paddleocr_vl_use_seal_recognition,omitempty"`
|
||||
PaddleOCRVLUseChartRecognition *bool `json:"paddleocr_vl_use_chart_recognition,omitempty"`
|
||||
|
||||
// PaddleOCR-VL AI Studio cloud API.
|
||||
PaddleOCRVLCloudToken string `json:"paddleocr_vl_cloud_token,omitempty"`
|
||||
PaddleOCRVLCloudModel string `json:"paddleocr_vl_cloud_model,omitempty"` // e.g. PaddleOCR-VL-1.6
|
||||
PaddleOCRVLCloudUseSealRecognition *bool `json:"paddleocr_vl_cloud_use_seal_recognition,omitempty"`
|
||||
PaddleOCRVLCloudUseChartRecognition *bool `json:"paddleocr_vl_cloud_use_chart_recognition,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
MinerUParseMethodAuto = "auto"
|
||||
MinerUParseMethodOCR = "ocr"
|
||||
MinerUParseMethodText = "txt"
|
||||
)
|
||||
|
||||
// ResolveMinerUParseMethod normalizes the explicit MinerU parse method and
|
||||
// maps the legacy OCR toggle to the closest safe behavior. The old enabled
|
||||
// value maps to auto instead of ocr so digital PDFs keep their native text
|
||||
// layer while scanned PDFs are still detected and OCRed by MinerU.
|
||||
func ResolveMinerUParseMethod(method string, legacyOCREnabled *bool) string {
|
||||
switch strings.ToLower(strings.TrimSpace(method)) {
|
||||
case MinerUParseMethodAuto:
|
||||
return MinerUParseMethodAuto
|
||||
case MinerUParseMethodOCR:
|
||||
return MinerUParseMethodOCR
|
||||
case MinerUParseMethodText:
|
||||
return MinerUParseMethodText
|
||||
}
|
||||
|
||||
if legacyOCREnabled != nil && !*legacyOCREnabled {
|
||||
return MinerUParseMethodText
|
||||
}
|
||||
return MinerUParseMethodAuto
|
||||
}
|
||||
|
||||
func (c *ParserEngineConfig) ResolveChatParserEngine(fileType string) string {
|
||||
if c != nil {
|
||||
normalized := normalizeParserFileType(fileType)
|
||||
for _, rule := range c.ChatParserEngineRules {
|
||||
for _, candidate := range rule.FileTypes {
|
||||
if normalizeParserFileType(candidate) == normalized {
|
||||
return strings.TrimSpace(rule.Engine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return DefaultParserEngine(fileType)
|
||||
}
|
||||
|
||||
// ToOverridesMap returns a map suitable for ParserEngineOverrides in parse requests.
|
||||
// Keys are snake_case (mineru_endpoint, mineru_api_key, etc.).
|
||||
func (c *ParserEngineConfig) ToOverridesMap() map[string]string {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
m := make(map[string]string)
|
||||
if c.MinerUEndpoint != "" {
|
||||
m["mineru_endpoint"] = c.MinerUEndpoint
|
||||
}
|
||||
if c.MinerUAPIKey != "" {
|
||||
m["mineru_api_key"] = c.MinerUAPIKey
|
||||
}
|
||||
if c.MinerUModel != "" {
|
||||
m["mineru_model"] = c.MinerUModel
|
||||
}
|
||||
if c.MinerUVLMServerURL != "" {
|
||||
m["mineru_vlm_server_url"] = c.MinerUVLMServerURL
|
||||
}
|
||||
if c.MinerUEnableFormula != nil {
|
||||
m["mineru_enable_formula"] = fmt.Sprintf("%v", *c.MinerUEnableFormula)
|
||||
}
|
||||
if c.MinerUEnableTable != nil {
|
||||
m["mineru_enable_table"] = fmt.Sprintf("%v", *c.MinerUEnableTable)
|
||||
}
|
||||
if c.MinerUParseMethod != "" || c.MinerUEnableOCR != nil {
|
||||
m["mineru_parse_method"] = ResolveMinerUParseMethod(c.MinerUParseMethod, c.MinerUEnableOCR)
|
||||
}
|
||||
if c.MinerUEnableOCR != nil {
|
||||
m["mineru_enable_ocr"] = fmt.Sprintf("%v", *c.MinerUEnableOCR)
|
||||
}
|
||||
if c.MinerULanguage != "" {
|
||||
m["mineru_language"] = c.MinerULanguage
|
||||
}
|
||||
if c.MinerUCloudModel != "" {
|
||||
m["mineru_cloud_model"] = c.MinerUCloudModel
|
||||
}
|
||||
if c.MinerUCloudEnableFormula != nil {
|
||||
m["mineru_cloud_enable_formula"] = fmt.Sprintf("%v", *c.MinerUCloudEnableFormula)
|
||||
}
|
||||
if c.MinerUCloudEnableTable != nil {
|
||||
m["mineru_cloud_enable_table"] = fmt.Sprintf("%v", *c.MinerUCloudEnableTable)
|
||||
}
|
||||
if c.MinerUCloudEnableOCR != nil {
|
||||
m["mineru_cloud_enable_ocr"] = fmt.Sprintf("%v", *c.MinerUCloudEnableOCR)
|
||||
}
|
||||
if c.MinerUCloudLanguage != "" {
|
||||
m["mineru_cloud_language"] = c.MinerUCloudLanguage
|
||||
}
|
||||
if c.ODLHybrid != "" {
|
||||
m["odl_hybrid"] = c.ODLHybrid
|
||||
}
|
||||
if c.ODLHybridURL != "" {
|
||||
m["odl_hybrid_url"] = c.ODLHybridURL
|
||||
}
|
||||
if c.ODLHybridMode != "" {
|
||||
m["odl_hybrid_mode"] = c.ODLHybridMode
|
||||
}
|
||||
if c.ODLHybridFallback != nil {
|
||||
m["odl_hybrid_fallback"] = fmt.Sprintf("%v", *c.ODLHybridFallback)
|
||||
}
|
||||
if c.ODLMarkdownWithHTML != nil {
|
||||
m["odl_markdown_with_html"] = fmt.Sprintf("%v", *c.ODLMarkdownWithHTML)
|
||||
}
|
||||
if c.PaddleOCRVLEndpoint != "" {
|
||||
m["paddleocr_vl_endpoint"] = c.PaddleOCRVLEndpoint
|
||||
}
|
||||
if c.PaddleOCRVLUseSealRecognition != nil {
|
||||
m["paddleocr_vl_use_seal_recognition"] = fmt.Sprintf("%v", *c.PaddleOCRVLUseSealRecognition)
|
||||
}
|
||||
if c.PaddleOCRVLUseChartRecognition != nil {
|
||||
m["paddleocr_vl_use_chart_recognition"] = fmt.Sprintf("%v", *c.PaddleOCRVLUseChartRecognition)
|
||||
}
|
||||
if c.PaddleOCRVLCloudToken != "" {
|
||||
m["paddleocr_vl_cloud_token"] = c.PaddleOCRVLCloudToken
|
||||
}
|
||||
if c.PaddleOCRVLCloudModel != "" {
|
||||
m["paddleocr_vl_cloud_model"] = c.PaddleOCRVLCloudModel
|
||||
}
|
||||
if c.PaddleOCRVLCloudUseSealRecognition != nil {
|
||||
m["paddleocr_vl_cloud_use_seal_recognition"] = fmt.Sprintf("%v", *c.PaddleOCRVLCloudUseSealRecognition)
|
||||
}
|
||||
if c.PaddleOCRVLCloudUseChartRecognition != nil {
|
||||
m["paddleocr_vl_cloud_use_chart_recognition"] = fmt.Sprintf("%v", *c.PaddleOCRVLCloudUseChartRecognition)
|
||||
}
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface for ParserEngineConfig
|
||||
func (c *ParserEngineConfig) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface for ParserEngineConfig
|
||||
func (c *ParserEngineConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(b, c)
|
||||
}
|
||||
|
||||
// StorageEngineConfig holds tenant-level storage engine parameters for Local, MinIO, COS, TOS, S3, OSS, KS3, and OBS.
|
||||
// Knowledge bases select which provider to use; parameters are read from here.
|
||||
type StorageEngineConfig struct {
|
||||
DefaultProvider string `json:"default_provider"` // "local", "minio", "cos", "tos", "s3", "oss", "ks3", "obs"
|
||||
Local *LocalEngineConfig `json:"local,omitempty"`
|
||||
MinIO *MinIOEngineConfig `json:"minio,omitempty"`
|
||||
COS *COSEngineConfig `json:"cos,omitempty"`
|
||||
TOS *TOSEngineConfig `json:"tos,omitempty"`
|
||||
S3 *S3EngineConfig `json:"s3,omitempty"`
|
||||
OSS *OSSEngineConfig `json:"oss,omitempty"`
|
||||
KS3 *KS3EngineConfig `json:"ks3,omitempty"`
|
||||
OBS *OBSEngineConfig `json:"obs,omitempty"`
|
||||
}
|
||||
|
||||
// LocalEngineConfig is for local file system storage (single-machine deployment only).
|
||||
type LocalEngineConfig struct {
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
}
|
||||
|
||||
// MinIOEngineConfig is for MinIO/S3-compatible object storage.
|
||||
// Mode "docker" uses env vars for endpoint/credentials; "remote" uses the fields below.
|
||||
type MinIOEngineConfig struct {
|
||||
Mode string `json:"mode"` // "docker" or "remote"
|
||||
Endpoint string `json:"endpoint"`
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
SecretAccessKey string `json:"secret_access_key"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
UseSSL bool `json:"use_ssl"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
}
|
||||
|
||||
// COSEngineConfig is for Tencent Cloud COS.
|
||||
type COSEngineConfig struct {
|
||||
SecretID string `json:"secret_id"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Region string `json:"region"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
AppID string `json:"app_id"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
TempBucketName string `json:"temp_bucket_name"`
|
||||
TempRegion string `json:"temp_region"`
|
||||
}
|
||||
|
||||
// TOSEngineConfig is for Volcengine TOS (火山引擎对象存储).
|
||||
type TOSEngineConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
TempBucketName string `json:"temp_bucket_name"`
|
||||
TempRegion string `json:"temp_region"`
|
||||
}
|
||||
|
||||
// S3EngineConfig is for AWS S3 and S3-compatible object storage.
|
||||
type S3EngineConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
UseSSL bool `json:"use_ssl"`
|
||||
ForcePathStyle bool `json:"force_path_style"`
|
||||
}
|
||||
|
||||
// OSSEngineConfig is for Alibaba Cloud OSS (对象存储服务).
|
||||
type OSSEngineConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
UseTempBucket bool `json:"use_temp_bucket"`
|
||||
TempBucketName string `json:"temp_bucket_name"`
|
||||
TempRegion string `json:"temp_region"`
|
||||
}
|
||||
|
||||
// KS3EngineConfig is for Kingsoft Cloud KS3 object storage.
|
||||
type KS3EngineConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
}
|
||||
|
||||
// OBSEngineConfig is for Huawei Cloud OBS (对象存储服务).
|
||||
type OBSEngineConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
UseSSL bool `json:"use_ssl"`
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface for StorageEngineConfig
|
||||
func (c *StorageEngineConfig) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface for StorageEngineConfig
|
||||
func (c *StorageEngineConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(b, c)
|
||||
}
|
||||
|
||||
// TenantSandboxConfig is one named sandbox backend a workspace maintains.
|
||||
//
|
||||
// It is self-contained: provider fields are not inherited from process
|
||||
// environment. Leaving a required provider field empty is rejected on save.
|
||||
type TenantSandboxConfig struct {
|
||||
// SandboxType is cube, e2b, or docker; disabled is the hidden policy row.
|
||||
SandboxType string `json:"sandbox_type,omitempty"`
|
||||
|
||||
// ── 通用配置(跨后端生效)──────────────────────────────────
|
||||
|
||||
// DefaultTimeoutSec is the per-execution timeout in seconds. 0 uses the
|
||||
// program's built-in default.
|
||||
DefaultTimeoutSec int `json:"default_timeout_sec,omitempty"`
|
||||
|
||||
// AllowPrivateEndpoints permits this workspace config to reach RFC1918 or
|
||||
// loopback cluster endpoints. Link-local/cloud-metadata addresses remain
|
||||
// blocked. It is explicit in the UI instead of hidden in process env.
|
||||
AllowPrivateEndpoints bool `json:"allow_private_endpoints,omitempty"`
|
||||
|
||||
// EnvVars are additional environment variables injected into every
|
||||
// sandbox created for this tenant. 🔒 Values are encrypted at rest.
|
||||
// These become visible to all scripts running in the tenant's
|
||||
// sandboxes — do not place secrets here that scripts must not access.
|
||||
EnvVars map[string]string `json:"env_vars,omitempty"`
|
||||
|
||||
// VolumeMount configures an optional shared volume mounted into every
|
||||
// sandbox created for this tenant. Currently used for tenant-installed
|
||||
// skills, but the configuration itself is skill-agnostic and can serve
|
||||
// any volume-mount use case (shared datasets, pre-installed toolchains,
|
||||
// etc.).
|
||||
VolumeMount *VolumeMountConfig `json:"volume_mount,omitempty"`
|
||||
|
||||
// SkillImage points at the snapshot that carries this config's installed
|
||||
// skills. Empty means "use the base template". Written only by the skill
|
||||
// install/remove path: MergeSandboxConfigForUpdate ignores client values
|
||||
// so a settings-form save cannot wipe or plant the pointer.
|
||||
SkillImage *SkillImageConfig `json:"skill_image,omitempty"`
|
||||
|
||||
// SkillRollout decides whether sessions that already hold a sandbox of
|
||||
// this config rebuild after a skill install or removal. Empty and
|
||||
// SkillRolloutNextTurn rebuild on the next chat turn. SkillRolloutNewSession
|
||||
// leaves those sandboxes on the previous image; only sessions that start
|
||||
// afterwards boot the new snapshot.
|
||||
SkillRollout string `json:"skill_rollout,omitempty"`
|
||||
|
||||
// Network is the outbound/inbound network policy applied to every sandbox
|
||||
// created from this config — chat sessions, skill installs and deep
|
||||
// connectivity probes alike. nil and the zero value mean the same thing:
|
||||
// outbound egress allowed, inbound public access closed.
|
||||
Network *SandboxNetworkPolicy `json:"network,omitempty"`
|
||||
|
||||
// ── 后端专属配置(同一时刻只有一个生效,由 SandboxType 决定)───
|
||||
|
||||
Cube *CubeSandboxConfig `json:"cube,omitempty"`
|
||||
E2B *E2BSandboxConfig `json:"e2b,omitempty"`
|
||||
Docker *DockerSandboxConfig `json:"docker,omitempty"`
|
||||
}
|
||||
|
||||
// CubeSandboxConfig addresses one CubeSandbox deployment. APIURL, ProxyURL,
|
||||
// SandboxDomain and TemplateID are all required; APIKey is optional because the
|
||||
// common single-node setup runs unauthenticated.
|
||||
type CubeSandboxConfig struct {
|
||||
APIURL string `json:"api_url,omitempty"`
|
||||
ProxyURL string `json:"proxy_url,omitempty"`
|
||||
SandboxDomain string `json:"sandbox_domain,omitempty"`
|
||||
APIKey string `json:"api_key,omitempty"` // 加密
|
||||
TemplateID string `json:"template_id,omitempty"`
|
||||
|
||||
// HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.
|
||||
// 0 means use the built-in default (30s), never the deployment's value.
|
||||
HTTPTimeoutSec int `json:"http_timeout_sec,omitempty"`
|
||||
|
||||
CubeSandboxTTLSeconds int `json:"cube_sandbox_ttl_seconds,omitempty"`
|
||||
|
||||
// DNSServers are Cube template nameserver IPs. Empty uses Cubelet's default.
|
||||
DNSServers []string `json:"dns_servers,omitempty"`
|
||||
}
|
||||
|
||||
// E2BSandboxConfig addresses one E2B-protocol control plane: E2B Cloud, a
|
||||
// self-hosted E2B Infrastructure, or any E2B-compatible implementation
|
||||
// (CubeSandbox, Agent-Sandbox, …). APIKey and TemplateID are required; APIURL
|
||||
// and SandboxDomain are optional because go-e2b resolves both on its own when
|
||||
// they are empty.
|
||||
type E2BSandboxConfig struct {
|
||||
APIURL string `json:"api_url,omitempty"`
|
||||
SandboxDomain string `json:"sandbox_domain,omitempty"`
|
||||
APIKey string `json:"api_key,omitempty"` // 加密
|
||||
TemplateID string `json:"template_id,omitempty"`
|
||||
|
||||
// ProxyURL is the data-plane gateway that fronts envd. E2B Cloud resolves
|
||||
// "<port>-<sandboxID>.<sandbox_domain>" through public DNS and TLS, so it
|
||||
// needs no value here. Self-hosted E2B-compatible control planes usually
|
||||
// serve every sandbox from one gateway address and expect the sandbox
|
||||
// authority in the Host header; setting this makes WeKnora dial the
|
||||
// gateway directly instead of requiring wildcard DNS and a certificate
|
||||
// for the sandbox domain. An "http://" gateway also downgrades the
|
||||
// data-plane scheme, which the E2B SDK otherwise pins to https.
|
||||
ProxyURL string `json:"proxy_url,omitempty"`
|
||||
|
||||
// HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.
|
||||
// 0 means use the built-in default (30s), never the deployment's value.
|
||||
HTTPTimeoutSec int `json:"http_timeout_sec,omitempty"`
|
||||
|
||||
E2BSandboxTTLSeconds int `json:"e2b_sandbox_ttl_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// DockerSandboxConfig addresses one Docker daemon. Image is required and plays
|
||||
// the role a template ID plays for the MicroVM backends: every session
|
||||
// container is created from it.
|
||||
//
|
||||
// The daemon endpoint is deliberately the only connection field, and TLS
|
||||
// material is referenced by path rather than stored here. Client certificates
|
||||
// are deployment infrastructure mounted onto the WeKnora host; keeping them
|
||||
// out of the database keeps them out of backups, exports and API responses.
|
||||
type DockerSandboxConfig struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
|
||||
// Host is the daemon endpoint in DOCKER_HOST form. Empty means the local
|
||||
// unix socket.
|
||||
Host string `json:"host,omitempty"`
|
||||
|
||||
// TLSCertPath is a directory on the WeKnora host containing ca.pem,
|
||||
// cert.pem and key.pem. Required when Host is a TCP endpoint.
|
||||
TLSCertPath string `json:"tls_cert_path,omitempty"`
|
||||
|
||||
// CPULimit is the number of CPU cores one sandbox may use. 0 uses the
|
||||
// built-in default.
|
||||
CPULimit float64 `json:"cpu_limit,omitempty"`
|
||||
|
||||
// MemoryLimitMB caps one sandbox's memory. 0 uses the built-in default.
|
||||
MemoryLimitMB int `json:"memory_limit_mb,omitempty"`
|
||||
|
||||
// PidsLimit caps how many processes one sandbox may run. 0 uses the
|
||||
// built-in default.
|
||||
PidsLimit int `json:"pids_limit,omitempty"`
|
||||
|
||||
// NetworkMode is the Docker network sandboxes join: "bridge" (default) or
|
||||
// "none" for no egress. Nothing else is accepted — host and container:
|
||||
// modes share another namespace outright, and a named network is usually
|
||||
// the deployment's own compose network, which would put the sandbox next
|
||||
// to Postgres and Redis.
|
||||
NetworkMode string `json:"network_mode,omitempty"`
|
||||
|
||||
// Runtime selects an alternative OCI runtime such as "runsc" (gVisor).
|
||||
// Empty uses the daemon default.
|
||||
Runtime string `json:"runtime,omitempty"`
|
||||
|
||||
// IdleTTLSeconds is how long a session container may go unused before it
|
||||
// is reclaimed. The daemon has no idle timeout of its own, so this is what
|
||||
// stops an abandoned session from pinning host memory indefinitely.
|
||||
IdleTTLSeconds int `json:"idle_ttl_seconds,omitempty"`
|
||||
|
||||
// HTTPTimeoutSec bounds each Engine API call. 0 uses the built-in default.
|
||||
HTTPTimeoutSec int `json:"http_timeout_sec,omitempty"`
|
||||
}
|
||||
|
||||
// VolumeMountConfig configures a shared volume mount into every sandbox
|
||||
// created for this tenant. Currently implemented for E2B volumes (used to
|
||||
// share tenant-installed skills across sandbox sessions), but the schema is
|
||||
// intentionally skill-agnostic so it can serve other volume-based use cases
|
||||
// in the future.
|
||||
type VolumeMountConfig struct {
|
||||
// Enabled toggles the volume mount for this tenant.
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// MountPath is the sandbox-internal path where the volume is mounted.
|
||||
// Default: /weknora/tenant/skills (customizable per use case).
|
||||
MountPath string `json:"mount_path,omitempty"`
|
||||
|
||||
// Provider identifies the volume backend. Currently "e2b" or "cube".
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
// VolumeID is the provider-specific volume identifier, populated after
|
||||
// EnsureVolume / CreateVolume succeeds.
|
||||
VolumeID string `json:"volume_id,omitempty"`
|
||||
|
||||
// VolumeName is the human-readable volume name, e.g.
|
||||
// "weknora-tenant-<id>-skills".
|
||||
VolumeName string `json:"volume_name,omitempty"`
|
||||
|
||||
// VolumeOwnerFingerprint = sha256(provider + APIKey + APIURL).
|
||||
// Used to detect when the tenant switched to a different backend or
|
||||
// API key, at which point the volume is no longer reachable and must
|
||||
// be recreated.
|
||||
VolumeOwnerFingerprint string `json:"volume_owner_fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// SkillRolloutNextTurn rebuilds a live session's sandbox on its next chat
|
||||
// turn after the skill image changes. This is the default.
|
||||
SkillRolloutNextTurn = "next_turn"
|
||||
// SkillRolloutNewSession leaves live sandboxes on the previous image.
|
||||
// Only a session that starts after the install or removal boots the new one.
|
||||
SkillRolloutNewSession = "new_session"
|
||||
)
|
||||
|
||||
// RebuildsExistingOnSkillChange reports whether an install or removal should
|
||||
// mark already-bound sandboxes stale. Unknown values fail toward rebuilding
|
||||
// so a corrupted row cannot pin every session on a retired image.
|
||||
func (c *TenantSandboxConfig) RebuildsExistingOnSkillChange() bool {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
return strings.TrimSpace(c.SkillRollout) != SkillRolloutNewSession
|
||||
}
|
||||
|
||||
// SkillImageConfig is the pointer to the snapshot that carries the skills
|
||||
// installed on this sandbox config. Snapshot IDs double as template IDs, so
|
||||
// nothing else is needed to boot sessions from it.
|
||||
//
|
||||
// It holds no secrets, so it is not encrypted by TenantSandboxConfig.Value.
|
||||
type SkillImageConfig struct {
|
||||
// SnapshotID is the currently effective snapshot; empty = base template.
|
||||
SnapshotID string `json:"snapshot_id,omitempty"`
|
||||
// Generation increments on every successful install/remove, for naming
|
||||
// and troubleshooting.
|
||||
Generation int `json:"generation,omitempty"`
|
||||
// BuiltAt records when this generation was produced.
|
||||
BuiltAt time.Time `json:"built_at,omitempty"`
|
||||
// BaseTemplateID is the template this chain was originally built from;
|
||||
// the rebuild path starts over from it.
|
||||
BaseTemplateID string `json:"base_template_id,omitempty"`
|
||||
// OwnerFingerprint identifies the provider account that owns the snapshot.
|
||||
// Snapshots are invisible across accounts, so a mismatch means "fall back
|
||||
// to the base template" rather than "fail".
|
||||
OwnerFingerprint string `json:"owner_fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface. Every secret-bearing field
|
||||
// (Cube.APIKey, E2B.APIKey, all EnvVars values, and injected header values) is
|
||||
// encrypted before persisting. EnvVars are included because environment
|
||||
// variables routinely carry credentials, and their values are handed to tenant
|
||||
// scripts verbatim. The receiver is never mutated: nested structs and the map
|
||||
// are copied first.
|
||||
func (c *TenantSandboxConfig) Value() (driver.Value, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
cp := *c
|
||||
key := utils.GetAESKey()
|
||||
|
||||
encrypt := func(plain string) string {
|
||||
if plain == "" || key == nil {
|
||||
return plain
|
||||
}
|
||||
encrypted, err := utils.EncryptAESGCM(plain, key)
|
||||
if err != nil {
|
||||
return plain
|
||||
}
|
||||
return encrypted
|
||||
}
|
||||
|
||||
if c.Cube != nil {
|
||||
cube := *c.Cube
|
||||
cube.APIKey = encrypt(cube.APIKey)
|
||||
cp.Cube = &cube
|
||||
}
|
||||
if c.E2B != nil {
|
||||
e2b := *c.E2B
|
||||
e2b.APIKey = encrypt(e2b.APIKey)
|
||||
cp.E2B = &e2b
|
||||
}
|
||||
// Keys stay readable so operators can still see which variables are set.
|
||||
if len(c.EnvVars) > 0 {
|
||||
envVars := make(map[string]string, len(c.EnvVars))
|
||||
for name, value := range c.EnvVars {
|
||||
envVars[name] = encrypt(value)
|
||||
}
|
||||
cp.EnvVars = envVars
|
||||
}
|
||||
// Injected headers are the sandbox-side way to call an API without the
|
||||
// credential ever entering the sandbox, so their values are secrets.
|
||||
if c.Network != nil {
|
||||
cp.Network = c.Network.CloneWithSecrets(encrypt)
|
||||
}
|
||||
|
||||
return json.Marshal(&cp)
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface. Secrets that cannot be decrypted
|
||||
// (missing or rotated SYSTEM_AES_KEY) are blanked and logged rather than
|
||||
// failing the whole load, matching ModelParameters.Scan — a tenant must stay
|
||||
// listable even if its sandbox credentials became unreadable.
|
||||
func (c *TenantSandboxConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(b, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decrypt := func(stored, label string) string {
|
||||
if stored == "" {
|
||||
return ""
|
||||
}
|
||||
if plain, ok := utils.DecryptStoredSecretLenient(stored); ok {
|
||||
return plain
|
||||
}
|
||||
log.Printf("[crypto] tenant_sandbox_config.%s: decrypt failed "+
|
||||
"(SYSTEM_AES_KEY missing/rotated?), treating as unconfigured", label)
|
||||
return ""
|
||||
}
|
||||
|
||||
if c.Cube != nil {
|
||||
c.Cube.APIKey = decrypt(c.Cube.APIKey, "cube.api_key")
|
||||
}
|
||||
if c.E2B != nil {
|
||||
c.E2B.APIKey = decrypt(c.E2B.APIKey, "e2b.api_key")
|
||||
}
|
||||
for name, stored := range c.EnvVars {
|
||||
c.EnvVars[name] = decrypt(stored, "env_vars."+name)
|
||||
}
|
||||
if c.Network != nil {
|
||||
// CloneWithSecrets has no rule/header context, so rotated-key failures
|
||||
// share one label rather than identifying the individual credential.
|
||||
c.Network = c.Network.CloneWithSecrets(func(stored string) string {
|
||||
return decrypt(stored, "network.injected_header")
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user