feat: add AI catalog description generation

This commit is contained in:
Codex
2026-08-29 16:42:56 +02:00
parent b0afba81ca
commit 376dd5a09d
76 changed files with 14860 additions and 102 deletions
+282 -2
View File
@@ -14,6 +14,7 @@ import (
"sort"
"strings"
"sync"
"unicode"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
"github.com/compose-spec/compose-go/v2/dotenv"
@@ -25,6 +26,10 @@ const installationFileName = "thothii-installation.yaml"
const maxEnvironmentFileBytes = 1 << 20
const maxMetadataSecretBundleBytes = 64 << 10
const maxMetadataGenerationModels = 64
const maxSecretSources = 32
var dotenvParseMu sync.Mutex
@@ -35,9 +40,34 @@ type descriptor struct {
EnvFile string `yaml:"envFile"`
WorkspaceRepository workspaceRepositoryDescriptor `yaml:"workspaceRepository"`
Authentication authenticationDescriptor `yaml:"authentication"`
MetadataGeneration metadataGenerationDescriptor `yaml:"metadataGeneration"`
Overrides []string `yaml:"overrides"`
}
type metadataGenerationDescriptor struct {
Default string `yaml:"default"`
Models []metadataGenerationModelDescriptor `yaml:"models"`
}
type metadataGenerationModelDescriptor struct {
ID string `yaml:"id"`
Label string `yaml:"label"`
LiteLLM liteLLMDescriptor `yaml:"litellm"`
APIKeyEnv string `yaml:"apiKeyEnv"`
}
type liteLLMDescriptor struct {
Provider string `yaml:"provider"`
Model string `yaml:"model"`
DisableThinking bool `yaml:"disableThinking"`
Endpoint *metadataEndpointDescriptor `yaml:"endpoint"`
}
type metadataEndpointDescriptor struct {
BaseURL string `yaml:"baseUrl"`
APIVersion string `yaml:"apiVersion"`
}
type authenticationDescriptor struct {
ConfigDirectory string `yaml:"configDirectory"`
RuntimeProjection *runtimeProjectionDescriptor `yaml:"runtimeProjection"`
@@ -75,6 +105,36 @@ type Authentication struct {
RuntimeProjection *RuntimeProjection
}
// MetadataGenerationEndpoint contains optional provider endpoint settings for one LiteLLM model.
type MetadataGenerationEndpoint struct {
BaseURL string
APIVersion string
}
// MetadataGenerationLiteLLM identifies the provider/model pair used by the internal completion helper.
type MetadataGenerationLiteLLM struct {
Provider string
Model string
DisableThinking bool
Endpoint *MetadataGenerationEndpoint
}
// MetadataGenerationModel is one selectable installation-owned metadata-generation model.
// APIKeyEnv is an optional reference only; credential values never enter Installation.
// An empty value is allowed only for a model with an explicit keyless endpoint.
type MetadataGenerationModel struct {
ID string
Label string
LiteLLM MetadataGenerationLiteLLM
APIKeyEnv string
}
// MetadataGeneration is the installation-owned model list and its default selection.
type MetadataGeneration struct {
Default string
Models []MetadataGenerationModel
}
// Installation is a validated local Compose installation. It intentionally contains paths, not
// environment values or secret content.
type Installation struct {
@@ -84,6 +144,7 @@ type Installation struct {
EnvFile string
WorkspaceRepository WorkspaceRepository
Authentication Authentication
MetadataGeneration MetadataGeneration
Overrides []string
}
@@ -137,6 +198,29 @@ func Load(path string) (Installation, error) {
GID: raw.Authentication.RuntimeProjection.GID,
}
}
metadataGeneration := MetadataGeneration{
Default: raw.MetadataGeneration.Default,
Models: make([]MetadataGenerationModel, 0, len(raw.MetadataGeneration.Models)),
}
for _, rawModel := range raw.MetadataGeneration.Models {
model := MetadataGenerationModel{
ID: rawModel.ID,
Label: rawModel.Label,
LiteLLM: MetadataGenerationLiteLLM{
Provider: rawModel.LiteLLM.Provider,
Model: rawModel.LiteLLM.Model,
DisableThinking: rawModel.LiteLLM.DisableThinking,
},
APIKeyEnv: rawModel.APIKeyEnv,
}
if rawModel.LiteLLM.Endpoint != nil {
model.LiteLLM.Endpoint = &MetadataGenerationEndpoint{
BaseURL: rawModel.LiteLLM.Endpoint.BaseURL,
APIVersion: rawModel.LiteLLM.Endpoint.APIVersion,
}
}
metadataGeneration.Models = append(metadataGeneration.Models, model)
}
installation := Installation{
Path: path,
Profile: raw.Profile,
@@ -147,13 +231,17 @@ func Load(path string) (Installation, error) {
Branch: raw.WorkspaceRepository.Branch,
Access: raw.WorkspaceRepository.Access,
},
Authentication: authentication,
Overrides: make([]string, 0, len(raw.Overrides)),
Authentication: authentication,
MetadataGeneration: metadataGeneration,
Overrides: make([]string, 0, len(raw.Overrides)),
}
values, err := installation.environmentValues()
if err != nil {
return Installation{}, errors.New("installation secret declarations could not be read")
}
if err := installation.validateMetadataGeneration(values); err != nil {
return Installation{}, err
}
if values["THT_AUTH_CONFIG_ROOT"] != installation.AuthenticationDirectory() {
return Installation{}, errors.New("authentication.configDirectory must match THT_AUTH_CONFIG_ROOT")
}
@@ -189,6 +277,198 @@ func Load(path string) (Installation, error) {
return installation, nil
}
func (i Installation) validateMetadataGeneration(values map[string]string) error {
if len(i.MetadataGeneration.Models) > maxMetadataGenerationModels {
return fmt.Errorf(
"metadataGeneration.models must contain at most %d entries",
maxMetadataGenerationModels,
)
}
seen := make(map[string]struct{}, len(i.MetadataGeneration.Models))
for index, model := range i.MetadataGeneration.Models {
if err := validateMetadataGenerationModel(index, model); err != nil {
return err
}
if _, exists := seen[model.ID]; exists {
return fmt.Errorf("duplicate metadataGeneration model id %q", model.ID)
}
seen[model.ID] = struct{}{}
}
if len(i.MetadataGeneration.Models) > 0 && i.MetadataGeneration.Default == "" {
return errors.New("metadataGeneration.default is required when models are configured")
}
if i.MetadataGeneration.Default != "" {
if _, exists := seen[i.MetadataGeneration.Default]; !exists {
return fmt.Errorf(
"metadataGeneration.default %q does not identify a configured model",
i.MetadataGeneration.Default,
)
}
}
if len(i.MetadataGeneration.Models) == 0 {
return nil
}
if values["THT_INSTALLATION_CONFIG_SOURCE"] != i.Path {
return errors.New("metadataGeneration requires THT_INSTALLATION_CONFIG_SOURCE to match the installation file")
}
requiresSecrets := false
for _, model := range i.MetadataGeneration.Models {
if model.APIKeyEnv != "" {
requiresSecrets = true
break
}
}
secrets := map[string]string{}
if requiresSecrets {
bundlePath := values["THT_SECRETS_FILE"]
if bundlePath == "" {
return errors.New("metadataGeneration keyed models require THT_SECRETS_FILE")
}
var err error
secrets, err = readMetadataGenerationSecrets(bundlePath)
if err != nil {
return err
}
}
for _, model := range i.MetadataGeneration.Models {
if model.APIKeyEnv == "" {
continue
}
value, exists := secrets[model.APIKeyEnv]
if !exists {
return fmt.Errorf(
"metadataGeneration model %q secret %q is missing from THT_SECRETS_FILE",
model.ID,
model.APIKeyEnv,
)
}
if !usableMetadataGenerationSecret(value) {
return fmt.Errorf(
"metadataGeneration model %q secret %q is unusable",
model.ID,
model.APIKeyEnv,
)
}
}
return nil
}
var metadataModelIDPattern = regexp.MustCompile(`^[a-z][a-z0-9._-]{0,63}$`)
var metadataProviderPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
var metadataProviderModelPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`)
var metadataAPIVersionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)
var metadataSecretBundleKeyPattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]{0,127}$`)
var metadataAPIKeyEnvironments = map[string]struct{}{
"THT_METADATA_API_KEY": {},
"ANTHROPIC_API_KEY": {},
"AZURE_API_KEY": {},
"GEMINI_API_KEY": {},
"DEEPSEEK_API_KEY": {},
"OPENAI_API_KEY": {},
"OPENROUTER_API_KEY": {},
"ZAI_API_KEY": {},
}
var metadataSecretBundleKeys = map[string]struct{}{
"THT_MODEL_API_KEY": {},
"THT_DWH_API_KEY": {},
"THT_VEC_API_KEY": {},
"THT_VEC_WRITE_API_KEY": {},
"THT_CA": {},
"THT_SSL_CA": {},
"THT_VECTOR_BOOTSTRAP_PASSWORD": {},
"THT_VECTOR_MIGRATOR_PASSWORD": {},
"THT_VECTOR_READER_PASSWORD": {},
"THT_VECTOR_WRITER_PASSWORD": {},
"PI_PROVIDER_API_KEY": {},
"THT_OIDC_CLIENT_SECRET": {},
"THT_AUTHENTIK_API_TOKEN": {},
"THT_METADATA_API_KEY": {},
"ANTHROPIC_API_KEY": {},
"AZURE_API_KEY": {},
"GEMINI_API_KEY": {},
"DEEPSEEK_API_KEY": {},
"OPENAI_API_KEY": {},
"OPENROUTER_API_KEY": {},
"ZAI_API_KEY": {},
}
func validateMetadataGenerationModel(index int, model MetadataGenerationModel) error {
prefix := fmt.Sprintf("metadataGeneration.models[%d]", index)
if !metadataModelIDPattern.MatchString(model.ID) {
return fmt.Errorf("%s.id is invalid", prefix)
}
if len(model.Label) == 0 || len(model.Label) > 128 || strings.TrimSpace(model.Label) != model.Label ||
strings.IndexFunc(model.Label, unicode.IsControl) >= 0 {
return fmt.Errorf("%s.label is invalid", prefix)
}
if !metadataProviderPattern.MatchString(model.LiteLLM.Provider) {
return fmt.Errorf("%s.litellm.provider is invalid", prefix)
}
if !metadataProviderModelPattern.MatchString(model.LiteLLM.Model) {
return fmt.Errorf("%s.litellm.model is invalid", prefix)
}
if model.APIKeyEnv == "" {
if model.LiteLLM.Endpoint == nil {
return fmt.Errorf("%s.apiKeyEnv is required unless an explicit keyless endpoint is configured", prefix)
}
} else {
if !metadataSecretBundleKeyPattern.MatchString(model.APIKeyEnv) {
return fmt.Errorf("%s.apiKeyEnv is invalid", prefix)
}
if _, allowed := metadataAPIKeyEnvironments[model.APIKeyEnv]; !allowed {
return fmt.Errorf("%s.apiKeyEnv is invalid", prefix)
}
}
if endpoint := model.LiteLLM.Endpoint; endpoint != nil {
parsed, err := url.Parse(endpoint.BaseURL)
if err != nil || strings.TrimSpace(endpoint.BaseURL) != endpoint.BaseURL ||
(parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" ||
parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
return fmt.Errorf("%s.litellm.endpoint.baseUrl is invalid", prefix)
}
if endpoint.APIVersion != "" && !metadataAPIVersionPattern.MatchString(endpoint.APIVersion) {
return fmt.Errorf("%s.litellm.endpoint.apiVersion is invalid", prefix)
}
}
if model.LiteLLM.DisableThinking && model.LiteLLM.Endpoint == nil {
return fmt.Errorf("%s.litellm.disableThinking requires an explicit endpoint", prefix)
}
return nil
}
func readMetadataGenerationSecrets(path string) (map[string]string, error) {
contents, err := safeio.ReadCanonicalPrivateRegular(path, maxMetadataSecretBundleBytes)
if err != nil {
return nil, errors.New("metadataGeneration secrets in THT_SECRETS_FILE are unavailable")
}
values := make(map[string]string)
for _, raw := range strings.Split(string(contents), "\n") {
line := strings.TrimSuffix(raw, "\r")
if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
continue
}
key, value, found := strings.Cut(line, "=")
if !found || !metadataSecretBundleKeyPattern.MatchString(key) {
return nil, errors.New("metadataGeneration secrets in THT_SECRETS_FILE are invalid")
}
if _, allowed := metadataSecretBundleKeys[key]; !allowed {
return nil, errors.New("metadataGeneration secrets in THT_SECRETS_FILE are invalid")
}
if _, duplicate := values[key]; duplicate {
return nil, errors.New("metadataGeneration secrets in THT_SECRETS_FILE contain duplicate keys")
}
values[key] = value
}
return values, nil
}
func usableMetadataGenerationSecret(value string) bool {
return len(value) > 0 && len(value) <= 16*1024 && strings.TrimSpace(value) == value &&
strings.IndexFunc(value, func(character rune) bool {
return unicode.IsSpace(character) || unicode.IsControl(character)
}) < 0
}
// AuthenticationDirectory returns the descriptor-owned, non-secret authentication root.
func (i Installation) AuthenticationDirectory() string { return i.Authentication.ConfigDirectory }
@@ -0,0 +1,454 @@
package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
)
func TestLoadAcceptsMetadataGenerationModels(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(secretBundle, []byte("OPENAI_API_KEY=provider-secret\n"), 0o600); err != nil {
t.Fatal(err)
}
appendFile(t, envFile, strings.Join([]string{
"THT_SECRETS_FILE=" + strconv.Quote(secretBundle),
"THT_INSTALLATION_CONFIG_SOURCE=" + strconv.Quote(installationPath),
}, "\n")+"\n")
appendFile(t, installationPath, `metadataGeneration:
default: openai-mini
models:
- id: openai-mini
label: OpenAI Mini
litellm:
provider: openai
model: gpt-4.1-mini
endpoint:
baseUrl: https://api.openai.example/v1
apiVersion: "2026-08-01"
apiKeyEnv: OPENAI_API_KEY
`)
installation, err := Load(installationPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if installation.MetadataGeneration.Default != "openai-mini" {
t.Fatalf("metadata default = %q", installation.MetadataGeneration.Default)
}
if len(installation.MetadataGeneration.Models) != 1 {
t.Fatalf("metadata models = %#v", installation.MetadataGeneration.Models)
}
model := installation.MetadataGeneration.Models[0]
if model.ID != "openai-mini" || model.Label != "OpenAI Mini" ||
model.LiteLLM.Provider != "openai" || model.LiteLLM.Model != "gpt-4.1-mini" ||
model.LiteLLM.Endpoint == nil || model.LiteLLM.Endpoint.BaseURL != "https://api.openai.example/v1" ||
model.LiteLLM.Endpoint.APIVersion != "2026-08-01" || model.APIKeyEnv != "OPENAI_API_KEY" {
t.Fatalf("metadata model = %#v", model)
}
if strings.Contains(strings.TrimSpace(model.APIKeyEnv), "provider-secret") {
t.Fatal("installation model exposed the credential value")
}
}
func TestLoadAcceptsMixedKeyedAndKeylessMetadataGenerationModels(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(
secretBundle,
[]byte("DEEPSEEK_API_KEY=deepseek-secret\nZAI_API_KEY=zai-secret\n"),
0o600,
); err != nil {
t.Fatal(err)
}
appendFile(t, envFile, "THT_SECRETS_FILE="+strconv.Quote(secretBundle)+"\n"+
"THT_INSTALLATION_CONFIG_SOURCE="+strconv.Quote(installationPath)+"\n")
appendFile(t, installationPath, `metadataGeneration:
default: glm-53
models:
- id: deepseek-v4-pro
label: DeepSeek V4 Pro
litellm: {provider: deepseek, model: deepseek-v4-pro}
apiKeyEnv: DEEPSEEK_API_KEY
- id: glm-53
label: GLM 5.3
litellm:
provider: openai
model: glm-5.3
endpoint: {baseUrl: https://api.z.ai/api/coding/paas/v4}
apiKeyEnv: ZAI_API_KEY
- id: qwen-36
label: Qwen 3.6
litellm:
provider: openai
model: qwen3.6-35b-a3b
disableThinking: true
endpoint: {baseUrl: https://models.internal.example/v1}
`)
installation, err := Load(installationPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if len(installation.MetadataGeneration.Models) != 3 {
t.Fatalf("metadata models = %#v", installation.MetadataGeneration.Models)
}
qwen := installation.MetadataGeneration.Models[2]
if qwen.APIKeyEnv != "" || !qwen.LiteLLM.DisableThinking || qwen.LiteLLM.Endpoint == nil {
t.Fatalf("keyless qwen model = %#v", qwen)
}
}
func TestLoadAcceptsOnlyExplicitKeylessMetadataGenerationModelWithoutBundle(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
appendFile(t, envFile, "THT_INSTALLATION_CONFIG_SOURCE="+strconv.Quote(installationPath)+"\n")
appendFile(t, installationPath, `metadataGeneration:
default: qwen-36
models:
- id: qwen-36
label: Qwen 3.6
litellm:
provider: openai
model: qwen3.6-35b-a3b
disableThinking: true
endpoint: {baseUrl: https://models.internal.example/v1}
`)
if _, err := Load(installationPath); err != nil {
t.Fatalf("Load() error = %v", err)
}
}
func TestLoadRejectsTooManyMetadataGenerationModels(t *testing.T) {
installationPath, _, _, _ := writeInstallation(t, "local")
var configuration strings.Builder
configuration.WriteString("metadataGeneration:\n default: model-0\n models:\n")
for index := 0; index <= maxMetadataGenerationModels; index++ {
fmt.Fprintf(&configuration, ` - id: model-%d
label: Model %d
litellm: {provider: openai, model: gpt-4.1-mini}
apiKeyEnv: OPENAI_API_KEY
`, index, index)
}
appendFile(t, installationPath, configuration.String())
_, err := Load(installationPath)
want := fmt.Sprintf(
"metadataGeneration.models must contain at most %d entries",
maxMetadataGenerationModels,
)
if err == nil || !strings.Contains(err.Error(), want) {
t.Fatalf("Load() error = %v, want %q", err, want)
}
}
func TestLoadRejectsDuplicateMetadataGenerationModelIDs(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(secretBundle, []byte("OPENAI_API_KEY=provider-secret\n"), 0o600); err != nil {
t.Fatal(err)
}
appendFile(t, envFile, "THT_SECRETS_FILE="+strconv.Quote(secretBundle)+"\n"+
"THT_INSTALLATION_CONFIG_SOURCE="+strconv.Quote(installationPath)+"\n")
appendFile(t, installationPath, `metadataGeneration:
default: openai-mini
models:
- id: openai-mini
label: OpenAI Mini
litellm: {provider: openai, model: gpt-4.1-mini}
apiKeyEnv: OPENAI_API_KEY
- id: openai-mini
label: Duplicate
litellm: {provider: openai, model: gpt-4.1}
apiKeyEnv: OPENAI_API_KEY
`)
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), `duplicate metadataGeneration model id "openai-mini"`) {
t.Fatalf("Load() error = %v, want actionable duplicate-id error", err)
}
}
func TestLoadRejectsMissingOrUnknownMetadataGenerationDefault(t *testing.T) {
for _, test := range []struct {
name string
defaultYAML string
want string
}{
{
name: "missing",
want: "metadataGeneration.default is required when models are configured",
},
{
name: "unknown",
defaultYAML: " default: unavailable\n",
want: `metadataGeneration.default "unavailable" does not identify a configured model`,
},
} {
t.Run(test.name, func(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(secretBundle, []byte("OPENAI_API_KEY=provider-secret\n"), 0o600); err != nil {
t.Fatal(err)
}
appendFile(t, envFile, "THT_SECRETS_FILE="+strconv.Quote(secretBundle)+"\n"+
"THT_INSTALLATION_CONFIG_SOURCE="+strconv.Quote(installationPath)+"\n")
appendFile(t, installationPath, "metadataGeneration:\n"+test.defaultYAML+` models:
- id: openai-mini
label: OpenAI Mini
litellm: {provider: openai, model: gpt-4.1-mini}
apiKeyEnv: OPENAI_API_KEY
`)
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Load() error = %v, want %q", err, test.want)
}
})
}
}
func TestLoadRejectsMalformedMetadataGenerationModelSettings(t *testing.T) {
validPrefix := ` - id: openai-mini
label: OpenAI Mini
litellm:
`
for _, test := range []struct {
name string
model string
want string
}{
{
name: "unstable id",
model: strings.Replace(validPrefix, "openai-mini", "OpenAI Mini", 1) +
" provider: openai\n model: gpt-4.1-mini\n apiKeyEnv: OPENAI_API_KEY\n",
want: "metadataGeneration.models[0].id is invalid",
},
{
name: "blank label",
model: strings.Replace(validPrefix, "OpenAI Mini", `" "`, 1) +
" provider: openai\n model: gpt-4.1-mini\n apiKeyEnv: OPENAI_API_KEY\n",
want: "metadataGeneration.models[0].label is invalid",
},
{
name: "provider",
model: validPrefix + " provider: open ai\n model: gpt-4.1-mini\n apiKeyEnv: OPENAI_API_KEY\n",
want: "metadataGeneration.models[0].litellm.provider is invalid",
},
{
name: "model",
model: validPrefix + " provider: openai\n model: \" gpt-4.1-mini\"\n apiKeyEnv: OPENAI_API_KEY\n",
want: "metadataGeneration.models[0].litellm.model is invalid",
},
{
name: "endpoint",
model: validPrefix + " provider: openai\n model: gpt-4.1-mini\n" +
" endpoint:\n baseUrl: https://operator@api.example/v1\n apiKeyEnv: OPENAI_API_KEY\n",
want: "metadataGeneration.models[0].litellm.endpoint.baseUrl is invalid",
},
{
name: "api version",
model: validPrefix + " provider: openai\n model: gpt-4.1-mini\n" +
" endpoint:\n baseUrl: https://api.example/v1\n apiVersion: \"bad version\"\n apiKeyEnv: OPENAI_API_KEY\n",
want: "metadataGeneration.models[0].litellm.endpoint.apiVersion is invalid",
},
} {
t.Run(test.name, func(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(secretBundle, []byte("OPENAI_API_KEY=provider-secret\n"), 0o600); err != nil {
t.Fatal(err)
}
appendFile(t, envFile, "THT_SECRETS_FILE="+strconv.Quote(secretBundle)+"\n"+
"THT_INSTALLATION_CONFIG_SOURCE="+strconv.Quote(installationPath)+"\n")
appendFile(t, installationPath, "metadataGeneration:\n default: openai-mini\n models:\n"+test.model)
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Load() error = %v, want %q", err, test.want)
}
})
}
}
func TestLoadRejectsMissingOrUnusableMetadataGenerationSecrets(t *testing.T) {
for _, test := range []struct {
name string
apiKeyEnvYAML string
bundle string
declareBundle bool
want string
}{
{
name: "missing reference",
apiKeyEnvYAML: "",
bundle: "OPENAI_API_KEY=provider-secret\n",
declareBundle: true,
want: "metadataGeneration.models[0].apiKeyEnv is required unless an explicit keyless endpoint is configured",
},
{
name: "malformed reference",
apiKeyEnvYAML: " apiKeyEnv: openai-api-key\n",
bundle: "OPENAI_API_KEY=provider-secret\n",
declareBundle: true,
want: "metadataGeneration.models[0].apiKeyEnv is invalid",
},
{
name: "unallowed reference",
apiKeyEnvYAML: " apiKeyEnv: THT_DWH_API_KEY\n",
bundle: "THT_DWH_API_KEY=dwh-secret\n",
declareBundle: true,
want: "metadataGeneration.models[0].apiKeyEnv is invalid",
},
{
name: "bundle not declared",
apiKeyEnvYAML: " apiKeyEnv: OPENAI_API_KEY\n",
bundle: "OPENAI_API_KEY=provider-secret\n",
want: "metadataGeneration keyed models require THT_SECRETS_FILE",
},
{
name: "reference absent from bundle",
apiKeyEnvYAML: " apiKeyEnv: OPENAI_API_KEY\n",
bundle: "THT_DWH_API_KEY=dwh-secret\n",
declareBundle: true,
want: "metadataGeneration model \"openai-mini\" secret \"OPENAI_API_KEY\" is missing",
},
{
name: "unusable value",
apiKeyEnvYAML: " apiKeyEnv: OPENAI_API_KEY\n",
bundle: "OPENAI_API_KEY=secret with whitespace\n",
declareBundle: true,
want: "metadataGeneration model \"openai-mini\" secret \"OPENAI_API_KEY\" is unusable",
},
} {
t.Run(test.name, func(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(secretBundle, []byte(test.bundle), 0o600); err != nil {
t.Fatal(err)
}
environment := "THT_INSTALLATION_CONFIG_SOURCE=" + strconv.Quote(installationPath) + "\n"
if test.declareBundle {
environment += "THT_SECRETS_FILE=" + strconv.Quote(secretBundle) + "\n"
}
appendFile(t, envFile, environment)
appendFile(t, installationPath, `metadataGeneration:
default: openai-mini
models:
- id: openai-mini
label: OpenAI Mini
litellm: {provider: openai, model: gpt-4.1-mini}
`+test.apiKeyEnvYAML)
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Load() error = %v, want %q", err, test.want)
}
if strings.Contains(strings.ToLower(err.Error()), "secret with whitespace") ||
strings.Contains(strings.ToLower(err.Error()), "provider-secret") {
t.Fatalf("Load() exposed secret content: %v", err)
}
})
}
}
func TestLoadRejectsUnprotectedMetadataGenerationSecretBundle(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX mode assertion; Windows ACL coverage lives in safeio")
}
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(secretBundle, []byte("OPENAI_API_KEY=provider-secret\n"), 0o644); err != nil {
t.Fatal(err)
}
appendFile(t, envFile, "THT_SECRETS_FILE="+strconv.Quote(secretBundle)+"\n"+
"THT_INSTALLATION_CONFIG_SOURCE="+strconv.Quote(installationPath)+"\n")
appendFile(t, installationPath, `metadataGeneration:
default: openai-mini
models:
- id: openai-mini
label: OpenAI Mini
litellm: {provider: openai, model: gpt-4.1-mini}
apiKeyEnv: OPENAI_API_KEY
`)
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), "metadataGeneration secrets in THT_SECRETS_FILE are unavailable") {
t.Fatalf("Load() error = %v, want protected-bundle error", err)
}
}
func TestLoadRejectsUnknownMetadataGenerationSecretBundleKeys(t *testing.T) {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(
secretBundle,
[]byte("OPENAI_API_KEY=provider-secret\nUNRECOGNIZED_API_KEY=unknown-secret\n"),
0o600,
); err != nil {
t.Fatal(err)
}
appendFile(t, envFile, "THT_SECRETS_FILE="+strconv.Quote(secretBundle)+"\n"+
"THT_INSTALLATION_CONFIG_SOURCE="+strconv.Quote(installationPath)+"\n")
appendFile(t, installationPath, `metadataGeneration:
default: openai-mini
models:
- id: openai-mini
label: OpenAI Mini
litellm: {provider: openai, model: gpt-4.1-mini}
apiKeyEnv: OPENAI_API_KEY
`)
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), "metadataGeneration secrets in THT_SECRETS_FILE are invalid") {
t.Fatalf("Load() error = %v, want invalid-bundle error", err)
}
if strings.Contains(err.Error(), "UNRECOGNIZED_API_KEY") || strings.Contains(err.Error(), "unknown-secret") {
t.Fatalf("Load() exposed rejected bundle content: %v", err)
}
}
func TestLoadRequiresConfiguredMetadataGenerationToUseTheSameMountedDescriptor(t *testing.T) {
for _, configuredSource := range []string{"", "/different/thothii-installation.yaml"} {
installationPath, _, envFile, _ := writeInstallation(t, "local")
secretBundle := filepath.Join(filepath.Dir(installationPath), "thothii.secrets")
if err := os.WriteFile(secretBundle, []byte("OPENAI_API_KEY=provider-secret\n"), 0o600); err != nil {
t.Fatal(err)
}
environment := "THT_SECRETS_FILE=" + strconv.Quote(secretBundle) + "\n"
if configuredSource != "" {
environment += "THT_INSTALLATION_CONFIG_SOURCE=" + strconv.Quote(configuredSource) + "\n"
}
appendFile(t, envFile, environment)
appendFile(t, installationPath, `metadataGeneration:
default: openai-mini
models:
- id: openai-mini
label: OpenAI Mini
litellm: {provider: openai, model: gpt-4.1-mini}
apiKeyEnv: OPENAI_API_KEY
`)
_, err := Load(installationPath)
if err == nil || !strings.Contains(err.Error(), "metadataGeneration requires THT_INSTALLATION_CONFIG_SOURCE to match the installation file") {
t.Fatalf("Load() source %q error = %v", configuredSource, err)
}
}
}
func appendFile(t *testing.T, path, contents string) {
t.Helper()
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
if err != nil {
t.Fatal(err)
}
defer file.Close()
if _, err := file.WriteString(contents); err != nil {
t.Fatal(err)
}
}
+1
View File
@@ -343,6 +343,7 @@ func render(root, descriptorPath string, value answers) ([]byte, []byte, error)
"THT_WORKSPACE_GIT_BRANCH=" + dotenvValue(value.workspaceBranch),
"THT_WORKSPACE_INSTALLATION_ID=" + dotenvValue(value.installationID),
"THT_AUTH_CONFIG_ROOT=" + dotenvValue(descriptor.Authentication.ConfigDirectory),
"THT_INSTALLATION_CONFIG_SOURCE=" + dotenvValue(descriptorPath),
"THT_SECRETS_FILE=" + dotenvValue(value.secretsFile),
"PI_AUTH_FILE=" + dotenvValue(value.piAuthFile),
"THOTH_HTTP_PORT=8080", "THOTH_CORE_HTTP_PORT=8787", "MAX_PI_PROCESSES=4",
@@ -0,0 +1,31 @@
package setup
import (
"os"
"strconv"
"strings"
"testing"
)
func TestEnsureFilesProjectsItsInstallationYAMLToTheApplication(t *testing.T) {
root := newProject(t, "metadata generation config")
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
result, err := EnsureFiles(Request{
ProjectRoot: root, InstallationID: "metadata", Profile: "local", NonInteractive: true,
}, strings.NewReader(""), ioDiscard{})
if err != nil {
t.Fatal(err)
}
environment, err := os.ReadFile(result.EnvironmentPath)
if err != nil {
t.Fatal(err)
}
want := "THT_INSTALLATION_CONFIG_SOURCE=" + strconv.Quote(result.DescriptorPath)
if !strings.Contains(string(environment), want) {
t.Fatalf("generated environment does not project installation YAML: want %q", want)
}
if strings.Contains(string(environment), "PI_MODEL") || strings.Contains(string(environment), "llm_policy") {
t.Fatal("metadata-generation projection was coupled to Pi or workspace policy")
}
}