diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 13c2867bf..37d299ee1 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -6,6 +6,8 @@ ### New Features and Improvements +* Added a `meta-harness` user-agent dimension that reports the omnigent meta-harness (detected via the `OMNIGENT` environment variable) independently of agent detection. + ### Bug Fixes ### Documentation diff --git a/config/api_client.go b/config/api_client.go index 949b797e6..1d4615f35 100644 --- a/config/api_client.go +++ b/config/api_client.go @@ -74,6 +74,17 @@ func HTTPClientConfigFromConfig(cfg *Config) (httpclient.ClientConfig, error) { *r = *r.WithContext(ctx) // replace request return nil }, + func(r *http.Request) error { + // Detect if we are running inside a known agent meta-harness. + provider := useragent.MetaHarnessProvider() + if provider == "" { + return nil + } + // Add the detected meta-harness to the user agent. + ctx := useragent.InContext(r.Context(), useragent.MetaHarnessKey, provider) + *r = *r.WithContext(ctx) // replace request + return nil + }, } return httpclient.ClientConfig{ diff --git a/useragent/meta_harness.go b/useragent/meta_harness.go new file mode 100644 index 000000000..82c734457 --- /dev/null +++ b/useragent/meta_harness.go @@ -0,0 +1,53 @@ +package useragent + +import ( + "os" + "sync" +) + +// knownMetaHarness describes a meta-harness that orchestrates AI agents (not +// itself an agent). Detected if envVar is set (any value, including empty, counts). +type knownMetaHarness struct { + envVar string + product string +} + +// listKnownMetaHarnesses returns the canonical list of agent meta-harnesses. +// Keep in sync with databricks-sdk-py and databricks-sdk-java. +func listKnownMetaHarnesses() []knownMetaHarness { + return []knownMetaHarness{ + {envVar: "OMNIGENT", product: "omnigent"}, // https://github.com/omnigent-ai/omnigent + } +} + +// lookupMetaHarnessProvider returns the matched meta-harness product, "multiple" +// if more than one matched, or "" if none matched. +func lookupMetaHarnessProvider() string { + var matches []string + for _, h := range listKnownMetaHarnesses() { + if _, ok := os.LookupEnv(h.envVar); ok { + matches = append(matches, h.product) + } + } + switch len(matches) { + case 1: + return matches[0] + case 0: + return "" + default: + return "multiple" + } +} + +var ( + metaHarnessOnce sync.Once + metaHarnessName string +) + +// MetaHarnessProvider returns the detected meta-harness name, cached for the process lifetime. +func MetaHarnessProvider() string { + metaHarnessOnce.Do(func() { + metaHarnessName = lookupMetaHarnessProvider() + }) + return metaHarnessName +} diff --git a/useragent/meta_harness_test.go b/useragent/meta_harness_test.go new file mode 100644 index 000000000..0c534050c --- /dev/null +++ b/useragent/meta_harness_test.go @@ -0,0 +1,66 @@ +package useragent + +import ( + "testing" + + "github.com/databricks/databricks-sdk-go/internal/env" +) + +func TestLookupMetaHarnessProvider(t *testing.T) { + tests := []struct { + name string + envs map[string]string + expect string + }{ + { + name: "no harness", + envs: nil, + expect: "", + }, + { + name: "omnigent", + envs: map[string]string{"OMNIGENT": "1"}, + expect: "omnigent", + }, + { + name: "omnigent empty value still counts as set", + envs: map[string]string{"OMNIGENT": ""}, + expect: "omnigent", + }, + { + name: "agent env var does not affect harness", + envs: map[string]string{"CLAUDECODE": "1"}, + expect: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env.CleanupEnvironment(t) + ClearCache() + for k, v := range tt.envs { + t.Setenv(k, v) + } + got := lookupMetaHarnessProvider() + if got != tt.expect { + t.Errorf("lookupMetaHarnessProvider() = %q, want %q", got, tt.expect) + } + }) + } +} + +func TestMetaHarnessProviderCached(t *testing.T) { + env.CleanupEnvironment(t) + ClearCache() + t.Setenv("OMNIGENT", "1") + got := MetaHarnessProvider() + if got != "omnigent" { + t.Errorf("MetaHarnessProvider() = %q, want %q", got, "omnigent") + } + // Change env after caching. Cached result should persist. + t.Setenv("OMNIGENT", "") + got = MetaHarnessProvider() + if got != "omnigent" { + t.Errorf("MetaHarnessProvider() after cache = %q, want %q", got, "omnigent") + } +} diff --git a/useragent/runtime.go b/useragent/runtime.go index 682f322d3..f26a36349 100644 --- a/useragent/runtime.go +++ b/useragent/runtime.go @@ -13,6 +13,7 @@ func ClearCache() { runtimeOnce = sync.Once{} providerOnce = sync.Once{} agentOnce = sync.Once{} + metaHarnessOnce = sync.Once{} } var runtimeOnce sync.Once diff --git a/useragent/user_agent.go b/useragent/user_agent.go index 508413e67..7e330713f 100644 --- a/useragent/user_agent.go +++ b/useragent/user_agent.go @@ -12,10 +12,11 @@ import ( ) const ( - RuntimeKey = "runtime" - CicdKey = "cicd" - AuthKey = "auth" - AgentKey = "agent" + RuntimeKey = "runtime" + CicdKey = "cicd" + AuthKey = "auth" + AgentKey = "agent" + MetaHarnessKey = "meta-harness" ) // WithProduct sets the product name and product version globally.