From 79ac94ad1c798e78da63c3b3a5c2d5625d544e31 Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary Date: Mon, 22 Jun 2026 18:37:36 +0530 Subject: [PATCH] feat: Allow custom schema URL overrides for config.json --- .../config-json/schema-reference.mdx | 4 +- helm-charts/bifrost/Chart.yaml | 2 +- helm-charts/bifrost/README.md | 6 +- helm-charts/bifrost/templates/_helpers.tpl | 2 +- helm-charts/bifrost/templates/deployment.yaml | 4 + helm-charts/bifrost/templates/stateful.yaml | 5 +- helm-charts/bifrost/values.schema.json | 5 + helm-charts/bifrost/values.yaml | 7 ++ terraform/modules/bifrost/README.md | 4 +- terraform/modules/bifrost/main.tf | 7 +- .../bifrost/tests/config_merging.tftest.hcl | 17 ++- terraform/modules/bifrost/tests/setup/main.tf | 39 +++---- .../modules/bifrost/tests/setup/variables.tf | 2 + terraform/modules/bifrost/variables.tf | 6 ++ transports/bifrost-http/lib/config.go | 6 +- transports/bifrost-http/lib/validator.go | 102 +++++++++++++++--- transports/bifrost-http/lib/validator_test.go | 96 +++++++++++++++++ transports/config.schema.json | 4 +- 18 files changed, 270 insertions(+), 48 deletions(-) diff --git a/docs/deployment-guides/config-json/schema-reference.mdx b/docs/deployment-guides/config-json/schema-reference.mdx index a3d38663e73..0e3940ad368 100644 --- a/docs/deployment-guides/config-json/schema-reference.mdx +++ b/docs/deployment-guides/config-json/schema-reference.mdx @@ -5,7 +5,7 @@ icon: "brackets-curly" --- -The live schema is published at [`https://www.getbifrost.ai/schema`](https://www.getbifrost.ai/schema). Add `"$schema": "https://www.getbifrost.ai/schema"` to your `config.json` for IDE autocomplete and inline validation. +The live schema is published at [`https://www.getbifrost.ai/schema`](https://www.getbifrost.ai/schema). Add `"$schema": "https://www.getbifrost.ai/schema"` to your `config.json` for IDE autocomplete and inline validation, or point it to a mirrored HTTP(S) URL, `file://` URL, or filesystem path in isolated deployments. You can also set the `BIFROST_SCHEMA_URL` environment variable, which takes precedence over the `$schema` value. When mirroring, snapshot a schema published by a Bifrost release that supports custom `$schema` values; older schema copies pin `$schema` to the public URL and will flag a mirrored location as invalid in IDEs. This page is a concise reference for every top-level key in `config.json`. Click the **Guide** links for full field-by-field documentation. @@ -16,7 +16,7 @@ This page is a concise reference for every top-level key in `config.json`. Click | Key | Type | Description | Guide | |-----|------|-------------|-------| -| `$schema` | string | Schema URL for IDE validation. Set to `"https://www.getbifrost.ai/schema"` | - | +| `$schema` | string | Schema location for IDE validation. Defaults to `"https://www.getbifrost.ai/schema"`; isolated deployments can use a mirrored URL, `file://` URL, or filesystem path. | - | | `version` | integer | Compatibility switch for empty allow-list arrays. Omit for current v2 semantics. | [`version`](#version) | | `source_of_truth` | string | Startup reconciliation mode for DB-backed `config.json`: `"split"` or `"config.json"` | [Source of Truth](/deployment-guides/config-json/source-of-truth) | | `encryption_key` | string | Optional AES-256 key (derived via Argon2id). Accepts `env.VAR` prefix and is also read from `BIFROST_ENCRYPTION_KEY`. If omitted, data is stored in plaintext. | [Client](/deployment-guides/config-json/client#encryption-key) | diff --git a/helm-charts/bifrost/Chart.yaml b/helm-charts/bifrost/Chart.yaml index 2d15a2ce8f4..45bbd5f0a82 100644 --- a/helm-charts/bifrost/Chart.yaml +++ b/helm-charts/bifrost/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: bifrost description: A Helm chart for deploying Bifrost - AI Gateway with unified interface for multiple providers type: application -version: 2.1.26 +version: 2.1.27 appVersion: "1.5.12" keywords: - ai diff --git a/helm-charts/bifrost/README.md b/helm-charts/bifrost/README.md index 841ae25e95c..e87ac861f9e 100644 --- a/helm-charts/bifrost/README.md +++ b/helm-charts/bifrost/README.md @@ -4,10 +4,14 @@ Official Helm charts for deploying [Bifrost](https://github.com/maximhq/bifrost) - a high-performance AI gateway with unified interface for multiple providers. -**Latest Version:** 2.1.26 +**Latest Version:** 2.1.27 ## Changelog +### 2.1.27 + +- Added `bifrost.schemaUrl` to override the generated `config.json` `$schema` location for isolated deployments. It accepts HTTP(S), `file://`, or filesystem paths. When set, it is also exported as `BIFROST_SCHEMA_URL` in the pod; when empty (default), the env var is not injected and the public schema URL is used. + ### 2.1.26 - Added `bifrost.client.mcpServerAuthMode` (`headers` | `both` | `oauth`) and `bifrost.client.oauth2ServerConfig` (`issuerUrl`, `authCodeTtl`, `accessTokenTtl`, `disableVkIdentity`) to control how `/mcp` authenticates inbound MCP clients. Renders into `client.mcp_server_auth_mode` and `client.oauth2_server_config`. `authCodeTtl` is capped at 900 seconds. diff --git a/helm-charts/bifrost/templates/_helpers.tpl b/helm-charts/bifrost/templates/_helpers.tpl index 3b14298e028..cb933985ac9 100644 --- a/helm-charts/bifrost/templates/_helpers.tpl +++ b/helm-charts/bifrost/templates/_helpers.tpl @@ -201,7 +201,7 @@ false {{- end -}} {{- define "bifrost.config" -}} -{{- $config := dict "$schema" "https://www.getbifrost.ai/schema" }} +{{- $config := dict "$schema" (.Values.bifrost.schemaUrl | default "https://www.getbifrost.ai/schema") }} {{- if .Values.bifrost.sourceOfTruth }} {{- $_ := set $config "source_of_truth" .Values.bifrost.sourceOfTruth }} {{- end }} diff --git a/helm-charts/bifrost/templates/deployment.yaml b/helm-charts/bifrost/templates/deployment.yaml index 0a1c608961e..e6a99f14359 100644 --- a/helm-charts/bifrost/templates/deployment.yaml +++ b/helm-charts/bifrost/templates/deployment.yaml @@ -97,6 +97,10 @@ spec: value: {{ .Values.bifrost.logLevel | quote }} - name: LOG_STYLE value: {{ .Values.bifrost.logStyle | quote }} + {{- if .Values.bifrost.schemaUrl }} + - name: BIFROST_SCHEMA_URL + value: {{ .Values.bifrost.schemaUrl | quote }} + {{- end }} {{- if .Values.bifrost.encryptionKeySecret.name }} - name: BIFROST_ENCRYPTION_KEY valueFrom: diff --git a/helm-charts/bifrost/templates/stateful.yaml b/helm-charts/bifrost/templates/stateful.yaml index 6e9e0dcbed2..32d3c1be848 100644 --- a/helm-charts/bifrost/templates/stateful.yaml +++ b/helm-charts/bifrost/templates/stateful.yaml @@ -93,6 +93,10 @@ spec: value: {{ .Values.bifrost.logLevel | quote }} - name: LOG_STYLE value: {{ .Values.bifrost.logStyle | quote }} + {{- if .Values.bifrost.schemaUrl }} + - name: BIFROST_SCHEMA_URL + value: {{ .Values.bifrost.schemaUrl | quote }} + {{- end }} {{- if .Values.bifrost.encryptionKeySecret.name }} - name: BIFROST_ENCRYPTION_KEY valueFrom: @@ -319,4 +323,3 @@ spec: requests: storage: {{ .Values.storage.persistence.size }} {{- end }} - diff --git a/helm-charts/bifrost/values.schema.json b/helm-charts/bifrost/values.schema.json index 6005c8084e8..6a56961f5d8 100644 --- a/helm-charts/bifrost/values.schema.json +++ b/helm-charts/bifrost/values.schema.json @@ -303,6 +303,11 @@ "description": "Short label (max 10 characters) displayed in the management UI sidebar to identify the environment (e.g. \"staging\", \"prod\"). Written into config.json as env_label.", "maxLength": 10 }, + "schemaUrl": { + "type": "string", + "default": "", + "description": "Schema location written to config.json $schema and exported as BIFROST_SCHEMA_URL. Leave empty to use the default public URL without injecting the env var. Set for isolated deployments that mirror the Bifrost schema internally. Accepts an HTTP(S) URL, file:// URL, or filesystem path." + }, "sourceOfTruth": { "type": "string", "enum": ["split", "config.json"], diff --git a/helm-charts/bifrost/values.yaml b/helm-charts/bifrost/values.yaml index 1271af794ea..53b53f834a4 100644 --- a/helm-charts/bifrost/values.yaml +++ b/helm-charts/bifrost/values.yaml @@ -206,6 +206,13 @@ bifrost: logStyle: json # envLabel: staging # Short label (max 10 chars) shown in the UI sidebar to identify the environment + # Schema location written to config.json $schema and exported as BIFROST_SCHEMA_URL. + # Leave empty to use the default (https://www.getbifrost.ai/schema); set it only for + # isolated deployments that mirror the schema internally. When empty, the env var is + # not injected, so a $schema override in a mounted config.json still takes effect. + # Accepts an HTTP(S) URL, file:// URL, or filesystem path. + schemaUrl: "" + # Controls how config.json is reconciled with the database on startup. # "split" (default): existing merge behavior — file and DB rows coexist. # "config.json": sections explicitly present in the file are authoritative; diff --git a/terraform/modules/bifrost/README.md b/terraform/modules/bifrost/README.md index 93db012b628..e73ca66fce0 100644 --- a/terraform/modules/bifrost/README.md +++ b/terraform/modules/bifrost/README.md @@ -108,6 +108,8 @@ The module supports three ways to provide Bifrost configuration, which are merge Individual variables always take precedence over the base config. This lets you keep secrets out of your config file and inject them via Terraform variables or a secrets manager. +Set `schema_url` to write a mirrored schema location into `$schema` for isolated deployments. It accepts an HTTP(S) URL, `file://` URL, or filesystem path, and defaults to `https://www.getbifrost.ai/schema`. + ### Configurable Sections All 18 top-level properties from the [Bifrost config schema](../../../transports/config.schema.json) are exposed as Terraform variables: @@ -157,7 +159,7 @@ Test files are in `tests/` and cover all 7 deployment targets: | File | Coverage | |-----------------------------|--------------------------------------------------| | `root_validation.tftest.hcl`| Valid/invalid cloud_provider + service combos | -| `config_merging.tftest.hcl` | Config precedence, schema URL injection | +| `config_merging.tftest.hcl` | Config precedence, schema location injection | | `aws_ecs.tftest.hcl` | ECS: ALB, autoscaling, private subnets | | `aws_eks.tftest.hcl` | EKS: cluster, HPA, ingress, HTTPS, nodes | | `aws_shared.tftest.hcl` | VPC/SG creation vs existing, ECS isolation | diff --git a/terraform/modules/bifrost/main.tf b/terraform/modules/bifrost/main.tf index 91ea5385ade..f3485a10736 100644 --- a/terraform/modules/bifrost/main.tf +++ b/terraform/modules/bifrost/main.tf @@ -20,10 +20,15 @@ locals { {} ) + # Schema precedence: explicit schema_url var > $schema already in base config > public default. + # Only fall through to the public URL when the base config has no $schema, so a mirrored-schema + # setup provided via config_json/config_json_file is never silently rewritten. + schema_url = coalesce(var.schema_url, try(local.base_config["$schema"], null), "https://www.getbifrost.ai/schema") + # Terraform variable overrides (non-null values only) overrides = { for k, v in { - "$schema" = "https://www.getbifrost.ai/schema" + "$schema" = local.schema_url encryption_key = var.encryption_key auth_config = var.auth_config client = var.client diff --git a/terraform/modules/bifrost/tests/config_merging.tftest.hcl b/terraform/modules/bifrost/tests/config_merging.tftest.hcl index 92e070bdcaf..c19c2dcd08b 100644 --- a/terraform/modules/bifrost/tests/config_merging.tftest.hcl +++ b/terraform/modules/bifrost/tests/config_merging.tftest.hcl @@ -70,7 +70,22 @@ run "schema_url_injected" { } assert { condition = jsondecode(output.config_json)["$schema"] == "https://www.getbifrost.ai/schema" - error_message = "Schema URL should always be injected" + error_message = "Schema location should always be injected" + } +} + +run "schema_url_override" { + command = plan + module { source = "./tests/setup" } + variables { + cloud_provider = "aws" + service = "ecs" + region = "us-east-1" + schema_url = "https://schema.internal/bifrost" + } + assert { + condition = jsondecode(output.config_json)["$schema"] == "https://schema.internal/bifrost" + error_message = "schema_url should override the default schema location" } } diff --git a/terraform/modules/bifrost/tests/setup/main.tf b/terraform/modules/bifrost/tests/setup/main.tf index ecab6167e22..f1c48468822 100644 --- a/terraform/modules/bifrost/tests/setup/main.tf +++ b/terraform/modules/bifrost/tests/setup/main.tf @@ -38,25 +38,26 @@ module "bifrost" { service = var.service # Config - config_json = var.config_json - config_json_file = var.config_json_file - encryption_key = var.encryption_key - auth_config = var.auth_config - client = var.client - framework = var.framework - providers_config = var.providers_config - governance = var.governance - mcp = var.mcp - vector_store = var.vector_store - config_store = var.config_store - logs_store = var.logs_store - cluster_config = var.cluster_config - scim_config = var.scim_config + config_json = var.config_json + config_json_file = var.config_json_file + schema_url = var.schema_url + encryption_key = var.encryption_key + auth_config = var.auth_config + client = var.client + framework = var.framework + providers_config = var.providers_config + governance = var.governance + mcp = var.mcp + vector_store = var.vector_store + config_store = var.config_store + logs_store = var.logs_store + cluster_config = var.cluster_config + scim_config = var.scim_config load_balancer_config = var.load_balancer_config - guardrails_config = var.guardrails_config - plugins = var.plugins - audit_logs = var.audit_logs - websocket = var.websocket + guardrails_config = var.guardrails_config + plugins = var.plugins + audit_logs = var.audit_logs + websocket = var.websocket # Image image_tag = var.image_tag @@ -97,7 +98,7 @@ module "bifrost" { volume_size_gb = var.volume_size_gb # Cloud-specific - gcp_project_id = var.gcp_project_id + gcp_project_id = var.gcp_project_id azure_resource_group_name = var.azure_resource_group_name # Generic K8s diff --git a/terraform/modules/bifrost/tests/setup/variables.tf b/terraform/modules/bifrost/tests/setup/variables.tf index 63d8fcadc10..fc091c1632d 100644 --- a/terraform/modules/bifrost/tests/setup/variables.tf +++ b/terraform/modules/bifrost/tests/setup/variables.tf @@ -13,6 +13,8 @@ variable "config_json" { variable "config_json_file" { default = null } +variable "schema_url" { default = null } + variable "encryption_key" { type = string default = null diff --git a/terraform/modules/bifrost/variables.tf b/terraform/modules/bifrost/variables.tf index dbd7103f27c..687cc9ef399 100644 --- a/terraform/modules/bifrost/variables.tf +++ b/terraform/modules/bifrost/variables.tf @@ -31,6 +31,12 @@ variable "config_json_file" { default = null } +variable "schema_url" { + description = "Schema location written to config.json $schema. Override for isolated deployments that mirror the Bifrost schema internally. Accepts an HTTP(S) URL, file:// URL, or filesystem path. When null, an existing $schema in config_json/config_json_file is preserved, otherwise the public Bifrost schema URL is injected." + type = string + default = null +} + # --- Config: individual sections (each mirrors a top-level property from config.schema.json) --- variable "encryption_key" { description = "Encryption key for sensitive data. Accepts any string; a secure 32-byte AES-256 key will be derived using Argon2id KDF." diff --git a/transports/bifrost-http/lib/config.go b/transports/bifrost-http/lib/config.go index 61bc0cf061c..1302eb2e477 100644 --- a/transports/bifrost-http/lib/config.go +++ b/transports/bifrost-http/lib/config.go @@ -807,10 +807,10 @@ func LoadConfig(ctx context.Context, configDirPath string) (*Config, error) { if err := json.Unmarshal(data, &schema); err != nil { return nil, fmt.Errorf("failed to unmarshal schema: %w", err) } - if schema["$schema"] != "https://www.getbifrost.ai/schema" { + if schemaURL, ok := schema["$schema"].(string); !ok || strings.TrimSpace(schemaURL) == "" { yellowColor := "\033[33m" resetColor := "\033[0m" - message := fmt.Sprintf("config file %s does not include \"$schema\":\"https://www.getbifrost.ai/schema\". Use our official schema file to avoid unexpected behavior.", absConfigFilePath) + message := fmt.Sprintf("config file %s does not include a \"$schema\" location. Set it to %q or your mirrored schema location to enable IDE validation.", absConfigFilePath, DefaultConfigSchemaURL) boxWidth := 100 contentWidth := boxWidth - 4 words := strings.Fields(message) @@ -839,7 +839,7 @@ func LoadConfig(ctx context.Context, configDirPath string) (*Config, error) { } fmt.Printf("%sā•š%sā•%s\n", yellowColor, strings.Repeat("═", boxWidth-2), resetColor) fmt.Println("") - logger.Warn("config file %s does not include \"$schema\":\"https://www.getbifrost.ai/schema\". Use our official schema file to avoid unexpected behavior.", absConfigFilePath) + logger.Warn("config file %s does not include a \"$schema\" location", absConfigFilePath) } // Parse config data if err := json.Unmarshal(data, &configData); err != nil { diff --git a/transports/bifrost-http/lib/validator.go b/transports/bifrost-http/lib/validator.go index 20458e81c80..c63feb64e97 100644 --- a/transports/bifrost-http/lib/validator.go +++ b/transports/bifrost-http/lib/validator.go @@ -8,11 +8,75 @@ import ( "fmt" "io" "net/http" + "net/url" "os" + "strings" + "time" "github.com/santhosh-tekuri/jsonschema/v6" ) +const ( + DefaultConfigSchemaURL = "https://www.getbifrost.ai/schema" + ConfigSchemaURLEnv = "BIFROST_SCHEMA_URL" +) + +const schemaFetchTimeout = 10 * time.Second + +func configuredConfigSchemaLocation() string { + return strings.TrimSpace(os.Getenv(ConfigSchemaURLEnv)) +} + +// loadSchemaFromLocation reads schema bytes from an HTTP(S) URL, a file:// URL, +// or a plain filesystem path. +func loadSchemaFromLocation(location string) ([]byte, error) { + if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") { + client := http.Client{Timeout: schemaFetchTimeout} + resp, err := client.Get(location) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return nil, fmt.Errorf("unexpected HTTP status %d fetching schema", resp.StatusCode) + } + return io.ReadAll(resp.Body) + } + return os.ReadFile(filePathFromSchemaLocation(location)) +} + +// filePathFromSchemaLocation converts a file:// URL to a filesystem path, +// honoring the optional localhost host (RFC 8089) and percent-encoding. +// Plain paths are returned unchanged. +func filePathFromSchemaLocation(location string) string { + if !strings.HasPrefix(location, "file://") { + return location + } + parsed, err := url.Parse(location) + if err != nil { + return strings.TrimPrefix(location, "file://") + } + if parsed.Opaque != "" { + return parsed.Opaque + } + if parsed.Host != "" && parsed.Host != "localhost" { + return parsed.Host + parsed.Path + } + return parsed.Path +} + +func schemaLocationFromConfig(data []byte) string { + var config map[string]any + if err := json.Unmarshal(data, &config); err != nil { + return "" + } + schemaLocation, ok := config["$schema"].(string) + if !ok { + return "" + } + return strings.TrimSpace(schemaLocation) +} + // localSchemaCandidates lists paths (relative to CWD) where config.schema.json may be found // when running from a source checkout. Checked in order before falling back to the remote URL. var localSchemaCandidates = []string{ @@ -36,27 +100,35 @@ func tryLoadLocalSchema() []byte { // ValidateConfigSchema validates config data against the JSON schema. // Returns nil if valid, or a formatted error describing all validation failures. -// An optional schemaOverride can be provided to use a local schema instead of fetching from the remote URL. +// An optional schemaOverride can be provided to use a local schema instead of loading from the configured location. +// Schema resolution order: schemaOverride arg, BIFROST_SCHEMA_URL env, the config's +// own non-default $schema value, a local source-checkout copy, the default public URL. func ValidateConfigSchema(data []byte, schemaOverride ...[]byte) error { var configSchemaJSONBytes []byte if len(schemaOverride) > 0 && len(schemaOverride[0]) > 0 { configSchemaJSONBytes = schemaOverride[0] - } else if localSchema := tryLoadLocalSchema(); localSchema != nil { - // Prefer the local schema file from the source checkout when available. - // This avoids validating against a potentially stale remote schema. - configSchemaJSONBytes = localSchema } else { - // Pulling config.schema from https://www.getbifrost.ai/schema - configSchemaJSON, err := http.Get("https://www.getbifrost.ai/schema") - if err != nil { - return fmt.Errorf("failed to get config schema: %w", err) + schemaLocation := configuredConfigSchemaLocation() + if schemaLocation == "" { + if fromConfig := schemaLocationFromConfig(data); fromConfig != "" && fromConfig != DefaultConfigSchemaURL { + schemaLocation = fromConfig + } + } + if schemaLocation == "" { + if localSchema := tryLoadLocalSchema(); localSchema != nil { + // Prefer the local schema file from the source checkout when available. + // This avoids validating against a potentially stale remote schema. + configSchemaJSONBytes = localSchema + } else { + schemaLocation = DefaultConfigSchemaURL + } } - defer configSchemaJSON.Body.Close() - var readErr error - configSchemaJSONBytes, readErr = io.ReadAll(configSchemaJSON.Body) - if readErr != nil { - logger.Warn("failed to download config schema: %v. running without config.json schema validation", readErr) - return nil + if configSchemaJSONBytes == nil { + var err error + configSchemaJSONBytes, err = loadSchemaFromLocation(schemaLocation) + if err != nil { + return fmt.Errorf("failed to get config schema from %s: %w", schemaLocation, err) + } } } // Parse the schema JSON diff --git a/transports/bifrost-http/lib/validator_test.go b/transports/bifrost-http/lib/validator_test.go index 8ce8a7c5371..a3966a43315 100644 --- a/transports/bifrost-http/lib/validator_test.go +++ b/transports/bifrost-http/lib/validator_test.go @@ -1,6 +1,8 @@ package lib import ( + "net/http" + "net/http/httptest" "os" "path/filepath" "runtime" @@ -57,6 +59,100 @@ func TestValidateConfigSchema_EmptyObject(t *testing.T) { } } +func TestValidateConfigSchema_CustomSchemaURL(t *testing.T) { + schema := loadLocalSchema(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(schema) + })) + t.Cleanup(server.Close) + t.Setenv(ConfigSchemaURLEnv, "") + + config := []byte(`{"$schema":"` + server.URL + `"}`) + err := ValidateConfigSchema(config) + if err != nil { + t.Errorf("expected schema to load from the config's custom $schema URL, got error: %v", err) + } +} + +func TestValidateConfigSchema_CustomSchemaURL_HTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "not found", http.StatusNotFound) + })) + t.Cleanup(server.Close) + t.Setenv(ConfigSchemaURLEnv, "") + + config := []byte(`{"$schema":"` + server.URL + `"}`) + err := ValidateConfigSchema(config) + if err == nil { + t.Fatal("expected a non-2xx schema response to fail") + } + if !strings.Contains(err.Error(), "failed to get config schema from") { + t.Errorf("expected load error to name the schema location, got: %v", err) + } +} + +func TestValidateConfigSchema_CustomSchemaFilePath(t *testing.T) { + schemaPath := filepath.Join(t.TempDir(), "config.schema.json") + if err := os.WriteFile(schemaPath, loadLocalSchema(t), 0644); err != nil { + t.Fatalf("failed to write temp schema: %v", err) + } + t.Setenv(ConfigSchemaURLEnv, schemaPath) + + err := ValidateConfigSchema([]byte(`{"$schema":"/opt/bifrost/config.schema.json"}`)) + if err != nil { + t.Errorf("expected custom schema filesystem path to pass validation, got error: %v", err) + } +} + +func TestValidateConfigSchema_ConfigSchemaFilePath(t *testing.T) { + schemaPath := filepath.Join(t.TempDir(), "config.schema.json") + if err := os.WriteFile(schemaPath, loadLocalSchema(t), 0644); err != nil { + t.Fatalf("failed to write temp schema: %v", err) + } + oldCandidates := localSchemaCandidates + localSchemaCandidates = nil + t.Cleanup(func() { localSchemaCandidates = oldCandidates }) + + config := []byte(`{"$schema":"` + schemaPath + `"}`) + err := ValidateConfigSchema(config) + if err != nil { + t.Errorf("expected config $schema filesystem path to load schema, got error: %v", err) + } +} + +func TestValidateConfigSchema_FileURL(t *testing.T) { + dir := filepath.Join(t.TempDir(), "schema dir") // space exercises percent-decoding + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + schemaPath := filepath.Join(dir, "config.schema.json") + if err := os.WriteFile(schemaPath, loadLocalSchema(t), 0644); err != nil { + t.Fatalf("failed to write temp schema: %v", err) + } + fileURL := "file://" + strings.ReplaceAll(schemaPath, " ", "%20") + t.Setenv(ConfigSchemaURLEnv, fileURL) + + err := ValidateConfigSchema([]byte(`{"$schema":"/opt/bifrost/config.schema.json"}`)) + if err != nil { + t.Errorf("expected file:// schema URL to load schema from filesystem, got error: %v", err) + } +} + +func TestFilePathFromSchemaLocation(t *testing.T) { + cases := map[string]string{ + "/opt/bifrost/config.schema.json": "/opt/bifrost/config.schema.json", + "file:///opt/bifrost/config.schema.json": "/opt/bifrost/config.schema.json", + "file://localhost/opt/config.schema.json": "/opt/config.schema.json", + "file:///opt/my%20dir/config.schema.json": "/opt/my dir/config.schema.json", + "https://www.getbifrost.ai/schema-as-path-only": "https://www.getbifrost.ai/schema-as-path-only", + } + for input, want := range cases { + if got := filePathFromSchemaLocation(input); got != want { + t.Errorf("filePathFromSchemaLocation(%q) = %q, want %q", input, got, want) + } + } +} + func TestValidateConfigSchema_InvalidJSON(t *testing.T) { invalidJSON := `{invalid json` diff --git a/transports/config.schema.json b/transports/config.schema.json index e705346bd14..be8eec12063 100644 --- a/transports/config.schema.json +++ b/transports/config.schema.json @@ -7,8 +7,8 @@ "properties": { "$schema": { "type": "string", - "description": "The schema version. This should be set to \"https://www.getbifrost.ai/schema\"", - "const": "https://www.getbifrost.ai/schema" + "description": "Schema location for IDE validation. Defaults to \"https://www.getbifrost.ai/schema\" and can be set to a mirrored URL, file:// URL, or filesystem path for isolated deployments.", + "default": "https://www.getbifrost.ai/schema" }, "server": { "type": "object",