From 621ee055565fbe5b3ec4d8a2f54afe9aaab197e7 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 12:28:34 +0000 Subject: [PATCH 01/14] docs: generate and verify environment variable reference Signed-off-by: Eitan Yarmush --- .github/workflows/env-docs.yaml | 27 ++++ DEVELOPMENT.md | 6 + Makefile | 19 +++ docs/env.md | 221 +++++++++++++++++++++++++++ go/core/cli/internal/commands/env.go | 2 +- go/core/pkg/app/app.go | 36 ++--- go/core/pkg/app/app_test.go | 36 +++++ go/core/pkg/env/cli.go | 2 +- go/core/pkg/env/controller.go | 29 ++++ go/core/pkg/env/database.go | 16 ++ go/core/pkg/env/kagent.go | 45 ++---- go/core/pkg/env/legacy.go | 19 +++ go/core/pkg/env/otel.go | 40 ++++- go/core/pkg/env/providers.go | 21 ++- go/core/pkg/env/runtime.go | 56 +++++++ go/core/pkg/env/substrate.go | 6 + go/core/pkg/env/testing.go | 33 ++-- go/core/pkg/env/ui.go | 17 +++ go/core/pkg/env/var.go | 12 +- go/core/pkg/env/var_test.go | 7 + 20 files changed, 569 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/env-docs.yaml create mode 100644 docs/env.md create mode 100644 go/core/pkg/env/controller.go create mode 100644 go/core/pkg/env/database.go create mode 100644 go/core/pkg/env/legacy.go create mode 100644 go/core/pkg/env/runtime.go create mode 100644 go/core/pkg/env/ui.go diff --git a/.github/workflows/env-docs.yaml b/.github/workflows/env-docs.yaml new file mode 100644 index 0000000000..c1dec42791 --- /dev/null +++ b/.github/workflows/env-docs.yaml @@ -0,0 +1,27 @@ +name: Environment Documentation + +on: + push: + branches: [main, "release/**"] + pull_request: + branches: [main, "release/**"] + merge_group: + workflow_dispatch: + +permissions: + contents: read + +jobs: + env-docs-check: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go/go.mod + cache: true + cache-dependency-path: go/go.sum + - name: Check environment documentation + run: make env-docs-check diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a9fbc3c527..ab093468ec 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,5 +1,11 @@ # Development +The [environment variable reference](docs/env.md) is generated from +`go/core/pkg/env`. Register new settings there, including settings consumed by +Python, the UI, or a native harness. Keep defaults and descriptions aligned with +their readers, then run `make env-docs`. Use `make env-docs-check` to run the same +freshness check as CI. + To understand how to develop for kagent, it's important to understand the architecture of the project. Please refer to the [README.md](README.md#architecture) file for an overview of the project. When making changes to `kagent`, the most important thing is to figure out which piece of the project is affected by the change, and then make the change in the appropriate folder. Each piece of the project has its own README with more information about how to setup the development environment and run that piece of the project. diff --git a/Makefile b/Makefile index 0e53fcf5b9..4855608c6d 100644 --- a/Makefile +++ b/Makefile @@ -160,6 +160,25 @@ proto-check: proto-lint proto-generate ## Regenerate protobuf artifacts and fail exit 1; \ fi +##@ Environment documentation + +ENV_DOCS_COMMAND = cd go && go run ./core/cli/cmd/kagent env --format markdown + +.PHONY: env-docs +env-docs: ## Generate docs/env.md from the environment variable registry + @set -eu; generated=$$(mktemp); trap 'rm -f "$$generated"' EXIT; \ + ($(ENV_DOCS_COMMAND)) > "$$generated"; \ + cp "$$generated" docs/env.md + +.PHONY: env-docs-check +env-docs-check: ## Check docs/env.md matches the environment variable registry + @set -eu; generated=$$(mktemp); trap 'rm -f "$$generated"' EXIT; \ + ($(ENV_DOCS_COMMAND)) > "$$generated"; \ + if ! diff -u docs/env.md "$$generated"; then \ + echo "Environment documentation is out of date. Run 'make env-docs' and commit docs/env.md."; \ + exit 1; \ + fi + ##@ Telemetry contract include telemetry/versions.env diff --git a/docs/env.md b/docs/env.md new file mode 100644 index 0000000000..baf6f27d77 --- /dev/null +++ b/docs/env.md @@ -0,0 +1,221 @@ +# Kagent Environment Variables + + + +Generated from `go/core/pkg/env`. Edit the registrations there, run `make env-docs`, and commit the result. CI runs `make env-docs-check`. + +This reference covers the controller, CLI, agent runtimes, UI, and registered test settings. Build scripts, sample applications, and third-party SDK settings not configured by kagent have their own documentation. Defaults describe the application without deployment overrides; Helm or a Harness may supply different values. `(none)` means no fixed default; see the description for required values and fallbacks. Runtime-injected variables are managed by kagent, as noted below. Only registered metadata is exported, never values from the current process environment. + +## agent-runtime + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `A2A_MAX_CONTENT_LENGTH` | String | `10485760` | Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default. | +| `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS` | String | `(none)` | Python Google ADK span content capture. When absent, derived from OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (true for SPAN_ONLY or SPAN_AND_EVENT, false otherwise). | +| `ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN` | String | `2` | Python Google ADK telemetry schema version; set by kagent when absent. | +| `ANTHROPIC_API_KEY` | String | `(none)` | API key for Anthropic. | +| `ANTHROPIC_BASE_URL` | String | `(none)` | Native Claude Anthropic API base URL, supplied from ModelConfig by the controller. | +| `ANTHROPIC_VERTEX_PROJECT_ID` | String | `(none)` | Native Claude Vertex AI project, supplied from ModelConfig by the controller. | +| `AWS_ACCESS_KEY_ID` | String | `(none)` | AWS access key ID for IAM authentication with Bedrock. | +| `AWS_BEARER_TOKEN_BEDROCK` | String | `(none)` | Bearer token for authentication with AWS Bedrock. | +| `AWS_CA_BUNDLE` | String | `(none)` | Managed runtime and sandbox CA bundle for AWS clients, injected by Substrate. | +| `AWS_DEFAULT_REGION` | String | `(none)` | Preferred region for Python Bedrock models and Go/Python Bedrock embeddings, before AWS_REGION and the us-east-1 fallback. | +| `AWS_REGION` | String | `(none)` | AWS region for Bedrock. Python Bedrock and Go Bedrock embeddings prefer AWS_DEFAULT_REGION, then AWS_REGION, then us-east-1. | +| `AWS_SECRET_ACCESS_KEY` | String | `(none)` | AWS secret access key for IAM authentication with Bedrock. | +| `AWS_SESSION_TOKEN` | String | `(none)` | AWS session token for temporary/SSO credentials with Bedrock. | +| `AZURE_AD_TOKEN` | String | `(none)` | Azure Active Directory authentication token for Azure OpenAI. | +| `AZURE_OPENAI_API_KEY` | String | `(none)` | API key for Azure OpenAI. | +| `AZURE_OPENAI_ENDPOINT` | String | `(none)` | Endpoint URL for Azure OpenAI service. | +| `BASH_VENV_PATH` | String | `(none)` | Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set. | +| `CLAUDE_CODE_ENABLE_TELEMETRY` | String | `(none)` | Set to 1 by the Claude harness when telemetry is enabled. | +| `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA` | String | `(none)` | Set to 1 by the Claude harness when telemetry is enabled. | +| `CLAUDE_CODE_USE_BEDROCK` | String | `(none)` | Set to 1 by the controller when the Claude ModelConfig uses Bedrock. | +| `CLAUDE_CODE_USE_VERTEX` | String | `(none)` | Set to 1 by the controller when the Claude ModelConfig uses Vertex AI. | +| `CLAUDE_CONFIG_DIR` | String | `(none)` | Claude configuration directory set by the harness under its durable private state. | +| `CLOUD_ML_REGION` | String | `(none)` | Native Claude Vertex AI region, supplied from ModelConfig by the controller. | +| `CODEX_HOME` | String | `(none)` | Codex home directory set by the harness under its durable private state. | +| `CONFIG_DIR` | String | `/config` | Go ADK configuration directory; --filepath takes precedence. | +| `CURL_CA_BUNDLE` | String | `(none)` | Managed runtime and sandbox CA bundle for curl, injected by Substrate. | +| `DISABLE_UPDATES` | String | `1` | Set by the Claude harness to disable native automatic updates. | +| `DOCKER_CONFIG` | String | `(none)` | Docker registry credentials directory used by skills-init for OCI pulls; mounted by the controller when image pull secrets are configured. | +| `FOUNDRY_API_KEY` | String | `(none)` | API key for Azure AI Foundry. | +| `FOUNDRY_API_VERSION` | String | `2024-10-21` | Azure AI Foundry OpenAI-compatible data-plane API version. | +| `FOUNDRY_DEPLOYMENT` | String | `(none)` | Azure AI Foundry model deployment name. | +| `FOUNDRY_ENDPOINT` | String | `(none)` | Endpoint URL for Azure AI Foundry or an Azure AI Services account. | +| `GEMINI_API_KEY` | String | `(none)` | Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs. | +| `GIT_SSL_CAINFO` | String | `(none)` | Managed runtime and sandbox CA bundle for Git, injected by Substrate. | +| `GOOGLE_API_KEY` | String | `(none)` | API key for Google Gemini. | +| `GOOGLE_APPLICATION_CREDENTIALS` | String | `(none)` | Path to Google Cloud service account JSON key file. | +| `GOOGLE_CLOUD_LOCATION` | String | `(none)` | Google Cloud region/location for Vertex AI. | +| `GOOGLE_CLOUD_PROJECT` | String | `(none)` | Google Cloud project ID for Vertex AI. | +| `GOOGLE_CLOUD_REGION` | String | `(none)` | Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset. | +| `GOOGLE_GENAI_USE_VERTEXAI` | String | `(none)` | When set to 'true', use Vertex AI for Gemini models. | +| `IS_SANDBOX` | String | `1` | Set by the controller for managed Claude runtimes. | +| `KAGENT_A2A_GRPC_ADDRESS` | String | `[::]:80` | Python ADK gRPC listen address. | +| `KAGENT_AGENT_CARD_JSON` | String | `(none)` | A2A agent card JSON injected by the controller. Required by Codex and Claude; materialized to agent-card.json by the ADKs when present. | +| `KAGENT_API_URL` | String | `(none)` | Base URL for kagent control-plane API calls, injected into managed runtimes. Required by Python runtimes. | +| `KAGENT_CLAUDE_GOOGLE_CREDENTIALS_JSON` | String | `(none)` | Vertex service-account JSON injected into Claude and materialized as the GOOGLE_APPLICATION_CREDENTIALS file; removed before launching Claude. | +| `KAGENT_CLAUDE_MCP_CREDENTIAL_` | String | `(none)` | Controller-generated Claude MCP header credentials; names depend on the configured tools. | +| `KAGENT_CODEX_MCP_CREDENTIAL_` | String | `(none)` | Controller-generated Codex MCP header credentials; names depend on the configured tools. | +| `KAGENT_CONFIG_JSON` | String | `(none)` | Compiled harness configuration JSON injected by the controller. Required by Codex and Claude; materialized to config.json by the ADKs when present. | +| `KAGENT_CREDENTIAL_` | String | `(none)` | Controller-generated credential variables referenced by compiled ADK configuration; names depend on the configured tools and models. | +| `KAGENT_ENABLE_FILE_SEARCH_TOOLS` | Boolean | `false` | When true, enables the list_files and grep_file skills tools, which let an agent enumerate and search the filesystem under its session/skills roots without a shell. Disabled by default; set in Harness env to opt in. | +| `KAGENT_GATEWAY_URL` | String | `(none)` | Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value. | +| `KAGENT_NAME` | String | `(none)` | Name of the agent, injected by the controller. Required by Python runtimes. | +| `KAGENT_OPENAI_AGENTS_NATIVE_TRACING` | Boolean | `false` | Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime. | +| `KAGENT_PROPAGATE_TOKEN` | String | `(none)` | Set to true to propagate authentication tokens to downstream services. Unset or any other value disables propagation. | +| `KAGENT_SKILLS_FOLDER` | String | `/skills` | Skills directory for standalone Python skills tools. The Python ADK adds skills tools when set; managed Go ADK runtimes use their compiled skill configuration. | +| `KAGENT_STS_AUDIENCE` | String | `(none)` | Comma-separated RFC 8693 audiences sent on STS token-exchange requests. Alternate to KAGENT_STS_RESOURCE for servers that key on audience. | +| `KAGENT_STS_RESOURCE` | String | `(none)` | Comma-separated RFC 8707 resource indicators sent on STS token-exchange requests to scope issued tokens to target backends. | +| `KAGENT_TOKEN` | String | `(none)` | Runtime authentication token, materialized to /var/run/secrets/tokens/kagent-token by the ADKs when present. | +| `MISTRAL_API_BASE` | String | `(none)` | Custom base URL for the Mistral AI API (defaults to https://api.mistral.ai/v1). | +| `MISTRAL_API_KEY` | String | `(none)` | API key for Mistral AI. | +| `NODE_EXTRA_CA_CERTS` | String | `(none)` | Managed runtime and sandbox CA bundle for Node.js, injected by Substrate. | +| `OLLAMA_API_BASE` | String | `(none)` | Base URL for the Ollama API endpoint; falls back to http://localhost:11434 when model configuration and this variable are unset. | +| `OLLAMA_API_KEY` | String | `(none)` | API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly. | +| `OPENAI_AGENTS_DISABLE_TRACING` | Boolean | `false` | Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1. | +| `OPENAI_API_BASE` | String | `(none)` | Custom base URL for the OpenAI API. | +| `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. | +| `OPENAI_API_VERSION` | String | `(none)` | Azure OpenAI API version. The Go and Python ADKs fall back to 2024-02-15-preview when model configuration and this variable are unset. | +| `OPENAI_ORGANIZATION` | String | `(none)` | OpenAI organization identifier. | +| `OTEL_EXPORTER_PROMETHEUS_HOST` | String | `(none)` | Private loopback Prometheus host assigned by the Claude driver for native telemetry readiness checks. | +| `OTEL_EXPORTER_PROMETHEUS_PORT` | String | `(none)` | Private Prometheus port assigned by the Claude driver for native telemetry readiness checks. | +| `OTEL_LOG_ASSISTANT_RESPONSES` | String | `(none)` | Set to 1 by the Claude harness when log export and content capture are enabled. | +| `OTEL_LOG_RAW_API_BODIES` | String | `(none)` | Set to 1 for Claude when KAGENT_OTEL_CAPTURE_RAW_API_BODIES enables raw body capture. | +| `OTEL_LOG_TOOL_CONTENT` | String | `(none)` | Set to 1 by the Claude harness when trace export and content capture are enabled. | +| `OTEL_LOG_TOOL_DETAILS` | String | `(none)` | Set to 1 by the Claude harness when content capture is enabled. | +| `OTEL_LOG_USER_PROMPTS` | String | `(none)` | Set to 1 by the Claude harness when content capture is enabled. | +| `OTEL_SEMCONV_STABILITY_OPT_IN` | String | `gen_ai_latest_experimental` | Python Google ADK semantic-convention opt-in; set by kagent when absent. | +| `PORT` | String | `8080` | Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port. | +| `REQUESTS_CA_BUNDLE` | String | `(none)` | Managed runtime and sandbox CA bundle for Python requests, injected by Substrate. | +| `SAP_AI_CORE_CLIENT_ID` | String | `(none)` | OAuth2 client ID for SAP AI Core authentication. | +| `SAP_AI_CORE_CLIENT_SECRET` | String | `(none)` | OAuth2 client secret for SAP AI Core authentication. | +| `SSL_CERT_DIR` | String | `(none)` | Managed agent runtime CA directory, injected by Substrate for egress TLS trust. | +| `SSL_CERT_FILE` | String | `(none)` | Managed runtime and sandbox CA bundle, injected by Substrate for egress TLS trust. | +| `STS_WELL_KNOWN_URI` | String | `(none)` | Well-known endpoint for the Security Token Service (STS) used for token exchange. | +| `TRACEPARENT` | String | `(none)` | W3C trace context supplied per invocation to the native Claude process. | +| `TRACESTATE` | String | `(none)` | W3C trace state supplied per invocation to the native Claude process. | +| `UVICORN_LOG_LEVEL` | String | `(none)` | Python ADK HTTP server log level. Falls back to LOG_LEVEL, then info. | + +## cli + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `KAGENT_DEFAULT_MODEL_PROVIDER` | String | `openAI` | Default LLM provider for agents (e.g. openAI, anthropic, ollama, azureOpenAI). | +| `KAGENT_HELM_EXTRA_ARGS` | String | `(none)` | Additional arguments to pass to Helm commands. | +| `KAGENT_HELM_REPO` | String | `oci://ghcr.io/kagent-dev/kagent/helm/` | Helm repository URL for kagent charts. | +| `KAGENT_HELM_VERSION` | String | `(none)` | Helm chart version to deploy. When unset, the CLI uses its own version. | + +## controller + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `AUTH_MODE` | String | `(none)` | Rendered by Helm but ignored by the current controller. Authentication is supplied through app.Options.Authenticator. **(deprecated)** | +| `AUTH_USER_ID_CLAIM` | String | `(none)` | Rendered by Helm but ignored by the current controller. Identity extraction belongs to the configured authenticator. **(deprecated)** | +| `GRPC_MAX_MESSAGE_BYTES` | String | `(none)` | Rendered by Helm but ignored by the current controller; does not change its gRPC message limits. **(deprecated)** | +| `GRPC_REFLECTION` | Boolean | `false` | Enable gRPC server reflection on the controller. | +| `GRPC_TLS_CERT_FILE` | String | `(none)` | Rendered by Helm but ignored by the current controller; does not enable TLS on its listener. **(deprecated)** | +| `GRPC_TLS_KEY_FILE` | String | `(none)` | Rendered by Helm but ignored by the current controller; does not enable TLS on its listener. **(deprecated)** | +| `HTTP_BIND_ADDRESS` | String | `:8083` | Listen address for the controller HTTP, gRPC, A2A, and MCP server. | +| `K8S_NODE_NAME` | String | `(none)` | Controller node name, injected by the Helm chart through the Downward API for telemetry. | +| `K8S_POD_NAME` | String | `(none)` | Controller pod name, injected by the Helm chart through the Downward API for telemetry. | +| `K8S_POD_UID` | String | `(none)` | Controller pod UID, injected by the Helm chart through the Downward API for telemetry. | +| `KAGENT_CONTROLLER_NAME` | String | `kagent-controller` | Name of the kagent controller service. | +| `KAGENT_NAMESPACE` | String | `kagent` | Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it. | +| `KAGENT_OTEL_CAPTURE_RAW_API_BODIES` | Boolean | `false` | Set to true to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data. | +| `KAGENT_OTEL_MAX_CAPTURE_BYTES` | Integer | `16384` | Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384. | +| `KAGENT_OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Resource attributes, as key=value pairs, added to every agent runtime. | +| `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | +| `LEADER_ELECT` | Boolean | `true` | Enable controller leader election, including during single-replica rolling updates. Required for sandbox lifecycle coordination. | +| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `METRICS_BIND_ADDRESS` | String | `0` | Address the controller-runtime metrics server binds to, e.g. :8080. "0" (the default) serves no metrics, so an installation that does not set this is unchanged. The Helm chart renders this variable, and its ServiceMonitor, from controller.metrics. | +| `METRICS_SECURE` | Boolean | `false` | Serve the metrics endpoint over HTTPS with authentication and authorization. A scraper then needs a token bound to the metrics-reader ClusterRole. | +| `OTEL_EXPORTER_OTLP_COMPRESSION` | String | `gzip` | OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip. | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | String | `(none)` | OTLP endpoint for every signal. `OTEL_EXPORTER_OTLP__ENDPOINT` overrides it for one signal. | +| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | String | `(none)` | Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path. | +| `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` | String | `(none)` | Log protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL. | +| `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` | String | `(none)` | Log SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller. | +| `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` | String | `base2_exponential_bucket_histogram` | Default SDK histogram aggregation applied by kagent and supplied to managed runtimes. | +| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | String | `(none)` | Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path. | +| `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` | String | `(none)` | Metric protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL. | +| `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` | String | `(none)` | Metric SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller. | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | String | `grpc` | OTLP protocol, grpc or http/protobuf. `OTEL_EXPORTER_OTLP__PROTOCOL` overrides it for one signal. | +| `OTEL_EXPORTER_OTLP_TIMEOUT` | String | `(none)` | OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms). | +| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | String | `(none)` | Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path. | +| `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` | String | `(none)` | Trace protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL. | +| `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` | String | `(none)` | Trace SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller. | +| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | String | `NO_CONTENT` | SPAN_ONLY records prompts and responses on agent spans. NO_CONTENT disables capture. Managed runtimes support these two modes; standalone Python ADK also recognizes SPAN_AND_EVENT. Captured content may be sensitive. | +| `OTEL_LOGS_EXPORTER` | String | `(none)` | Log exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | +| `OTEL_METRICS_EXPORTER` | String | `(none)` | Metric exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | +| `OTEL_PROPAGATORS` | String | `tracecontext` | SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes. | +| `OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents. | +| `OTEL_SDK_DISABLED` | Boolean | `false` | Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes. | +| `OTEL_SERVICE_NAME` | String | `(none)` | SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes. | +| `OTEL_TRACES_EXPORTER` | String | `(none)` | Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | +| `SANDBOX_CPU` | String | `1` | CPU limit for standalone sandbox runtimes. | +| `SANDBOX_DEFAULT_TTL` | Duration | `1h0m0s` | Default standalone sandbox lifetime. | +| `SANDBOX_GUEST_IMAGE` | String | `(none)` | Guest package image pinned by sha256 digest. Required for sandbox preparation and passed unchanged to Substrate. | +| `SANDBOX_MAX_TTL` | Duration | `24h0m0s` | Maximum standalone sandbox lifetime, at most 24h. | +| `SANDBOX_MEMORY` | String | `1Gi` | Memory limit for standalone sandbox runtimes. | +| `SUBSTRATE_ATENET_ROUTER_URL` | String | `http://atenet-router.ate-system.svc:80` | Substrate router endpoint for agent and sandbox guest traffic. | +| `SUBSTRATE_ATE_API_CA_FILE` | String | `(none)` | PEM CA bundle used to verify the Substrate API server. Empty uses system trust roots. | +| `SUBSTRATE_ATE_API_CLIENT_CERT_FILE` | String | `(none)` | PEM bundle containing both the client certificate and private key for Substrate API mTLS. Reloaded for each TLS handshake. | +| `SUBSTRATE_ATE_API_ENDPOINT` | String | `dns:///api.ate-system.svc:443` | Substrate control-plane gRPC endpoint. | +| `SUBSTRATE_DEFAULT_WORKERPOOL_NAME` | String | `(none)` | Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration. **(deprecated)** | +| `SUBSTRATE_DEFAULT_WORKERPOOL_NAMESPACE` | String | `(none)` | Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration. **(deprecated)** | +| `WATCH_NAMESPACES` | String | `(none)` | Comma-separated namespaces to watch. Empty watches all namespaces. | + +## database + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | +| `PGDATA` | String | `(none)` | Data directory for bundled PostgreSQL, supplied by the Helm chart. | +| `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | +| `POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller. | +| `POSTGRES_DB` | String | `(none)` | Initial database name for bundled PostgreSQL, supplied by the Helm chart. | +| `POSTGRES_PASSWORD` | String | `(none)` | Password supplied from the chart-managed Secret to bundled PostgreSQL and expanded into the controller's POSTGRES_DATABASE_URL by Kubernetes. | +| `POSTGRES_USER` | String | `(none)` | Initial user for bundled PostgreSQL, supplied by the Helm chart. | +| `SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | + +## testing + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `KAGENT_E2E_API_URL` | String | `(none)` | Controller URL for Go E2E tests. Falls back to KAGENT_API_URL; tests skip when neither is set. | +| `KAGENT_E2E_BYO_IMAGE` | String | `(none)` | Digest-pinned BYO image substituted into E2E lifecycle manifests. | +| `KAGENT_E2E_CLAUDE_IMAGE` | String | `(none)` | Digest-pinned Claude harness image substituted into E2E lifecycle manifests. | +| `KAGENT_E2E_CLI` | String | `(none)` | CLI executable for catalog lifecycle E2E tests; make -C go e2e builds and supplies it. | +| `KAGENT_E2E_CODEX_IMAGE` | String | `(none)` | Digest-pinned Codex harness image substituted into E2E lifecycle manifests. | +| `KAGENT_E2E_OTLP_LISTEN_ADDRESS` | String | `(none)` | Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver. | +| `KAGENT_E2E_REQUIRE_TRACING` | Boolean | `false` | Fail instead of skip when expected native harness tracing support is unavailable. | +| `KAGENT_E2E_RUNTIME_IMAGE` | String | `(none)` | Required digest-pinned Go ADK image for sandbox E2E tests and lifecycle manifests. | +| `KAGENT_E2E_SANDBOX_NAMESPACE` | String | `kagent` | Namespace for sandbox E2E resources. | +| `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | +| `KAGENT_LOCAL_HOST` | String | `(none)` | Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems. | +| `KAGENT_TEST_PYTHON` | String | `(none)` | Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset. | +| `LLM_PORT` | String | `8090` | Port for the mock LLM server. | +| `RUN_ROLLING_UPGRADE_TESTS` | Boolean | `false` | Run rolling upgrade integration tests when exactly true. | +| `RUN_UPGRADE_TESTS` | Boolean | `false` | Run upgrade integration tests when exactly true. | +| `STS_PORT` | String | `8091` | Port for the mock STS (Security Token Service) server. | +| `UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to UI_LOOP_PORT plus 50. | +| `UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | +| `UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | +| `UI_LOOP_PORT` | String | `8001` | Vite development/preview port and UI browser-test port. | +| `UPDATE_GOLDEN` | Boolean | `false` | When true, update golden test files instead of comparing. | + +## ui + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `API_BASE_URL` | String | `/api` | Browser API base URL. Set directly for Vite development; use KAGENT_API_BASE_URL in the UI container. | +| `BASE_PATH` | String | `(none)` | Browser public path prefix. Set directly for Vite development; use KAGENT_UI_BASE_PATH in the UI container. | +| `ENABLE_MOCK_UI` | Boolean | `false` | Serve the UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Applies in development and containers. | +| `EXTENSION_` | String | `(none)` | UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public. | +| `KAGENT_API_BASE_URL` | String | `/api` | UI container setting mapped to the browser's API_BASE_URL at startup. | +| `KAGENT_DEV_CONTROLLER_URL` | String | `http://127.0.0.1:8083` | Vite development proxy target for /api and /a2a; not sent to the browser. | +| `KAGENT_STREAM_TIMEOUT_MS` | String | `1800000` | UI container chat stream inactivity timeout in milliseconds, mapped to STREAM_TIMEOUT_MS. 0 disables the timeout. | +| `KAGENT_UI_BASE_PATH` | String | `(none)` | UI container public path prefix, such as /ui; empty serves at the root. Mapped to BASE_PATH; invalid or reserved prefixes fall back to the root. | +| `SSO_REDIRECT_PATH` | String | `/oauth2/start` | UI path used by Sign in with SSO. | +| `STREAM_TIMEOUT_MS` | String | `1800000` | Browser chat stream inactivity timeout in milliseconds. Set directly for Vite development; use KAGENT_STREAM_TIMEOUT_MS in the UI container. 0 disables the timeout. | +| `VITE_EXAMPLE_EXTENSION` | Boolean | `false` | Build-time switch enabling the bundled example UI extension. | diff --git a/go/core/cli/internal/commands/env.go b/go/core/cli/internal/commands/env.go index c0a680638d..cc37d23914 100644 --- a/go/core/cli/internal/commands/env.go +++ b/go/core/cli/internal/commands/env.go @@ -29,7 +29,7 @@ func NewEnvCmd() *cobra.Command { } cmd.Flags().StringVar(&format, "format", "markdown", "Output format: markdown, json") - cmd.Flags().StringVar(&component, "component", "all", "Filter by component: controller, cli, agent-runtime, database, testing, all") + cmd.Flags().StringVar(&component, "component", "all", "Filter by component: controller, cli, agent-runtime, database, testing, ui, all") return cmd } diff --git a/go/core/pkg/app/app.go b/go/core/pkg/app/app.go index 32d5249aa7..0096378ced 100644 --- a/go/core/pkg/app/app.go +++ b/go/core/pkg/app/app.go @@ -10,13 +10,13 @@ package app import ( + "cmp" "context" "fmt" "log/slog" "maps" "net/http" "os" - "strconv" "strings" "time" @@ -189,7 +189,7 @@ func Run(ctx context.Context, opts Options) error { } }() - dbURL, err := database.ResolveURL(env("POSTGRES_DATABASE_URL", "postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres"), os.Getenv("POSTGRES_DATABASE_URL_FILE")) + dbURL, err := database.ResolveURL(env(kagentenv.PostgresDatabaseURL), kagentenv.PostgresDatabaseURLFile.Get()) if err != nil { return err } @@ -226,7 +226,7 @@ func Run(ctx context.Context, opts Options) error { utilruntime.Must(kagentv1alpha3.AddToScheme(managerScheme)) utilruntime.Must(atev1alpha1.AddToScheme(managerScheme)) utilruntime.Must(kmcp.AddToScheme(managerScheme)) - watchNamespaces := namespaces(os.Getenv("WATCH_NAMESPACES")) + watchNamespaces := namespaces(kagentenv.WatchNamespaces.Get()) managerClientOptions := client.Options{} managerCacheOptions := cache.Options{DefaultNamespaces: namespaceCache(watchNamespaces)} if len(watchNamespaces) > 0 { @@ -252,7 +252,7 @@ func Run(ctx context.Context, opts Options) error { Metrics: metricsOptions, LeaderElection: kagentenv.LeaderElect.Get(), LeaderElectionID: "0e9f6799.kagent.dev", - LeaderElectionNamespace: env("KAGENT_NAMESPACE", "kagent"), + LeaderElectionNamespace: env(kagentenv.KagentNamespace), }) if err != nil { return fmt.Errorf("create controller manager: %w", err) @@ -262,9 +262,9 @@ func Run(ctx context.Context, opts Options) error { return err } actors, err := substrate.Dial(ctx, substrate.Config{ - AteAPIEndpoint: env("SUBSTRATE_ATE_API_ENDPOINT", "dns:///api.ate-system.svc:443"), - CAFile: os.Getenv("SUBSTRATE_ATE_API_CA_FILE"), - ClientCertFile: os.Getenv("SUBSTRATE_ATE_API_CLIENT_CERT_FILE"), + AteAPIEndpoint: env(kagentenv.SubstrateATEAPIEndpoint), + CAFile: kagentenv.SubstrateATEAPICAFile.Get(), + ClientCertFile: kagentenv.SubstrateATEAPIClientCertFile.Get(), CallTimeout: 30 * time.Second, }) if err != nil { @@ -297,7 +297,7 @@ func Run(ctx context.Context, opts Options) error { } authenticator, authorizer := opts.resolve() - resourceNamespace := env("KAGENT_NAMESPACE", "kagent") + resourceNamespace := env(kagentenv.KagentNamespace) models := modelservice.NewService(manager.GetClient(), authorizer, resourceNamespace) tools := toolservice.NewService(manager.GetClient(), store, authorizer, resourceNamespace, mcpClient) prompts := prompttemplateservice.NewService(manager.GetClient(), authorizer) @@ -319,7 +319,7 @@ func Run(ctx context.Context, opts Options) error { } agents := kubecrud.NewService(manager.GetClient(), authorizer, &kagentv1alpha3.Agent{}, &kagentv1alpha3.AgentList{}, "Agent") interactions := sessionsvc.NewInteractionService(store, agents, sessions) - gateway := a2agateway.New(interactions, gatewayDialer, env("KAGENT_GATEWAY_URL", "http://127.0.0.1:8083")) + gateway := a2agateway.New(interactions, gatewayDialer, cmp.Or(kagentenv.KagentGatewayURL.Get(), "http://127.0.0.1:8083")) schedules := scheduledrun.NewService(store, manager.GetClient(), authorizer) if err := manager.Add(scheduledruncontroller.NewScheduler(store)); err != nil { return fmt.Errorf("add scheduled run scheduler: %w", err) @@ -335,7 +335,7 @@ func Run(ctx context.Context, opts Options) error { } defer guests.Close() policy := substrate.SandboxPolicy{ - GuestImage: env(kagentenv.SandboxGuestImage.Name(), kagentenv.SandboxGuestImage.DefaultValue()), + GuestImage: env(kagentenv.SandboxGuestImage), CPU: kagentenv.SandboxCPU.Get(), Memory: kagentenv.SandboxMemory.Get(), } @@ -371,8 +371,8 @@ func Run(ctx context.Context, opts Options) error { server, err := grpcserver.New(grpcserver.Config{ MethodPolicies: policies, RegisterServices: opts.GRPCServices, - BindAddress: env("HTTP_BIND_ADDRESS", ":8083"), - Reflection: envBool("GRPC_REFLECTION"), + BindAddress: env(kagentenv.HTTPBindAddress), + Reflection: kagentenv.GRPCReflection.Get(), Authenticator: authenticator, RuntimeAuthenticator: &taskstore.Authenticator{}, ShareStore: store, @@ -422,16 +422,12 @@ func mergePolicies(defaults grpcserver.MethodPolicies, extra map[string]auth.Acc return merged, nil } -func env(name, fallback string) string { - if value := os.Getenv(name); value != "" { +// env preserves the controller's default-on-empty behavior for string settings. +func env(variable kagentenv.StringVar) string { + if value := variable.Get(); value != "" { return value } - return fallback -} - -func envBool(name string) bool { - value, _ := strconv.ParseBool(os.Getenv(name)) - return value + return variable.DefaultValue() } // metricsBindAddress resolves METRICS_BIND_ADDRESS. controller-runtime reads an diff --git a/go/core/pkg/app/app_test.go b/go/core/pkg/app/app_test.go index f6b5fb7c6a..0bffe44d3b 100644 --- a/go/core/pkg/app/app_test.go +++ b/go/core/pkg/app/app_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "os" "reflect" "testing" @@ -136,6 +137,41 @@ func TestNamespaces(t *testing.T) { } } +func TestRegisteredStringDefaults(t *testing.T) { + for _, variable := range []kagentenv.StringVar{ + kagentenv.HTTPBindAddress, + kagentenv.PostgresDatabaseURL, + kagentenv.SubstrateATEAPIEndpoint, + kagentenv.KagentNamespace, + } { + t.Run(variable.Name(), func(t *testing.T) { + for _, test := range []struct { + name string + value *string + want string + }{ + {name: "unset", want: variable.DefaultValue()}, + {name: "empty", value: new(""), want: variable.DefaultValue()}, + {name: "override", value: new("override"), want: "override"}, + } { + t.Run(test.name, func(t *testing.T) { + t.Setenv(variable.Name(), "") + if test.value == nil { + if err := os.Unsetenv(variable.Name()); err != nil { + t.Fatal(err) + } + } else { + t.Setenv(variable.Name(), *test.value) + } + if got := env(variable); got != test.want { + t.Errorf("env() = %q, want %q", got, test.want) + } + }) + } + }) + } +} + func TestNamespaceCache(t *testing.T) { if got := namespaceCache(nil); got != nil { t.Fatalf("namespaceCache(nil) = %#v, want nil", got) diff --git a/go/core/pkg/env/cli.go b/go/core/pkg/env/cli.go index 0e6342fcfc..3ae71f4e7d 100644 --- a/go/core/pkg/env/cli.go +++ b/go/core/pkg/env/cli.go @@ -19,7 +19,7 @@ var ( KagentHelmVersion = RegisterStringVar( "KAGENT_HELM_VERSION", "", - "Helm chart version to deploy.", + "Helm chart version to deploy. When unset, the CLI uses its own version.", ComponentCLI, ) diff --git a/go/core/pkg/env/controller.go b/go/core/pkg/env/controller.go new file mode 100644 index 0000000000..67fe295718 --- /dev/null +++ b/go/core/pkg/env/controller.go @@ -0,0 +1,29 @@ +package env + +var ( + HTTPBindAddress = RegisterStringVar( + "HTTP_BIND_ADDRESS", ":8083", + "Listen address for the controller HTTP, gRPC, A2A, and MCP server.", ComponentController, + ) + GRPCReflection = RegisterBoolVar( + "GRPC_REFLECTION", false, + "Enable gRPC server reflection on the controller.", ComponentController, + ) + WatchNamespaces = RegisterStringVar( + "WATCH_NAMESPACES", "", + "Comma-separated namespaces to watch. Empty watches all namespaces.", ComponentController, + ) +) + +// Shared settings read by logging and Kubernetes libraries. +var ( + _ = RegisterStringVar("LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController) + _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController) +) + +// The chart supplies these for Kubernetes expansion in OTEL_RESOURCE_ATTRIBUTES. +var ( + _ = RegisterStringVar("K8S_POD_NAME", "", "Controller pod name, injected by the Helm chart through the Downward API for telemetry.", ComponentController) + _ = RegisterStringVar("K8S_POD_UID", "", "Controller pod UID, injected by the Helm chart through the Downward API for telemetry.", ComponentController) + _ = RegisterStringVar("K8S_NODE_NAME", "", "Controller node name, injected by the Helm chart through the Downward API for telemetry.", ComponentController) +) diff --git a/go/core/pkg/env/database.go b/go/core/pkg/env/database.go new file mode 100644 index 0000000000..b69ab53034 --- /dev/null +++ b/go/core/pkg/env/database.go @@ -0,0 +1,16 @@ +package env + +var ( + PostgresDatabaseURL = RegisterStringVar( + "POSTGRES_DATABASE_URL", "postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres", + "PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL.", ComponentDatabase, + ) + PostgresDatabaseURLFile = RegisterStringVar( + "POSTGRES_DATABASE_URL_FILE", "", + "File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller.", ComponentDatabase, + ) + _ = RegisterStringVar("POSTGRES_PASSWORD", "", "Password supplied from the chart-managed Secret to bundled PostgreSQL and expanded into the controller's POSTGRES_DATABASE_URL by Kubernetes.", ComponentDatabase) + _ = RegisterStringVar("POSTGRES_DB", "", "Initial database name for bundled PostgreSQL, supplied by the Helm chart.", ComponentDatabase) + _ = RegisterStringVar("POSTGRES_USER", "", "Initial user for bundled PostgreSQL, supplied by the Helm chart.", ComponentDatabase) + _ = RegisterStringVar("PGDATA", "", "Data directory for bundled PostgreSQL, supplied by the Helm chart.", ComponentDatabase) +) diff --git a/go/core/pkg/env/kagent.go b/go/core/pkg/env/kagent.go index fdd2fd576e..c906a93043 100644 --- a/go/core/pkg/env/kagent.go +++ b/go/core/pkg/env/kagent.go @@ -30,7 +30,7 @@ var ( KagentNamespace = RegisterStringVar( "KAGENT_NAMESPACE", "kagent", - "Kubernetes namespace where kagent resources are deployed.", + "Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it.", ComponentController, ) @@ -41,65 +41,40 @@ var ( ComponentController, ) - KagentA2ADebugAddr = RegisterStringVar( - "KAGENT_A2A_DEBUG_ADDR", - "", - "Debug address for the A2A server. When set, all A2A HTTP requests are dialed to this address.", - ComponentController, - ) - - KagentA2AClientTimeout = RegisterDurationVar( - "KAGENT_A2A_CLIENT_TIMEOUT", - 0, - "HTTP client timeout for A2A requests from the controller to agent pods. "+ - "0 (the default) means no timeout, which is recommended for long-running agents "+ - "that stream responses over SSE. Set a positive duration (e.g. 30m) only if you "+ - "need a hard upper bound on individual A2A calls.", - ComponentController, - ) - - // Variables injected into agent pods (not read by the controller itself). + // Variables injected into agent runtimes (not read by the controller itself). KagentName = RegisterStringVar( "KAGENT_NAME", "", - "Name of the agent. Injected into agent pods via the controller.", + "Name of the agent, injected by the controller. Required by Python runtimes.", ComponentAgentRuntime, ) KagentAPIURL = RegisterStringVar( "KAGENT_API_URL", "", - "Base URL for kagent control-plane API calls.", + "Base URL for kagent control-plane API calls, injected into managed runtimes. Required by Python runtimes.", ComponentAgentRuntime, ) KagentGatewayURL = RegisterStringVar( "KAGENT_GATEWAY_URL", "", - "Base URL for A2A and MCP traffic.", - ComponentAgentRuntime, - ) - - KagentUIURL = RegisterStringVar( - "KAGENT_UI_URL", - "", - "Public base URL of the kagent UI (e.g. https://kagent.example.com). "+ - "When set, share link tools return full clickable URLs instead of paths.", + "Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value.", ComponentAgentRuntime, ) KagentSkillsFolder = RegisterStringVar( "KAGENT_SKILLS_FOLDER", "/skills", - "Directory path where agent skills are mounted.", + "Skills directory for standalone Python skills tools. The Python ADK adds skills tools when set; managed Go ADK runtimes use their compiled skill configuration.", ComponentAgentRuntime, ) KagentPropagateToken = RegisterStringVar( "KAGENT_PROPAGATE_TOKEN", "", - "When set, propagates the authentication token to downstream services.", + "Set to true to propagate authentication tokens to downstream services. Unset or any other value disables propagation.", ComponentAgentRuntime, ) @@ -113,7 +88,7 @@ var ( false, "When true, enables the list_files and grep_file skills tools, which let an agent "+ "enumerate and search the filesystem under its session/skills roots without a "+ - "shell. Disabled by default; set on the Agent's env to opt in.", + "shell. Disabled by default; set in Harness env to opt in.", ComponentAgentRuntime, ) @@ -127,14 +102,14 @@ var ( KagentSTSResource = RegisterStringVar( "KAGENT_STS_RESOURCE", "", - "RFC 8707 resource indicator sent on STS token-exchange requests to scope the issued token to a target backend.", + "Comma-separated RFC 8707 resource indicators sent on STS token-exchange requests to scope issued tokens to target backends.", ComponentAgentRuntime, ) KagentSTSAudience = RegisterStringVar( "KAGENT_STS_AUDIENCE", "", - "RFC 8693 audience sent on STS token-exchange requests. Alternate to KAGENT_STS_RESOURCE for servers that key on audience.", + "Comma-separated RFC 8693 audiences sent on STS token-exchange requests. Alternate to KAGENT_STS_RESOURCE for servers that key on audience.", ComponentAgentRuntime, ) diff --git a/go/core/pkg/env/legacy.go b/go/core/pkg/env/legacy.go new file mode 100644 index 0000000000..6cd6b2ae27 --- /dev/null +++ b/go/core/pkg/env/legacy.go @@ -0,0 +1,19 @@ +package env + +// The chart still emits these names, but the current controller has no readers. +// Keep them discoverable without promising that setting them changes behavior. +func init() { + for _, variable := range []Var{ + {Name: "AUTH_MODE", Description: "Rendered by Helm but ignored by the current controller. Authentication is supplied through app.Options.Authenticator."}, + {Name: "AUTH_USER_ID_CLAIM", Description: "Rendered by Helm but ignored by the current controller. Identity extraction belongs to the configured authenticator."}, + {Name: "GRPC_MAX_MESSAGE_BYTES", Description: "Rendered by Helm but ignored by the current controller; does not change its gRPC message limits."}, + {Name: "GRPC_TLS_CERT_FILE", Description: "Rendered by Helm but ignored by the current controller; does not enable TLS on its listener."}, + {Name: "GRPC_TLS_KEY_FILE", Description: "Rendered by Helm but ignored by the current controller; does not enable TLS on its listener."}, + {Name: "SUBSTRATE_DEFAULT_WORKERPOOL_NAMESPACE", Description: "Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration."}, + {Name: "SUBSTRATE_DEFAULT_WORKERPOOL_NAME", Description: "Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration."}, + } { + variable.Component = ComponentController + variable.Deprecated = true + register(variable) + } +} diff --git a/go/core/pkg/env/otel.go b/go/core/pkg/env/otel.go index b1b4e93f38..f5fd2e2785 100644 --- a/go/core/pkg/env/otel.go +++ b/go/core/pkg/env/otel.go @@ -1,47 +1,47 @@ package env -// OpenTelemetry environment variables. The controller exports with them and -// forwards them to every agent runtime. +// OpenTelemetry environment variables. The controller forwards supported signal +// settings to runtimes; SDK-only settings below apply to the receiving process. var ( OtelTracesExporter = RegisterStringVar( "OTEL_TRACES_EXPORTER", "", - "Trace exporter, otlp or none. Only otlp is forwarded to agent runtimes.", + "Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp.", ComponentController, ) OtelMetricsExporter = RegisterStringVar( "OTEL_METRICS_EXPORTER", "", - "Metric exporter, otlp or none. Only otlp is forwarded to agent runtimes.", + "Metric exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp.", ComponentController, ) OtelLogsExporter = RegisterStringVar( "OTEL_LOGS_EXPORTER", "", - "Log exporter, otlp or none. Only otlp is forwarded to agent runtimes.", + "Log exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp.", ComponentController, ) OtelExporterOTLPEndpoint = RegisterStringVar( "OTEL_EXPORTER_OTLP_ENDPOINT", "", - "OTLP endpoint for every signal. OTEL_EXPORTER_OTLP__ENDPOINT overrides it for one signal.", + "OTLP endpoint for every signal. `OTEL_EXPORTER_OTLP__ENDPOINT` overrides it for one signal.", ComponentController, ) OtelExporterOTLPProtocol = RegisterStringVar( "OTEL_EXPORTER_OTLP_PROTOCOL", "grpc", - "OTLP protocol, grpc or http/protobuf. OTEL_EXPORTER_OTLP__PROTOCOL overrides it for one signal.", + "OTLP protocol, grpc or http/protobuf. `OTEL_EXPORTER_OTLP__PROTOCOL` overrides it for one signal.", ComponentController, ) OtelCaptureMessageContent = RegisterStringVar( "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "NO_CONTENT", - "SPAN_ONLY records prompts and responses on agent spans. This may expose sensitive content.", + "SPAN_ONLY records prompts and responses on agent spans. NO_CONTENT disables capture. Managed runtimes support these two modes; standalone Python ADK also recognizes SPAN_AND_EVENT. Captured content may be sensitive.", ComponentController, ) @@ -52,3 +52,27 @@ var ( ComponentController, ) ) + +var ( + _ = RegisterBoolVar("OTEL_SDK_DISABLED", false, "Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes.", ComponentController) + _ = RegisterStringVar("OTEL_SERVICE_NAME", "", "SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes.", ComponentController) + _ = RegisterStringVar("OTEL_RESOURCE_ATTRIBUTES", "", "Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TIMEOUT", "", "OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms).", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "", "Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "", "Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "", "Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "", "Trace protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "", "Metric protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "", "Log protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "", "Trace SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "", "Metric SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "", "Log SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController) + _ = RegisterStringVar("OTEL_PROPAGATORS", "tracecontext", "SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip", "OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip.", ComponentController) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "base2_exponential_bucket_histogram", "Default SDK histogram aggregation applied by kagent and supplied to managed runtimes.", ComponentController) + _ = RegisterBoolVar("KAGENT_OTEL_CAPTURE_RAW_API_BODIES", false, "Set to true to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data.", ComponentController) + _ = RegisterIntVar("KAGENT_OTEL_MAX_CAPTURE_BYTES", 16384, "Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384.", ComponentController) + _ = RegisterStringVar("ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN", "2", "Python Google ADK telemetry schema version; set by kagent when absent.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental", "Python Google ADK semantic-convention opt-in; set by kagent when absent.", ComponentAgentRuntime) + _ = RegisterStringVar("ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS", "", "Python Google ADK span content capture. When absent, derived from OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (true for SPAN_ONLY or SPAN_AND_EVENT, false otherwise).", ComponentAgentRuntime) +) diff --git a/go/core/pkg/env/providers.go b/go/core/pkg/env/providers.go index 414aecc968..368c3a566d 100644 --- a/go/core/pkg/env/providers.go +++ b/go/core/pkg/env/providers.go @@ -1,7 +1,8 @@ package env -// LLM provider environment variables. These are injected into agent pods -// by the controller and consumed by the agent runtime. +// LLM provider environment variables consumed by runtimes and provider SDKs. +// Managed runtimes receive configuration from the controller; credentials may +// instead be injected into requests at the egress gateway. // OpenAI var ( @@ -56,7 +57,7 @@ var ( OpenAIAPIVersion = RegisterStringVar( "OPENAI_API_VERSION", "", - "Azure OpenAI API version (e.g. 2024-02-15-preview).", + "Azure OpenAI API version. The Go and Python ADKs fall back to 2024-02-15-preview when model configuration and this variable are unset.", ComponentAgentRuntime, ) @@ -111,7 +112,7 @@ var ( AWSRegion = RegisterStringVar( "AWS_REGION", "", - "AWS region for Bedrock.", + "AWS region for Bedrock. Python Bedrock and Go Bedrock embeddings prefer AWS_DEFAULT_REGION, then AWS_REGION, then us-east-1.", ComponentAgentRuntime, ) @@ -149,7 +150,7 @@ var ( OllamaAPIBase = RegisterStringVar( "OLLAMA_API_BASE", "", - "Base URL for the Ollama API endpoint.", + "Base URL for the Ollama API endpoint; falls back to http://localhost:11434 when model configuration and this variable are unset.", ComponentAgentRuntime, ) @@ -161,6 +162,16 @@ var ( ) ) +// Provider aliases and native Claude provider settings. +var ( + _ = RegisterStringVar("GEMINI_API_KEY", "", "Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs.", ComponentAgentRuntime) + _ = RegisterStringVar("GOOGLE_CLOUD_REGION", "", "Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset.", ComponentAgentRuntime) + _ = RegisterStringVar("AWS_DEFAULT_REGION", "", "Preferred region for Python Bedrock models and Go/Python Bedrock embeddings, before AWS_REGION and the us-east-1 fallback.", ComponentAgentRuntime) + _ = RegisterStringVar("ANTHROPIC_BASE_URL", "", "Native Claude Anthropic API base URL, supplied from ModelConfig by the controller.", ComponentAgentRuntime) + _ = RegisterStringVar("ANTHROPIC_VERTEX_PROJECT_ID", "", "Native Claude Vertex AI project, supplied from ModelConfig by the controller.", ComponentAgentRuntime) + _ = RegisterStringVar("CLOUD_ML_REGION", "", "Native Claude Vertex AI region, supplied from ModelConfig by the controller.", ComponentAgentRuntime) +) + // SAP AI Core var ( SAPAICoreClientID = RegisterStringVar( diff --git a/go/core/pkg/env/runtime.go b/go/core/pkg/env/runtime.go new file mode 100644 index 0000000000..fe5fa5ef34 --- /dev/null +++ b/go/core/pkg/env/runtime.go @@ -0,0 +1,56 @@ +package env + +// Documentation-only registrations for settings consumed outside the controller. +// Keep their defaults aligned with the Go/Python readers; importing this package +// is not required for a runtime or an upstream SDK to read its environment. +var ( + _ = RegisterStringVar("PORT", "8080", "Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port.", ComponentAgentRuntime) + _ = RegisterStringVar("CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) + _ = RegisterStringVar("A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_A2A_GRPC_ADDRESS", "[::]:80", "Python ADK gRPC listen address.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_CONFIG_JSON", "", "Compiled harness configuration JSON injected by the controller. Required by Codex and Claude; materialized to config.json by the ADKs when present.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_AGENT_CARD_JSON", "", "A2A agent card JSON injected by the controller. Required by Codex and Claude; materialized to agent-card.json by the ADKs when present.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_TOKEN", "", "Runtime authentication token, materialized to /var/run/secrets/tokens/kagent-token by the ADKs when present.", ComponentAgentRuntime) + _ = RegisterStringVar("UVICORN_LOG_LEVEL", "", "Python ADK HTTP server log level. Falls back to LOG_LEVEL, then info.", ComponentAgentRuntime) + _ = RegisterStringVar("BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) + _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) + _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_CREDENTIAL_", "", "Controller-generated credential variables referenced by compiled ADK configuration; names depend on the configured tools and models.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_CLAUDE_MCP_CREDENTIAL_", "", "Controller-generated Claude MCP header credentials; names depend on the configured tools.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_CODEX_MCP_CREDENTIAL_", "", "Controller-generated Codex MCP header credentials; names depend on the configured tools.", ComponentAgentRuntime) +) + +// Harness-owned process environment. These are outputs of harness compilation or +// runtime setup, not independent switches for operators to configure. +var ( + _ = RegisterStringVar("CODEX_HOME", "", "Codex home directory set by the harness under its durable private state.", ComponentAgentRuntime) + _ = RegisterStringVar("CLAUDE_CONFIG_DIR", "", "Claude configuration directory set by the harness under its durable private state.", ComponentAgentRuntime) + _ = RegisterStringVar("DISABLE_UPDATES", "1", "Set by the Claude harness to disable native automatic updates.", ComponentAgentRuntime) + _ = RegisterStringVar("IS_SANDBOX", "1", "Set by the controller for managed Claude runtimes.", ComponentAgentRuntime) + _ = RegisterStringVar("CLAUDE_CODE_USE_BEDROCK", "", "Set to 1 by the controller when the Claude ModelConfig uses Bedrock.", ComponentAgentRuntime) + _ = RegisterStringVar("CLAUDE_CODE_USE_VERTEX", "", "Set to 1 by the controller when the Claude ModelConfig uses Vertex AI.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_CLAUDE_GOOGLE_CREDENTIALS_JSON", "", "Vertex service-account JSON injected into Claude and materialized as the GOOGLE_APPLICATION_CREDENTIALS file; removed before launching Claude.", ComponentAgentRuntime) + _ = RegisterStringVar("CLAUDE_CODE_ENABLE_TELEMETRY", "", "Set to 1 by the Claude harness when telemetry is enabled.", ComponentAgentRuntime) + _ = RegisterStringVar("CLAUDE_CODE_ENHANCED_TELEMETRY_BETA", "", "Set to 1 by the Claude harness when telemetry is enabled.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_LOG_USER_PROMPTS", "", "Set to 1 by the Claude harness when content capture is enabled.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_LOG_TOOL_DETAILS", "", "Set to 1 by the Claude harness when content capture is enabled.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_LOG_TOOL_CONTENT", "", "Set to 1 by the Claude harness when trace export and content capture are enabled.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_LOG_ASSISTANT_RESPONSES", "", "Set to 1 by the Claude harness when log export and content capture are enabled.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_LOG_RAW_API_BODIES", "", "Set to 1 for Claude when KAGENT_OTEL_CAPTURE_RAW_API_BODIES enables raw body capture.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_PROMETHEUS_HOST", "", "Private loopback Prometheus host assigned by the Claude driver for native telemetry readiness checks.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_PROMETHEUS_PORT", "", "Private Prometheus port assigned by the Claude driver for native telemetry readiness checks.", ComponentAgentRuntime) + _ = RegisterStringVar("TRACEPARENT", "", "W3C trace context supplied per invocation to the native Claude process.", ComponentAgentRuntime) + _ = RegisterStringVar("TRACESTATE", "", "W3C trace state supplied per invocation to the native Claude process.", ComponentAgentRuntime) +) + +// Substrate replaces these with the managed egress trust bundle. +var ( + _ = RegisterStringVar("SSL_CERT_FILE", "", "Managed runtime and sandbox CA bundle, injected by Substrate for egress TLS trust.", ComponentAgentRuntime) + _ = RegisterStringVar("SSL_CERT_DIR", "", "Managed agent runtime CA directory, injected by Substrate for egress TLS trust.", ComponentAgentRuntime) + _ = RegisterStringVar("REQUESTS_CA_BUNDLE", "", "Managed runtime and sandbox CA bundle for Python requests, injected by Substrate.", ComponentAgentRuntime) + _ = RegisterStringVar("AWS_CA_BUNDLE", "", "Managed runtime and sandbox CA bundle for AWS clients, injected by Substrate.", ComponentAgentRuntime) + _ = RegisterStringVar("CURL_CA_BUNDLE", "", "Managed runtime and sandbox CA bundle for curl, injected by Substrate.", ComponentAgentRuntime) + _ = RegisterStringVar("NODE_EXTRA_CA_CERTS", "", "Managed runtime and sandbox CA bundle for Node.js, injected by Substrate.", ComponentAgentRuntime) + _ = RegisterStringVar("GIT_SSL_CAINFO", "", "Managed runtime and sandbox CA bundle for Git, injected by Substrate.", ComponentAgentRuntime) + _ = RegisterStringVar("DOCKER_CONFIG", "", "Docker registry credentials directory used by skills-init for OCI pulls; mounted by the controller when image pull secrets are configured.", ComponentAgentRuntime) +) diff --git a/go/core/pkg/env/substrate.go b/go/core/pkg/env/substrate.go index d0aecc5abe..2a8aaf57f6 100644 --- a/go/core/pkg/env/substrate.go +++ b/go/core/pkg/env/substrate.go @@ -1,5 +1,11 @@ package env +var ( + SubstrateATEAPIEndpoint = RegisterStringVar("SUBSTRATE_ATE_API_ENDPOINT", "dns:///api.ate-system.svc:443", "Substrate control-plane gRPC endpoint.", ComponentController) + SubstrateATEAPICAFile = RegisterStringVar("SUBSTRATE_ATE_API_CA_FILE", "", "PEM CA bundle used to verify the Substrate API server. Empty uses system trust roots.", ComponentController) + SubstrateATEAPIClientCertFile = RegisterStringVar("SUBSTRATE_ATE_API_CLIENT_CERT_FILE", "", "PEM bundle containing both the client certificate and private key for Substrate API mTLS. Reloaded for each TLS handshake.", ComponentController) +) + var SubstrateAtenetRouterURL = RegisterStringVar( "SUBSTRATE_ATENET_ROUTER_URL", "http://atenet-router.ate-system.svc:80", diff --git a/go/core/pkg/env/testing.go b/go/core/pkg/env/testing.go index 2f366d66ee..acbf5403eb 100644 --- a/go/core/pkg/env/testing.go +++ b/go/core/pkg/env/testing.go @@ -5,14 +5,7 @@ var ( KagentLocalHost = RegisterStringVar( "KAGENT_LOCAL_HOST", "", - "Local host override for E2E tests.", - ComponentTesting, - ) - - SkipCleanup = RegisterBoolVar( - "SKIP_CLEANUP", - false, - "When true, skip cleanup after E2E tests for debugging.", + "Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems.", ComponentTesting, ) @@ -25,15 +18,35 @@ var ( STSPort = RegisterStringVar( "STS_PORT", - "", + "8091", "Port for the mock STS (Security Token Service) server.", ComponentTesting, ) LLMPort = RegisterStringVar( "LLM_PORT", - "", + "8090", "Port for the mock LLM server.", ComponentTesting, ) ) + +var ( + _ = RegisterStringVar("KAGENT_TEST_PYTHON", "", "Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_API_URL", "", "Controller URL for Go E2E tests. Falls back to KAGENT_API_URL; tests skip when neither is set.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_CLI", "", "CLI executable for catalog lifecycle E2E tests; make -C go e2e builds and supplies it.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_RUNTIME_IMAGE", "", "Required digest-pinned Go ADK image for sandbox E2E tests and lifecycle manifests.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_BYO_IMAGE", "", "Digest-pinned BYO image substituted into E2E lifecycle manifests.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_CLAUDE_IMAGE", "", "Digest-pinned Claude harness image substituted into E2E lifecycle manifests.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_CODEX_IMAGE", "", "Digest-pinned Codex harness image substituted into E2E lifecycle manifests.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_SANDBOX_NAMESPACE", "kagent", "Namespace for sandbox E2E resources.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_E2E_REQUIRE_TRACING", false, "Fail instead of skip when expected native harness tracing support is unavailable.", ComponentTesting) + _ = RegisterBoolVar("RUN_UPGRADE_TESTS", false, "Run upgrade integration tests when exactly true.", ComponentTesting) + _ = RegisterBoolVar("RUN_ROLLING_UPGRADE_TESTS", false, "Run rolling upgrade integration tests when exactly true.", ComponentTesting) + _ = RegisterStringVar("UI_LOOP_PORT", "8001", "Vite development/preview port and UI browser-test port.", ComponentTesting) + _ = RegisterStringVar("UI_LOOP_EXTENSION_PORT", "", "Example-extension browser-test port. Defaults to UI_LOOP_PORT plus 50.", ComponentTesting) + _ = RegisterBoolVar("UI_LOOP_LIVE", false, "Run UI browser tests against a real cluster when exactly true.", ComponentTesting) + _ = RegisterStringVar("UI_LOOP_LIVE_PORT", "8301", "Live-cluster UI browser-test port.", ComponentTesting) +) diff --git a/go/core/pkg/env/ui.go b/go/core/pkg/env/ui.go new file mode 100644 index 0000000000..cfab3ea81b --- /dev/null +++ b/go/core/pkg/env/ui.go @@ -0,0 +1,17 @@ +package env + +// The container entrypoint maps KAGENT_* deployment names to browser keys. +// Vite reads the browser key names directly from the development environment. +var ( + _ = RegisterStringVar("KAGENT_API_BASE_URL", "/api", "UI container setting mapped to the browser's API_BASE_URL at startup.", ComponentUI) + _ = RegisterStringVar("KAGENT_STREAM_TIMEOUT_MS", "1800000", "UI container chat stream inactivity timeout in milliseconds, mapped to STREAM_TIMEOUT_MS. 0 disables the timeout.", ComponentUI) + _ = RegisterStringVar("KAGENT_UI_BASE_PATH", "", "UI container public path prefix, such as /ui; empty serves at the root. Mapped to BASE_PATH; invalid or reserved prefixes fall back to the root.", ComponentUI) + _ = RegisterStringVar("API_BASE_URL", "/api", "Browser API base URL. Set directly for Vite development; use KAGENT_API_BASE_URL in the UI container.", ComponentUI) + _ = RegisterStringVar("STREAM_TIMEOUT_MS", "1800000", "Browser chat stream inactivity timeout in milliseconds. Set directly for Vite development; use KAGENT_STREAM_TIMEOUT_MS in the UI container. 0 disables the timeout.", ComponentUI) + _ = RegisterStringVar("BASE_PATH", "", "Browser public path prefix. Set directly for Vite development; use KAGENT_UI_BASE_PATH in the UI container.", ComponentUI) + _ = RegisterStringVar("SSO_REDIRECT_PATH", "/oauth2/start", "UI path used by Sign in with SSO.", ComponentUI) + _ = RegisterBoolVar("ENABLE_MOCK_UI", false, "Serve the UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Applies in development and containers.", ComponentUI) + _ = RegisterStringVar("EXTENSION_", "", "UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public.", ComponentUI) + _ = RegisterStringVar("KAGENT_DEV_CONTROLLER_URL", "http://127.0.0.1:8083", "Vite development proxy target for /api and /a2a; not sent to the browser.", ComponentUI) + _ = RegisterBoolVar("VITE_EXAMPLE_EXTENSION", false, "Build-time switch enabling the bundled example UI extension.", ComponentUI) +) diff --git a/go/core/pkg/env/var.go b/go/core/pkg/env/var.go index d8a9f6725c..538eb9569f 100644 --- a/go/core/pkg/env/var.go +++ b/go/core/pkg/env/var.go @@ -62,6 +62,7 @@ const ( ComponentAgentRuntime Component = "agent-runtime" ComponentTesting Component = "testing" ComponentDatabase Component = "database" + ComponentUI Component = "ui" ) // Var holds the metadata for a single registered environment variable. @@ -311,6 +312,15 @@ func ExportMarkdown(component string) string { var sb strings.Builder sb.WriteString("# Kagent Environment Variables\n\n") + sb.WriteString("\n\n") + sb.WriteString("Generated from `go/core/pkg/env`. Edit the registrations there, run `make env-docs`, " + + "and commit the result. CI runs `make env-docs-check`.\n\n") + sb.WriteString("This reference covers the controller, CLI, agent runtimes, UI, and registered test settings. " + + "Build scripts, sample applications, and third-party SDK settings not configured by kagent have their own documentation. " + + "Defaults describe the application without deployment overrides; Helm or a Harness may supply different values. " + + "`(none)` means no fixed default; see the description for required values and fallbacks. " + + "Runtime-injected variables are managed by kagent, as noted below. " + + "Only registered metadata is exported, never values from the current process environment.\n\n") // Group by component grouped := make(map[Component][]Var) @@ -353,7 +363,7 @@ func ExportMarkdown(component string) string { sb.WriteString("\n") } - return sb.String() + return strings.TrimSuffix(sb.String(), "\n") } // ExportJSON generates a JSON array of all registered variables. diff --git a/go/core/pkg/env/var_test.go b/go/core/pkg/env/var_test.go index 88c51c14d3..4cf5d68f4b 100644 --- a/go/core/pkg/env/var_test.go +++ b/go/core/pkg/env/var_test.go @@ -264,6 +264,13 @@ func TestExportMarkdown(t *testing.T) { RegisterBoolVar("TEST_MD_BOOL", true, "A bool variable.", ComponentController) md := ExportMarkdown("all") + if !strings.HasSuffix(md, "\n") || strings.HasSuffix(md, "\n\n") { + t.Error("markdown should end with exactly one newline") + } + t.Setenv("TEST_MD_VAR", "secret-from-current-environment") + if got := ExportMarkdown("all"); got != md { + t.Error("documentation must contain registered defaults, not live environment values") + } if !strings.Contains(md, "TEST_MD_VAR") { t.Error("markdown should contain TEST_MD_VAR") } From 13f288a6f97854aca561fe415b5cda52fed443a5 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 12:38:26 +0000 Subject: [PATCH 02/14] refactor: document configurable environment variables across components Signed-off-by: Eitan Yarmush --- .github/workflows/ci.yaml | 1 - DEVELOPMENT.md | 17 +++- .../architecture/oidc-proxy-authentication.md | 12 +-- docs/env.md | 96 +++++++++---------- go/core/pkg/env/controller.go | 11 +-- go/core/pkg/env/database.go | 8 +- go/core/pkg/env/kagent.go | 16 ++-- go/core/pkg/env/legacy.go | 19 ---- go/core/pkg/env/otel.go | 44 ++++----- go/core/pkg/env/providers.go | 17 ++-- go/core/pkg/env/runtime.go | 43 +-------- go/core/pkg/env/var.go | 46 +++++---- go/core/pkg/env/var_test.go | 87 ++++++++++++++++- .../templates/controller-deployment.yaml | 20 ---- helm/kagent/templates/toolserver-api.yaml | 2 +- .../tests/controller-deployment_test.yaml | 7 +- helm/kagent/tests/toolserver-api_test.yaml | 12 --- helm/kagent/values.yaml | 21 +--- scripts/setup-cluster/setup-cluster.sh | 1 - 19 files changed, 225 insertions(+), 255 deletions(-) delete mode 100644 go/core/pkg/env/legacy.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 451cc93980..f15c857bfa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -134,7 +134,6 @@ jobs: --set controller.substrate.enabled=true --set controller.substrate.ateApiEndpoint=dns:///api.ate-system.svc:443 --set controller.substrate.atenetRouterURL=http://atenet-router.ate-system.svc:80 - --set controller.substrate.defaultWorkerPool.name=kagent-default --set substrateWorkerPool.create=true --set substrateWorkerPool.replicas=4 --set-string substrateWorkerPool.workerImage=ghcr.io/kagent-dev/substrate/ateom-gvisor:v${{ env.SUBSTRATE_VERSION }} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index ab093468ec..7a966ca320 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,11 +1,24 @@ # Development The [environment variable reference](docs/env.md) is generated from -`go/core/pkg/env`. Register new settings there, including settings consumed by -Python, the UI, or a native harness. Keep defaults and descriptions aligned with +`go/core/pkg/env`. Register user-configurable settings there, including settings +consumed by Python, the UI, or standalone runtimes. Exclude controller-generated +payloads, credentials, private paths, and other internal process wiring. +Keep defaults and descriptions aligned with their readers, then run `make env-docs`. Use `make env-docs-check` to run the same freshness check as CI. +Register shared settings once, passing each consuming component to +`RegisterStringVar`, `RegisterBoolVar`, `RegisterIntVar`, or +`RegisterDurationVar`, for example: + +```go +RegisterStringVar("LOG_LEVEL", "info", "Logging level.", ComponentController, ComponentCLI, ComponentAgentRuntime) +``` + +The reference lists shared settings under each component. `kagent env --component` +matches any registered component; JSON output includes a `components` array. + To understand how to develop for kagent, it's important to understand the architecture of the project. Please refer to the [README.md](README.md#architecture) file for an overview of the project. When making changes to `kagent`, the most important thing is to figure out which piece of the project is affected by the change, and then make the change in the appropriate folder. Each piece of the project has its own README with more information about how to setup the development environment and run that piece of the project. diff --git a/docs/architecture/oidc-proxy-authentication.md b/docs/architecture/oidc-proxy-authentication.md index ccd2bf0d65..aec78b121f 100644 --- a/docs/architecture/oidc-proxy-authentication.md +++ b/docs/architecture/oidc-proxy-authentication.md @@ -119,14 +119,10 @@ differs. | `oauth2-proxy.enabled` | `false` | Install the authentication proxy | | `oauth2-proxy.config.existingSecret` | Empty | Reference client and cookie credentials | | `ui.auth.ssoRedirectPath` | `/oauth2/start` | Start or restart browser sign-in | -| `controller.auth.mode` | `unsecure` | Render the controller's `AUTH_MODE` environment variable | -| `controller.auth.userIdClaim` | Empty | Render `AUTH_USER_ID_CLAIM` when supplied | - -The last two settings remain in the chart, but the current controller does not -consume them. Setting `controller.auth.mode: trusted-proxy` alone does not -activate `ProxyAuthenticator`, and the controller has no `--auth-mode` or -`--auth-user-id-claim` flags. A library integration must supply its own -`auth.AuthProvider` through `app.Options.Authenticator`. + +Enabling the proxy does not activate controller authentication. A library +integration must supply its own `auth.AuthProvider` through +`app.Options.Authenticator`. The UI reads `SSO_REDIRECT_PATH` from `window.environmentVariables` at runtime and applies its configured base path. Helm supplies that value from diff --git a/docs/env.md b/docs/env.md index baf6f27d77..246e523cb0 100644 --- a/docs/env.md +++ b/docs/env.md @@ -4,7 +4,7 @@ Generated from `go/core/pkg/env`. Edit the registrations there, run `make env-docs`, and commit the result. CI runs `make env-docs-check`. -This reference covers the controller, CLI, agent runtimes, UI, and registered test settings. Build scripts, sample applications, and third-party SDK settings not configured by kagent have their own documentation. Defaults describe the application without deployment overrides; Helm or a Harness may supply different values. `(none)` means no fixed default; see the description for required values and fallbacks. Runtime-injected variables are managed by kagent, as noted below. Only registered metadata is exported, never values from the current process environment. +This reference covers user-configurable settings for the controller, CLI, standalone agent runtimes, UI, and tests. Controller-generated runtime payloads, credentials, private paths, and other internal process wiring are excluded. Build scripts, sample applications, and third-party SDK settings not configured by kagent have their own documentation. Defaults describe the application without deployment overrides; Helm or a Harness may supply different values. `(none)` means no fixed default; see the description for required values and fallbacks. Shared variables appear under each consuming component. Only registered metadata is exported, never values from the current process environment. ## agent-runtime @@ -14,11 +14,8 @@ This reference covers the controller, CLI, agent runtimes, UI, and registered te | `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS` | String | `(none)` | Python Google ADK span content capture. When absent, derived from OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (true for SPAN_ONLY or SPAN_AND_EVENT, false otherwise). | | `ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN` | String | `2` | Python Google ADK telemetry schema version; set by kagent when absent. | | `ANTHROPIC_API_KEY` | String | `(none)` | API key for Anthropic. | -| `ANTHROPIC_BASE_URL` | String | `(none)` | Native Claude Anthropic API base URL, supplied from ModelConfig by the controller. | -| `ANTHROPIC_VERTEX_PROJECT_ID` | String | `(none)` | Native Claude Vertex AI project, supplied from ModelConfig by the controller. | | `AWS_ACCESS_KEY_ID` | String | `(none)` | AWS access key ID for IAM authentication with Bedrock. | | `AWS_BEARER_TOKEN_BEDROCK` | String | `(none)` | Bearer token for authentication with AWS Bedrock. | -| `AWS_CA_BUNDLE` | String | `(none)` | Managed runtime and sandbox CA bundle for AWS clients, injected by Substrate. | | `AWS_DEFAULT_REGION` | String | `(none)` | Preferred region for Python Bedrock models and Go/Python Bedrock embeddings, before AWS_REGION and the us-east-1 fallback. | | `AWS_REGION` | String | `(none)` | AWS region for Bedrock. Python Bedrock and Go Bedrock embeddings prefer AWS_DEFAULT_REGION, then AWS_REGION, then us-east-1. | | `AWS_SECRET_ACCESS_KEY` | String | `(none)` | AWS secret access key for IAM authentication with Bedrock. | @@ -27,50 +24,32 @@ This reference covers the controller, CLI, agent runtimes, UI, and registered te | `AZURE_OPENAI_API_KEY` | String | `(none)` | API key for Azure OpenAI. | | `AZURE_OPENAI_ENDPOINT` | String | `(none)` | Endpoint URL for Azure OpenAI service. | | `BASH_VENV_PATH` | String | `(none)` | Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set. | -| `CLAUDE_CODE_ENABLE_TELEMETRY` | String | `(none)` | Set to 1 by the Claude harness when telemetry is enabled. | -| `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA` | String | `(none)` | Set to 1 by the Claude harness when telemetry is enabled. | -| `CLAUDE_CODE_USE_BEDROCK` | String | `(none)` | Set to 1 by the controller when the Claude ModelConfig uses Bedrock. | -| `CLAUDE_CODE_USE_VERTEX` | String | `(none)` | Set to 1 by the controller when the Claude ModelConfig uses Vertex AI. | -| `CLAUDE_CONFIG_DIR` | String | `(none)` | Claude configuration directory set by the harness under its durable private state. | -| `CLOUD_ML_REGION` | String | `(none)` | Native Claude Vertex AI region, supplied from ModelConfig by the controller. | -| `CODEX_HOME` | String | `(none)` | Codex home directory set by the harness under its durable private state. | | `CONFIG_DIR` | String | `/config` | Go ADK configuration directory; --filepath takes precedence. | -| `CURL_CA_BUNDLE` | String | `(none)` | Managed runtime and sandbox CA bundle for curl, injected by Substrate. | -| `DISABLE_UPDATES` | String | `1` | Set by the Claude harness to disable native automatic updates. | -| `DOCKER_CONFIG` | String | `(none)` | Docker registry credentials directory used by skills-init for OCI pulls; mounted by the controller when image pull secrets are configured. | | `FOUNDRY_API_KEY` | String | `(none)` | API key for Azure AI Foundry. | | `FOUNDRY_API_VERSION` | String | `2024-10-21` | Azure AI Foundry OpenAI-compatible data-plane API version. | | `FOUNDRY_DEPLOYMENT` | String | `(none)` | Azure AI Foundry model deployment name. | | `FOUNDRY_ENDPOINT` | String | `(none)` | Endpoint URL for Azure AI Foundry or an Azure AI Services account. | | `GEMINI_API_KEY` | String | `(none)` | Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs. | -| `GIT_SSL_CAINFO` | String | `(none)` | Managed runtime and sandbox CA bundle for Git, injected by Substrate. | | `GOOGLE_API_KEY` | String | `(none)` | API key for Google Gemini. | | `GOOGLE_APPLICATION_CREDENTIALS` | String | `(none)` | Path to Google Cloud service account JSON key file. | | `GOOGLE_CLOUD_LOCATION` | String | `(none)` | Google Cloud region/location for Vertex AI. | | `GOOGLE_CLOUD_PROJECT` | String | `(none)` | Google Cloud project ID for Vertex AI. | | `GOOGLE_CLOUD_REGION` | String | `(none)` | Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset. | | `GOOGLE_GENAI_USE_VERTEXAI` | String | `(none)` | When set to 'true', use Vertex AI for Gemini models. | -| `IS_SANDBOX` | String | `1` | Set by the controller for managed Claude runtimes. | | `KAGENT_A2A_GRPC_ADDRESS` | String | `[::]:80` | Python ADK gRPC listen address. | -| `KAGENT_AGENT_CARD_JSON` | String | `(none)` | A2A agent card JSON injected by the controller. Required by Codex and Claude; materialized to agent-card.json by the ADKs when present. | -| `KAGENT_API_URL` | String | `(none)` | Base URL for kagent control-plane API calls, injected into managed runtimes. Required by Python runtimes. | -| `KAGENT_CLAUDE_GOOGLE_CREDENTIALS_JSON` | String | `(none)` | Vertex service-account JSON injected into Claude and materialized as the GOOGLE_APPLICATION_CREDENTIALS file; removed before launching Claude. | -| `KAGENT_CLAUDE_MCP_CREDENTIAL_` | String | `(none)` | Controller-generated Claude MCP header credentials; names depend on the configured tools. | -| `KAGENT_CODEX_MCP_CREDENTIAL_` | String | `(none)` | Controller-generated Codex MCP header credentials; names depend on the configured tools. | -| `KAGENT_CONFIG_JSON` | String | `(none)` | Compiled harness configuration JSON injected by the controller. Required by Codex and Claude; materialized to config.json by the ADKs when present. | -| `KAGENT_CREDENTIAL_` | String | `(none)` | Controller-generated credential variables referenced by compiled ADK configuration; names depend on the configured tools and models. | +| `KAGENT_API_URL` | String | `(none)` | Base URL for kagent control-plane API calls. Required by Python runtimes and supplied by the controller in managed runtimes; also used as the E2E test URL when KAGENT_E2E_API_URL is unset. | | `KAGENT_ENABLE_FILE_SEARCH_TOOLS` | Boolean | `false` | When true, enables the list_files and grep_file skills tools, which let an agent enumerate and search the filesystem under its session/skills roots without a shell. Disabled by default; set in Harness env to opt in. | | `KAGENT_GATEWAY_URL` | String | `(none)` | Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value. | -| `KAGENT_NAME` | String | `(none)` | Name of the agent, injected by the controller. Required by Python runtimes. | +| `KAGENT_NAME` | String | `(none)` | Agent name for standalone runtimes. Required by Python runtimes; supplied by the controller in managed runtimes. | +| `KAGENT_NAMESPACE` | String | `kagent` | Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it. | | `KAGENT_OPENAI_AGENTS_NATIVE_TRACING` | Boolean | `false` | Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime. | | `KAGENT_PROPAGATE_TOKEN` | String | `(none)` | Set to true to propagate authentication tokens to downstream services. Unset or any other value disables propagation. | | `KAGENT_SKILLS_FOLDER` | String | `/skills` | Skills directory for standalone Python skills tools. The Python ADK adds skills tools when set; managed Go ADK runtimes use their compiled skill configuration. | | `KAGENT_STS_AUDIENCE` | String | `(none)` | Comma-separated RFC 8693 audiences sent on STS token-exchange requests. Alternate to KAGENT_STS_RESOURCE for servers that key on audience. | | `KAGENT_STS_RESOURCE` | String | `(none)` | Comma-separated RFC 8707 resource indicators sent on STS token-exchange requests to scope issued tokens to target backends. | -| `KAGENT_TOKEN` | String | `(none)` | Runtime authentication token, materialized to /var/run/secrets/tokens/kagent-token by the ADKs when present. | +| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels. | | `MISTRAL_API_BASE` | String | `(none)` | Custom base URL for the Mistral AI API (defaults to https://api.mistral.ai/v1). | | `MISTRAL_API_KEY` | String | `(none)` | API key for Mistral AI. | -| `NODE_EXTRA_CA_CERTS` | String | `(none)` | Managed runtime and sandbox CA bundle for Node.js, injected by Substrate. | | `OLLAMA_API_BASE` | String | `(none)` | Base URL for the Ollama API endpoint; falls back to http://localhost:11434 when model configuration and this variable are unset. | | `OLLAMA_API_KEY` | String | `(none)` | API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly. | | `OPENAI_AGENTS_DISABLE_TRACING` | Boolean | `false` | Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1. | @@ -78,49 +57,63 @@ This reference covers the controller, CLI, agent runtimes, UI, and registered te | `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. | | `OPENAI_API_VERSION` | String | `(none)` | Azure OpenAI API version. The Go and Python ADKs fall back to 2024-02-15-preview when model configuration and this variable are unset. | | `OPENAI_ORGANIZATION` | String | `(none)` | OpenAI organization identifier. | -| `OTEL_EXPORTER_PROMETHEUS_HOST` | String | `(none)` | Private loopback Prometheus host assigned by the Claude driver for native telemetry readiness checks. | -| `OTEL_EXPORTER_PROMETHEUS_PORT` | String | `(none)` | Private Prometheus port assigned by the Claude driver for native telemetry readiness checks. | -| `OTEL_LOG_ASSISTANT_RESPONSES` | String | `(none)` | Set to 1 by the Claude harness when log export and content capture are enabled. | -| `OTEL_LOG_RAW_API_BODIES` | String | `(none)` | Set to 1 for Claude when KAGENT_OTEL_CAPTURE_RAW_API_BODIES enables raw body capture. | -| `OTEL_LOG_TOOL_CONTENT` | String | `(none)` | Set to 1 by the Claude harness when trace export and content capture are enabled. | -| `OTEL_LOG_TOOL_DETAILS` | String | `(none)` | Set to 1 by the Claude harness when content capture is enabled. | -| `OTEL_LOG_USER_PROMPTS` | String | `(none)` | Set to 1 by the Claude harness when content capture is enabled. | +| `OTEL_EXPORTER_OTLP_COMPRESSION` | String | `gzip` | OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip. | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | String | `(none)` | OTLP endpoint for every signal. `OTEL_EXPORTER_OTLP__ENDPOINT` overrides it for one signal. | +| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | String | `(none)` | Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path. | +| `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` | String | `(none)` | Log protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL. | +| `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` | String | `(none)` | Log SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller. | +| `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` | String | `base2_exponential_bucket_histogram` | Default SDK histogram aggregation applied by kagent and supplied to managed runtimes. | +| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | String | `(none)` | Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path. | +| `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` | String | `(none)` | Metric protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL. | +| `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` | String | `(none)` | Metric SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller. | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | String | `grpc` | OTLP protocol, grpc or http/protobuf. `OTEL_EXPORTER_OTLP__PROTOCOL` overrides it for one signal. | +| `OTEL_EXPORTER_OTLP_TIMEOUT` | String | `(none)` | OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms). | +| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | String | `(none)` | Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path. | +| `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` | String | `(none)` | Trace protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL. | +| `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` | String | `(none)` | Trace SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller. | +| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | String | `NO_CONTENT` | SPAN_ONLY records prompts and responses on agent spans. NO_CONTENT disables capture. Managed runtimes support these two modes; standalone Python ADK also recognizes SPAN_AND_EVENT. Captured content may be sensitive. | +| `OTEL_LOGS_EXPORTER` | String | `(none)` | Log exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | +| `OTEL_METRICS_EXPORTER` | String | `(none)` | Metric exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | +| `OTEL_PROPAGATORS` | String | `tracecontext` | SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes. | +| `OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents. | +| `OTEL_SDK_DISABLED` | Boolean | `false` | Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes. | | `OTEL_SEMCONV_STABILITY_OPT_IN` | String | `gen_ai_latest_experimental` | Python Google ADK semantic-convention opt-in; set by kagent when absent. | +| `OTEL_SERVICE_NAME` | String | `(none)` | SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes. | +| `OTEL_TRACES_EXPORTER` | String | `(none)` | Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | | `PORT` | String | `8080` | Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port. | -| `REQUESTS_CA_BUNDLE` | String | `(none)` | Managed runtime and sandbox CA bundle for Python requests, injected by Substrate. | | `SAP_AI_CORE_CLIENT_ID` | String | `(none)` | OAuth2 client ID for SAP AI Core authentication. | | `SAP_AI_CORE_CLIENT_SECRET` | String | `(none)` | OAuth2 client secret for SAP AI Core authentication. | -| `SSL_CERT_DIR` | String | `(none)` | Managed agent runtime CA directory, injected by Substrate for egress TLS trust. | -| `SSL_CERT_FILE` | String | `(none)` | Managed runtime and sandbox CA bundle, injected by Substrate for egress TLS trust. | | `STS_WELL_KNOWN_URI` | String | `(none)` | Well-known endpoint for the Security Token Service (STS) used for token exchange. | -| `TRACEPARENT` | String | `(none)` | W3C trace context supplied per invocation to the native Claude process. | -| `TRACESTATE` | String | `(none)` | W3C trace state supplied per invocation to the native Claude process. | | `UVICORN_LOG_LEVEL` | String | `(none)` | Python ADK HTTP server log level. Falls back to LOG_LEVEL, then info. | ## cli | Variable | Type | Default | Description | |----------|------|---------|-------------| +| `ANTHROPIC_API_KEY` | String | `(none)` | API key for Anthropic. | +| `AZURE_OPENAI_API_KEY` | String | `(none)` | API key for Azure OpenAI. | +| `DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | +| `GEMINI_API_KEY` | String | `(none)` | Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs. | +| `GOOGLE_API_KEY` | String | `(none)` | API key for Google Gemini. | | `KAGENT_DEFAULT_MODEL_PROVIDER` | String | `openAI` | Default LLM provider for agents (e.g. openAI, anthropic, ollama, azureOpenAI). | | `KAGENT_HELM_EXTRA_ARGS` | String | `(none)` | Additional arguments to pass to Helm commands. | | `KAGENT_HELM_REPO` | String | `oci://ghcr.io/kagent-dev/kagent/helm/` | Helm repository URL for kagent charts. | | `KAGENT_HELM_VERSION` | String | `(none)` | Helm chart version to deploy. When unset, the CLI uses its own version. | +| `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | +| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `OLLAMA_API_KEY` | String | `(none)` | API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly. | +| `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. | +| `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | ## controller | Variable | Type | Default | Description | |----------|------|---------|-------------| -| `AUTH_MODE` | String | `(none)` | Rendered by Helm but ignored by the current controller. Authentication is supplied through app.Options.Authenticator. **(deprecated)** | -| `AUTH_USER_ID_CLAIM` | String | `(none)` | Rendered by Helm but ignored by the current controller. Identity extraction belongs to the configured authenticator. **(deprecated)** | -| `GRPC_MAX_MESSAGE_BYTES` | String | `(none)` | Rendered by Helm but ignored by the current controller; does not change its gRPC message limits. **(deprecated)** | +| `DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | | `GRPC_REFLECTION` | Boolean | `false` | Enable gRPC server reflection on the controller. | -| `GRPC_TLS_CERT_FILE` | String | `(none)` | Rendered by Helm but ignored by the current controller; does not enable TLS on its listener. **(deprecated)** | -| `GRPC_TLS_KEY_FILE` | String | `(none)` | Rendered by Helm but ignored by the current controller; does not enable TLS on its listener. **(deprecated)** | | `HTTP_BIND_ADDRESS` | String | `:8083` | Listen address for the controller HTTP, gRPC, A2A, and MCP server. | -| `K8S_NODE_NAME` | String | `(none)` | Controller node name, injected by the Helm chart through the Downward API for telemetry. | -| `K8S_POD_NAME` | String | `(none)` | Controller pod name, injected by the Helm chart through the Downward API for telemetry. | -| `K8S_POD_UID` | String | `(none)` | Controller pod UID, injected by the Helm chart through the Downward API for telemetry. | | `KAGENT_CONTROLLER_NAME` | String | `kagent-controller` | Name of the kagent controller service. | +| `KAGENT_GATEWAY_URL` | String | `(none)` | Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value. | | `KAGENT_NAMESPACE` | String | `kagent` | Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it. | | `KAGENT_OTEL_CAPTURE_RAW_API_BODIES` | Boolean | `false` | Set to true to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data. | | `KAGENT_OTEL_MAX_CAPTURE_BYTES` | Integer | `16384` | Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384. | @@ -152,17 +145,18 @@ This reference covers the controller, CLI, agent runtimes, UI, and registered te | `OTEL_SDK_DISABLED` | Boolean | `false` | Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes. | | `OTEL_SERVICE_NAME` | String | `(none)` | SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes. | | `OTEL_TRACES_EXPORTER` | String | `(none)` | Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | +| `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | +| `POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller. | | `SANDBOX_CPU` | String | `1` | CPU limit for standalone sandbox runtimes. | | `SANDBOX_DEFAULT_TTL` | Duration | `1h0m0s` | Default standalone sandbox lifetime. | | `SANDBOX_GUEST_IMAGE` | String | `(none)` | Guest package image pinned by sha256 digest. Required for sandbox preparation and passed unchanged to Substrate. | | `SANDBOX_MAX_TTL` | Duration | `24h0m0s` | Maximum standalone sandbox lifetime, at most 24h. | | `SANDBOX_MEMORY` | String | `1Gi` | Memory limit for standalone sandbox runtimes. | +| `SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | | `SUBSTRATE_ATENET_ROUTER_URL` | String | `http://atenet-router.ate-system.svc:80` | Substrate router endpoint for agent and sandbox guest traffic. | | `SUBSTRATE_ATE_API_CA_FILE` | String | `(none)` | PEM CA bundle used to verify the Substrate API server. Empty uses system trust roots. | | `SUBSTRATE_ATE_API_CLIENT_CERT_FILE` | String | `(none)` | PEM bundle containing both the client certificate and private key for Substrate API mTLS. Reloaded for each TLS handshake. | | `SUBSTRATE_ATE_API_ENDPOINT` | String | `dns:///api.ate-system.svc:443` | Substrate control-plane gRPC endpoint. | -| `SUBSTRATE_DEFAULT_WORKERPOOL_NAME` | String | `(none)` | Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration. **(deprecated)** | -| `SUBSTRATE_DEFAULT_WORKERPOOL_NAMESPACE` | String | `(none)` | Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration. **(deprecated)** | | `WATCH_NAMESPACES` | String | `(none)` | Comma-separated namespaces to watch. Empty watches all namespaces. | ## database @@ -170,18 +164,15 @@ This reference covers the controller, CLI, agent runtimes, UI, and registered te | Variable | Type | Default | Description | |----------|------|---------|-------------| | `DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | -| `PGDATA` | String | `(none)` | Data directory for bundled PostgreSQL, supplied by the Helm chart. | | `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | | `POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller. | -| `POSTGRES_DB` | String | `(none)` | Initial database name for bundled PostgreSQL, supplied by the Helm chart. | -| `POSTGRES_PASSWORD` | String | `(none)` | Password supplied from the chart-managed Secret to bundled PostgreSQL and expanded into the controller's POSTGRES_DATABASE_URL by Kubernetes. | -| `POSTGRES_USER` | String | `(none)` | Initial user for bundled PostgreSQL, supplied by the Helm chart. | | `SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | ## testing | Variable | Type | Default | Description | |----------|------|---------|-------------| +| `KAGENT_API_URL` | String | `(none)` | Base URL for kagent control-plane API calls. Required by Python runtimes and supplied by the controller in managed runtimes; also used as the E2E test URL when KAGENT_E2E_API_URL is unset. | | `KAGENT_E2E_API_URL` | String | `(none)` | Controller URL for Go E2E tests. Falls back to KAGENT_API_URL; tests skip when neither is set. | | `KAGENT_E2E_BYO_IMAGE` | String | `(none)` | Digest-pinned BYO image substituted into E2E lifecycle manifests. | | `KAGENT_E2E_CLAUDE_IMAGE` | String | `(none)` | Digest-pinned Claude harness image substituted into E2E lifecycle manifests. | @@ -194,6 +185,7 @@ This reference covers the controller, CLI, agent runtimes, UI, and registered te | `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | | `KAGENT_LOCAL_HOST` | String | `(none)` | Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems. | | `KAGENT_TEST_PYTHON` | String | `(none)` | Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset. | +| `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | | `LLM_PORT` | String | `8090` | Port for the mock LLM server. | | `RUN_ROLLING_UPGRADE_TESTS` | Boolean | `false` | Run rolling upgrade integration tests when exactly true. | | `RUN_UPGRADE_TESTS` | Boolean | `false` | Run upgrade integration tests when exactly true. | diff --git a/go/core/pkg/env/controller.go b/go/core/pkg/env/controller.go index 67fe295718..b929977c97 100644 --- a/go/core/pkg/env/controller.go +++ b/go/core/pkg/env/controller.go @@ -17,13 +17,6 @@ var ( // Shared settings read by logging and Kubernetes libraries. var ( - _ = RegisterStringVar("LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController) - _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController) -) - -// The chart supplies these for Kubernetes expansion in OTEL_RESOURCE_ATTRIBUTES. -var ( - _ = RegisterStringVar("K8S_POD_NAME", "", "Controller pod name, injected by the Helm chart through the Downward API for telemetry.", ComponentController) - _ = RegisterStringVar("K8S_POD_UID", "", "Controller pod UID, injected by the Helm chart through the Downward API for telemetry.", ComponentController) - _ = RegisterStringVar("K8S_NODE_NAME", "", "Controller node name, injected by the Helm chart through the Downward API for telemetry.", ComponentController) + _ = RegisterStringVar("LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) + _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController, ComponentCLI, ComponentTesting) ) diff --git a/go/core/pkg/env/database.go b/go/core/pkg/env/database.go index b69ab53034..74c04c4df6 100644 --- a/go/core/pkg/env/database.go +++ b/go/core/pkg/env/database.go @@ -3,14 +3,10 @@ package env var ( PostgresDatabaseURL = RegisterStringVar( "POSTGRES_DATABASE_URL", "postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres", - "PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL.", ComponentDatabase, + "PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL.", ComponentDatabase, ComponentController, ComponentCLI, ) PostgresDatabaseURLFile = RegisterStringVar( "POSTGRES_DATABASE_URL_FILE", "", - "File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller.", ComponentDatabase, + "File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller.", ComponentDatabase, ComponentController, ) - _ = RegisterStringVar("POSTGRES_PASSWORD", "", "Password supplied from the chart-managed Secret to bundled PostgreSQL and expanded into the controller's POSTGRES_DATABASE_URL by Kubernetes.", ComponentDatabase) - _ = RegisterStringVar("POSTGRES_DB", "", "Initial database name for bundled PostgreSQL, supplied by the Helm chart.", ComponentDatabase) - _ = RegisterStringVar("POSTGRES_USER", "", "Initial user for bundled PostgreSQL, supplied by the Helm chart.", ComponentDatabase) - _ = RegisterStringVar("PGDATA", "", "Data directory for bundled PostgreSQL, supplied by the Helm chart.", ComponentDatabase) ) diff --git a/go/core/pkg/env/kagent.go b/go/core/pkg/env/kagent.go index c906a93043..2053a078a4 100644 --- a/go/core/pkg/env/kagent.go +++ b/go/core/pkg/env/kagent.go @@ -31,7 +31,7 @@ var ( "KAGENT_NAMESPACE", "kagent", "Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it.", - ComponentController, + ComponentController, ComponentAgentRuntime, ) KagentControllerName = RegisterStringVar( @@ -41,27 +41,27 @@ var ( ComponentController, ) - // Variables injected into agent runtimes (not read by the controller itself). + // Standalone runtime settings supplied by the controller in managed runtimes. KagentName = RegisterStringVar( "KAGENT_NAME", "", - "Name of the agent, injected by the controller. Required by Python runtimes.", + "Agent name for standalone runtimes. Required by Python runtimes; supplied by the controller in managed runtimes.", ComponentAgentRuntime, ) KagentAPIURL = RegisterStringVar( "KAGENT_API_URL", "", - "Base URL for kagent control-plane API calls, injected into managed runtimes. Required by Python runtimes.", - ComponentAgentRuntime, + "Base URL for kagent control-plane API calls. Required by Python runtimes and supplied by the controller in managed runtimes; also used as the E2E test URL when KAGENT_E2E_API_URL is unset.", + ComponentAgentRuntime, ComponentTesting, ) KagentGatewayURL = RegisterStringVar( "KAGENT_GATEWAY_URL", "", "Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value.", - ComponentAgentRuntime, + ComponentAgentRuntime, ComponentController, ) KagentSkillsFolder = RegisterStringVar( @@ -117,13 +117,13 @@ var ( "DATABASE_VECTOR_ENABLED", false, "Enable vector database migrations and vector-backed database functionality.", - ComponentDatabase, + ComponentDatabase, ComponentController, ComponentCLI, ) SkipMigrations = RegisterBoolVar( "SKIP_MIGRATIONS", false, "Verify required database migrations at startup without applying them.", - ComponentDatabase, + ComponentDatabase, ComponentController, ) ) diff --git a/go/core/pkg/env/legacy.go b/go/core/pkg/env/legacy.go deleted file mode 100644 index 6cd6b2ae27..0000000000 --- a/go/core/pkg/env/legacy.go +++ /dev/null @@ -1,19 +0,0 @@ -package env - -// The chart still emits these names, but the current controller has no readers. -// Keep them discoverable without promising that setting them changes behavior. -func init() { - for _, variable := range []Var{ - {Name: "AUTH_MODE", Description: "Rendered by Helm but ignored by the current controller. Authentication is supplied through app.Options.Authenticator."}, - {Name: "AUTH_USER_ID_CLAIM", Description: "Rendered by Helm but ignored by the current controller. Identity extraction belongs to the configured authenticator."}, - {Name: "GRPC_MAX_MESSAGE_BYTES", Description: "Rendered by Helm but ignored by the current controller; does not change its gRPC message limits."}, - {Name: "GRPC_TLS_CERT_FILE", Description: "Rendered by Helm but ignored by the current controller; does not enable TLS on its listener."}, - {Name: "GRPC_TLS_KEY_FILE", Description: "Rendered by Helm but ignored by the current controller; does not enable TLS on its listener."}, - {Name: "SUBSTRATE_DEFAULT_WORKERPOOL_NAMESPACE", Description: "Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration."}, - {Name: "SUBSTRATE_DEFAULT_WORKERPOOL_NAME", Description: "Rendered by Helm but ignored by the current controller. Worker pools are resolved from Harness runtime configuration."}, - } { - variable.Component = ComponentController - variable.Deprecated = true - register(variable) - } -} diff --git a/go/core/pkg/env/otel.go b/go/core/pkg/env/otel.go index f5fd2e2785..37f7d02cf2 100644 --- a/go/core/pkg/env/otel.go +++ b/go/core/pkg/env/otel.go @@ -7,42 +7,42 @@ var ( "OTEL_TRACES_EXPORTER", "", "Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp.", - ComponentController, + ComponentController, ComponentAgentRuntime, ) OtelMetricsExporter = RegisterStringVar( "OTEL_METRICS_EXPORTER", "", "Metric exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp.", - ComponentController, + ComponentController, ComponentAgentRuntime, ) OtelLogsExporter = RegisterStringVar( "OTEL_LOGS_EXPORTER", "", "Log exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp.", - ComponentController, + ComponentController, ComponentAgentRuntime, ) OtelExporterOTLPEndpoint = RegisterStringVar( "OTEL_EXPORTER_OTLP_ENDPOINT", "", "OTLP endpoint for every signal. `OTEL_EXPORTER_OTLP__ENDPOINT` overrides it for one signal.", - ComponentController, + ComponentController, ComponentAgentRuntime, ) OtelExporterOTLPProtocol = RegisterStringVar( "OTEL_EXPORTER_OTLP_PROTOCOL", "grpc", "OTLP protocol, grpc or http/protobuf. `OTEL_EXPORTER_OTLP__PROTOCOL` overrides it for one signal.", - ComponentController, + ComponentController, ComponentAgentRuntime, ) OtelCaptureMessageContent = RegisterStringVar( "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "NO_CONTENT", "SPAN_ONLY records prompts and responses on agent spans. NO_CONTENT disables capture. Managed runtimes support these two modes; standalone Python ADK also recognizes SPAN_AND_EVENT. Captured content may be sensitive.", - ComponentController, + ComponentController, ComponentAgentRuntime, ) OtelResourceAttributes = RegisterStringVar( @@ -54,22 +54,22 @@ var ( ) var ( - _ = RegisterBoolVar("OTEL_SDK_DISABLED", false, "Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes.", ComponentController) - _ = RegisterStringVar("OTEL_SERVICE_NAME", "", "SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes.", ComponentController) - _ = RegisterStringVar("OTEL_RESOURCE_ATTRIBUTES", "", "Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TIMEOUT", "", "OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms).", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "", "Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "", "Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "", "Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "", "Trace protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "", "Metric protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "", "Log protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "", "Trace SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "", "Metric SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "", "Log SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController) - _ = RegisterStringVar("OTEL_PROPAGATORS", "tracecontext", "SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip", "OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip.", ComponentController) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "base2_exponential_bucket_histogram", "Default SDK histogram aggregation applied by kagent and supplied to managed runtimes.", ComponentController) + _ = RegisterBoolVar("OTEL_SDK_DISABLED", false, "Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_SERVICE_NAME", "", "SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_RESOURCE_ATTRIBUTES", "", "Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TIMEOUT", "", "OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms).", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "", "Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "", "Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "", "Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "", "Trace protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "", "Metric protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "", "Log protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "", "Trace SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "", "Metric SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "", "Log SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_PROPAGATORS", "tracecontext", "SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip", "OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "base2_exponential_bucket_histogram", "Default SDK histogram aggregation applied by kagent and supplied to managed runtimes.", ComponentController, ComponentAgentRuntime) _ = RegisterBoolVar("KAGENT_OTEL_CAPTURE_RAW_API_BODIES", false, "Set to true to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data.", ComponentController) _ = RegisterIntVar("KAGENT_OTEL_MAX_CAPTURE_BYTES", 16384, "Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384.", ComponentController) _ = RegisterStringVar("ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN", "2", "Python Google ADK telemetry schema version; set by kagent when absent.", ComponentAgentRuntime) diff --git a/go/core/pkg/env/providers.go b/go/core/pkg/env/providers.go index 368c3a566d..69a8dc3c37 100644 --- a/go/core/pkg/env/providers.go +++ b/go/core/pkg/env/providers.go @@ -10,7 +10,7 @@ var ( "OPENAI_API_KEY", "", "API key for OpenAI.", - ComponentAgentRuntime, + ComponentAgentRuntime, ComponentCLI, ) OpenAIOrganization = RegisterStringVar( @@ -34,7 +34,7 @@ var ( "ANTHROPIC_API_KEY", "", "API key for Anthropic.", - ComponentAgentRuntime, + ComponentAgentRuntime, ComponentCLI, ) ) @@ -44,7 +44,7 @@ var ( "AZURE_OPENAI_API_KEY", "", "API key for Azure OpenAI.", - ComponentAgentRuntime, + ComponentAgentRuntime, ComponentCLI, ) AzureADToken = RegisterStringVar( @@ -75,7 +75,7 @@ var ( "GOOGLE_API_KEY", "", "API key for Google Gemini.", - ComponentAgentRuntime, + ComponentAgentRuntime, ComponentCLI, ) GoogleCloudProject = RegisterStringVar( @@ -158,18 +158,15 @@ var ( "OLLAMA_API_KEY", "", "API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly.", - ComponentAgentRuntime, + ComponentAgentRuntime, ComponentCLI, ) ) -// Provider aliases and native Claude provider settings. +// Provider aliases. var ( - _ = RegisterStringVar("GEMINI_API_KEY", "", "Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs.", ComponentAgentRuntime) + _ = RegisterStringVar("GEMINI_API_KEY", "", "Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs.", ComponentAgentRuntime, ComponentCLI) _ = RegisterStringVar("GOOGLE_CLOUD_REGION", "", "Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset.", ComponentAgentRuntime) _ = RegisterStringVar("AWS_DEFAULT_REGION", "", "Preferred region for Python Bedrock models and Go/Python Bedrock embeddings, before AWS_REGION and the us-east-1 fallback.", ComponentAgentRuntime) - _ = RegisterStringVar("ANTHROPIC_BASE_URL", "", "Native Claude Anthropic API base URL, supplied from ModelConfig by the controller.", ComponentAgentRuntime) - _ = RegisterStringVar("ANTHROPIC_VERTEX_PROJECT_ID", "", "Native Claude Vertex AI project, supplied from ModelConfig by the controller.", ComponentAgentRuntime) - _ = RegisterStringVar("CLOUD_ML_REGION", "", "Native Claude Vertex AI region, supplied from ModelConfig by the controller.", ComponentAgentRuntime) ) // SAP AI Core diff --git a/go/core/pkg/env/runtime.go b/go/core/pkg/env/runtime.go index fe5fa5ef34..1a043bcfef 100644 --- a/go/core/pkg/env/runtime.go +++ b/go/core/pkg/env/runtime.go @@ -1,6 +1,6 @@ package env -// Documentation-only registrations for settings consumed outside the controller. +// Documentation-only registrations for user-configurable runtime settings. // Keep their defaults aligned with the Go/Python readers; importing this package // is not required for a runtime or an upstream SDK to read its environment. var ( @@ -8,49 +8,8 @@ var ( _ = RegisterStringVar("CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) _ = RegisterStringVar("A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) _ = RegisterStringVar("KAGENT_A2A_GRPC_ADDRESS", "[::]:80", "Python ADK gRPC listen address.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_CONFIG_JSON", "", "Compiled harness configuration JSON injected by the controller. Required by Codex and Claude; materialized to config.json by the ADKs when present.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_AGENT_CARD_JSON", "", "A2A agent card JSON injected by the controller. Required by Codex and Claude; materialized to agent-card.json by the ADKs when present.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_TOKEN", "", "Runtime authentication token, materialized to /var/run/secrets/tokens/kagent-token by the ADKs when present.", ComponentAgentRuntime) _ = RegisterStringVar("UVICORN_LOG_LEVEL", "", "Python ADK HTTP server log level. Falls back to LOG_LEVEL, then info.", ComponentAgentRuntime) _ = RegisterStringVar("BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_CREDENTIAL_", "", "Controller-generated credential variables referenced by compiled ADK configuration; names depend on the configured tools and models.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_CLAUDE_MCP_CREDENTIAL_", "", "Controller-generated Claude MCP header credentials; names depend on the configured tools.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_CODEX_MCP_CREDENTIAL_", "", "Controller-generated Codex MCP header credentials; names depend on the configured tools.", ComponentAgentRuntime) -) - -// Harness-owned process environment. These are outputs of harness compilation or -// runtime setup, not independent switches for operators to configure. -var ( - _ = RegisterStringVar("CODEX_HOME", "", "Codex home directory set by the harness under its durable private state.", ComponentAgentRuntime) - _ = RegisterStringVar("CLAUDE_CONFIG_DIR", "", "Claude configuration directory set by the harness under its durable private state.", ComponentAgentRuntime) - _ = RegisterStringVar("DISABLE_UPDATES", "1", "Set by the Claude harness to disable native automatic updates.", ComponentAgentRuntime) - _ = RegisterStringVar("IS_SANDBOX", "1", "Set by the controller for managed Claude runtimes.", ComponentAgentRuntime) - _ = RegisterStringVar("CLAUDE_CODE_USE_BEDROCK", "", "Set to 1 by the controller when the Claude ModelConfig uses Bedrock.", ComponentAgentRuntime) - _ = RegisterStringVar("CLAUDE_CODE_USE_VERTEX", "", "Set to 1 by the controller when the Claude ModelConfig uses Vertex AI.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_CLAUDE_GOOGLE_CREDENTIALS_JSON", "", "Vertex service-account JSON injected into Claude and materialized as the GOOGLE_APPLICATION_CREDENTIALS file; removed before launching Claude.", ComponentAgentRuntime) - _ = RegisterStringVar("CLAUDE_CODE_ENABLE_TELEMETRY", "", "Set to 1 by the Claude harness when telemetry is enabled.", ComponentAgentRuntime) - _ = RegisterStringVar("CLAUDE_CODE_ENHANCED_TELEMETRY_BETA", "", "Set to 1 by the Claude harness when telemetry is enabled.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_LOG_USER_PROMPTS", "", "Set to 1 by the Claude harness when content capture is enabled.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_LOG_TOOL_DETAILS", "", "Set to 1 by the Claude harness when content capture is enabled.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_LOG_TOOL_CONTENT", "", "Set to 1 by the Claude harness when trace export and content capture are enabled.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_LOG_ASSISTANT_RESPONSES", "", "Set to 1 by the Claude harness when log export and content capture are enabled.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_LOG_RAW_API_BODIES", "", "Set to 1 for Claude when KAGENT_OTEL_CAPTURE_RAW_API_BODIES enables raw body capture.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_PROMETHEUS_HOST", "", "Private loopback Prometheus host assigned by the Claude driver for native telemetry readiness checks.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_PROMETHEUS_PORT", "", "Private Prometheus port assigned by the Claude driver for native telemetry readiness checks.", ComponentAgentRuntime) - _ = RegisterStringVar("TRACEPARENT", "", "W3C trace context supplied per invocation to the native Claude process.", ComponentAgentRuntime) - _ = RegisterStringVar("TRACESTATE", "", "W3C trace state supplied per invocation to the native Claude process.", ComponentAgentRuntime) -) - -// Substrate replaces these with the managed egress trust bundle. -var ( - _ = RegisterStringVar("SSL_CERT_FILE", "", "Managed runtime and sandbox CA bundle, injected by Substrate for egress TLS trust.", ComponentAgentRuntime) - _ = RegisterStringVar("SSL_CERT_DIR", "", "Managed agent runtime CA directory, injected by Substrate for egress TLS trust.", ComponentAgentRuntime) - _ = RegisterStringVar("REQUESTS_CA_BUNDLE", "", "Managed runtime and sandbox CA bundle for Python requests, injected by Substrate.", ComponentAgentRuntime) - _ = RegisterStringVar("AWS_CA_BUNDLE", "", "Managed runtime and sandbox CA bundle for AWS clients, injected by Substrate.", ComponentAgentRuntime) - _ = RegisterStringVar("CURL_CA_BUNDLE", "", "Managed runtime and sandbox CA bundle for curl, injected by Substrate.", ComponentAgentRuntime) - _ = RegisterStringVar("NODE_EXTRA_CA_CERTS", "", "Managed runtime and sandbox CA bundle for Node.js, injected by Substrate.", ComponentAgentRuntime) - _ = RegisterStringVar("GIT_SSL_CAINFO", "", "Managed runtime and sandbox CA bundle for Git, injected by Substrate.", ComponentAgentRuntime) - _ = RegisterStringVar("DOCKER_CONFIG", "", "Docker registry credentials directory used by skills-init for OCI pulls; mounted by the controller when image pull secrets are configured.", ComponentAgentRuntime) ) diff --git a/go/core/pkg/env/var.go b/go/core/pkg/env/var.go index 538eb9569f..0daf49bda8 100644 --- a/go/core/pkg/env/var.go +++ b/go/core/pkg/env/var.go @@ -1,7 +1,8 @@ // Package env provides a centralized registry for environment variables used // throughout kagent. Variables are self-registering: calling any Register* // function records the variable's metadata (name, default, description, type, -// component) in a process-wide registry and returns a typed accessor. +// components) in a process-wide registry and returns a typed accessor. +// Supply every consuming component in one registration; at least one is required. // // This design is inspired by Istio's pkg/env package and enables automatic // documentation generation via `kagent env`. @@ -75,8 +76,8 @@ type Var struct { Description string `json:"description"` // Type is the data type. Type VarType `json:"type"` - // Component identifies which kagent component uses this variable. - Component Component `json:"component"` + // Components identifies the kagent components that use this variable. + Components []Component `json:"components"` // Hidden, when true, excludes the variable from generated documentation. Hidden bool `json:"-"` // Deprecated, when true, marks the variable as deprecated in documentation. @@ -89,6 +90,12 @@ var ( ) func register(v Var) { + if len(v.Components) == 0 || slices.Contains(v.Components, Component("")) { + panic(fmt.Sprintf("environment variable %s requires non-empty components", v.Name)) + } + v.Components = slices.Clone(v.Components) + slices.Sort(v.Components) + v.Components = slices.Compact(v.Components) mu.Lock() defer mu.Unlock() allVars[v.Name] = v @@ -101,6 +108,7 @@ func VarDescriptions() []Var { out := make([]Var, 0, len(allVars)) for _, v := range allVars { + v.Components = slices.Clone(v.Components) out = append(out, v) } slices.SortFunc(out, func(a, b Var) int { @@ -114,6 +122,7 @@ func VarByName(name string) (Var, bool) { mu.Lock() defer mu.Unlock() v, ok := allVars[name] + v.Components = slices.Clone(v.Components) return v, ok } @@ -125,13 +134,13 @@ type StringVar struct { } // RegisterStringVar registers a string environment variable and returns a typed accessor. -func RegisterStringVar(name, defaultValue, description string, component Component) StringVar { +func RegisterStringVar(name, defaultValue, description string, components ...Component) StringVar { v := Var{ Name: name, DefaultValue: defaultValue, Description: description, Type: TypeString, - Component: component, + Components: components, } register(v) return StringVar{v: v} @@ -169,13 +178,13 @@ type BoolVar struct { } // RegisterBoolVar registers a boolean environment variable and returns a typed accessor. -func RegisterBoolVar(name string, defaultValue bool, description string, component Component) BoolVar { +func RegisterBoolVar(name string, defaultValue bool, description string, components ...Component) BoolVar { v := Var{ Name: name, DefaultValue: strconv.FormatBool(defaultValue), Description: description, Type: TypeBool, - Component: component, + Components: components, } register(v) return BoolVar{v: v, defaultValue: defaultValue} @@ -217,13 +226,13 @@ type IntVar struct { } // RegisterIntVar registers an integer environment variable and returns a typed accessor. -func RegisterIntVar(name string, defaultValue int, description string, component Component) IntVar { +func RegisterIntVar(name string, defaultValue int, description string, components ...Component) IntVar { v := Var{ Name: name, DefaultValue: strconv.Itoa(defaultValue), Description: description, Type: TypeInt, - Component: component, + Components: components, } register(v) return IntVar{v: v, defaultValue: defaultValue} @@ -265,13 +274,13 @@ type DurationVar struct { } // RegisterDurationVar registers a duration environment variable and returns a typed accessor. -func RegisterDurationVar(name string, defaultValue time.Duration, description string, component Component) DurationVar { +func RegisterDurationVar(name string, defaultValue time.Duration, description string, components ...Component) DurationVar { v := Var{ Name: name, DefaultValue: defaultValue.String(), Description: description, Type: TypeDuration, - Component: component, + Components: components, } register(v) return DurationVar{v: v, defaultValue: defaultValue} @@ -315,11 +324,12 @@ func ExportMarkdown(component string) string { sb.WriteString("\n\n") sb.WriteString("Generated from `go/core/pkg/env`. Edit the registrations there, run `make env-docs`, " + "and commit the result. CI runs `make env-docs-check`.\n\n") - sb.WriteString("This reference covers the controller, CLI, agent runtimes, UI, and registered test settings. " + + sb.WriteString("This reference covers user-configurable settings for the controller, CLI, standalone agent runtimes, UI, and tests. " + + "Controller-generated runtime payloads, credentials, private paths, and other internal process wiring are excluded. " + "Build scripts, sample applications, and third-party SDK settings not configured by kagent have their own documentation. " + "Defaults describe the application without deployment overrides; Helm or a Harness may supply different values. " + "`(none)` means no fixed default; see the description for required values and fallbacks. " + - "Runtime-injected variables are managed by kagent, as noted below. " + + "Shared variables appear under each consuming component. " + "Only registered metadata is exported, never values from the current process environment.\n\n") // Group by component @@ -328,10 +338,12 @@ func ExportMarkdown(component string) string { if v.Hidden { continue } - if component != "" && component != "all" && string(v.Component) != component { - continue + for _, comp := range v.Components { + if component != "" && component != "all" && string(comp) != component { + continue + } + grouped[comp] = append(grouped[comp], v) } - grouped[v.Component] = append(grouped[v.Component], v) } // Sort component keys for deterministic output @@ -374,7 +386,7 @@ func ExportJSON(component string) string { if v.Hidden { continue } - if component != "" && component != "all" && string(v.Component) != component { + if component != "" && component != "all" && !slices.Contains(v.Components, Component(component)) { continue } out = append(out, v) diff --git a/go/core/pkg/env/var_test.go b/go/core/pkg/env/var_test.go index 4cf5d68f4b..20d9117651 100644 --- a/go/core/pkg/env/var_test.go +++ b/go/core/pkg/env/var_test.go @@ -1,10 +1,13 @@ package env import ( + "encoding/json" "os" "strings" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestRegisterStringVar(t *testing.T) { @@ -311,6 +314,88 @@ func TestExportJSON(t *testing.T) { } } +func TestRegisterMultipleComponents(t *testing.T) { + tests := []struct { + name string + register func(...Component) + }{ + {"string", func(c ...Component) { RegisterStringVar("SHARED", "value", "shared setting", c...) }}, + {"bool", func(c ...Component) { RegisterBoolVar("SHARED", true, "shared setting", c...) }}, + {"int", func(c ...Component) { RegisterIntVar("SHARED", 42, "shared setting", c...) }}, + {"duration", func(c ...Component) { RegisterDurationVar("SHARED", time.Second, "shared setting", c...) }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + allVars = make(map[string]Var) + components := []Component{ComponentController, ComponentCLI, ComponentController} + tt.register(components...) + want := []Component{ComponentCLI, ComponentController} + v, ok := VarByName("SHARED") + require.True(t, ok) + require.Equal(t, want, v.Components) + + // Neither caller-owned nor returned slices can change registered metadata. + components[0] = ComponentTesting + v.Components[0] = ComponentTesting + VarDescriptions()[0].Components[0] = ComponentTesting + v, ok = VarByName("SHARED") + require.True(t, ok) + require.Equal(t, want, v.Components) + require.Len(t, VarDescriptions(), 1) + require.Panics(t, func() { tt.register() }) + require.Panics(t, func() { tt.register(ComponentController, "") }) + }) + } +} + +func TestExportMultipleComponents(t *testing.T) { + type exportedVar struct { + Name string `json:"name"` + Components []Component `json:"components"` + } + allVars = make(map[string]Var) + RegisterStringVar("SHARED", "value", "shared setting", ComponentController, ComponentCLI, ComponentController) + RegisterBoolVar("CLI_ONLY", true, "CLI setting", ComponentCLI) + register(Var{Name: "HIDDEN", Components: []Component{ComponentCLI, ComponentController}, Hidden: true}) + wantComponents := []Component{ComponentCLI, ComponentController} + + tests := []struct { + component string + wantNames []string + sharedRows int + }{ + {"", []string{"CLI_ONLY", "SHARED"}, 2}, + {"all", []string{"CLI_ONLY", "SHARED"}, 2}, + {"controller", []string{"SHARED"}, 1}, + {"cli", []string{"CLI_ONLY", "SHARED"}, 1}, + {"agent-runtime", []string{}, 0}, + {"unknown", []string{}, 0}, + } + for _, tt := range tests { + t.Run(tt.component, func(t *testing.T) { + var exported []exportedVar + require.NoError(t, json.Unmarshal([]byte(ExportJSON(tt.component)), &exported)) + names := make([]string, 0, len(exported)) + for _, v := range exported { + names = append(names, v.Name) + if v.Name == "SHARED" { + require.Equal(t, wantComponents, v.Components) + } + } + require.Equal(t, tt.wantNames, names) + + md := ExportMarkdown(tt.component) + require.Equal(t, tt.sharedRows, strings.Count(md, "| `SHARED` |")) + require.NotContains(t, md, "HIDDEN") + if tt.component == "controller" { + require.Contains(t, md, "## controller\n") + require.NotContains(t, md, "## cli\n") + require.NotContains(t, md, "CLI_ONLY") + } + }) + } +} + func TestHiddenVarsExcluded(t *testing.T) { allVars = make(map[string]Var) @@ -320,7 +405,7 @@ func TestHiddenVarsExcluded(t *testing.T) { register(Var{ Name: "HIDDEN_VAR", Description: "hidden", - Component: ComponentController, + Components: []Component{ComponentController}, Hidden: true, }) diff --git a/helm/kagent/templates/controller-deployment.yaml b/helm/kagent/templates/controller-deployment.yaml index e5044d77d6..c0e6d6e4e3 100644 --- a/helm/kagent/templates/controller-deployment.yaml +++ b/helm/kagent/templates/controller-deployment.yaml @@ -98,12 +98,6 @@ spec: fieldPath: spec.nodeName - name: OTEL_RESOURCE_ATTRIBUTES value: {{ list (include "kagent.otel.resourceAttributes" .) "service.instance.id=$(K8S_POD_UID),k8s.pod.uid=$(K8S_POD_UID),k8s.pod.name=$(K8S_POD_NAME),k8s.namespace.name=$(KAGENT_NAMESPACE),k8s.node.name=$(K8S_NODE_NAME),k8s.container.name=controller" | compact | join "," | quote }} - - name: AUTH_MODE - value: {{ .Values.controller.auth.mode | default "unsecure" | quote }} - {{- if .Values.controller.auth.userIdClaim }} - - name: AUTH_USER_ID_CLAIM - value: {{ .Values.controller.auth.userIdClaim | quote }} - {{- end }} {{- if .Values.database.postgres.urlFile }} - name: POSTGRES_DATABASE_URL_FILE value: {{ .Values.database.postgres.urlFile | quote }} @@ -127,18 +121,8 @@ spec: - name: METRICS_SECURE value: {{ .Values.controller.metrics.secureServing | quote }} {{- end }} - - name: GRPC_MAX_MESSAGE_BYTES - value: {{ printf "%.0f" (.Values.controller.grpc.maxMessageBytes | float64) | quote }} - name: GRPC_REFLECTION value: {{ .Values.controller.grpc.reflection | quote }} - {{- with .Values.controller.grpc.tlsCertFile }} - - name: GRPC_TLS_CERT_FILE - value: {{ . | quote }} - {{- end }} - {{- with .Values.controller.grpc.tlsKeyFile }} - - name: GRPC_TLS_KEY_FILE - value: {{ . | quote }} - {{- end }} {{- with .Values.controller.env }} {{- toYaml . | nindent 12 }} {{- end }} @@ -153,10 +137,6 @@ spec: - name: SUBSTRATE_ATENET_ROUTER_URL value: {{ . | quote }} {{- end }} - - name: SUBSTRATE_DEFAULT_WORKERPOOL_NAMESPACE - value: {{ .Values.controller.substrate.defaultWorkerPool.namespace | default (include "kagent.namespace" .) | quote }} - - name: SUBSTRATE_DEFAULT_WORKERPOOL_NAME - value: {{ .Values.controller.substrate.defaultWorkerPool.name | default (ternary .Values.substrateWorkerPool.name "" .Values.substrateWorkerPool.create) | quote }} {{- end }} envFrom: - configMapRef: diff --git a/helm/kagent/templates/toolserver-api.yaml b/helm/kagent/templates/toolserver-api.yaml index bdac820fa6..6634235060 100644 --- a/helm/kagent/templates/toolserver-api.yaml +++ b/helm/kagent/templates/toolserver-api.yaml @@ -8,6 +8,6 @@ metadata: spec: description: "Kagent API tools for Sessions, checkpoints, and sandboxes" protocol: STREAMABLE_HTTP - url: "{{ if .Values.controller.grpc.tlsCertFile }}https{{ else }}http{{ end }}://{{ include "kagent.controllerServiceAuthority" . }}/mcp" + url: "http://{{ include "kagent.controllerServiceAuthority" . }}/mcp" timeout: 30s sseReadTimeout: 5m0s diff --git a/helm/kagent/tests/controller-deployment_test.yaml b/helm/kagent/tests/controller-deployment_test.yaml index f23da10f0d..7f7ba59984 100644 --- a/helm/kagent/tests/controller-deployment_test.yaml +++ b/helm/kagent/tests/controller-deployment_test.yaml @@ -77,14 +77,9 @@ tests: path: spec.template.spec.containers[0].ports[0].containerPort value: 8083 - - it: should configure the gRPC server + - it: should disable gRPC reflection by default template: controller-deployment.yaml asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: GRPC_MAX_MESSAGE_BYTES - value: "16777216" - contains: path: spec.template.spec.containers[0].env content: diff --git a/helm/kagent/tests/toolserver-api_test.yaml b/helm/kagent/tests/toolserver-api_test.yaml index ce85e0d7fd..44901a649b 100644 --- a/helm/kagent/tests/toolserver-api_test.yaml +++ b/helm/kagent/tests/toolserver-api_test.yaml @@ -59,15 +59,3 @@ tests: - equal: path: spec.url value: http://staging-control-controller.agents.svc:8083/mcp - - - it: uses HTTPS when TLS is configured on the controller - release: - name: kagent - namespace: kagent - set: - controller.grpc.tlsCertFile: /certs/tls.crt - controller.grpc.tlsKeyFile: /certs/tls.key - asserts: - - equal: - path: spec.url - value: https://kagent-controller.kagent.svc:8083/mcp diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 92a2ec2ec4..b7f5906fcf 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -218,14 +218,6 @@ controller: annotations: {} replicas: 1 loglevel: "info" - # Authentication mode: "unsecure" (default) or "trusted-proxy" - # - unsecure: uses X-User-Id header/query param or defaults to admin@kagent.dev - # - trusted-proxy: trusts JWT token from Authorization header (set by oauth2-proxy) - auth: - mode: unsecure - # JWT claim for user identity (default: "sub") - # Override only if your OIDC provider uses a different claim - userIdClaim: "" # -- Public gateway base URL advertised by Session Agent Cards for gRPC and HTTP/JSON-RPC. # @default -- `http://-controller..svc:` a2aGatewayUrl: "" @@ -403,12 +395,9 @@ controller: # -- gRPC application API settings. Native gRPC, gRPC-Web, MCP, and health # share the controller service port. grpc: - maxMessageBytes: 16777216 reflection: false - tlsCertFile: "" - tlsKeyFile: "" - # Extra controller env (mapped to flags via SUBSTRATE_* env names). + # Extra controller environment variables. env: [] # Standalone workspaces, enabled by default. @@ -430,15 +419,10 @@ controller: enabled: false ateApiEndpoint: "" atenetRouterURL: "" - defaultWorkerPool: - namespace: "" - name: "" # Example when enabled: # enabled: true # ateApiEndpoint: "dns:///api.ate-system.svc:443" # atenetRouterURL: "http://atenet-router.ate-system.svc:80" - # defaultWorkerPool: - # name: "kagent-default" envFrom: [] # Additional volumes on the output Deployment definition. @@ -875,7 +859,8 @@ grafana-mcp: # OAUTH2-PROXY CONFIGURATION (Optional) # ============================================================================== # Enable oauth2-proxy subchart for OIDC authentication -# Requires controller.auth.mode: trusted-proxy +# Controller authentication requires a library integration; see +# docs/architecture/oidc-proxy-authentication.md. oauth2-proxy: enabled: false diff --git a/scripts/setup-cluster/setup-cluster.sh b/scripts/setup-cluster/setup-cluster.sh index 600e4b39bd..d1a31f49b1 100755 --- a/scripts/setup-cluster/setup-cluster.sh +++ b/scripts/setup-cluster/setup-cluster.sh @@ -85,7 +85,6 @@ make helm-install KAGENT_HELM_EXTRA_ARGS="\ --set controller.substrate.enabled=true \ --set controller.substrate.ateApiEndpoint=dns:///api.ate-system.svc:443 \ --set controller.substrate.atenetRouterURL=http://atenet-router.ate-system.svc:80 \ - --set controller.substrate.defaultWorkerPool.name=kagent-default \ --set substrateWorkerPool.create=true \ --set substrateWorkerPool.replicas=8 \ --set-string substrateWorkerPool.workerImage=ghcr.io/kagent-dev/substrate/ateom-gvisor:v${SUBSTRATE_VERSION}" From eed1688e90b32c498cef614b63926f605719f19c Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 12:42:47 +0000 Subject: [PATCH 03/14] refactor: use LOG_LEVEL for the Python ADK HTTP server Signed-off-by: Eitan Yarmush --- docs/env.md | 7 +++---- go/core/pkg/env/controller.go | 2 +- go/core/pkg/env/runtime.go | 1 - python/packages/kagent-adk/src/kagent/adk/cli.py | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/env.md b/docs/env.md index 246e523cb0..b032ed0319 100644 --- a/docs/env.md +++ b/docs/env.md @@ -47,7 +47,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_SKILLS_FOLDER` | String | `/skills` | Skills directory for standalone Python skills tools. The Python ADK adds skills tools when set; managed Go ADK runtimes use their compiled skill configuration. | | `KAGENT_STS_AUDIENCE` | String | `(none)` | Comma-separated RFC 8693 audiences sent on STS token-exchange requests. Alternate to KAGENT_STS_RESOURCE for servers that key on audience. | | `KAGENT_STS_RESOURCE` | String | `(none)` | Comma-separated RFC 8707 resource indicators sent on STS token-exchange requests to scope issued tokens to target backends. | -| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | | `MISTRAL_API_BASE` | String | `(none)` | Custom base URL for the Mistral AI API (defaults to https://api.mistral.ai/v1). | | `MISTRAL_API_KEY` | String | `(none)` | API key for Mistral AI. | | `OLLAMA_API_BASE` | String | `(none)` | Base URL for the Ollama API endpoint; falls back to http://localhost:11434 when model configuration and this variable are unset. | @@ -84,7 +84,6 @@ This reference covers user-configurable settings for the controller, CLI, standa | `SAP_AI_CORE_CLIENT_ID` | String | `(none)` | OAuth2 client ID for SAP AI Core authentication. | | `SAP_AI_CORE_CLIENT_SECRET` | String | `(none)` | OAuth2 client secret for SAP AI Core authentication. | | `STS_WELL_KNOWN_URI` | String | `(none)` | Well-known endpoint for the Security Token Service (STS) used for token exchange. | -| `UVICORN_LOG_LEVEL` | String | `(none)` | Python ADK HTTP server log level. Falls back to LOG_LEVEL, then info. | ## cli @@ -100,7 +99,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_HELM_REPO` | String | `oci://ghcr.io/kagent-dev/kagent/helm/` | Helm repository URL for kagent charts. | | `KAGENT_HELM_VERSION` | String | `(none)` | Helm chart version to deploy. When unset, the CLI uses its own version. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | -| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | | `OLLAMA_API_KEY` | String | `(none)` | API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly. | | `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. | | `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | @@ -120,7 +119,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Resource attributes, as key=value pairs, added to every agent runtime. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | | `LEADER_ELECT` | Boolean | `true` | Enable controller leader election, including during single-replica rolling updates. Required for sandbox lifecycle coordination. | -| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | | `METRICS_BIND_ADDRESS` | String | `0` | Address the controller-runtime metrics server binds to, e.g. :8080. "0" (the default) serves no metrics, so an installation that does not set this is unchanged. The Helm chart renders this variable, and its ServiceMonitor, from controller.metrics. | | `METRICS_SECURE` | Boolean | `false` | Serve the metrics endpoint over HTTPS with authentication and authorization. A scraper then needs a token bound to the metrics-reader ClusterRole. | | `OTEL_EXPORTER_OTLP_COMPRESSION` | String | `gzip` | OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip. | diff --git a/go/core/pkg/env/controller.go b/go/core/pkg/env/controller.go index b929977c97..3aab994407 100644 --- a/go/core/pkg/env/controller.go +++ b/go/core/pkg/env/controller.go @@ -17,6 +17,6 @@ var ( // Shared settings read by logging and Kubernetes libraries. var ( - _ = RegisterStringVar("LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) + _ = RegisterStringVar("LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController, ComponentCLI, ComponentTesting) ) diff --git a/go/core/pkg/env/runtime.go b/go/core/pkg/env/runtime.go index 1a043bcfef..67ccc7ac9a 100644 --- a/go/core/pkg/env/runtime.go +++ b/go/core/pkg/env/runtime.go @@ -8,7 +8,6 @@ var ( _ = RegisterStringVar("CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) _ = RegisterStringVar("A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) _ = RegisterStringVar("KAGENT_A2A_GRPC_ADDRESS", "[::]:80", "Python ADK gRPC listen address.", ComponentAgentRuntime) - _ = RegisterStringVar("UVICORN_LOG_LEVEL", "", "Python ADK HTTP server log level. Falls back to LOG_LEVEL, then info.", ComponentAgentRuntime) _ = RegisterStringVar("BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) diff --git a/python/packages/kagent-adk/src/kagent/adk/cli.py b/python/packages/kagent-adk/src/kagent/adk/cli.py index 0f2f2f3e37..954abb2e6b 100644 --- a/python/packages/kagent-adk/src/kagent/adk/cli.py +++ b/python/packages/kagent-adk/src/kagent/adk/cli.py @@ -42,7 +42,7 @@ def _split_csv(value: Optional[str]) -> Optional[list[str]]: propagate_token = os.getenv("KAGENT_PROPAGATE_TOKEN", "").lower() == "true" token_resource = _split_csv(os.getenv("KAGENT_STS_RESOURCE")) token_audience = _split_csv(os.getenv("KAGENT_STS_AUDIENCE")) -uvicorn_log_level = os.getenv("UVICORN_LOG_LEVEL", os.getenv("LOG_LEVEL", "info")).lower() +uvicorn_log_level = os.getenv("LOG_LEVEL", "info").lower() def create_sts_integration() -> Optional[ADKTokenPropagationPlugin]: From 2738e69525af1d109202cfea060f7d189ba5f0b9 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 12:52:33 +0000 Subject: [PATCH 04/14] refactor: prefix kagent-owned environment variables Signed-off-by: Eitan Yarmush --- DEVELOPMENT.md | 13 ++- Makefile | 4 +- STYLE.md | 4 +- .../architecture/oidc-proxy-authentication.md | 5 +- docs/architecture/sandboxes.md | 2 +- docs/env.md | 103 +++++++++--------- go/Makefile | 2 +- go/adk/cmd/main.go | 10 +- go/adk/examples/byo/main.go | 2 +- go/adk/pkg/a2a/server/server.go | 2 +- go/adk/pkg/app/app.go | 6 +- go/adk/pkg/app/app_test.go | 6 +- go/adk/pkg/runner/adapter.go | 2 +- go/core/cli/internal/commands/db/db.go | 4 +- go/core/cli/internal/db/migrate/migrate.go | 4 +- go/core/hack/mockllm/main.go | 4 +- .../internal/translator/kagent/compiler.go | 2 +- go/core/pkg/app/app.go | 6 +- go/core/pkg/app/app_test.go | 16 +-- go/core/pkg/env/controller.go | 8 +- go/core/pkg/env/database.go | 6 +- go/core/pkg/env/kagent.go | 12 +- go/core/pkg/env/runtime.go | 8 +- go/core/pkg/env/sandbox.go | 10 +- go/core/pkg/env/substrate.go | 8 +- go/core/pkg/env/testing.go | 18 +-- go/core/pkg/env/ui.go | 21 ++-- go/core/test/upgrade/rolling_upgrade_test.go | 4 +- go/core/test/upgrade/upgrade_test.go | 4 +- go/pkg/logging/logging.go | 4 +- go/sandbox/guest/README.md | 2 +- .../templates/controller-configmap.yaml | 18 +-- .../templates/controller-deployment.yaml | 20 ++-- helm/kagent/templates/ui-deployment.yaml | 2 +- .../tests/controller-configmap_test.yaml | 26 ++--- .../tests/controller-deployment_test.yaml | 82 +++++++------- helm/kagent/tests/ui-deployment_test.yaml | 4 +- helm/kagent/values.yaml | 4 +- .../packages/kagent-adk/src/kagent/adk/cli.py | 4 +- .../kagent-core/src/kagent/core/_logging.py | 4 +- .../src/kagent/core/a2a/_config.py | 4 +- .../kagent-core/tests/test_a2a_config.py | 20 ++-- python/packages/kagent-openai/README.md | 2 +- .../kagent-openai/src/kagent/openai/_a2a.py | 2 +- .../kagent-skills/src/kagent/skills/shell.py | 2 +- scripts/setup-cluster/README.md | 2 +- ui/.env.example | 25 +++-- ui/README.md | 12 +- ui/docs/app-extensions.md | 20 ++-- ui/package.json | 2 +- ui/playwright.config.ts | 26 ++--- ui/playwright/README.md | 14 +-- ui/playwright/globalSetup.ts | 2 +- ui/scripts/init.sh | 8 +- ui/src/api/config.ts | 6 +- ui/src/appExtensions/activeExtensions.ts | 4 +- ui/src/env.test.ts | 6 +- ui/src/env.ts | 16 +-- ui/src/mocks/startMockBackend.ts | 2 +- ui/src/vite-env.d.ts | 8 +- ui/vite.config.ts | 24 ++-- 61 files changed, 344 insertions(+), 329 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7a966ca320..79119bab43 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -13,12 +13,23 @@ Register shared settings once, passing each consuming component to `RegisterDurationVar`, for example: ```go -RegisterStringVar("LOG_LEVEL", "info", "Logging level.", ComponentController, ComponentCLI, ComponentAgentRuntime) +RegisterStringVar("KAGENT_LOG_LEVEL", "info", "Logging level.", ComponentController, ComponentCLI, ComponentAgentRuntime) ``` The reference lists shared settings under each component. `kagent env --component` matches any registered component; JSON output includes a `components` array. +Kagent-owned settings use the `KAGENT_` prefix. Keep names defined by upstream +SDKs and tools, such as `OTEL_*`, provider credentials, and `KUBECONFIG`, unchanged. +The pre-release rename replaces the old unprefixed names: for example, +`LOG_LEVEL` becomes `KAGENT_LOG_LEVEL`, `HTTP_BIND_ADDRESS` becomes +`KAGENT_HTTP_BIND_ADDRESS`, and the Go ADK's `PORT` becomes `KAGENT_PORT`. +The old names are no longer read by kagent. + +UI containers and Vite development use the same `KAGENT_*` inputs, documented in +`ui/.env.example`. UI build-time switches use `KAGENT_VITE_*`; extension settings +use `KAGENT_EXTENSION_*`. Other `KAGENT_*` settings are not exposed to the browser. + To understand how to develop for kagent, it's important to understand the architecture of the project. Please refer to the [README.md](README.md#architecture) file for an overview of the project. When making changes to `kagent`, the most important thing is to figure out which piece of the project is affected by the change, and then make the change in the appropriate folder. Each piece of the project has its own README with more information about how to setup the development environment and run that piece of the project. diff --git a/Makefile b/Makefile index 4855608c6d..8f7ceffc01 100644 --- a/Makefile +++ b/Makefile @@ -596,7 +596,7 @@ run-upgrade-tests: announce-upgrade-from build install-previous-release ## Test kind_gw="$$($(CONTAINER_RUNTIME) network inspect kind -f '{{range .IPAM.Config}}{{if .Gateway}}{{.Gateway}}{{"\n"}}{{end}}{{end}}' | grep -E '^[0-9]+\.' | head -1)"; \ echo "kind gateway (KAGENT_LOCAL_HOST): $$kind_gw"; \ cd go && \ - RUN_UPGRADE_TESTS=true \ + KAGENT_RUN_UPGRADE_TESTS=true \ REPO_ROOT=$(CURDIR) \ KAGENT_LOCAL_HOST="$$kind_gw" \ UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ @@ -614,7 +614,7 @@ run-rolling-upgrade-tests: UPGRADE_PREV_EXTRA_ARGS = --set controller.replicas=2 run-rolling-upgrade-tests: announce-upgrade-from build install-previous-release ## Install the previous release with 2 controller replicas, build the current images, and run the rolling upgrade e2e test @echo "=== Rolling upgrade test: $(UPGRADE_FROM_VERSION) -> $(VERSION) (registry=$(DOCKER_REGISTRY)) ===" cd go && \ - RUN_ROLLING_UPGRADE_TESTS=true \ + KAGENT_RUN_ROLLING_UPGRADE_TESTS=true \ REPO_ROOT=$(CURDIR) \ UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ VERSION=$(VERSION) \ diff --git a/STYLE.md b/STYLE.md index 5bd51e1938..d6c354fc10 100644 --- a/STYLE.md +++ b/STYLE.md @@ -208,7 +208,7 @@ Go code lives in the `go/` workspace (`go/api`, `go/core`, `go/adk`). Run ### Logging - Use standard-library `log/slog`. Binaries write JSON to stderr and read the - minimum level from `LOG_LEVEL` (`debug`, `info`, `warn`, or `error`). + minimum level from `KAGENT_LOG_LEVEL` (`debug`, `info`, `warn`, or `error`). - Carry loggers in `context.Context` with `pkg/logging`; do not add logger parameters, package-global loggers, or `NewXxxWithLogger` constructors. - Use `DebugContext`, `InfoContext`, `WarnContext`, or `ErrorContext` whenever @@ -315,7 +315,7 @@ All new CRD API surface goes in `v1alpha3`. Side effects and async logic belong in the reconciler (or a dedicated controller). - Every behavioral change to a translator **SHOULD** come with golden tests - (`UPDATE_GOLDEN=true go test ...` to regenerate). + (`KAGENT_UPDATE_GOLDEN=true go test ...` to regenerate). ### Database diff --git a/docs/architecture/oidc-proxy-authentication.md b/docs/architecture/oidc-proxy-authentication.md index aec78b121f..b8ea1b88d0 100644 --- a/docs/architecture/oidc-proxy-authentication.md +++ b/docs/architecture/oidc-proxy-authentication.md @@ -126,8 +126,9 @@ integration must supply its own `auth.AuthProvider` through The UI reads `SSO_REDIRECT_PATH` from `window.environmentVariables` at runtime and applies its configured base path. Helm supplies that value from -`ui.auth.ssoRedirectPath`. The chart allows unauthenticated access to the login -page, health checks, and the assets required to render the login page. +`ui.auth.ssoRedirectPath` through `KAGENT_SSO_REDIRECT_PATH`. The chart allows +unauthenticated access to the login page, health checks, and the assets required +to render the login page. ## Trust boundary diff --git a/docs/architecture/sandboxes.md b/docs/architecture/sandboxes.md index 13c9d64ada..1200dac58a 100644 --- a/docs/architecture/sandboxes.md +++ b/docs/architecture/sandboxes.md @@ -103,7 +103,7 @@ pin the runtime artifacts they need. Sandbox execution is enabled by default. Lifecycle calls run on the serving API replica; expiration workers coordinate through PostgreSQL. -Configure `SANDBOX_GUEST_IMAGE` with an `image@sha256:...` reference, or set +Configure `KAGENT_SANDBOX_GUEST_IMAGE` with an `image@sha256:...` reference, or set `controller.sandbox.guestImage.digest` in Helm alongside its registry and repository. The guest image is required for sandbox preparation and passes unchanged to Substrate. The controller does not resolve tags or contact image diff --git a/docs/env.md b/docs/env.md index b032ed0319..7f5ae86f72 100644 --- a/docs/env.md +++ b/docs/env.md @@ -10,7 +10,6 @@ This reference covers user-configurable settings for the controller, CLI, standa | Variable | Type | Default | Description | |----------|------|---------|-------------| -| `A2A_MAX_CONTENT_LENGTH` | String | `10485760` | Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default. | | `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS` | String | `(none)` | Python Google ADK span content capture. When absent, derived from OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (true for SPAN_ONLY or SPAN_AND_EVENT, false otherwise). | | `ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN` | String | `2` | Python Google ADK telemetry schema version; set by kagent when absent. | | `ANTHROPIC_API_KEY` | String | `(none)` | API key for Anthropic. | @@ -23,8 +22,6 @@ This reference covers user-configurable settings for the controller, CLI, standa | `AZURE_AD_TOKEN` | String | `(none)` | Azure Active Directory authentication token for Azure OpenAI. | | `AZURE_OPENAI_API_KEY` | String | `(none)` | API key for Azure OpenAI. | | `AZURE_OPENAI_ENDPOINT` | String | `(none)` | Endpoint URL for Azure OpenAI service. | -| `BASH_VENV_PATH` | String | `(none)` | Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set. | -| `CONFIG_DIR` | String | `/config` | Go ADK configuration directory; --filepath takes precedence. | | `FOUNDRY_API_KEY` | String | `(none)` | API key for Azure AI Foundry. | | `FOUNDRY_API_VERSION` | String | `2024-10-21` | Azure AI Foundry OpenAI-compatible data-plane API version. | | `FOUNDRY_DEPLOYMENT` | String | `(none)` | Azure AI Foundry model deployment name. | @@ -37,17 +34,22 @@ This reference covers user-configurable settings for the controller, CLI, standa | `GOOGLE_CLOUD_REGION` | String | `(none)` | Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset. | | `GOOGLE_GENAI_USE_VERTEXAI` | String | `(none)` | When set to 'true', use Vertex AI for Gemini models. | | `KAGENT_A2A_GRPC_ADDRESS` | String | `[::]:80` | Python ADK gRPC listen address. | +| `KAGENT_A2A_MAX_CONTENT_LENGTH` | String | `10485760` | Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default. | | `KAGENT_API_URL` | String | `(none)` | Base URL for kagent control-plane API calls. Required by Python runtimes and supplied by the controller in managed runtimes; also used as the E2E test URL when KAGENT_E2E_API_URL is unset. | +| `KAGENT_BASH_VENV_PATH` | String | `(none)` | Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set. | +| `KAGENT_CONFIG_DIR` | String | `/config` | Go ADK configuration directory; --filepath takes precedence. | | `KAGENT_ENABLE_FILE_SEARCH_TOOLS` | Boolean | `false` | When true, enables the list_files and grep_file skills tools, which let an agent enumerate and search the filesystem under its session/skills roots without a shell. Disabled by default; set in Harness env to opt in. | | `KAGENT_GATEWAY_URL` | String | `(none)` | Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value. | +| `KAGENT_LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | | `KAGENT_NAME` | String | `(none)` | Agent name for standalone runtimes. Required by Python runtimes; supplied by the controller in managed runtimes. | | `KAGENT_NAMESPACE` | String | `kagent` | Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it. | | `KAGENT_OPENAI_AGENTS_NATIVE_TRACING` | Boolean | `false` | Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime. | +| `KAGENT_PORT` | String | `8080` | Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port. | | `KAGENT_PROPAGATE_TOKEN` | String | `(none)` | Set to true to propagate authentication tokens to downstream services. Unset or any other value disables propagation. | | `KAGENT_SKILLS_FOLDER` | String | `/skills` | Skills directory for standalone Python skills tools. The Python ADK adds skills tools when set; managed Go ADK runtimes use their compiled skill configuration. | | `KAGENT_STS_AUDIENCE` | String | `(none)` | Comma-separated RFC 8693 audiences sent on STS token-exchange requests. Alternate to KAGENT_STS_RESOURCE for servers that key on audience. | | `KAGENT_STS_RESOURCE` | String | `(none)` | Comma-separated RFC 8707 resource indicators sent on STS token-exchange requests to scope issued tokens to target backends. | -| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `KAGENT_STS_WELL_KNOWN_URI` | String | `(none)` | Well-known endpoint for the Security Token Service (STS) used for token exchange. | | `MISTRAL_API_BASE` | String | `(none)` | Custom base URL for the Mistral AI API (defaults to https://api.mistral.ai/v1). | | `MISTRAL_API_KEY` | String | `(none)` | API key for Mistral AI. | | `OLLAMA_API_BASE` | String | `(none)` | Base URL for the Ollama API endpoint; falls back to http://localhost:11434 when model configuration and this variable are unset. | @@ -80,10 +82,8 @@ This reference covers user-configurable settings for the controller, CLI, standa | `OTEL_SEMCONV_STABILITY_OPT_IN` | String | `gen_ai_latest_experimental` | Python Google ADK semantic-convention opt-in; set by kagent when absent. | | `OTEL_SERVICE_NAME` | String | `(none)` | SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes. | | `OTEL_TRACES_EXPORTER` | String | `(none)` | Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | -| `PORT` | String | `8080` | Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port. | | `SAP_AI_CORE_CLIENT_ID` | String | `(none)` | OAuth2 client ID for SAP AI Core authentication. | | `SAP_AI_CORE_CLIENT_SECRET` | String | `(none)` | OAuth2 client secret for SAP AI Core authentication. | -| `STS_WELL_KNOWN_URI` | String | `(none)` | Well-known endpoint for the Security Token Service (STS) used for token exchange. | ## cli @@ -91,37 +91,50 @@ This reference covers user-configurable settings for the controller, CLI, standa |----------|------|---------|-------------| | `ANTHROPIC_API_KEY` | String | `(none)` | API key for Anthropic. | | `AZURE_OPENAI_API_KEY` | String | `(none)` | API key for Azure OpenAI. | -| `DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | | `GEMINI_API_KEY` | String | `(none)` | Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs. | | `GOOGLE_API_KEY` | String | `(none)` | API key for Google Gemini. | +| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | | `KAGENT_DEFAULT_MODEL_PROVIDER` | String | `openAI` | Default LLM provider for agents (e.g. openAI, anthropic, ollama, azureOpenAI). | | `KAGENT_HELM_EXTRA_ARGS` | String | `(none)` | Additional arguments to pass to Helm commands. | | `KAGENT_HELM_REPO` | String | `oci://ghcr.io/kagent-dev/kagent/helm/` | Helm repository URL for kagent charts. | | `KAGENT_HELM_VERSION` | String | `(none)` | Helm chart version to deploy. When unset, the CLI uses its own version. | +| `KAGENT_LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `KAGENT_POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | -| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | | `OLLAMA_API_KEY` | String | `(none)` | API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly. | | `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. | -| `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | ## controller | Variable | Type | Default | Description | |----------|------|---------|-------------| -| `DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | -| `GRPC_REFLECTION` | Boolean | `false` | Enable gRPC server reflection on the controller. | -| `HTTP_BIND_ADDRESS` | String | `:8083` | Listen address for the controller HTTP, gRPC, A2A, and MCP server. | | `KAGENT_CONTROLLER_NAME` | String | `kagent-controller` | Name of the kagent controller service. | +| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | | `KAGENT_GATEWAY_URL` | String | `(none)` | Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value. | +| `KAGENT_GRPC_REFLECTION` | Boolean | `false` | Enable gRPC server reflection on the controller. | +| `KAGENT_HTTP_BIND_ADDRESS` | String | `:8083` | Listen address for the controller HTTP, gRPC, A2A, and MCP server. | +| `KAGENT_LEADER_ELECT` | Boolean | `true` | Enable controller leader election, including during single-replica rolling updates. Required for sandbox lifecycle coordination. | +| `KAGENT_LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | +| `KAGENT_METRICS_BIND_ADDRESS` | String | `0` | Address the controller-runtime metrics server binds to, e.g. :8080. "0" (the default) serves no metrics, so an installation that does not set this is unchanged. The Helm chart renders this variable, and its ServiceMonitor, from controller.metrics. | +| `KAGENT_METRICS_SECURE` | Boolean | `false` | Serve the metrics endpoint over HTTPS with authentication and authorization. A scraper then needs a token bound to the metrics-reader ClusterRole. | | `KAGENT_NAMESPACE` | String | `kagent` | Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it. | | `KAGENT_OTEL_CAPTURE_RAW_API_BODIES` | Boolean | `false` | Set to true to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data. | | `KAGENT_OTEL_MAX_CAPTURE_BYTES` | Integer | `16384` | Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384. | | `KAGENT_OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Resource attributes, as key=value pairs, added to every agent runtime. | +| `KAGENT_POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | +| `KAGENT_POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over KAGENT_POSTGRES_DATABASE_URL in the controller. | +| `KAGENT_SANDBOX_CPU` | String | `1` | CPU limit for standalone sandbox runtimes. | +| `KAGENT_SANDBOX_DEFAULT_TTL` | Duration | `1h0m0s` | Default standalone sandbox lifetime. | +| `KAGENT_SANDBOX_GUEST_IMAGE` | String | `(none)` | Guest package image pinned by sha256 digest. Required for sandbox preparation and passed unchanged to Substrate. | +| `KAGENT_SANDBOX_MAX_TTL` | Duration | `24h0m0s` | Maximum standalone sandbox lifetime, at most 24h. | +| `KAGENT_SANDBOX_MEMORY` | String | `1Gi` | Memory limit for standalone sandbox runtimes. | +| `KAGENT_SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | +| `KAGENT_SUBSTRATE_ATENET_ROUTER_URL` | String | `http://atenet-router.ate-system.svc:80` | Substrate router endpoint for agent and sandbox guest traffic. | +| `KAGENT_SUBSTRATE_ATE_API_CA_FILE` | String | `(none)` | PEM CA bundle used to verify the Substrate API server. Empty uses system trust roots. | +| `KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE` | String | `(none)` | PEM bundle containing both the client certificate and private key for Substrate API mTLS. Reloaded for each TLS handshake. | +| `KAGENT_SUBSTRATE_ATE_API_ENDPOINT` | String | `dns:///api.ate-system.svc:443` | Substrate control-plane gRPC endpoint. | +| `KAGENT_WATCH_NAMESPACES` | String | `(none)` | Comma-separated namespaces to watch. Empty watches all namespaces. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | -| `LEADER_ELECT` | Boolean | `true` | Enable controller leader election, including during single-replica rolling updates. Required for sandbox lifecycle coordination. | -| `LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | -| `METRICS_BIND_ADDRESS` | String | `0` | Address the controller-runtime metrics server binds to, e.g. :8080. "0" (the default) serves no metrics, so an installation that does not set this is unchanged. The Helm chart renders this variable, and its ServiceMonitor, from controller.metrics. | -| `METRICS_SECURE` | Boolean | `false` | Serve the metrics endpoint over HTTPS with authentication and authorization. A scraper then needs a token bound to the metrics-reader ClusterRole. | | `OTEL_EXPORTER_OTLP_COMPRESSION` | String | `gzip` | OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip. | | `OTEL_EXPORTER_OTLP_ENDPOINT` | String | `(none)` | OTLP endpoint for every signal. `OTEL_EXPORTER_OTLP__ENDPOINT` overrides it for one signal. | | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | String | `(none)` | Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path. | @@ -144,28 +157,15 @@ This reference covers user-configurable settings for the controller, CLI, standa | `OTEL_SDK_DISABLED` | Boolean | `false` | Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes. | | `OTEL_SERVICE_NAME` | String | `(none)` | SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes. | | `OTEL_TRACES_EXPORTER` | String | `(none)` | Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | -| `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | -| `POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller. | -| `SANDBOX_CPU` | String | `1` | CPU limit for standalone sandbox runtimes. | -| `SANDBOX_DEFAULT_TTL` | Duration | `1h0m0s` | Default standalone sandbox lifetime. | -| `SANDBOX_GUEST_IMAGE` | String | `(none)` | Guest package image pinned by sha256 digest. Required for sandbox preparation and passed unchanged to Substrate. | -| `SANDBOX_MAX_TTL` | Duration | `24h0m0s` | Maximum standalone sandbox lifetime, at most 24h. | -| `SANDBOX_MEMORY` | String | `1Gi` | Memory limit for standalone sandbox runtimes. | -| `SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | -| `SUBSTRATE_ATENET_ROUTER_URL` | String | `http://atenet-router.ate-system.svc:80` | Substrate router endpoint for agent and sandbox guest traffic. | -| `SUBSTRATE_ATE_API_CA_FILE` | String | `(none)` | PEM CA bundle used to verify the Substrate API server. Empty uses system trust roots. | -| `SUBSTRATE_ATE_API_CLIENT_CERT_FILE` | String | `(none)` | PEM bundle containing both the client certificate and private key for Substrate API mTLS. Reloaded for each TLS handshake. | -| `SUBSTRATE_ATE_API_ENDPOINT` | String | `dns:///api.ate-system.svc:443` | Substrate control-plane gRPC endpoint. | -| `WATCH_NAMESPACES` | String | `(none)` | Comma-separated namespaces to watch. Empty watches all namespaces. | ## database | Variable | Type | Default | Description | |----------|------|---------|-------------| -| `DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | -| `POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | -| `POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller. | -| `SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | +| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | +| `KAGENT_POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | +| `KAGENT_POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over KAGENT_POSTGRES_DATABASE_URL in the controller. | +| `KAGENT_SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | ## testing @@ -182,31 +182,30 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_E2E_RUNTIME_IMAGE` | String | `(none)` | Required digest-pinned Go ADK image for sandbox E2E tests and lifecycle manifests. | | `KAGENT_E2E_SANDBOX_NAMESPACE` | String | `kagent` | Namespace for sandbox E2E resources. | | `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | +| `KAGENT_LLM_PORT` | String | `8090` | Port for the mock LLM server. | | `KAGENT_LOCAL_HOST` | String | `(none)` | Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems. | +| `KAGENT_RUN_ROLLING_UPGRADE_TESTS` | Boolean | `false` | Run rolling upgrade integration tests when exactly true. | +| `KAGENT_RUN_UPGRADE_TESTS` | Boolean | `false` | Run upgrade integration tests when exactly true. | +| `KAGENT_STS_PORT` | String | `8091` | Port for the mock STS (Security Token Service) server. | | `KAGENT_TEST_PYTHON` | String | `(none)` | Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset. | +| `KAGENT_UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to KAGENT_UI_LOOP_PORT plus 50. | +| `KAGENT_UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | +| `KAGENT_UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | +| `KAGENT_UI_LOOP_PORT` | String | `8001` | Vite development/preview port and UI browser-test port. | +| `KAGENT_UPDATE_GOLDEN` | Boolean | `false` | When true, update golden test files instead of comparing. | +| `KAGENT_VITE_API_MODE` | String | `(none)` | Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_ENABLE_MOCK_UI; leave unset for normal development. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | -| `LLM_PORT` | String | `8090` | Port for the mock LLM server. | -| `RUN_ROLLING_UPGRADE_TESTS` | Boolean | `false` | Run rolling upgrade integration tests when exactly true. | -| `RUN_UPGRADE_TESTS` | Boolean | `false` | Run upgrade integration tests when exactly true. | -| `STS_PORT` | String | `8091` | Port for the mock STS (Security Token Service) server. | -| `UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to UI_LOOP_PORT plus 50. | -| `UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | -| `UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | -| `UI_LOOP_PORT` | String | `8001` | Vite development/preview port and UI browser-test port. | -| `UPDATE_GOLDEN` | Boolean | `false` | When true, update golden test files instead of comparing. | ## ui | Variable | Type | Default | Description | |----------|------|---------|-------------| -| `API_BASE_URL` | String | `/api` | Browser API base URL. Set directly for Vite development; use KAGENT_API_BASE_URL in the UI container. | -| `BASE_PATH` | String | `(none)` | Browser public path prefix. Set directly for Vite development; use KAGENT_UI_BASE_PATH in the UI container. | -| `ENABLE_MOCK_UI` | Boolean | `false` | Serve the UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Applies in development and containers. | -| `EXTENSION_` | String | `(none)` | UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public. | -| `KAGENT_API_BASE_URL` | String | `/api` | UI container setting mapped to the browser's API_BASE_URL at startup. | +| `KAGENT_API_BASE_URL` | String | `/api` | Browser API base URL for UI containers and Vite development. | | `KAGENT_DEV_CONTROLLER_URL` | String | `http://127.0.0.1:8083` | Vite development proxy target for /api and /a2a; not sent to the browser. | -| `KAGENT_STREAM_TIMEOUT_MS` | String | `1800000` | UI container chat stream inactivity timeout in milliseconds, mapped to STREAM_TIMEOUT_MS. 0 disables the timeout. | -| `KAGENT_UI_BASE_PATH` | String | `(none)` | UI container public path prefix, such as /ui; empty serves at the root. Mapped to BASE_PATH; invalid or reserved prefixes fall back to the root. | -| `SSO_REDIRECT_PATH` | String | `/oauth2/start` | UI path used by Sign in with SSO. | -| `STREAM_TIMEOUT_MS` | String | `1800000` | Browser chat stream inactivity timeout in milliseconds. Set directly for Vite development; use KAGENT_STREAM_TIMEOUT_MS in the UI container. 0 disables the timeout. | -| `VITE_EXAMPLE_EXTENSION` | Boolean | `false` | Build-time switch enabling the bundled example UI extension. | +| `KAGENT_ENABLE_MOCK_UI` | Boolean | `false` | Serve the development UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Release bundles do not include the mock backend. | +| `KAGENT_EXTENSION_` | String | `(none)` | UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public. | +| `KAGENT_SSO_REDIRECT_PATH` | String | `/oauth2/start` | UI path used by Sign in with SSO. | +| `KAGENT_STREAM_TIMEOUT_MS` | String | `1800000` | UI chat stream inactivity timeout in milliseconds. 0 disables the timeout. Applies in containers and Vite development. | +| `KAGENT_UI_BASE_PATH` | String | `(none)` | UI public path prefix, such as /ui; empty serves at the root. Applies in containers and Vite development; the container falls back to the root for invalid or reserved prefixes. | +| `KAGENT_VITE_API_MODE` | String | `(none)` | Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_ENABLE_MOCK_UI; leave unset for normal development. | +| `KAGENT_VITE_EXAMPLE_EXTENSION` | Boolean | `false` | Build-time switch enabling the bundled example UI extension. | diff --git a/go/Makefile b/go/Makefile index 0e23a79f2b..80132282eb 100644 --- a/go/Makefile +++ b/go/Makefile @@ -128,7 +128,7 @@ run: fmt vet ## Run a controller from your host. .PHONY: test test: ## Run all unit tests. - UPDATE_GOLDEN=$(UPDATE_GOLDEN) go test -race -skip 'TestE2E.*' -v ./... + KAGENT_UPDATE_GOLDEN=$(KAGENT_UPDATE_GOLDEN) go test -race -skip 'TestE2E.*' -v ./... # Pin e2e tests to the kind cluster so a stray current-context (or an # ambient KUBECONFIG) can't leak resources onto a remote cluster. Override diff --git a/go/adk/cmd/main.go b/go/adk/cmd/main.go index a56781ed11..f0043eb1ee 100644 --- a/go/adk/cmd/main.go +++ b/go/adk/cmd/main.go @@ -32,10 +32,10 @@ const ( ) func main() { - logLevel := flag.String("log-level", cmp.Or(os.Getenv("LOG_LEVEL"), "info"), "Set the logging level (debug, info, warn, error)") + logLevel := flag.String("log-level", cmp.Or(os.Getenv("KAGENT_LOG_LEVEL"), "info"), "Set the logging level (debug, info, warn, error)") host := flag.String("host", "", "Set the host address to bind to (default: empty, binds to all interfaces)") - portFlag := flag.String("port", "", "Set the port to listen on (overrides PORT environment variable)") - filepathFlag := flag.String("filepath", "", "Set the config directory path (overrides CONFIG_DIR environment variable)") + portFlag := flag.String("port", "", "Set the port to listen on (overrides KAGENT_PORT environment variable)") + filepathFlag := flag.String("filepath", "", "Set the config directory path (overrides KAGENT_CONFIG_DIR environment variable)") flag.Parse() logger, err := logging.New(os.Stderr, *logLevel) @@ -48,12 +48,12 @@ func main() { port := *portFlag if port == "" { - port = os.Getenv("PORT") + port = os.Getenv("KAGENT_PORT") } configDir := *filepathFlag if configDir == "" { - configDir = os.Getenv("CONFIG_DIR") + configDir = os.Getenv("KAGENT_CONFIG_DIR") } if configDir == "" { configDir = "/config" diff --git a/go/adk/examples/byo/main.go b/go/adk/examples/byo/main.go index 0725ddd569..fc3759c37e 100644 --- a/go/adk/examples/byo/main.go +++ b/go/adk/examples/byo/main.go @@ -11,7 +11,7 @@ // // - KAGENT_NAMESPACE / KAGENT_NAME: used to derive the app name for session // scoping. Falls back to the agent card name. -// - PORT: the port to listen on (default "8080"). +// - KAGENT_PORT: the port to listen on (default "8080"). // // Required environment variables: // diff --git a/go/adk/pkg/a2a/server/server.go b/go/adk/pkg/a2a/server/server.go index 9cc16fd035..719eb86d77 100644 --- a/go/adk/pkg/a2a/server/server.go +++ b/go/adk/pkg/a2a/server/server.go @@ -32,7 +32,7 @@ import ( ) const ( - a2aMaxContentLengthEnvVar = "A2A_MAX_CONTENT_LENGTH" + a2aMaxContentLengthEnvVar = "KAGENT_A2A_MAX_CONTENT_LENGTH" defaultMaxContentLength = int64(10 * 1024 * 1024) ) diff --git a/go/adk/pkg/app/app.go b/go/adk/pkg/app/app.go index 95f916bb3e..b0e30896ff 100644 --- a/go/adk/pkg/app/app.go +++ b/go/adk/pkg/app/app.go @@ -41,7 +41,7 @@ type AppConfig struct { // Host is the address to bind to. Empty string binds to all interfaces. Host string - // Port is the port to listen on. Defaults to the PORT env var, then "8080". + // Port is the port to listen on. Defaults to the KAGENT_PORT env var, then "8080". Port string // AppName identifies this application for session and tracing purposes. @@ -90,7 +90,7 @@ func New(cfg AppConfig, executor a2asrv.AgentExecutor) (*KAgentApp, error) { if cfg.Logger == nil { logger, err := logging.NewFromEnv(os.Stderr) if err != nil { - return nil, fmt.Errorf("parse LOG_LEVEL: %w", err) + return nil, fmt.Errorf("parse KAGENT_LOG_LEVEL: %w", err) } cfg.Logger = logger } @@ -181,7 +181,7 @@ func (a *KAgentApp) Logger() *slog.Logger { // applyDefaults fills in zero-value fields with sensible defaults. func applyDefaults(cfg AppConfig) AppConfig { if cfg.Port == "" { - cfg.Port = os.Getenv("PORT") + cfg.Port = os.Getenv("KAGENT_PORT") } if cfg.Port == "" { cfg.Port = defaultPort diff --git a/go/adk/pkg/app/app_test.go b/go/adk/pkg/app/app_test.go index 3db845d2a7..62e13fbab8 100644 --- a/go/adk/pkg/app/app_test.go +++ b/go/adk/pkg/app/app_test.go @@ -76,7 +76,7 @@ func TestNew_ProvidedController(t *testing.T) { } func TestApplyDefaults_Port(t *testing.T) { - t.Setenv("PORT", "") + t.Setenv("KAGENT_PORT", "") cfg := applyDefaults(AppConfig{}) if cfg.Port != defaultPort { t.Errorf("expected port %q, got %q", defaultPort, cfg.Port) @@ -84,7 +84,7 @@ func TestApplyDefaults_Port(t *testing.T) { } func TestApplyDefaults_PortFromEnv(t *testing.T) { - t.Setenv("PORT", "9090") + t.Setenv("KAGENT_PORT", "9090") cfg := applyDefaults(AppConfig{}) if cfg.Port != "9090" { t.Errorf("expected port %q, got %q", "9090", cfg.Port) @@ -92,7 +92,7 @@ func TestApplyDefaults_PortFromEnv(t *testing.T) { } func TestApplyDefaults_PortExplicit(t *testing.T) { - t.Setenv("PORT", "9090") + t.Setenv("KAGENT_PORT", "9090") cfg := applyDefaults(AppConfig{Port: "3000"}) if cfg.Port != "3000" { t.Errorf("expected port %q, got %q", "3000", cfg.Port) diff --git a/go/adk/pkg/runner/adapter.go b/go/adk/pkg/runner/adapter.go index 6d38b05f28..71c40dac00 100644 --- a/go/adk/pkg/runner/adapter.go +++ b/go/adk/pkg/runner/adapter.go @@ -108,7 +108,7 @@ func CreateRunnerConfig( func buildTokenPropagationPlugin(ctx context.Context, log *slog.Logger) (*sts.TokenPropagationPlugin, error) { propagateToken := strings.EqualFold(strings.TrimSpace(os.Getenv("KAGENT_PROPAGATE_TOKEN")), "true") - stsWellKnownURI := strings.TrimSpace(os.Getenv("STS_WELL_KNOWN_URI")) + stsWellKnownURI := strings.TrimSpace(os.Getenv("KAGENT_STS_WELL_KNOWN_URI")) if !propagateToken && stsWellKnownURI == "" { return nil, nil } diff --git a/go/core/cli/internal/commands/db/db.go b/go/core/cli/internal/commands/db/db.go index a3ce67f12a..f906d12887 100644 --- a/go/core/cli/internal/commands/db/db.go +++ b/go/core/cli/internal/commands/db/db.go @@ -20,7 +20,7 @@ import ( ) // vectorEnabledKey names two lookups that deliberately share it: the CLI's -// own DATABASE_VECTOR_ENABLED env var (a local operator override), and the +// own KAGENT_DATABASE_VECTOR_ENABLED env var (a local operator override), and the // controller-configmap key the chart renders — the value the controller pod // itself consumes via envFrom. Same name, two different places. var vectorEnabledKey = kagentenv.DatabaseVectorEnabled.Name() @@ -45,7 +45,7 @@ func NewDBCmd() *cobra.Command { // migrationSources resolves the built-in migration tracks when a db // subcommand runs (never during command construction, so unrelated commands // do no work and print no warnings). The vector track is gated, in order of -// precedence, on: the DATABASE_VECTOR_ENABLED env var in the CLI's own +// precedence, on: the KAGENT_DATABASE_VECTOR_ENABLED env var in the CLI's own // environment (explicit operator intent, works without a cluster), the // controller's configmap on the live cluster (the same value the server // reads), and finally the controller's default (enabled). diff --git a/go/core/cli/internal/db/migrate/migrate.go b/go/core/cli/internal/db/migrate/migrate.go index 7a91718785..028bf637a0 100644 --- a/go/core/cli/internal/db/migrate/migrate.go +++ b/go/core/cli/internal/db/migrate/migrate.go @@ -21,7 +21,7 @@ import ( ) const ( - dbURLEnv = "POSTGRES_DATABASE_URL" + dbURLEnv = "KAGENT_POSTGRES_DATABASE_URL" sourceFlag = "source" ) @@ -93,7 +93,7 @@ func NewCommandFromFunc(fn SourcesFunc) *cobra.Command { Use: "migrate", Short: "Apply, roll back, and inspect database migrations", Long: `Apply, roll back, and inspect database migrations. -The command reads POSTGRES_DATABASE_URL when --db-url is empty.`, +The command reads KAGENT_POSTGRES_DATABASE_URL when --db-url is empty.`, } command.PersistentFlags().StringVar(&state.dbURL, "db-url", "", "PostgreSQL connection URL") command.PersistentFlags().StringVar(&state.source, sourceFlag, "", "Migration source for down, goto, or version") diff --git a/go/core/hack/mockllm/main.go b/go/core/hack/mockllm/main.go index 163f7f5787..7fad59f7a0 100644 --- a/go/core/hack/mockllm/main.go +++ b/go/core/hack/mockllm/main.go @@ -21,7 +21,7 @@ func main() { ctx := logging.IntoContext(context.Background(), logger) agentServiceAccount := "system:serviceaccount:kagent:test-sts" stsPort := 8091 - if port := os.Getenv("STS_PORT"); port != "" { + if port := os.Getenv("KAGENT_STS_PORT"); port != "" { stsPort, _ = strconv.Atoi(port) } stsServer := e2emocks.NewMockSTSServer(agentServiceAccount, uint16(stsPort)) @@ -40,7 +40,7 @@ func main() { return } mockllmCfg.ListenAddr = ":8090" - if port := os.Getenv("LLM_PORT"); port != "" { + if port := os.Getenv("KAGENT_LLM_PORT"); port != "" { mockllmCfg.ListenAddr = ":" + port } server := mockllm.NewServer(mockllmCfg) diff --git a/go/core/internal/translator/kagent/compiler.go b/go/core/internal/translator/kagent/compiler.go index 10011a7c05..81e08bf091 100644 --- a/go/core/internal/translator/kagent/compiler.go +++ b/go/core/internal/translator/kagent/compiler.go @@ -56,7 +56,7 @@ func (c *Compiler) Compile(ctx context.Context, input *v2translator.HarnessInput corev1.EnvVar{Name: env.KagentNamespace.Name(), Value: template.Namespace}, corev1.EnvVar{Name: env.KagentAPIURL.Name(), Value: fmt.Sprintf("http://%s.%s:8083", utils.GetControllerName(), utils.GetResourceNamespace())}, corev1.EnvVar{Name: env.KagentGatewayURL.Name(), Value: fmt.Sprintf("http://%s.%s:8083", utils.GetControllerName(), utils.GetResourceNamespace())}, - corev1.EnvVar{Name: "PORT", Value: "80"}, + corev1.EnvVar{Name: "KAGENT_PORT", Value: "80"}, corev1.EnvVar{Name: "KAGENT_A2A_GRPC_ADDRESS", Value: "[::]:80"}, ) environment = append(environment, telemetryConfig.TelemetryEnvironment(tracing.RuntimeTelemetry{ diff --git a/go/core/pkg/app/app.go b/go/core/pkg/app/app.go index 0096378ced..cc6055448e 100644 --- a/go/core/pkg/app/app.go +++ b/go/core/pkg/app/app.go @@ -131,7 +131,7 @@ func (o Options) resolve() (auth.AuthProvider, auth.CollectionAuthorizer) { } // SetupLogger installs the controller-runtime logger, at the level named by -// LOG_LEVEL. +// KAGENT_LOG_LEVEL. // // Run calls this itself, so a library consumer needs it only when it logs // before Run — and it must then call it first, because controller-runtime @@ -145,7 +145,7 @@ func (o Options) resolve() (auth.AuthProvider, auth.CollectionAuthorizer) { func SetupLogger() error { logger, err := logging.NewFromEnv(os.Stderr) if err != nil { - return fmt.Errorf("parse LOG_LEVEL: %w", err) + return fmt.Errorf("parse KAGENT_LOG_LEVEL: %w", err) } slog.SetDefault(logger) ctrl.SetLogger(logging.AsLogr(logger)) @@ -430,7 +430,7 @@ func env(variable kagentenv.StringVar) string { return variable.DefaultValue() } -// metricsBindAddress resolves METRICS_BIND_ADDRESS. controller-runtime reads an +// metricsBindAddress resolves KAGENT_METRICS_BIND_ADDRESS. controller-runtime reads an // empty address as "unset" and falls back to :8080, so an empty value would // serve metrics on a port nobody asked for. "0" disables the metrics server. func metricsBindAddress() string { diff --git a/go/core/pkg/app/app_test.go b/go/core/pkg/app/app_test.go index 0bffe44d3b..cd9881443c 100644 --- a/go/core/pkg/app/app_test.go +++ b/go/core/pkg/app/app_test.go @@ -88,11 +88,11 @@ func TestOptionsResolve(t *testing.T) { } func TestLeaderElectionConfiguration(t *testing.T) { - t.Setenv("LEADER_ELECT", "") + t.Setenv("KAGENT_LEADER_ELECT", "") if !kagentenv.LeaderElect.Get() { t.Fatal("leader election must default to enabled") } - t.Setenv("LEADER_ELECT", "false") + t.Setenv("KAGENT_LEADER_ELECT", "false") if kagentenv.LeaderElect.Get() { t.Fatal("an explicit false must be preserved for startup validation") } @@ -111,7 +111,7 @@ func TestMetricsBindAddressNeverFallsBackToTheControllerRuntimeDefault(t *testin } { t.Run(name, func(t *testing.T) { if testCase.value != nil { - t.Setenv("METRICS_BIND_ADDRESS", *testCase.value) + t.Setenv("KAGENT_METRICS_BIND_ADDRESS", *testCase.value) } if got := metricsBindAddress(); got != testCase.want { t.Fatalf("metricsBindAddress() = %q, want %q", got, testCase.want) @@ -122,11 +122,11 @@ func TestMetricsBindAddressNeverFallsBackToTheControllerRuntimeDefault(t *testin func TestMetricsSecureDefaultsOff(t *testing.T) { if kagentenv.MetricsSecure.Get() { - t.Fatal("METRICS_SECURE must default to false") + t.Fatal("KAGENT_METRICS_SECURE must default to false") } - t.Setenv("METRICS_SECURE", "true") + t.Setenv("KAGENT_METRICS_SECURE", "true") if !kagentenv.MetricsSecure.Get() { - t.Fatal("METRICS_SECURE=true must enable secure serving") + t.Fatal("KAGENT_METRICS_SECURE=true must enable secure serving") } } @@ -208,9 +208,9 @@ func TestExtraMigrationsAppendAfterBuiltins(t *testing.T) { // SetupLogger rejects a bad level before it touches the global logger, so this // case does not disturb whatever logger the rest of the suite runs under. func TestSetupLoggerRejectsBadLevel(t *testing.T) { - t.Setenv("LOG_LEVEL", "not-a-level") + t.Setenv("KAGENT_LOG_LEVEL", "not-a-level") if err := SetupLogger(); err == nil { - t.Fatal("SetupLogger accepted an unparseable LOG_LEVEL") + t.Fatal("SetupLogger accepted an unparseable KAGENT_LOG_LEVEL") } } diff --git a/go/core/pkg/env/controller.go b/go/core/pkg/env/controller.go index 3aab994407..44b56ff505 100644 --- a/go/core/pkg/env/controller.go +++ b/go/core/pkg/env/controller.go @@ -2,21 +2,21 @@ package env var ( HTTPBindAddress = RegisterStringVar( - "HTTP_BIND_ADDRESS", ":8083", + "KAGENT_HTTP_BIND_ADDRESS", ":8083", "Listen address for the controller HTTP, gRPC, A2A, and MCP server.", ComponentController, ) GRPCReflection = RegisterBoolVar( - "GRPC_REFLECTION", false, + "KAGENT_GRPC_REFLECTION", false, "Enable gRPC server reflection on the controller.", ComponentController, ) WatchNamespaces = RegisterStringVar( - "WATCH_NAMESPACES", "", + "KAGENT_WATCH_NAMESPACES", "", "Comma-separated namespaces to watch. Empty watches all namespaces.", ComponentController, ) ) // Shared settings read by logging and Kubernetes libraries. var ( - _ = RegisterStringVar("LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController, ComponentCLI, ComponentTesting) ) diff --git a/go/core/pkg/env/database.go b/go/core/pkg/env/database.go index 74c04c4df6..1bcfd002c4 100644 --- a/go/core/pkg/env/database.go +++ b/go/core/pkg/env/database.go @@ -2,11 +2,11 @@ package env var ( PostgresDatabaseURL = RegisterStringVar( - "POSTGRES_DATABASE_URL", "postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres", + "KAGENT_POSTGRES_DATABASE_URL", "postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres", "PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL.", ComponentDatabase, ComponentController, ComponentCLI, ) PostgresDatabaseURLFile = RegisterStringVar( - "POSTGRES_DATABASE_URL_FILE", "", - "File containing the PostgreSQL connection URL; takes precedence over POSTGRES_DATABASE_URL in the controller.", ComponentDatabase, ComponentController, + "KAGENT_POSTGRES_DATABASE_URL_FILE", "", + "File containing the PostgreSQL connection URL; takes precedence over KAGENT_POSTGRES_DATABASE_URL in the controller.", ComponentDatabase, ComponentController, ) ) diff --git a/go/core/pkg/env/kagent.go b/go/core/pkg/env/kagent.go index 2053a078a4..ccbcaa081d 100644 --- a/go/core/pkg/env/kagent.go +++ b/go/core/pkg/env/kagent.go @@ -3,14 +3,14 @@ package env // Core kagent environment variables used by the controller and agent runtime. var ( LeaderElect = RegisterBoolVar( - "LEADER_ELECT", + "KAGENT_LEADER_ELECT", true, "Enable controller leader election, including during single-replica rolling updates. Required for sandbox lifecycle coordination.", ComponentController, ) MetricsBindAddress = RegisterStringVar( - "METRICS_BIND_ADDRESS", + "KAGENT_METRICS_BIND_ADDRESS", "0", "Address the controller-runtime metrics server binds to, e.g. :8080. "+ "\"0\" (the default) serves no metrics, so an installation that does not "+ @@ -20,7 +20,7 @@ var ( ) MetricsSecure = RegisterBoolVar( - "METRICS_SECURE", + "KAGENT_METRICS_SECURE", false, "Serve the metrics endpoint over HTTPS with authentication and authorization. "+ "A scraper then needs a token bound to the metrics-reader ClusterRole.", @@ -93,7 +93,7 @@ var ( ) StsWellKnownURI = RegisterStringVar( - "STS_WELL_KNOWN_URI", + "KAGENT_STS_WELL_KNOWN_URI", "", "Well-known endpoint for the Security Token Service (STS) used for token exchange.", ComponentAgentRuntime, @@ -114,14 +114,14 @@ var ( ) DatabaseVectorEnabled = RegisterBoolVar( - "DATABASE_VECTOR_ENABLED", + "KAGENT_DATABASE_VECTOR_ENABLED", false, "Enable vector database migrations and vector-backed database functionality.", ComponentDatabase, ComponentController, ComponentCLI, ) SkipMigrations = RegisterBoolVar( - "SKIP_MIGRATIONS", + "KAGENT_SKIP_MIGRATIONS", false, "Verify required database migrations at startup without applying them.", ComponentDatabase, ComponentController, diff --git a/go/core/pkg/env/runtime.go b/go/core/pkg/env/runtime.go index 67ccc7ac9a..ba226fa2c8 100644 --- a/go/core/pkg/env/runtime.go +++ b/go/core/pkg/env/runtime.go @@ -4,11 +4,11 @@ package env // Keep their defaults aligned with the Go/Python readers; importing this package // is not required for a runtime or an upstream SDK to read its environment. var ( - _ = RegisterStringVar("PORT", "8080", "Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port.", ComponentAgentRuntime) - _ = RegisterStringVar("CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) - _ = RegisterStringVar("A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_PORT", "8080", "Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) _ = RegisterStringVar("KAGENT_A2A_GRPC_ADDRESS", "[::]:80", "Python ADK gRPC listen address.", ComponentAgentRuntime) - _ = RegisterStringVar("BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) ) diff --git a/go/core/pkg/env/sandbox.go b/go/core/pkg/env/sandbox.go index 8d20a35e65..f6288fac20 100644 --- a/go/core/pkg/env/sandbox.go +++ b/go/core/pkg/env/sandbox.go @@ -3,9 +3,9 @@ package env import "time" var ( - SandboxGuestImage = RegisterStringVar("SANDBOX_GUEST_IMAGE", "", "Guest package image pinned by sha256 digest. Required for sandbox preparation and passed unchanged to Substrate.", ComponentController) - SandboxCPU = RegisterStringVar("SANDBOX_CPU", "1", "CPU limit for standalone sandbox runtimes.", ComponentController) - SandboxMemory = RegisterStringVar("SANDBOX_MEMORY", "1Gi", "Memory limit for standalone sandbox runtimes.", ComponentController) - SandboxDefaultTTL = RegisterDurationVar("SANDBOX_DEFAULT_TTL", time.Hour, "Default standalone sandbox lifetime.", ComponentController) - SandboxMaxTTL = RegisterDurationVar("SANDBOX_MAX_TTL", 24*time.Hour, "Maximum standalone sandbox lifetime, at most 24h.", ComponentController) + SandboxGuestImage = RegisterStringVar("KAGENT_SANDBOX_GUEST_IMAGE", "", "Guest package image pinned by sha256 digest. Required for sandbox preparation and passed unchanged to Substrate.", ComponentController) + SandboxCPU = RegisterStringVar("KAGENT_SANDBOX_CPU", "1", "CPU limit for standalone sandbox runtimes.", ComponentController) + SandboxMemory = RegisterStringVar("KAGENT_SANDBOX_MEMORY", "1Gi", "Memory limit for standalone sandbox runtimes.", ComponentController) + SandboxDefaultTTL = RegisterDurationVar("KAGENT_SANDBOX_DEFAULT_TTL", time.Hour, "Default standalone sandbox lifetime.", ComponentController) + SandboxMaxTTL = RegisterDurationVar("KAGENT_SANDBOX_MAX_TTL", 24*time.Hour, "Maximum standalone sandbox lifetime, at most 24h.", ComponentController) ) diff --git a/go/core/pkg/env/substrate.go b/go/core/pkg/env/substrate.go index 2a8aaf57f6..3693b9cbe1 100644 --- a/go/core/pkg/env/substrate.go +++ b/go/core/pkg/env/substrate.go @@ -1,13 +1,13 @@ package env var ( - SubstrateATEAPIEndpoint = RegisterStringVar("SUBSTRATE_ATE_API_ENDPOINT", "dns:///api.ate-system.svc:443", "Substrate control-plane gRPC endpoint.", ComponentController) - SubstrateATEAPICAFile = RegisterStringVar("SUBSTRATE_ATE_API_CA_FILE", "", "PEM CA bundle used to verify the Substrate API server. Empty uses system trust roots.", ComponentController) - SubstrateATEAPIClientCertFile = RegisterStringVar("SUBSTRATE_ATE_API_CLIENT_CERT_FILE", "", "PEM bundle containing both the client certificate and private key for Substrate API mTLS. Reloaded for each TLS handshake.", ComponentController) + SubstrateATEAPIEndpoint = RegisterStringVar("KAGENT_SUBSTRATE_ATE_API_ENDPOINT", "dns:///api.ate-system.svc:443", "Substrate control-plane gRPC endpoint.", ComponentController) + SubstrateATEAPICAFile = RegisterStringVar("KAGENT_SUBSTRATE_ATE_API_CA_FILE", "", "PEM CA bundle used to verify the Substrate API server. Empty uses system trust roots.", ComponentController) + SubstrateATEAPIClientCertFile = RegisterStringVar("KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE", "", "PEM bundle containing both the client certificate and private key for Substrate API mTLS. Reloaded for each TLS handshake.", ComponentController) ) var SubstrateAtenetRouterURL = RegisterStringVar( - "SUBSTRATE_ATENET_ROUTER_URL", + "KAGENT_SUBSTRATE_ATENET_ROUTER_URL", "http://atenet-router.ate-system.svc:80", "Substrate router endpoint for agent and sandbox guest traffic.", ComponentController, diff --git a/go/core/pkg/env/testing.go b/go/core/pkg/env/testing.go index acbf5403eb..d2bd2458d2 100644 --- a/go/core/pkg/env/testing.go +++ b/go/core/pkg/env/testing.go @@ -10,21 +10,21 @@ var ( ) UpdateGolden = RegisterBoolVar( - "UPDATE_GOLDEN", + "KAGENT_UPDATE_GOLDEN", false, "When true, update golden test files instead of comparing.", ComponentTesting, ) STSPort = RegisterStringVar( - "STS_PORT", + "KAGENT_STS_PORT", "8091", "Port for the mock STS (Security Token Service) server.", ComponentTesting, ) LLMPort = RegisterStringVar( - "LLM_PORT", + "KAGENT_LLM_PORT", "8090", "Port for the mock LLM server.", ComponentTesting, @@ -43,10 +43,10 @@ var ( _ = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) _ = RegisterBoolVar("KAGENT_E2E_REQUIRE_TRACING", false, "Fail instead of skip when expected native harness tracing support is unavailable.", ComponentTesting) - _ = RegisterBoolVar("RUN_UPGRADE_TESTS", false, "Run upgrade integration tests when exactly true.", ComponentTesting) - _ = RegisterBoolVar("RUN_ROLLING_UPGRADE_TESTS", false, "Run rolling upgrade integration tests when exactly true.", ComponentTesting) - _ = RegisterStringVar("UI_LOOP_PORT", "8001", "Vite development/preview port and UI browser-test port.", ComponentTesting) - _ = RegisterStringVar("UI_LOOP_EXTENSION_PORT", "", "Example-extension browser-test port. Defaults to UI_LOOP_PORT plus 50.", ComponentTesting) - _ = RegisterBoolVar("UI_LOOP_LIVE", false, "Run UI browser tests against a real cluster when exactly true.", ComponentTesting) - _ = RegisterStringVar("UI_LOOP_LIVE_PORT", "8301", "Live-cluster UI browser-test port.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_RUN_UPGRADE_TESTS", false, "Run upgrade integration tests when exactly true.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_RUN_ROLLING_UPGRADE_TESTS", false, "Run rolling upgrade integration tests when exactly true.", ComponentTesting) + _ = RegisterStringVar("KAGENT_UI_LOOP_PORT", "8001", "Vite development/preview port and UI browser-test port.", ComponentTesting) + _ = RegisterStringVar("KAGENT_UI_LOOP_EXTENSION_PORT", "", "Example-extension browser-test port. Defaults to KAGENT_UI_LOOP_PORT plus 50.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_UI_LOOP_LIVE", false, "Run UI browser tests against a real cluster when exactly true.", ComponentTesting) + _ = RegisterStringVar("KAGENT_UI_LOOP_LIVE_PORT", "8301", "Live-cluster UI browser-test port.", ComponentTesting) ) diff --git a/go/core/pkg/env/ui.go b/go/core/pkg/env/ui.go index cfab3ea81b..07e3d174d2 100644 --- a/go/core/pkg/env/ui.go +++ b/go/core/pkg/env/ui.go @@ -1,17 +1,14 @@ package env -// The container entrypoint maps KAGENT_* deployment names to browser keys. -// Vite reads the browser key names directly from the development environment. +// The container entrypoint and Vite map the same deployment names to browser keys. var ( - _ = RegisterStringVar("KAGENT_API_BASE_URL", "/api", "UI container setting mapped to the browser's API_BASE_URL at startup.", ComponentUI) - _ = RegisterStringVar("KAGENT_STREAM_TIMEOUT_MS", "1800000", "UI container chat stream inactivity timeout in milliseconds, mapped to STREAM_TIMEOUT_MS. 0 disables the timeout.", ComponentUI) - _ = RegisterStringVar("KAGENT_UI_BASE_PATH", "", "UI container public path prefix, such as /ui; empty serves at the root. Mapped to BASE_PATH; invalid or reserved prefixes fall back to the root.", ComponentUI) - _ = RegisterStringVar("API_BASE_URL", "/api", "Browser API base URL. Set directly for Vite development; use KAGENT_API_BASE_URL in the UI container.", ComponentUI) - _ = RegisterStringVar("STREAM_TIMEOUT_MS", "1800000", "Browser chat stream inactivity timeout in milliseconds. Set directly for Vite development; use KAGENT_STREAM_TIMEOUT_MS in the UI container. 0 disables the timeout.", ComponentUI) - _ = RegisterStringVar("BASE_PATH", "", "Browser public path prefix. Set directly for Vite development; use KAGENT_UI_BASE_PATH in the UI container.", ComponentUI) - _ = RegisterStringVar("SSO_REDIRECT_PATH", "/oauth2/start", "UI path used by Sign in with SSO.", ComponentUI) - _ = RegisterBoolVar("ENABLE_MOCK_UI", false, "Serve the UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Applies in development and containers.", ComponentUI) - _ = RegisterStringVar("EXTENSION_", "", "UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public.", ComponentUI) + _ = RegisterStringVar("KAGENT_API_BASE_URL", "/api", "Browser API base URL for UI containers and Vite development.", ComponentUI) + _ = RegisterStringVar("KAGENT_STREAM_TIMEOUT_MS", "1800000", "UI chat stream inactivity timeout in milliseconds. 0 disables the timeout. Applies in containers and Vite development.", ComponentUI) + _ = RegisterStringVar("KAGENT_UI_BASE_PATH", "", "UI public path prefix, such as /ui; empty serves at the root. Applies in containers and Vite development; the container falls back to the root for invalid or reserved prefixes.", ComponentUI) + _ = RegisterStringVar("KAGENT_SSO_REDIRECT_PATH", "/oauth2/start", "UI path used by Sign in with SSO.", ComponentUI) + _ = RegisterBoolVar("KAGENT_ENABLE_MOCK_UI", false, "Serve the development UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Release bundles do not include the mock backend.", ComponentUI) + _ = RegisterStringVar("KAGENT_EXTENSION_", "", "UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public.", ComponentUI) _ = RegisterStringVar("KAGENT_DEV_CONTROLLER_URL", "http://127.0.0.1:8083", "Vite development proxy target for /api and /a2a; not sent to the browser.", ComponentUI) - _ = RegisterBoolVar("VITE_EXAMPLE_EXTENSION", false, "Build-time switch enabling the bundled example UI extension.", ComponentUI) + _ = RegisterBoolVar("KAGENT_VITE_EXAMPLE_EXTENSION", false, "Build-time switch enabling the bundled example UI extension.", ComponentUI) + _ = RegisterStringVar("KAGENT_VITE_API_MODE", "", "Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_ENABLE_MOCK_UI; leave unset for normal development.", ComponentUI, ComponentTesting) ) diff --git a/go/core/test/upgrade/rolling_upgrade_test.go b/go/core/test/upgrade/rolling_upgrade_test.go index a437ce0d44..adc33afbe6 100644 --- a/go/core/test/upgrade/rolling_upgrade_test.go +++ b/go/core/test/upgrade/rolling_upgrade_test.go @@ -12,8 +12,8 @@ import ( ) func TestRollingUpgradeCompatibility(t *testing.T) { - if os.Getenv("RUN_ROLLING_UPGRADE_TESTS") != "true" { - t.Skip("set RUN_ROLLING_UPGRADE_TESTS=true to run rolling upgrade tests") + if os.Getenv("KAGENT_RUN_ROLLING_UPGRADE_TESTS") != "true" { + t.Skip("set KAGENT_RUN_ROLLING_UPGRADE_TESTS=true to run rolling upgrade tests") } env := loadUpgradeEnv(t) diff --git a/go/core/test/upgrade/upgrade_test.go b/go/core/test/upgrade/upgrade_test.go index 34efa1644c..304d653bf6 100644 --- a/go/core/test/upgrade/upgrade_test.go +++ b/go/core/test/upgrade/upgrade_test.go @@ -45,8 +45,8 @@ type postgresMigrationState struct { } func TestUpgrade(t *testing.T) { - if os.Getenv("RUN_UPGRADE_TESTS") != "true" { - t.Skip("set RUN_UPGRADE_TESTS=true to run upgrade tests") + if os.Getenv("KAGENT_RUN_UPGRADE_TESTS") != "true" { + t.Skip("set KAGENT_RUN_UPGRADE_TESTS=true to run upgrade tests") } env := loadUpgradeEnv(t) diff --git a/go/pkg/logging/logging.go b/go/pkg/logging/logging.go index 9ad74f3999..4141a53eba 100644 --- a/go/pkg/logging/logging.go +++ b/go/pkg/logging/logging.go @@ -43,9 +43,9 @@ func (h traceHandler) WithGroup(name string) slog.Handler { return traceHandler{h.Handler.WithGroup(name)} } -// NewFromEnv returns a JSON logger using LOG_LEVEL, defaulting to info. +// NewFromEnv returns a JSON logger using KAGENT_LOG_LEVEL, defaulting to info. func NewFromEnv(w io.Writer) (*slog.Logger, error) { - return New(w, cmp.Or(os.Getenv("LOG_LEVEL"), "info")) + return New(w, cmp.Or(os.Getenv("KAGENT_LOG_LEVEL"), "info")) } // IntoContext stores logger in the logr context slot used by controller-runtime. diff --git a/go/sandbox/guest/README.md b/go/sandbox/guest/README.md index e49d3fb95c..1fa37d07e4 100644 --- a/go/sandbox/guest/README.md +++ b/go/sandbox/guest/README.md @@ -19,7 +19,7 @@ Sessions do not run this guest. - Process output is spooled under `/data/guest-logs`. - Both directories must be writable by the runtime user when mounting `/data`. - Flags `--listen`, `--workspace`, and `--log-dir` override these defaults. -- JSON logs go to stderr; `LOG_LEVEL` selects debug, info (default), warn, or error. +- JSON logs go to stderr; `KAGENT_LOG_LEVEL` selects debug, info (default), warn, or error. - SIGINT/SIGTERM stop serving and disconnect active RPCs, including output observers, before cleaning up the guest services and exiting. diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index 6c43d5237c..2a45eab3dd 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -16,11 +16,11 @@ data: IMAGE_REPOSITORY: {{ .Values.controller.agentImage.repository | quote }} IMAGE_TAG: {{ coalesce .Values.controller.agentImage.tag .Values.tag .Chart.Version | quote }} {{- with .Values.controller.sandbox }} - SANDBOX_GUEST_IMAGE: {{ include "kagent.sandboxGuestImage" $ | quote }} - SANDBOX_CPU: {{ .cpu | quote }} - SANDBOX_MEMORY: {{ .memory | quote }} - SANDBOX_DEFAULT_TTL: {{ .defaultTTL | quote }} - SANDBOX_MAX_TTL: {{ .maxTTL | quote }} + KAGENT_SANDBOX_GUEST_IMAGE: {{ include "kagent.sandboxGuestImage" $ | quote }} + KAGENT_SANDBOX_CPU: {{ .cpu | quote }} + KAGENT_SANDBOX_MEMORY: {{ .memory | quote }} + KAGENT_SANDBOX_DEFAULT_TTL: {{ .defaultTTL | quote }} + KAGENT_SANDBOX_MAX_TTL: {{ .maxTTL | quote }} {{- end }} {{- with .Values.otel }} {{- if not (or .traces.enabled .metrics.enabled .logs.enabled $.Values.controller.metrics.enabled) }} @@ -57,8 +57,8 @@ data: {{- if .Values.proxy.url }} PROXY_URL: {{ .Values.proxy.url | quote }} {{- end }} - DATABASE_VECTOR_ENABLED: {{ .Values.database.postgres.vectorEnabled | quote }} - SKIP_MIGRATIONS: {{ .Values.database.postgres.skipMigrations | default false | quote }} + KAGENT_DATABASE_VECTOR_ENABLED: {{ .Values.database.postgres.vectorEnabled | quote }} + KAGENT_SKIP_MIGRATIONS: {{ .Values.database.postgres.skipMigrations | default false | quote }} {{- with .Values.database.postgres.pool }} {{- if and (hasKey . "maxConns") (ne .maxConns nil) }} DB_MAX_CONNS: {{ .maxConns | quote }} @@ -73,9 +73,9 @@ data: DB_MAX_CONN_LIFETIME: {{ .maxConnLifetime | quote }} {{- end }} {{- end }} - WATCH_NAMESPACES: {{ include "kagent.watchNamespaces" . | quote }} + KAGENT_WATCH_NAMESPACES: {{ include "kagent.watchNamespaces" . | quote }} MCP_EGRESS_PLAINTEXT: {{ .Values.controller.mcpEgressPlaintext | default false | quote }} {{- if .Values.controller.a2aClientTimeout }} KAGENT_A2A_CLIENT_TIMEOUT: {{ .Values.controller.a2aClientTimeout | quote }} {{- end }} - LOG_LEVEL: {{ .Values.controller.loglevel | quote }} + KAGENT_LOG_LEVEL: {{ .Values.controller.loglevel | quote }} diff --git a/helm/kagent/templates/controller-deployment.yaml b/helm/kagent/templates/controller-deployment.yaml index c0e6d6e4e3..4d11a901a0 100644 --- a/helm/kagent/templates/controller-deployment.yaml +++ b/helm/kagent/templates/controller-deployment.yaml @@ -99,10 +99,10 @@ spec: - name: OTEL_RESOURCE_ATTRIBUTES value: {{ list (include "kagent.otel.resourceAttributes" .) "service.instance.id=$(K8S_POD_UID),k8s.pod.uid=$(K8S_POD_UID),k8s.pod.name=$(K8S_POD_NAME),k8s.namespace.name=$(KAGENT_NAMESPACE),k8s.node.name=$(K8S_NODE_NAME),k8s.container.name=controller" | compact | join "," | quote }} {{- if .Values.database.postgres.urlFile }} - - name: POSTGRES_DATABASE_URL_FILE + - name: KAGENT_POSTGRES_DATABASE_URL_FILE value: {{ .Values.database.postgres.urlFile | quote }} {{- else if .Values.database.postgres.url }} - - name: POSTGRES_DATABASE_URL + - name: KAGENT_POSTGRES_DATABASE_URL value: {{ .Values.database.postgres.url | quote }} {{- else if .Values.database.postgres.bundled.enabled }} - name: POSTGRES_PASSWORD @@ -110,31 +110,31 @@ spec: secretKeyRef: name: {{ include "kagent.passwordSecretName" . }} key: POSTGRES_PASSWORD - - name: POSTGRES_DATABASE_URL + - name: KAGENT_POSTGRES_DATABASE_URL value: {{ printf "postgres://kagent:$(POSTGRES_PASSWORD)@%s.%s.svc:5432/kagent?sslmode=disable" (include "kagent.postgresqlServiceName" .) (include "kagent.namespace" .) | quote }} {{- else }} {{ fail "No database connection configured. Set database.postgres.url, database.postgres.urlFile, or enable database.postgres.bundled." }} {{- end }} {{- if include "kagent.controller.metricsEnabled" . }} - - name: METRICS_BIND_ADDRESS + - name: KAGENT_METRICS_BIND_ADDRESS value: {{ .Values.controller.metrics.bindAddress | quote }} - - name: METRICS_SECURE + - name: KAGENT_METRICS_SECURE value: {{ .Values.controller.metrics.secureServing | quote }} {{- end }} - - name: GRPC_REFLECTION + - name: KAGENT_GRPC_REFLECTION value: {{ .Values.controller.grpc.reflection | quote }} {{- with .Values.controller.env }} {{- toYaml . | nindent 12 }} {{- end }} {{- if and .Values.controller.substrate .Values.controller.substrate.enabled }} - - name: SUBSTRATE_ATE_API_ENDPOINT + - name: KAGENT_SUBSTRATE_ATE_API_ENDPOINT value: {{ include "kagent.substrate.ateApiEndpoint" . | quote }} - - name: SUBSTRATE_ATE_API_CA_FILE + - name: KAGENT_SUBSTRATE_ATE_API_CA_FILE value: /run/substrate-servicedns/trust-bundle.pem - - name: SUBSTRATE_ATE_API_CLIENT_CERT_FILE + - name: KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE value: /run/substrate-podidentity/credential-bundle.pem {{- with include "kagent.substrate.atenetRouterURL" . }} - - name: SUBSTRATE_ATENET_ROUTER_URL + - name: KAGENT_SUBSTRATE_ATENET_ROUTER_URL value: {{ . | quote }} {{- end }} {{- end }} diff --git a/helm/kagent/templates/ui-deployment.yaml b/helm/kagent/templates/ui-deployment.yaml index 288adc7bde..31933b8656 100644 --- a/helm/kagent/templates/ui-deployment.yaml +++ b/helm/kagent/templates/ui-deployment.yaml @@ -79,7 +79,7 @@ spec: value: {{ . | quote }} {{- end }} {{- if .Values.ui.auth }} - - name: SSO_REDIRECT_PATH + - name: KAGENT_SSO_REDIRECT_PATH value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }} {{- end }} {{- if .Values.ui.streamTimeoutSeconds }} diff --git a/helm/kagent/tests/controller-configmap_test.yaml b/helm/kagent/tests/controller-configmap_test.yaml index f1f05967d5..b8507aca8f 100644 --- a/helm/kagent/tests/controller-configmap_test.yaml +++ b/helm/kagent/tests/controller-configmap_test.yaml @@ -7,11 +7,11 @@ tests: - isSubset: path: data content: - SANDBOX_GUEST_IMAGE: "" - SANDBOX_CPU: "1" - SANDBOX_MEMORY: 1Gi - SANDBOX_DEFAULT_TTL: 1h - SANDBOX_MAX_TTL: 24h + KAGENT_SANDBOX_GUEST_IMAGE: "" + KAGENT_SANDBOX_CPU: "1" + KAGENT_SANDBOX_MEMORY: 1Gi + KAGENT_SANDBOX_DEFAULT_TTL: 1h + KAGENT_SANDBOX_MAX_TTL: 24h - it: applies sandbox policy overrides through the ConfigMap set: @@ -23,10 +23,10 @@ tests: - isSubset: path: data content: - SANDBOX_CPU: "2" - SANDBOX_MEMORY: 2Gi - SANDBOX_DEFAULT_TTL: 30m - SANDBOX_MAX_TTL: 2h + KAGENT_SANDBOX_CPU: "2" + KAGENT_SANDBOX_MEMORY: 2Gi + KAGENT_SANDBOX_DEFAULT_TTL: 30m + KAGENT_SANDBOX_MAX_TTL: 2h - it: uses the guest image registry repository and digest set: @@ -35,7 +35,7 @@ tests: controller.sandbox.guestImage.digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa asserts: - equal: - path: data.SANDBOX_GUEST_IMAGE + path: data.KAGENT_SANDBOX_GUEST_IMAGE value: registry.example.com/team/guest@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - it: uses the top-level registry when the guest registry is unset @@ -45,7 +45,7 @@ tests: controller.sandbox.guestImage.digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa asserts: - equal: - path: data.SANDBOX_GUEST_IMAGE + path: data.KAGENT_SANDBOX_GUEST_IMAGE value: registry.example.com/kagent-dev/kagent/sandbox-guest@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - it: respects the global mirror without applying the global tag to the guest @@ -56,7 +56,7 @@ tests: global.imageRegistry: mirror.example.com/ asserts: - equal: - path: data.SANDBOX_GUEST_IMAGE + path: data.KAGENT_SANDBOX_GUEST_IMAGE value: mirror.example.com/kagent-dev/kagent/sandbox-guest@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - it: preserves a custom guest repository under the global mirror @@ -68,5 +68,5 @@ tests: global.imageRegistry: mirror.example.com asserts: - equal: - path: data.SANDBOX_GUEST_IMAGE + path: data.KAGENT_SANDBOX_GUEST_IMAGE value: mirror.example.com/team/guest@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/helm/kagent/tests/controller-deployment_test.yaml b/helm/kagent/tests/controller-deployment_test.yaml index 7f7ba59984..43aaca0bf1 100644 --- a/helm/kagent/tests/controller-deployment_test.yaml +++ b/helm/kagent/tests/controller-deployment_test.yaml @@ -83,7 +83,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: GRPC_REFLECTION + name: KAGENT_GRPC_REFLECTION value: "false" - it: should set global annotations on deployment @@ -126,17 +126,17 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: SUBSTRATE_ATE_API_ENDPOINT + name: KAGENT_SUBSTRATE_ATE_API_ENDPOINT value: "dns:///api.ate-system.svc:443" - contains: path: spec.template.spec.containers[0].env content: - name: SUBSTRATE_ATE_API_CA_FILE + name: KAGENT_SUBSTRATE_ATE_API_CA_FILE value: /run/substrate-servicedns/trust-bundle.pem - contains: path: spec.template.spec.containers[0].env content: - name: SUBSTRATE_ATE_API_CLIENT_CERT_FILE + name: KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE value: /run/substrate-podidentity/credential-bundle.pem - equal: path: spec.template.spec.volumes[0].name @@ -355,7 +355,7 @@ tests: loglevel: "debug" asserts: - equal: - path: data.LOG_LEVEL + path: data.KAGENT_LOG_LEVEL value: "debug" - it: should configure watch namespaces template: controller-configmap.yaml @@ -366,7 +366,7 @@ tests: - namespace-2 asserts: - equal: - path: data.WATCH_NAMESPACES + path: data.KAGENT_WATCH_NAMESPACES value: "namespace-1,namespace-2" - it: should derive watch namespaces from rbac.namespaces when controller.watchNamespaces is unset @@ -378,10 +378,10 @@ tests: - ns2 asserts: - equal: - path: data.WATCH_NAMESPACES + path: data.KAGENT_WATCH_NAMESPACES value: "NAMESPACE,ns2" - - it: controller.watchNamespaces should override rbac.namespaces for WATCH_NAMESPACES + - it: controller.watchNamespaces should override rbac.namespaces for KAGENT_WATCH_NAMESPACES template: controller-configmap.yaml set: controller: @@ -392,7 +392,7 @@ tests: - NAMESPACE asserts: - equal: - path: data.WATCH_NAMESPACES + path: data.KAGENT_WATCH_NAMESPACES value: "explicit-ns" - it: should derive watch namespaces from global.watchNamespaces, install namespace included @@ -403,7 +403,7 @@ tests: - team-a asserts: - equal: - path: data.WATCH_NAMESPACES + path: data.KAGENT_WATCH_NAMESPACES value: "NAMESPACE,team-a" - it: should watch all namespaces when an explicit empty rbac.namespaces overrides the global @@ -416,7 +416,7 @@ tests: - team-a asserts: - equal: - path: data.WATCH_NAMESPACES + path: data.KAGENT_WATCH_NAMESPACES value: "" - it: should set podAnnotations @@ -564,7 +564,7 @@ tests: # Database Configuration Tests # ============================================================================= - - it: should set POSTGRES_PASSWORD from secret and POSTGRES_DATABASE_URL with bundled connection string by default + - it: should set POSTGRES_PASSWORD from secret and KAGENT_POSTGRES_DATABASE_URL with bundled connection string by default template: controller-deployment.yaml asserts: - contains: @@ -578,10 +578,10 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_DATABASE_URL + name: KAGENT_POSTGRES_DATABASE_URL value: "postgres://kagent:$(POSTGRES_PASSWORD)@RELEASE-NAME-postgresql.NAMESPACE.svc:5432/kagent?sslmode=disable" - - it: should set POSTGRES_DATABASE_URL with external url when url is set + - it: should set KAGENT_POSTGRES_DATABASE_URL with external url when url is set template: controller-deployment.yaml set: database: @@ -591,10 +591,10 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_DATABASE_URL + name: KAGENT_POSTGRES_DATABASE_URL value: "postgres://user:pass@external-host:5432/db" - - it: should set POSTGRES_DATABASE_URL_FILE and omit POSTGRES_PASSWORD when urlFile is set + - it: should set KAGENT_POSTGRES_DATABASE_URL_FILE and omit POSTGRES_PASSWORD when urlFile is set template: controller-deployment.yaml set: database: @@ -604,21 +604,21 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_DATABASE_URL_FILE + name: KAGENT_POSTGRES_DATABASE_URL_FILE value: "/var/secrets/db-url" - notContains: path: spec.template.spec.containers[0].env content: name: POSTGRES_PASSWORD - - it: should set DATABASE_VECTOR_ENABLED to false by default + - it: should set KAGENT_DATABASE_VECTOR_ENABLED to false by default template: controller-configmap.yaml asserts: - equal: - path: data.DATABASE_VECTOR_ENABLED + path: data.KAGENT_DATABASE_VECTOR_ENABLED value: "false" - - it: should set DATABASE_VECTOR_ENABLED to true when enabled + - it: should set KAGENT_DATABASE_VECTOR_ENABLED to true when enabled template: controller-configmap.yaml set: database: @@ -626,7 +626,7 @@ tests: vectorEnabled: true asserts: - equal: - path: data.DATABASE_VECTOR_ENABLED + path: data.KAGENT_DATABASE_VECTOR_ENABLED value: "true" - it: should not set DB pool env vars by default @@ -665,11 +665,11 @@ tests: path: data.DB_MAX_CONN_LIFETIME value: "10m" - - it: should not set POSTGRES_DATABASE_URL in configmap + - it: should not set KAGENT_POSTGRES_DATABASE_URL in configmap template: controller-configmap.yaml asserts: - notExists: - path: data.POSTGRES_DATABASE_URL + path: data.KAGENT_POSTGRES_DATABASE_URL - it: should not set POSTGRES_PASSWORD when url is set and bundled is enabled template: controller-deployment.yaml @@ -699,7 +699,7 @@ tests: content: name: POSTGRES_PASSWORD - - it: should set POSTGRES_DATABASE_URL_FILE and omit POSTGRES_PASSWORD when urlFile and bundled are both enabled + - it: should set KAGENT_POSTGRES_DATABASE_URL_FILE and omit POSTGRES_PASSWORD when urlFile and bundled are both enabled template: controller-deployment.yaml set: database: @@ -711,7 +711,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_DATABASE_URL_FILE + name: KAGENT_POSTGRES_DATABASE_URL_FILE value: "/var/secrets/db-url" - notContains: path: spec.template.spec.containers[0].env @@ -720,9 +720,9 @@ tests: - notContains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_DATABASE_URL + name: KAGENT_POSTGRES_DATABASE_URL - - it: should set external POSTGRES_DATABASE_URL and omit POSTGRES_PASSWORD when url and bundled are both enabled + - it: should set external KAGENT_POSTGRES_DATABASE_URL and omit POSTGRES_PASSWORD when url and bundled are both enabled template: controller-deployment.yaml set: database: @@ -734,7 +734,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_DATABASE_URL + name: KAGENT_POSTGRES_DATABASE_URL value: "postgres://user:pass@external-host:5432/db" - notContains: path: spec.template.spec.containers[0].env @@ -830,12 +830,12 @@ tests: - notContains: path: spec.template.spec.containers[0].env content: - name: METRICS_BIND_ADDRESS + name: KAGENT_METRICS_BIND_ADDRESS any: true - notContains: path: spec.template.spec.containers[0].env content: - name: METRICS_SECURE + name: KAGENT_METRICS_SECURE any: true - it: should expose metrics container port when enabled @@ -858,12 +858,12 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: METRICS_BIND_ADDRESS + name: KAGENT_METRICS_BIND_ADDRESS value: ":8443" - contains: path: spec.template.spec.containers[0].env content: - name: METRICS_SECURE + name: KAGENT_METRICS_SECURE value: "true" - it: should reflect insecure serving in env @@ -875,7 +875,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: METRICS_SECURE + name: KAGENT_METRICS_SECURE value: "false" - it: should derive metrics container port from bindAddress @@ -887,7 +887,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: METRICS_BIND_ADDRESS + name: KAGENT_METRICS_BIND_ADDRESS value: ":9443" - contains: path: spec.template.spec.containers[0].ports @@ -931,12 +931,12 @@ tests: - notContains: path: spec.template.spec.containers[0].env content: - name: METRICS_BIND_ADDRESS + name: KAGENT_METRICS_BIND_ADDRESS any: true - notContains: path: spec.template.spec.containers[0].env content: - name: METRICS_SECURE + name: KAGENT_METRICS_SECURE any: true - notContains: path: spec.template.spec.containers[0].ports @@ -953,12 +953,12 @@ tests: - notContains: path: spec.template.spec.containers[0].env content: - name: METRICS_BIND_ADDRESS + name: KAGENT_METRICS_BIND_ADDRESS any: true - notContains: path: spec.template.spec.containers[0].env content: - name: METRICS_SECURE + name: KAGENT_METRICS_SECURE any: true - notContains: path: spec.template.spec.containers[0].ports @@ -971,16 +971,16 @@ tests: set: controller.metrics.enabled: true controller.env: - - name: METRICS_BIND_ADDRESS + - name: KAGENT_METRICS_BIND_ADDRESS value: "0" asserts: - contains: path: spec.template.spec.containers[0].env content: - name: METRICS_BIND_ADDRESS + name: KAGENT_METRICS_BIND_ADDRESS value: ":8443" - contains: path: spec.template.spec.containers[0].env content: - name: METRICS_BIND_ADDRESS + name: KAGENT_METRICS_BIND_ADDRESS value: "0" diff --git a/helm/kagent/tests/ui-deployment_test.yaml b/helm/kagent/tests/ui-deployment_test.yaml index ed3bc5f438..3dc248cac0 100644 --- a/helm/kagent/tests/ui-deployment_test.yaml +++ b/helm/kagent/tests/ui-deployment_test.yaml @@ -313,7 +313,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: SSO_REDIRECT_PATH + name: KAGENT_SSO_REDIRECT_PATH value: "/oauth2/start" - contains: path: spec.template.spec.containers[0].env @@ -416,7 +416,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: SSO_REDIRECT_PATH + name: KAGENT_SSO_REDIRECT_PATH value: "/custom/start" - contains: path: spec.template.spec.containers[0].env diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index b7f5906fcf..9b7934b566 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -196,7 +196,7 @@ rbac: # If that is also unset, the chart creates cluster-scoped ClusterRole and # ClusterRoleBinding resources, and the controller watches all namespaces. # If set, the chart creates a Role and a RoleBinding per listed namespace. - # The controller's WATCH_NAMESPACES derives from the same list, unless + # The controller's KAGENT_WATCH_NAMESPACES derives from the same list, unless # controller.watchNamespaces overrides it. # An explicit empty list forces cluster-scoped RBAC, even under a parent that # sets global.watchNamespaces. This is why the key ships commented out: a @@ -325,7 +325,7 @@ controller: # `-metrics-reader` to your Prometheus ServiceAccount to grant # scrape access. Use `bindAddress` for any port change: the Service # `targetPort` and the pod `containerPort` are derived from it at - # template time, so overriding `METRICS_BIND_ADDRESS` via + # template time, so overriding `KAGENT_METRICS_BIND_ADDRESS` via # `controller.env` shifts only the runtime listener and leaves the # rendered Service pointing at the chart-time port. Setting # `bindAddress: "0"` (or empty) is treated as a disable signal — diff --git a/python/packages/kagent-adk/src/kagent/adk/cli.py b/python/packages/kagent-adk/src/kagent/adk/cli.py index 954abb2e6b..980eb026bd 100644 --- a/python/packages/kagent-adk/src/kagent/adk/cli.py +++ b/python/packages/kagent-adk/src/kagent/adk/cli.py @@ -38,11 +38,11 @@ def _split_csv(value: Optional[str]) -> Optional[list[str]]: return entries or None -sts_well_known_uri = os.getenv("STS_WELL_KNOWN_URI") +sts_well_known_uri = os.getenv("KAGENT_STS_WELL_KNOWN_URI") propagate_token = os.getenv("KAGENT_PROPAGATE_TOKEN", "").lower() == "true" token_resource = _split_csv(os.getenv("KAGENT_STS_RESOURCE")) token_audience = _split_csv(os.getenv("KAGENT_STS_AUDIENCE")) -uvicorn_log_level = os.getenv("LOG_LEVEL", "info").lower() +uvicorn_log_level = os.getenv("KAGENT_LOG_LEVEL", "info").lower() def create_sts_integration() -> Optional[ADKTokenPropagationPlugin]: diff --git a/python/packages/kagent-core/src/kagent/core/_logging.py b/python/packages/kagent-core/src/kagent/core/_logging.py index 870a9ab21f..ddab17cb7d 100644 --- a/python/packages/kagent-core/src/kagent/core/_logging.py +++ b/python/packages/kagent-core/src/kagent/core/_logging.py @@ -7,10 +7,10 @@ def configure_logging() -> None: - """Configure logging based on LOG_LEVEL environment variable.""" + """Configure logging based on KAGENT_LOG_LEVEL environment variable.""" global _logging_configured - log_level = os.getenv("LOG_LEVEL", "INFO").upper() + log_level = os.getenv("KAGENT_LOG_LEVEL", "INFO").upper() formatter = logging.Formatter(LOG_FORMAT) # Only configure if not already configured (avoid duplicate handlers) diff --git a/python/packages/kagent-core/src/kagent/core/a2a/_config.py b/python/packages/kagent-core/src/kagent/core/a2a/_config.py index b3831bfc02..0f736576c3 100644 --- a/python/packages/kagent-core/src/kagent/core/a2a/_config.py +++ b/python/packages/kagent-core/src/kagent/core/a2a/_config.py @@ -5,7 +5,7 @@ logger = logging.getLogger(__name__) -A2A_MAX_CONTENT_LENGTH_ENV_VAR = "A2A_MAX_CONTENT_LENGTH" +A2A_MAX_CONTENT_LENGTH_ENV_VAR = "KAGENT_A2A_MAX_CONTENT_LENGTH" DEFAULT_A2A_MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB (preserves the pre-v1 SDK default) @@ -16,7 +16,7 @@ def get_a2a_max_content_length() -> int | None: middleware. Environment variable: - A2A_MAX_CONTENT_LENGTH: Maximum payload size in bytes. + KAGENT_A2A_MAX_CONTENT_LENGTH: Maximum payload size in bytes. Default: 10485760 (10MB, a2a-sdk default) Example: 52428800 (50MB) Set to "0" or "none" for unlimited. diff --git a/python/packages/kagent-core/tests/test_a2a_config.py b/python/packages/kagent-core/tests/test_a2a_config.py index 58e3355683..c5a5428697 100644 --- a/python/packages/kagent-core/tests/test_a2a_config.py +++ b/python/packages/kagent-core/tests/test_a2a_config.py @@ -10,8 +10,8 @@ def test_get_a2a_max_content_length_with_env_var(): - """Test that setting A2A_MAX_CONTENT_LENGTH env var returns the configured value.""" - with patch.dict(os.environ, {"A2A_MAX_CONTENT_LENGTH": "52428800"}): + """Test that setting KAGENT_A2A_MAX_CONTENT_LENGTH env var returns the configured value.""" + with patch.dict(os.environ, {"KAGENT_A2A_MAX_CONTENT_LENGTH": "52428800"}): result = get_a2a_max_content_length() assert result == 52428800 # 50MB @@ -20,47 +20,47 @@ def test_get_a2a_max_content_length_without_env_var(): """Test that without env var, the former a2a-sdk default is preserved.""" with patch.dict(os.environ, {}, clear=True): # Ensure env var is not set - os.environ.pop("A2A_MAX_CONTENT_LENGTH", None) + os.environ.pop("KAGENT_A2A_MAX_CONTENT_LENGTH", None) result = get_a2a_max_content_length() assert result == DEFAULT_A2A_MAX_CONTENT_LENGTH def test_get_a2a_max_content_length_with_zero(): """Test that setting env var to '0' returns None (unlimited).""" - with patch.dict(os.environ, {"A2A_MAX_CONTENT_LENGTH": "0"}): + with patch.dict(os.environ, {"KAGENT_A2A_MAX_CONTENT_LENGTH": "0"}): result = get_a2a_max_content_length() assert result is None def test_get_a2a_max_content_length_with_none_string(): """Test that setting env var to 'none' returns None (unlimited).""" - with patch.dict(os.environ, {"A2A_MAX_CONTENT_LENGTH": "none"}): + with patch.dict(os.environ, {"KAGENT_A2A_MAX_CONTENT_LENGTH": "none"}): result = get_a2a_max_content_length() assert result is None def test_get_a2a_max_content_length_with_unlimited_string(): """Test that setting env var to 'unlimited' returns None.""" - with patch.dict(os.environ, {"A2A_MAX_CONTENT_LENGTH": "unlimited"}): + with patch.dict(os.environ, {"KAGENT_A2A_MAX_CONTENT_LENGTH": "unlimited"}): result = get_a2a_max_content_length() assert result is None def test_get_a2a_max_content_length_with_invalid_value(caplog): """Test that invalid env var value logs a warning and returns default.""" - with patch.dict(os.environ, {"A2A_MAX_CONTENT_LENGTH": "not_a_number"}): + with patch.dict(os.environ, {"KAGENT_A2A_MAX_CONTENT_LENGTH": "not_a_number"}): result = get_a2a_max_content_length() assert result == DEFAULT_A2A_MAX_CONTENT_LENGTH - assert "Invalid A2A_MAX_CONTENT_LENGTH value" in caplog.text + assert "Invalid KAGENT_A2A_MAX_CONTENT_LENGTH value" in caplog.text assert "not_a_number" in caplog.text def test_get_a2a_max_content_length_with_negative_value(caplog): """Test that negative env var value logs a warning and returns default.""" - with patch.dict(os.environ, {"A2A_MAX_CONTENT_LENGTH": "-1"}): + with patch.dict(os.environ, {"KAGENT_A2A_MAX_CONTENT_LENGTH": "-1"}): result = get_a2a_max_content_length() assert result == DEFAULT_A2A_MAX_CONTENT_LENGTH - assert "Invalid A2A_MAX_CONTENT_LENGTH value" in caplog.text + assert "Invalid KAGENT_A2A_MAX_CONTENT_LENGTH value" in caplog.text assert "-1" in caplog.text assert "must be non-negative" in caplog.text diff --git a/python/packages/kagent-openai/README.md b/python/packages/kagent-openai/README.md index 43e9b942d0..778da72551 100644 --- a/python/packages/kagent-openai/README.md +++ b/python/packages/kagent-openai/README.md @@ -132,7 +132,7 @@ Set `KAGENT_API_URL` and `KAGENT_GATEWAY_URL` to connect to kagent. - `KAGENT_API_URL` - KAgent control-plane API URL - `KAGENT_GATEWAY_URL` - KAgent A2A and MCP gateway URL -- `LOG_LEVEL` - Logging level (default: INFO) +- `KAGENT_LOG_LEVEL` - Logging level (default: INFO) --- diff --git a/python/packages/kagent-openai/src/kagent/openai/_a2a.py b/python/packages/kagent-openai/src/kagent/openai/_a2a.py index 48c62b06a1..fa645806fa 100644 --- a/python/packages/kagent-openai/src/kagent/openai/_a2a.py +++ b/python/packages/kagent-openai/src/kagent/openai/_a2a.py @@ -51,7 +51,7 @@ def thread_dump(request: Request) -> PlainTextResponse: return PlainTextResponse(tmp.read()) -sts_well_known_uri = os.getenv("STS_WELL_KNOWN_URI") +sts_well_known_uri = os.getenv("KAGENT_STS_WELL_KNOWN_URI") def _configure_openai_client() -> None: diff --git a/python/packages/kagent-skills/src/kagent/skills/shell.py b/python/packages/kagent-skills/src/kagent/skills/shell.py index a6369798ca..8257bb6b95 100644 --- a/python/packages/kagent-skills/src/kagent/skills/shell.py +++ b/python/packages/kagent-skills/src/kagent/skills/shell.py @@ -416,7 +416,7 @@ async def execute_command( # If a separate venv for shell commands is specified, use its python and pip # Otherwise the system python/pip will be used for backward compatibility - bash_venv_path = os.environ.get("BASH_VENV_PATH") + bash_venv_path = os.environ.get("KAGENT_BASH_VENV_PATH") if bash_venv_path: bash_venv_bin = os.path.join(bash_venv_path, "bin") # Prepend bash venv to PATH so its python and pip are used diff --git a/scripts/setup-cluster/README.md b/scripts/setup-cluster/README.md index ab320edb98..844de82055 100644 --- a/scripts/setup-cluster/README.md +++ b/scripts/setup-cluster/README.md @@ -61,7 +61,7 @@ If you would rather not build one, the UI runs entirely on in-browser fixtures: ```sh cd ui -ENABLE_MOCK_UI=true yarn dev +KAGENT_ENABLE_MOCK_UI=true yarn dev ``` Every page works and says on the page that the data is not real. `?mock=empty`, diff --git a/ui/.env.example b/ui/.env.example index 8c675f6173..94a997166f 100644 --- a/ui/.env.example +++ b/ui/.env.example @@ -4,9 +4,9 @@ # configure. The dev server inlines them the way the container renders them at startup, # so a change needs no restart but does need a full page reload. # -# Only the keys below reach the browser: `CORE_ENV_KEYS` in `src/env.ts` for the -# application's own, and anything named `EXTENSION_*` for an installed extension's. -# The one `VITE_` key at the bottom is a different mechanism — see its own section. +# Only the keys below reach the browser: `CORE_ENV_VARS` in `src/env.ts` for the +# application's own, and anything named `KAGENT_EXTENSION_*` for an installed extension's. +# The `KAGENT_VITE_` keys below use a different mechanism — see their own section. # --- Settings the application reads --- @@ -18,10 +18,10 @@ # # `?mock=ok|empty|error|slow` picks which scenario the fixtures play, and does nothing # unless this is already on. -# ENABLE_MOCK_UI=true +# KAGENT_ENABLE_MOCK_UI=true # Where the browser calls the API. Relative by default, which the dev server proxies. -# API_BASE_URL=/api +# KAGENT_API_BASE_URL=/api # Where the dev server sends those calls. Read by `vite.config.ts`, so it never reaches # the browser. Defaults to the controller on 8083, which is what @@ -31,28 +31,31 @@ # KAGENT_DEV_CONTROLLER_URL=http://127.0.0.1:8080 # Chat stream inactivity timeout, in milliseconds. -# STREAM_TIMEOUT_MS=1800000 +# KAGENT_STREAM_TIMEOUT_MS=1800000 # Where "Sign in with SSO" sends the browser. -# SSO_REDIRECT_PATH=/oauth2/start +# KAGENT_SSO_REDIRECT_PATH=/oauth2/start + +# Public path prefix when serving below a sub-path. +# KAGENT_UI_BASE_PATH=/ui # --- Build-time switches --- # # Read by the bundler through `import.meta.env`, not from the browser's runtime -# config, which is why these carry Vite's `VITE_` prefix and the settings above do -# not. Changing one needs the dev server restarted, not just a reload. +# config. Vite is configured to expose only the `KAGENT_VITE_` prefix. +# Changing one needs the dev server restarted, not just a reload. # Install the bundled Example App Extension, which is off by default. A switch for # that one example: it names no extension and cannot, because an extension has to be # imported to be in the bundle at all. Which extensions a build installs is the array # in `src/appExtensions/activeExtensions.ts`. -# VITE_EXAMPLE_EXTENSION=true +# KAGENT_VITE_EXAMPLE_EXTENSION=true # --- Settings an installed extension reads --- # -# Anything named `EXTENSION_*` is passed through untouched. The application reads none +# Anything named `KAGENT_EXTENSION_*` is passed through untouched. The application reads none # of them and does not know what they mean; the extension that reads them documents # them. Nothing needs adding here for an extension to gain one. diff --git a/ui/README.md b/ui/README.md index e6409700a1..3104d8e65a 100644 --- a/ui/README.md +++ b/ui/README.md @@ -58,7 +58,7 @@ kubectl -n kagent port-forward svc/kagent-controller 8083:8083 Either way `8083` is where the proxy looks, so neither case needs a `.env` entry. Two settings change that when you want them to: `KAGENT_DEV_CONTROLLER_URL=http://127.0.0.1:8080` sends the proxy through the UI pod's nginx instead, which is the hop a deployed build -takes; and `API_BASE_URL` calls a backend directly, bypassing the proxy. +takes; and `KAGENT_API_BASE_URL` calls a backend directly, bypassing the proxy. Which backend is serving is decided in exactly one place, `src/api/config.ts`. Nothing above the data layer knows or cares. @@ -79,12 +79,12 @@ to have landed. | Setting | Effect | | --- | --- | -| `API_BASE_URL` | where the browser calls the API (default `/api`) | -| `ENABLE_MOCK_UI` | `true` serves the whole API from in-browser fixtures | -| `SSO_REDIRECT_PATH` | where "Sign in with SSO" sends the browser | -| `STREAM_TIMEOUT_MS` | chat stream inactivity timeout | +| `KAGENT_API_BASE_URL` | where the browser calls the API (default `/api`) | +| `KAGENT_ENABLE_MOCK_UI` | `true` serves the whole API from in-browser fixtures | +| `KAGENT_SSO_REDIRECT_PATH` | where "Sign in with SSO" sends the browser | +| `KAGENT_STREAM_TIMEOUT_MS` | chat stream inactivity timeout | -`ENABLE_MOCK_UI` is a development setting. The release image deliberately ships no +`KAGENT_ENABLE_MOCK_UI` is a development setting. The release image deliberately ships no mock backend, so a built bundle logs a warning and uses the real API rather than entering a mode it cannot serve. diff --git a/ui/docs/app-extensions.md b/ui/docs/app-extensions.md index 65719a7498..5898ca2ebb 100644 --- a/ui/docs/app-extensions.md +++ b/ui/docs/app-extensions.md @@ -17,8 +17,8 @@ That is the whole integration surface. Installing a second extension is one more entry; nothing else in the application changes. Nothing is installed by default, including the bundled example. To see the example -running without editing the file, set `VITE_EXAMPLE_EXTENSION=true` — in `ui/.env`, -or on the command line as `VITE_EXAMPLE_EXTENSION=true yarn dev`. +running without editing the file, set `KAGENT_VITE_EXAMPLE_EXTENSION=true` — in `ui/.env`, +or on the command line as `KAGENT_VITE_EXAMPLE_EXTENSION=true yarn dev`. --- @@ -83,10 +83,10 @@ installed, so a default build renders only what this project itself provides. Switch it on with: ```bash -VITE_EXAMPLE_EXTENSION=true yarn dev +KAGENT_VITE_EXAMPLE_EXTENSION=true yarn dev ``` -or by putting `VITE_EXAMPLE_EXTENSION=true` in `ui/.env`, which `.env.example` +or by putting `KAGENT_VITE_EXAMPLE_EXTENSION=true` in `ui/.env`, which `.env.example` documents. It is read by the bundler rather than at runtime, so changing it needs the dev server restarted. @@ -567,7 +567,7 @@ rule applies one level up: the earlier extension's providers wrap the later one' ## Settings an extension reads An extension that needs configuring — an API root, a feature flag, an account id — -names a variable `EXTENSION_*`. Three steps, and none of them touch this +names a variable `KAGENT_EXTENSION_*`. Three steps, and none of them touch this application. **Read it** with `readEnv`, which takes any key and a fallback: @@ -575,7 +575,7 @@ application. ```ts import { readEnv } from "@/appExtensions"; -const apiUrl = readEnv("EXTENSION_EXAMPLE_API_URL", "https://api.example.test"); +const apiUrl = readEnv("KAGENT_EXTENSION_EXAMPLE_API_URL", "https://api.example.test"); ``` **Set it in a deployment** through `ui.env` in the `kagent` chart, which is passed @@ -585,7 +585,7 @@ through to the UI deployment verbatim: # values.yaml ui: env: - - name: EXTENSION_EXAMPLE_API_URL + - name: KAGENT_EXTENSION_EXAMPLE_API_URL value: https://api.example.test ``` @@ -594,11 +594,11 @@ section at the bottom for exactly this, so a branch that installs an extension appends rather than editing the application's own settings above it: ```sh -EXTENSION_EXAMPLE_API_URL=https://api.example.test +KAGENT_EXTENSION_EXAMPLE_API_URL=https://api.example.test ``` That is the whole mechanism, and nothing needs registering. The container's -`scripts/init.sh` copies every `EXTENSION_*` variable out of the pod's environment +`scripts/init.sh` copies every `KAGENT_EXTENSION_*` variable out of the pod's environment **by prefix**, and the dev server does the same from `.env` and your shell. Neither enumerates the keys, so this application never learns what your settings mean and needs no change when your extension grows one. @@ -618,7 +618,7 @@ two consequences of it are worth knowing: **Do not add a `VITE_` prefix.** That prefix is Vite's `envPrefix` filter, which decides what `import.meta.env` exposes to bundled code; it applies to build-time variables and to nothing else. A prefixed name would no longer match the -`EXTENSION_` prefix the init script and the dev server select on, so the value +`KAGENT_EXTENSION_` prefix the init script and the dev server select on, so the value would never arrive and `readEnv` would quietly return the fallback. --- diff --git a/ui/package.json b/ui/package.json index 0086f8b7a6..08189f47fc 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,7 +17,7 @@ "test:watch": "vitest", "test:pw": "playwright test", "test:e2e": "yarn test:pw", - "test:pw:live": "UI_LOOP_LIVE=true playwright test" + "test:pw:live": "KAGENT_UI_LOOP_LIVE=true playwright test" }, "dependencies": { "@bufbuild/protobuf": "2.15.0", diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index ece9ddc181..d13e82b0c9 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -6,7 +6,7 @@ import { defineConfig, devices } from "@playwright/test"; * main process and again in every worker, so anything varying per process gives * each worker a different base URL from the one the servers were started on. */ -const PORT = Number(process.env.UI_LOOP_PORT ?? 8001); +const PORT = Number(process.env.KAGENT_UI_LOOP_PORT ?? 8001); const BASE_URL = `http://localhost:${PORT}`; /** @@ -21,7 +21,7 @@ const BASE_URL = `http://localhost:${PORT}`; // Deliberately not PORT + 1: Vite falls forward to the next free port when the // one it is told to use is busy, so adjacent ports let a slow-to-die server from // a previous run push one app onto the other's port. -const EXTENSION_PORT = Number(process.env.UI_LOOP_EXTENSION_PORT ?? PORT + 50); +const EXTENSION_PORT = Number(process.env.KAGENT_UI_LOOP_EXTENSION_PORT ?? PORT + 50); const EXTENSION_BASE_URL = `http://localhost:${EXTENSION_PORT}`; /** Specs that need an extension installed opt in by filename. */ @@ -30,10 +30,10 @@ const EXTENSION_SPECS = /\.withExtension\.spec\.ts$/; /** * The suite is the acceptance bar, so what it runs against cannot depend on the * shell it was started from: both servers are pinned to the in-browser mock - * backend. An inherited VITE_API_MODE=live would otherwise point a whole run at + * backend. An inherited KAGENT_VITE_API_MODE=live would otherwise point a whole run at * a real cluster. */ -const MOCK_BACKEND = { VITE_API_MODE: "mock" }; +const MOCK_BACKEND = { KAGENT_VITE_API_MODE: "mock" }; /** * What each of the three servers is pinned to. @@ -42,13 +42,13 @@ const MOCK_BACKEND = { VITE_API_MODE: "mock" }; * stated in one place — and so a branch that installs an extension changes a * value instead of restructuring the `projects`/`webServer` blocks. * - * `VITE_EXAMPLE_EXTENSION` is pinned on the bare server for the same reason - * `VITE_API_MODE` is: an inherited value must not be able to decide what a run + * `KAGENT_VITE_EXAMPLE_EXTENSION` is pinned on the bare server for the same reason + * `KAGENT_VITE_API_MODE` is: an inherited value must not be able to decide what a run * measures. Left unpinned, the bare project measures whatever the shell happened * to export. */ -const BARE_APP = { ...MOCK_BACKEND, VITE_EXAMPLE_EXTENSION: "false" }; -const EXAMPLE_APP = { ...MOCK_BACKEND, VITE_EXAMPLE_EXTENSION: "true" }; +const BARE_APP = { ...MOCK_BACKEND, KAGENT_VITE_EXAMPLE_EXTENSION: "false" }; +const EXAMPLE_APP = { ...MOCK_BACKEND, KAGENT_VITE_EXAMPLE_EXTENSION: "true" }; /** * The third mode: one app, wired to a real backend. @@ -67,7 +67,7 @@ const EXAMPLE_APP = { ...MOCK_BACKEND, VITE_EXAMPLE_EXTENSION: "true" }; * So `LIVE` swaps the whole `projects`/`webServer` pair rather than appending to * it. `yarn test:pw` and `yarn test:pw:live` are two disjoint runs. */ -const LIVE = process.env.UI_LOOP_LIVE === "true"; +const LIVE = process.env.KAGENT_UI_LOOP_LIVE === "true"; /** * Its own port, far from the mock servers' 8001/8051, for the same reason those @@ -75,7 +75,7 @@ const LIVE = process.env.UI_LOOP_LIVE === "true"; * told to use is busy, so a live run must not be able to land on a port a mock * server is about to want, or vice versa. */ -const LIVE_PORT = Number(process.env.UI_LOOP_LIVE_PORT ?? 8301); +const LIVE_PORT = Number(process.env.KAGENT_UI_LOOP_LIVE_PORT ?? 8301); const LIVE_BASE_URL = `http://localhost:${LIVE_PORT}`; /** Read by `playwright/globalSetup.ts` to decide what to verify about a server. */ @@ -86,11 +86,11 @@ export const LIVE_PROJECT = "chromium-live"; * build reaches it through nginx — so the app uses the same relative URLs either * way and this mode tests the addressing a real deployment uses. * - * `VITE_API_MODE` is pinned as well as the runtime flag: the build-time pin is + * `KAGENT_VITE_API_MODE` is pinned as well as the runtime flag: the build-time pin is * the one thing an inherited `.env` cannot override, and a live suite that * silently answered from fixtures would be worse than a red one. */ -const LIVE_APP = { VITE_API_MODE: "live", ENABLE_MOCK_UI: "false" }; +const LIVE_APP = { KAGENT_VITE_API_MODE: "live", KAGENT_ENABLE_MOCK_UI: "false" }; /** * How the live server is started. @@ -197,7 +197,7 @@ export default defineConfig({ // extension specs then fail looking for contributions that were never installed. // That was an intermittent failure whose frequency depended only on whether // something happened to linger. Refusing to adopt makes an occupied port a - // loud startup error instead; set UI_LOOP_PORT / UI_LOOP_EXTENSION_PORT to run + // loud startup error instead; set KAGENT_UI_LOOP_PORT / KAGENT_UI_LOOP_EXTENSION_PORT to run // alongside a dev server you want to keep. webServer: LIVE ? [ diff --git a/ui/playwright/README.md b/ui/playwright/README.md index 42cf3794d8..ba5de4165b 100644 --- a/ui/playwright/README.md +++ b/ui/playwright/README.md @@ -8,7 +8,7 @@ rewrite is done when the same general set of journeys still passes. ```bash cd ui yarn test:pw # or: yarn test:e2e -UI_LOOP_PORT=8012 yarn test:pw # when something else owns the default port +KAGENT_UI_LOOP_PORT=8012 yarn test:pw # when something else owns the default port ``` Nothing else is needed — no cluster, no port-forward, no provider key. @@ -38,7 +38,7 @@ here. ## What it runs against The suite runs against the in-browser mock backend (`src/mocks/`), pinned by the -`webServer` block in `playwright.config.ts` so an inherited `VITE_API_MODE=live` +`webServer` block in `playwright.config.ts` so an inherited `KAGENT_VITE_API_MODE=live` cannot silently point a run at a real cluster. That buys three things the old kind-cluster setup could not offer: the data is fixed, so a spec can assert exact rows; the run takes seconds; and failure is a first-class state rather than @@ -90,8 +90,8 @@ Which extensions a build installs is decided at build time, so "installed" and | Project | Server | Specs | |---|---|---| -| `chromium` | bare — no extension, on `UI_LOOP_PORT` | everything not matching `*.withExtension.spec.ts` | -| `chromium-with-extension` | `VITE_EXAMPLE_EXTENSION=true`, on `UI_LOOP_PORT + 50` | `*.withExtension.spec.ts` | +| `chromium` | bare — no extension, on `KAGENT_UI_LOOP_PORT` | everything not matching `*.withExtension.spec.ts` | +| `chromium-with-extension` | `KAGENT_VITE_EXAMPLE_EXTENSION=true`, on `KAGENT_UI_LOOP_PORT + 50` | `*.withExtension.spec.ts` | A spec opts into the extension-installed app by being named `*.withExtension.spec.ts`. @@ -211,7 +211,7 @@ conventions below: the shared fixture import, and antd's class names. ```bash cd ui yarn test:pw:live -UI_LOOP_LIVE_PORT=8312 yarn test:pw:live # to run beside something on 8301 +KAGENT_UI_LOOP_LIVE_PORT=8312 yarn test:pw:live # to run beside something on 8301 ``` Unlike `yarn test:pw`, this one **does** need a cluster, with the controller @@ -222,7 +222,7 @@ A live run reaches the controller through Vite's proxy, exactly as a deployed build reaches it through nginx, so the app uses the same relative URLs either way and this mode tests the addressing a real deployment uses. -**Why a separate mode rather than a third project.** `UI_LOOP_LIVE=true` swaps the +**Why a separate mode rather than a third project.** `KAGENT_UI_LOOP_LIVE=true` swaps the whole `projects`/`webServer` pair in `playwright.config.ts` instead of appending to it, because the two modes' requirements are mutually exclusive. A live project in the default list would make `yarn test:pw` — which is meant to need nothing but a @@ -232,7 +232,7 @@ would cost every live run the time to boot Vite twice for nothing. The two runs are disjoint. The live project also gets its own port, 8301, far from the mock servers' 8001/8051 for the same reason those two are 50 apart. -**A green live run has to have been live.** `VITE_API_MODE` is pinned at build +**A green live run has to have been live.** `KAGENT_VITE_API_MODE` is pinned at build time as well as at runtime, because a build-time pin is the one thing an inherited `.env` cannot override — and a live suite that quietly answered from fixtures would be worse than a red one, since a green one gets taken as evidence the diff --git a/ui/playwright/globalSetup.ts b/ui/playwright/globalSetup.ts index 0d5b103e20..da14bc041e 100644 --- a/ui/playwright/globalSetup.ts +++ b/ui/playwright/globalSetup.ts @@ -53,7 +53,7 @@ export default async function globalSetup(config: FullConfig): Promise { `${baseUrl} was expected to serve the app with the example extension ` + `installed (project "${project.name}"), but no extension points are ` + `mounted. The server on that port came up without ` + - `VITE_EXAMPLE_EXTENSION=true.`, + `KAGENT_VITE_EXAMPLE_EXTENSION=true.`, ); } if (!wantsExtension && slots > 0) { diff --git a/ui/scripts/init.sh b/ui/scripts/init.sh index e117ee9324..0d4c856c41 100644 --- a/ui/scripts/init.sh +++ b/ui/scripts/init.sh @@ -26,9 +26,9 @@ mkdir -p /tmp/nginx/client_temp \ CONFIG_PATH=/tmp/kagent/env-config.js API_BASE_URL="${KAGENT_API_BASE_URL:-/api}" -SSO_REDIRECT_PATH="${SSO_REDIRECT_PATH:-/oauth2/start}" +SSO_REDIRECT_PATH="${KAGENT_SSO_REDIRECT_PATH:-/oauth2/start}" STREAM_TIMEOUT_MS="${KAGENT_STREAM_TIMEOUT_MS:-1800000}" -ENABLE_MOCK_UI="${ENABLE_MOCK_UI:-false}" +ENABLE_MOCK_UI="${KAGENT_ENABLE_MOCK_UI:-false}" # Public path prefix when a reverse proxy serves the UI under a sub-path, e.g. /ui. BASE_PATH="${KAGENT_UI_BASE_PATH:-}" BASE_PATH="${BASE_PATH%/}" @@ -57,7 +57,7 @@ json_escape() { } # Anything an installed extension reads, passed through verbatim and by name rather -# than by list: whatever the chart sets as `EXTENSION_*` arrives, so this image needs +# than by list: whatever the chart sets as `KAGENT_EXTENSION_*` arrives, so this image needs # no change when an extension grows a setting, and this repository does not enumerate # another product's configuration. The application ignores keys it has no use for. # @@ -66,7 +66,7 @@ extension_json="" while IFS='=' read -r name value; do extension_json+=" \"$(json_escape "$name")\": \"$(json_escape "$value")\", " -done < <(env | grep '^EXTENSION_' | sort) +done < <(env | grep '^KAGENT_EXTENSION_' | sort) cat > "$CONFIG_PATH" < { describe("readEnv", () => { it("reads a key the application knows nothing about", () => { - window.environmentVariables = { EXTENSION_ANYTHING: "a-value" }; - expect(readEnv("EXTENSION_ANYTHING", "a-fallback")).toBe("a-value"); + window.environmentVariables = { KAGENT_EXTENSION_ANYTHING: "a-value" }; + expect(readEnv("KAGENT_EXTENSION_ANYTHING", "a-fallback")).toBe("a-value"); }); it("uses the caller's fallback when it is absent", () => { - expect(readEnv("EXTENSION_ANYTHING", "a-fallback")).toBe("a-fallback"); + expect(readEnv("KAGENT_EXTENSION_ANYTHING", "a-fallback")).toBe("a-fallback"); }); }); diff --git a/ui/src/env.ts b/ui/src/env.ts index ca24e011ec..217bb228a1 100644 --- a/ui/src/env.ts +++ b/ui/src/env.ts @@ -36,17 +36,17 @@ export interface EnvironmentVariables { * publish every credential on the developer's machine into the HTML. * * An extension's own keys are not listed here, and are not listed anywhere: anything - * named `EXTENSION_*` is passed through by the dev server and by `scripts/init.sh`, + * named `KAGENT_EXTENSION_*` is passed through by the dev server and by `scripts/init.sh`, * and read back with `readEnv`, which takes any key. This application does not know * what an extension's settings are and does not need to. */ -export const CORE_ENV_KEYS = [ - "API_BASE_URL", - "SSO_REDIRECT_PATH", - "STREAM_TIMEOUT_MS", - "ENABLE_MOCK_UI", - "BASE_PATH", -] as const satisfies readonly (keyof EnvironmentVariables)[]; +export const CORE_ENV_VARS = { + API_BASE_URL: "KAGENT_API_BASE_URL", + SSO_REDIRECT_PATH: "KAGENT_SSO_REDIRECT_PATH", + STREAM_TIMEOUT_MS: "KAGENT_STREAM_TIMEOUT_MS", + ENABLE_MOCK_UI: "KAGENT_ENABLE_MOCK_UI", + BASE_PATH: "KAGENT_UI_BASE_PATH", +} as const satisfies Record; /** * What the app uses when a key is absent — the normal case for `yarn dev` and diff --git a/ui/src/mocks/startMockBackend.ts b/ui/src/mocks/startMockBackend.ts index e80a1fb632..f242a3b745 100644 --- a/ui/src/mocks/startMockBackend.ts +++ b/ui/src/mocks/startMockBackend.ts @@ -9,7 +9,7 @@ * * Order matters: the transport is installed before anything can call an * operation, which is why this is awaited in `main.tsx` before the app renders. - * Only `main.tsx` calls it, and only when `VITE_API_MODE` is not `"live"` — a page + * Only `main.tsx` calls it, and only when `KAGENT_VITE_API_MODE` is not `"live"` — a page * that quietly served fixtures because the backend was unreachable would look * healthy while showing data that was never real. */ diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts index b85fa3d6f1..dd434a0eb4 100644 --- a/ui/src/vite-env.d.ts +++ b/ui/src/vite-env.d.ts @@ -1,10 +1,8 @@ /// interface ImportMetaEnv { - /** "mock" (default) serves from the in-browser mock backend; "live" talks to a real API. */ - readonly VITE_API_MODE?: "mock" | "live"; - /** Base URL of the real API, used when VITE_API_MODE is "live". */ - readonly VITE_API_BASE_URL?: string; + /** Optional build-time override; normal development uses runtime configuration. */ + readonly KAGENT_VITE_API_MODE?: "mock" | "live"; /** * `"true"` installs the bundled Example App Extension. * @@ -12,7 +10,7 @@ interface ImportMetaEnv { * extension has to be imported to be in the bundle at all, so which ones are * installed is the array in `appExtensions/activeExtensions.ts`. */ - readonly VITE_EXAMPLE_EXTENSION?: "true" | "false"; + readonly KAGENT_VITE_EXAMPLE_EXTENSION?: "true" | "false"; } interface ImportMeta { diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 8582030b76..4cd915d0f7 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config"; import { loadEnv, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; import path from "node:path"; -import { CORE_ENV_KEYS, ENV_DEFAULTS } from "./src/env.ts"; +import { CORE_ENV_VARS, ENV_DEFAULTS } from "./src/env.ts"; /** * Where the dev server forwards `/api` and `/a2a`. @@ -42,17 +42,17 @@ function devProxy(mode: string) { * * A prefix instead. `readEnv` already takes any key and `window.environmentVariables` * is already an open record, so nothing in the application changes when an extension - * adds a setting: it names it `EXTENSION_SOMETHING` and it arrives. + * adds a setting: it names it `KAGENT_EXTENSION_SOMETHING` and it arrives. * - * Still bounded, which is the point of `CORE_ENV_KEYS` being a list at all: the dev + * Still bounded, which is the point of `CORE_ENV_VARS` being an explicit map: the dev * server inlines these into the document, so a wholesale copy of the environment would * publish every credential on the machine into the HTML. A variable has to be * deliberately named for an extension to be copied — but only that deliberately, so a - * stray `EXTENSION_` variable in a shell will be inlined. It is a development server + * stray `KAGENT_EXTENSION_` variable in a shell will be inlined. It is a development server * rendering a page for the developer who started it; the container path below is the * one that faces a cluster, and there the environment is the chart's own. */ -const EXTENSION_ENV_PREFIX = "EXTENSION_"; +const EXTENSION_ENV_PREFIX = "KAGENT_EXTENSION_"; /** * Serves the deployment configuration the way the container does. @@ -84,7 +84,11 @@ function devEnvConfig(mode: string): Plugin { ...Object.keys(process.env), ].filter((key) => key.startsWith(EXTENSION_ENV_PREFIX)); - for (const key of [...CORE_ENV_KEYS, ...extensionKeys]) { + for (const [key, variable] of Object.entries(CORE_ENV_VARS)) { + const value = process.env[variable] ?? fromFiles[variable]; + if (value !== undefined && value !== "") merged[key] = value; + } + for (const key of extensionKeys) { const value = process.env[key] ?? fromFiles[key]; if (value !== undefined && value !== "") merged[key] = value; } @@ -114,6 +118,8 @@ function devEnvConfig(mode: string): Plugin { } export default defineConfig(({ mode }) => ({ + // Only explicit build-time settings are exposed through import.meta.env. + envPrefix: "KAGENT_VITE_", // Relative asset URLs, resolved against the `` in index.html. base: "./", // JSX is transformed by oxc, which routes the factory at @emotion/react — @@ -142,7 +148,7 @@ export default defineConfig(({ mode }) => ({ alias: { "@": path.resolve(import.meta.dirname, "./src") }, }, server: { - port: Number(process.env.UI_LOOP_PORT ?? 8001), + port: Number(process.env.KAGENT_UI_LOOP_PORT ?? 8001), host: "0.0.0.0", // Stands in for what nginx does in a deployed cluster, so a dev server // talking to a real controller uses the same relative URLs as production. @@ -151,7 +157,7 @@ export default defineConfig(({ mode }) => ({ proxy: devProxy(mode), }, preview: { - port: Number(process.env.UI_LOOP_PORT ?? 8001), + port: Number(process.env.KAGENT_UI_LOOP_PORT ?? 8001), host: "0.0.0.0", }, test: { @@ -166,6 +172,6 @@ export default defineConfig(({ mode }) => ({ // mode is stated rather than inherited. It used to come free from the dev // default; that default is gone, because a page that quietly serves fixtures // when the backend is down is worse than one that says so. - env: { VITE_API_MODE: "mock" }, + env: { KAGENT_VITE_API_MODE: "mock" }, }, })); From 0c404930be36b476f473f74365a3923324402076 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:10:43 +0000 Subject: [PATCH 05/14] refactor: unify ADK port and registered logging settings Signed-off-by: Eitan Yarmush --- DEVELOPMENT.md | 5 +++ .../configuration-and-compilation.md | 4 ++- docs/env.md | 3 +- go/adk/cmd/main.go | 9 ++---- go/adk/pkg/app/app.go | 2 +- go/core/internal/translator/compiler_test.go | 4 +++ .../internal/translator/kagent/compiler.go | 3 +- go/core/pkg/env/controller.go | 4 +-- go/core/pkg/env/runtime.go | 15 ++++----- go/pkg/logging/logging.go | 4 +-- go/pkg/logging/logging_test.go | 32 +++++++++++++++++++ python/README.md | 5 +-- .../kagent-adk/src/kagent/adk/_a2a.py | 2 +- .../tests/unittests/test_a2a_grpc.py | 23 ++++++++++--- 14 files changed, 82 insertions(+), 33 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 79119bab43..358b28574f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -26,6 +26,11 @@ The pre-release rename replaces the old unprefixed names: for example, `KAGENT_HTTP_BIND_ADDRESS`, and the Go ADK's `PORT` becomes `KAGENT_PORT`. The old names are no longer read by kagent. +Both ADKs use `KAGENT_PORT` for their A2A listener; Python's former +`KAGENT_A2A_GRPC_ADDRESS` is removed. The controller sets `80`. Standalone +defaults remain `8080` for Go's shared HTTP/gRPC listener and `80` for Python's +gRPC listener; Python's HTTP port is configured separately with `--port`. + UI containers and Vite development use the same `KAGENT_*` inputs, documented in `ui/.env.example`. UI build-time switches use `KAGENT_VITE_*`; extension settings use `KAGENT_EXTENSION_*`. Other `KAGENT_*` settings are not exposed to the browser. diff --git a/docs/architecture/configuration-and-compilation.md b/docs/architecture/configuration-and-compilation.md index e21a9948e7..298e7f6620 100644 --- a/docs/architecture/configuration-and-compilation.md +++ b/docs/architecture/configuration-and-compilation.md @@ -194,7 +194,9 @@ Dedicated agent bindings are not compiled yet. The Python ADK image's default entrypoint runs a named Python agent module. To consume the kagent compiler's configuration, set `spec.workload.command` to `["/.kagent/.venv/bin/kagent-adk", "static", "--host", "0.0.0.0", "--port", "8080"]`. -The compiler supplies the private A2A gRPC address separately. +The compiler sets `KAGENT_PORT=80` for private A2A traffic in both ADKs. Go uses +that port for its shared HTTP/gRPC listener; Python uses it for gRPC and keeps +the HTTP port separate. `spec.kagent.compaction` on the Harness is runtime policy, like `spec.kagent.memory`: it belongs to the runner that drives the root agent and is not part of the diff --git a/docs/env.md b/docs/env.md index 7f5ae86f72..1e939c482c 100644 --- a/docs/env.md +++ b/docs/env.md @@ -33,7 +33,6 @@ This reference covers user-configurable settings for the controller, CLI, standa | `GOOGLE_CLOUD_PROJECT` | String | `(none)` | Google Cloud project ID for Vertex AI. | | `GOOGLE_CLOUD_REGION` | String | `(none)` | Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset. | | `GOOGLE_GENAI_USE_VERTEXAI` | String | `(none)` | When set to 'true', use Vertex AI for Gemini models. | -| `KAGENT_A2A_GRPC_ADDRESS` | String | `[::]:80` | Python ADK gRPC listen address. | | `KAGENT_A2A_MAX_CONTENT_LENGTH` | String | `10485760` | Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default. | | `KAGENT_API_URL` | String | `(none)` | Base URL for kagent control-plane API calls. Required by Python runtimes and supplied by the controller in managed runtimes; also used as the E2E test URL when KAGENT_E2E_API_URL is unset. | | `KAGENT_BASH_VENV_PATH` | String | `(none)` | Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set. | @@ -44,7 +43,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_NAME` | String | `(none)` | Agent name for standalone runtimes. Required by Python runtimes; supplied by the controller in managed runtimes. | | `KAGENT_NAMESPACE` | String | `kagent` | Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it. | | `KAGENT_OPENAI_AGENTS_NATIVE_TRACING` | Boolean | `false` | Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime. | -| `KAGENT_PORT` | String | `8080` | Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port. | +| `KAGENT_PORT` | String | `(none)` | ADK A2A listen port: the Go HTTP/gRPC listener defaults to 8080; the Python gRPC listener defaults to 80. Explicit Go --port/AppConfig.Port or Python a2a_grpc_address takes precedence. The controller sets 80 for managed kagent runtimes. Python's HTTP --port is separate. | | `KAGENT_PROPAGATE_TOKEN` | String | `(none)` | Set to true to propagate authentication tokens to downstream services. Unset or any other value disables propagation. | | `KAGENT_SKILLS_FOLDER` | String | `/skills` | Skills directory for standalone Python skills tools. The Python ADK adds skills tools when set; managed Go ADK runtimes use their compiled skill configuration. | | `KAGENT_STS_AUDIENCE` | String | `(none)` | Comma-separated RFC 8693 audiences sent on STS token-exchange requests. Alternate to KAGENT_STS_RESOURCE for servers that key on audience. | diff --git a/go/adk/cmd/main.go b/go/adk/cmd/main.go index f0043eb1ee..de1ab1dc7e 100644 --- a/go/adk/cmd/main.go +++ b/go/adk/cmd/main.go @@ -32,7 +32,7 @@ const ( ) func main() { - logLevel := flag.String("log-level", cmp.Or(os.Getenv("KAGENT_LOG_LEVEL"), "info"), "Set the logging level (debug, info, warn, error)") + logLevel := flag.String("log-level", cmp.Or(env.LogLevel.Get(), env.LogLevel.DefaultValue()), "Set the logging level (debug, info, warn, error)") host := flag.String("host", "", "Set the host address to bind to (default: empty, binds to all interfaces)") portFlag := flag.String("port", "", "Set the port to listen on (overrides KAGENT_PORT environment variable)") filepathFlag := flag.String("filepath", "", "Set the config directory path (overrides KAGENT_CONFIG_DIR environment variable)") @@ -46,11 +46,6 @@ func main() { slog.SetDefault(logger) logger.Info("logger initialized", "level", *logLevel) - port := *portFlag - if port == "" { - port = os.Getenv("KAGENT_PORT") - } - configDir := *filepathFlag if configDir == "" { configDir = os.Getenv("KAGENT_CONFIG_DIR") @@ -59,7 +54,7 @@ func main() { configDir = "/config" } - if err := run(logger, *host, port, configDir); err != nil { + if err := run(logger, *host, *portFlag, configDir); err != nil { logger.Error("agent stopped", "error", err) os.Exit(1) } diff --git a/go/adk/pkg/app/app.go b/go/adk/pkg/app/app.go index b0e30896ff..f658531b05 100644 --- a/go/adk/pkg/app/app.go +++ b/go/adk/pkg/app/app.go @@ -181,7 +181,7 @@ func (a *KAgentApp) Logger() *slog.Logger { // applyDefaults fills in zero-value fields with sensible defaults. func applyDefaults(cfg AppConfig) AppConfig { if cfg.Port == "" { - cfg.Port = os.Getenv("KAGENT_PORT") + cfg.Port = env.KagentPort.Get() } if cfg.Port == "" { cfg.Port = defaultPort diff --git a/go/core/internal/translator/compiler_test.go b/go/core/internal/translator/compiler_test.go index 8987b467e9..5405f0c1ea 100644 --- a/go/core/internal/translator/compiler_test.go +++ b/go/core/internal/translator/compiler_test.go @@ -70,6 +70,10 @@ func TestCompileAgentPreservesWorkloadOverrides(t *testing.T) { require.NoError(t, err) require.Equal(t, tt.command, result.Command) require.Equal(t, tt.args, result.Args) + require.Contains(t, result.Environment, corev1.EnvVar{Name: "KAGENT_PORT", Value: "80"}) + for _, variable := range result.Environment { + require.NotEqual(t, "KAGENT_A2A_GRPC_ADDRESS", variable.Name) + } revisionID, err := result.Digest() require.NoError(t, err) diff --git a/go/core/internal/translator/kagent/compiler.go b/go/core/internal/translator/kagent/compiler.go index 81e08bf091..d10efcf4c2 100644 --- a/go/core/internal/translator/kagent/compiler.go +++ b/go/core/internal/translator/kagent/compiler.go @@ -56,8 +56,7 @@ func (c *Compiler) Compile(ctx context.Context, input *v2translator.HarnessInput corev1.EnvVar{Name: env.KagentNamespace.Name(), Value: template.Namespace}, corev1.EnvVar{Name: env.KagentAPIURL.Name(), Value: fmt.Sprintf("http://%s.%s:8083", utils.GetControllerName(), utils.GetResourceNamespace())}, corev1.EnvVar{Name: env.KagentGatewayURL.Name(), Value: fmt.Sprintf("http://%s.%s:8083", utils.GetControllerName(), utils.GetResourceNamespace())}, - corev1.EnvVar{Name: "KAGENT_PORT", Value: "80"}, - corev1.EnvVar{Name: "KAGENT_A2A_GRPC_ADDRESS", Value: "[::]:80"}, + corev1.EnvVar{Name: env.KagentPort.Name(), Value: "80"}, ) environment = append(environment, telemetryConfig.TelemetryEnvironment(tracing.RuntimeTelemetry{ AgentName: input.AgentName, AgentNamespace: template.Namespace, diff --git a/go/core/pkg/env/controller.go b/go/core/pkg/env/controller.go index 44b56ff505..d75d870b58 100644 --- a/go/core/pkg/env/controller.go +++ b/go/core/pkg/env/controller.go @@ -17,6 +17,6 @@ var ( // Shared settings read by logging and Kubernetes libraries. var ( - _ = RegisterStringVar("KAGENT_LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) - _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController, ComponentCLI, ComponentTesting) + LogLevel = RegisterStringVar("KAGENT_LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) + _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController, ComponentCLI, ComponentTesting) ) diff --git a/go/core/pkg/env/runtime.go b/go/core/pkg/env/runtime.go index ba226fa2c8..87c201ce94 100644 --- a/go/core/pkg/env/runtime.go +++ b/go/core/pkg/env/runtime.go @@ -1,14 +1,13 @@ package env -// Documentation-only registrations for user-configurable runtime settings. +// Registrations for user-configurable runtime settings. // Keep their defaults aligned with the Go/Python readers; importing this package // is not required for a runtime or an upstream SDK to read its environment. var ( - _ = RegisterStringVar("KAGENT_PORT", "8080", "Go ADK listen port; --port takes precedence. The controller injects 80 for managed kagent runtimes. Codex and Claude use a fixed private port.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_A2A_GRPC_ADDRESS", "[::]:80", "Python ADK gRPC listen address.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) - _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) - _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) + KagentPort = RegisterStringVar("KAGENT_PORT", "", "ADK A2A listen port: the Go HTTP/gRPC listener defaults to 8080; the Python gRPC listener defaults to 80. Explicit Go --port/AppConfig.Port or Python a2a_grpc_address takes precedence. The controller sets 80 for managed kagent runtimes. Python's HTTP --port is separate.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) + _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) + _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) ) diff --git a/go/pkg/logging/logging.go b/go/pkg/logging/logging.go index 4141a53eba..519c4d5815 100644 --- a/go/pkg/logging/logging.go +++ b/go/pkg/logging/logging.go @@ -5,9 +5,9 @@ import ( "context" "io" "log/slog" - "os" "github.com/go-logr/logr" + "github.com/kagent-dev/kagent/go/core/pkg/env" "go.opentelemetry.io/otel/trace" ) @@ -45,7 +45,7 @@ func (h traceHandler) WithGroup(name string) slog.Handler { // NewFromEnv returns a JSON logger using KAGENT_LOG_LEVEL, defaulting to info. func NewFromEnv(w io.Writer) (*slog.Logger, error) { - return New(w, cmp.Or(os.Getenv("KAGENT_LOG_LEVEL"), "info")) + return New(w, cmp.Or(env.LogLevel.Get(), env.LogLevel.DefaultValue())) } // IntoContext stores logger in the logr context slot used by controller-runtime. diff --git a/go/pkg/logging/logging_test.go b/go/pkg/logging/logging_test.go index 58160625d1..d8206381ff 100644 --- a/go/pkg/logging/logging_test.go +++ b/go/pkg/logging/logging_test.go @@ -7,6 +7,7 @@ import ( "log/slog" "testing" + "github.com/kagent-dev/kagent/go/core/pkg/env" "go.opentelemetry.io/otel/trace" ) @@ -28,6 +29,37 @@ func TestNew(t *testing.T) { } } +func TestNewFromEnv(t *testing.T) { + for _, tt := range []struct { + name string + level string + want slog.Level + wantErr bool + }{ + {name: "empty defaults to info", want: slog.LevelInfo}, + {name: "debug", level: "debug", want: slog.LevelDebug}, + {name: "case insensitive", level: "WARN", want: slog.LevelWarn}, + {name: "invalid", level: "verbose", wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(env.LogLevel.Name(), tt.level) + logger, err := NewFromEnv(&bytes.Buffer{}) + if tt.wantErr { + if err == nil { + t.Fatal("expected invalid log level to fail") + } + return + } + if err != nil { + t.Fatal(err) + } + if !logger.Enabled(t.Context(), tt.want) || logger.Enabled(t.Context(), tt.want-1) { + t.Fatalf("expected minimum log level %s", tt.want) + } + }) + } +} + func TestContextBridge(t *testing.T) { var output bytes.Buffer logger := slog.New(slog.NewJSONHandler(&output, nil)).With("component", "test") diff --git a/python/README.md b/python/README.md index 110e184538..0516b3b8c8 100644 --- a/python/README.md +++ b/python/README.md @@ -46,5 +46,6 @@ spec: ``` In this example, the HTTP listener uses port `8080`. Substrate readiness uses a -separate listener on port `8081`. A2A gRPC uses the address configured by -`KAGENT_A2A_GRPC_ADDRESS`. +separate listener on port `8081`. A2A gRPC binds to `[::]` on `KAGENT_PORT`, +which the controller sets to `80`. The same variable selects the Go ADK's +shared HTTP/gRPC port. diff --git a/python/packages/kagent-adk/src/kagent/adk/_a2a.py b/python/packages/kagent-adk/src/kagent/adk/_a2a.py index e8ab210a10..8819a20872 100644 --- a/python/packages/kagent-adk/src/kagent/adk/_a2a.py +++ b/python/packages/kagent-adk/src/kagent/adk/_a2a.py @@ -84,7 +84,7 @@ def __init__( """ self.root_agent_factory = root_agent_factory self.kagent_api_url = kagent_api_url - self.a2a_grpc_address = a2a_grpc_address or os.getenv("KAGENT_A2A_GRPC_ADDRESS", "[::]:80") + self.a2a_grpc_address = a2a_grpc_address or f"[::]:{os.getenv('KAGENT_PORT') or '80'}" self.app_name = app_name self.agent_card = agent_card self._lifespan = lifespan diff --git a/python/packages/kagent-adk/tests/unittests/test_a2a_grpc.py b/python/packages/kagent-adk/tests/unittests/test_a2a_grpc.py index 811d1b8443..8ab0d1915a 100644 --- a/python/packages/kagent-adk/tests/unittests/test_a2a_grpc.py +++ b/python/packages/kagent-adk/tests/unittests/test_a2a_grpc.py @@ -9,7 +9,7 @@ from kagent.adk import KAgentApp -def make_app(address: str = "127.0.0.1:0"): +def make_app(address: str | None = "127.0.0.1:0"): card = ParseDict( { "name": "test-app", @@ -29,12 +29,25 @@ def make_app(address: str = "127.0.0.1:0"): kagent_api_url="http://unused", app_name="test-app", a2a_grpc_address=address, - ).build(local=True) + ) + + +@pytest.mark.parametrize("port,expected", [(None, "[::]:80"), ("", "[::]:80"), ("9090", "[::]:9090")]) +def test_grpc_address_from_shared_port(monkeypatch, port, expected): + monkeypatch.delenv("KAGENT_PORT", raising=False) + if port is not None: + monkeypatch.setenv("KAGENT_PORT", port) + assert make_app(address=None).a2a_grpc_address == expected @pytest.mark.asyncio -async def test_grpc_health(): - app = make_app() +@pytest.mark.parametrize("address", ["127.0.0.1:0", None]) +async def test_grpc_health(monkeypatch, address): + # An explicit address takes precedence; otherwise the shared port selects + # an ephemeral listener. The removed variable must have no effect. + monkeypatch.setenv("KAGENT_PORT", "invalid" if address else "0") + monkeypatch.setenv("KAGENT_A2A_GRPC_ADDRESS", "invalid") + app = make_app(address).build(local=True) async with app.router.lifespan_context(app): async with grpc.aio.insecure_channel(f"127.0.0.1:{app.state.a2a_grpc_port}") as channel: @@ -61,7 +74,7 @@ def capture_grpc_handler(handler, *args, **kwargs): monkeypatch.setattr(_a2a, "create_jsonrpc_routes", capture_jsonrpc_handler) monkeypatch.setattr(_a2a, "GrpcHandler", capture_grpc_handler) - app = make_app() + app = make_app().build(local=True) async with app.router.lifespan_context(app): async with grpc.aio.insecure_channel(f"127.0.0.1:{app.state.a2a_grpc_port}") as channel: From 0bf51e8064b479f98bff04f50ee7867d18a7b684 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:10:50 +0000 Subject: [PATCH 06/14] refactor: load Helm environment settings from ConfigMaps Signed-off-by: Eitan Yarmush --- .../templates/controller-configmap.yaml | 16 ++ .../templates/controller-deployment.yaml | 27 +-- .../templates/postgresql-configmap.yaml | 14 ++ helm/kagent/templates/postgresql.yaml | 11 +- helm/kagent/templates/ui-configmap.yaml | 21 +++ helm/kagent/templates/ui-deployment.yaml | 30 +-- .../tests/controller-deployment_test.yaml | 173 +++++++++--------- helm/kagent/tests/global-values_test.yaml | 5 +- helm/kagent/tests/pod-labels_test.yaml | 2 + helm/kagent/tests/postgresql_test.yaml | 61 ++++-- helm/kagent/tests/security-context_test.yaml | 1 + helm/kagent/tests/ui-deployment_test.yaml | 163 ++++++++--------- helm/kagent/values.yaml | 10 +- 13 files changed, 286 insertions(+), 248 deletions(-) create mode 100644 helm/kagent/templates/postgresql-configmap.yaml create mode 100644 helm/kagent/templates/ui-configmap.yaml diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index 2a45eab3dd..ba0551e02e 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -59,6 +59,9 @@ data: {{- end }} KAGENT_DATABASE_VECTOR_ENABLED: {{ .Values.database.postgres.vectorEnabled | quote }} KAGENT_SKIP_MIGRATIONS: {{ .Values.database.postgres.skipMigrations | default false | quote }} + {{- with .Values.database.postgres.urlFile }} + KAGENT_POSTGRES_DATABASE_URL_FILE: {{ . | quote }} + {{- end }} {{- with .Values.database.postgres.pool }} {{- if and (hasKey . "maxConns") (ne .maxConns nil) }} DB_MAX_CONNS: {{ .maxConns | quote }} @@ -79,3 +82,16 @@ data: KAGENT_A2A_CLIENT_TIMEOUT: {{ .Values.controller.a2aClientTimeout | quote }} {{- end }} KAGENT_LOG_LEVEL: {{ .Values.controller.loglevel | quote }} + {{- if include "kagent.controller.metricsEnabled" . }} + KAGENT_METRICS_BIND_ADDRESS: {{ .Values.controller.metrics.bindAddress | quote }} + KAGENT_METRICS_SECURE: {{ .Values.controller.metrics.secureServing | quote }} + {{- end }} + KAGENT_GRPC_REFLECTION: {{ .Values.controller.grpc.reflection | quote }} + {{- if and .Values.controller.substrate .Values.controller.substrate.enabled }} + KAGENT_SUBSTRATE_ATE_API_ENDPOINT: {{ include "kagent.substrate.ateApiEndpoint" . | quote }} + KAGENT_SUBSTRATE_ATE_API_CA_FILE: /run/substrate-servicedns/trust-bundle.pem + KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE: /run/substrate-podidentity/credential-bundle.pem + {{- with include "kagent.substrate.atenetRouterURL" . }} + KAGENT_SUBSTRATE_ATENET_ROUTER_URL: {{ . | quote }} + {{- end }} + {{- end }} diff --git a/helm/kagent/templates/controller-deployment.yaml b/helm/kagent/templates/controller-deployment.yaml index 4d11a901a0..13631ebf4e 100644 --- a/helm/kagent/templates/controller-deployment.yaml +++ b/helm/kagent/templates/controller-deployment.yaml @@ -96,12 +96,12 @@ spec: valueFrom: fieldRef: fieldPath: spec.nodeName + {{- /* Kubelet expands pod identity here; envFrom values are not expanded. */}} - name: OTEL_RESOURCE_ATTRIBUTES value: {{ list (include "kagent.otel.resourceAttributes" .) "service.instance.id=$(K8S_POD_UID),k8s.pod.uid=$(K8S_POD_UID),k8s.pod.name=$(K8S_POD_NAME),k8s.namespace.name=$(KAGENT_NAMESPACE),k8s.node.name=$(K8S_NODE_NAME),k8s.container.name=controller" | compact | join "," | quote }} - {{- if .Values.database.postgres.urlFile }} - - name: KAGENT_POSTGRES_DATABASE_URL_FILE - value: {{ .Values.database.postgres.urlFile | quote }} - {{- else if .Values.database.postgres.url }} + {{- /* Keep credentials and secret-dependent expansion out of the ConfigMap. */}} + {{- if not .Values.database.postgres.urlFile }} + {{- if .Values.database.postgres.url }} - name: KAGENT_POSTGRES_DATABASE_URL value: {{ .Values.database.postgres.url | quote }} {{- else if .Values.database.postgres.bundled.enabled }} @@ -115,29 +115,10 @@ spec: {{- else }} {{ fail "No database connection configured. Set database.postgres.url, database.postgres.urlFile, or enable database.postgres.bundled." }} {{- end }} - {{- if include "kagent.controller.metricsEnabled" . }} - - name: KAGENT_METRICS_BIND_ADDRESS - value: {{ .Values.controller.metrics.bindAddress | quote }} - - name: KAGENT_METRICS_SECURE - value: {{ .Values.controller.metrics.secureServing | quote }} {{- end }} - - name: KAGENT_GRPC_REFLECTION - value: {{ .Values.controller.grpc.reflection | quote }} {{- with .Values.controller.env }} {{- toYaml . | nindent 12 }} {{- end }} - {{- if and .Values.controller.substrate .Values.controller.substrate.enabled }} - - name: KAGENT_SUBSTRATE_ATE_API_ENDPOINT - value: {{ include "kagent.substrate.ateApiEndpoint" . | quote }} - - name: KAGENT_SUBSTRATE_ATE_API_CA_FILE - value: /run/substrate-servicedns/trust-bundle.pem - - name: KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE - value: /run/substrate-podidentity/credential-bundle.pem - {{- with include "kagent.substrate.atenetRouterURL" . }} - - name: KAGENT_SUBSTRATE_ATENET_ROUTER_URL - value: {{ . | quote }} - {{- end }} - {{- end }} envFrom: - configMapRef: name: {{ include "kagent.fullname" . }}-controller diff --git a/helm/kagent/templates/postgresql-configmap.yaml b/helm/kagent/templates/postgresql-configmap.yaml new file mode 100644 index 0000000000..b1b144b7b8 --- /dev/null +++ b/helm/kagent/templates/postgresql-configmap.yaml @@ -0,0 +1,14 @@ +{{- if .Values.database.postgres.bundled.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "kagent.postgresqlServiceName" . }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} + app.kubernetes.io/component: database +data: + POSTGRES_DB: "kagent" + POSTGRES_USER: "kagent" + PGDATA: /var/lib/postgresql/data/pgdata +{{- end }} diff --git a/helm/kagent/templates/postgresql.yaml b/helm/kagent/templates/postgresql.yaml index 0f735c1aa2..a49c9cfb7f 100644 --- a/helm/kagent/templates/postgresql.yaml +++ b/helm/kagent/templates/postgresql.yaml @@ -47,6 +47,8 @@ spec: app.kubernetes.io/component: database template: metadata: + annotations: + checksum/configmap: {{ include (print $.Template.BasePath "/postgresql-configmap.yaml") . | sha256sum }} labels: {{- /* extra pod labels: component merged over global, selector labels applied last so they can never be overridden */}} @@ -84,17 +86,14 @@ spec: containerPort: 5432 protocol: TCP env: - - name: POSTGRES_DB - value: "kagent" - - name: POSTGRES_USER - value: "kagent" - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: {{ include "kagent.passwordSecretName" . }} key: POSTGRES_PASSWORD - - name: PGDATA - value: /var/lib/postgresql/data/pgdata + envFrom: + - configMapRef: + name: {{ $fullname }} livenessProbe: exec: command: diff --git a/helm/kagent/templates/ui-configmap.yaml b/helm/kagent/templates/ui-configmap.yaml new file mode 100644 index 0000000000..b26ac89908 --- /dev/null +++ b/helm/kagent/templates/ui-configmap.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "kagent.fullname" . }}-ui + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +data: + KAGENT_API_BASE_URL: {{ .Values.ui.publicBackendUrl | default "/api" | quote }} + {{- with .Values.ui.basePath }} + {{- /* Keep in sync with ui/scripts/init.sh. nginx strips the prefix, so it must not start with a path nginx serves. */}} + {{- if or (not (regexMatch "^(/[A-Za-z0-9._~-]+)*/?$" .)) (regexMatch "(^|/)\\.\\.?(/|$)" .) }}{{ fail "ui.basePath must be a path like /ui" }}{{ end }} + {{- if regexMatch "^/(api|a2a|assets|health|env-config\\.js|index\\.html|mockServiceWorker\\.js)(/|$)" . }}{{ fail "ui.basePath cannot start with a path nginx serves, such as /api or /assets" }}{{ end }} + KAGENT_UI_BASE_PATH: {{ . | quote }} + {{- end }} + {{- if .Values.ui.auth }} + KAGENT_SSO_REDIRECT_PATH: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }} + {{- end }} + {{- if .Values.ui.streamTimeoutSeconds }} + KAGENT_STREAM_TIMEOUT_MS: {{ mul (int .Values.ui.streamTimeoutSeconds) 1000 | quote }} + {{- end }} diff --git a/helm/kagent/templates/ui-deployment.yaml b/helm/kagent/templates/ui-deployment.yaml index 31933b8656..200dfc0831 100644 --- a/helm/kagent/templates/ui-deployment.yaml +++ b/helm/kagent/templates/ui-deployment.yaml @@ -18,6 +18,7 @@ spec: template: metadata: annotations: + checksum/configmap: {{ include (print $.Template.BasePath "/ui-configmap.yaml") . | sha256sum }} checksum/ui-config: {{ include (print $.Template.BasePath "/ui-nginx-configmap.yaml") . | sha256sum }} {{- with .Values.ui.podAnnotations | default .Values.podAnnotations }} {{- toYaml . | nindent 8 }} @@ -65,30 +66,13 @@ spec: {{- end }} image: {{ include "kagent.uiImage" . | quote }} imagePullPolicy: {{ include "kagent.imagePullPolicy" (dict "root" . "local" .Values.ui.image.pullPolicy) }} - {{- /* The UI is a static bundle; these are read by init.sh, which - renders them into the config.json the browser fetches at - startup. Nothing here is consumed by a server process. */}} + {{- with .Values.ui.env }} env: - - name: KAGENT_API_BASE_URL - value: {{ .Values.ui.publicBackendUrl | default "/api" | quote }} - {{- with .Values.ui.basePath }} - {{- /* Keep in sync with ui/scripts/init.sh. nginx strips the prefix, so it must not start with a path nginx serves. */}} - {{- if or (not (regexMatch "^(/[A-Za-z0-9._~-]+)*/?$" .)) (regexMatch "(^|/)\\.\\.?(/|$)" .) }}{{ fail "ui.basePath must be a path like /ui" }}{{ end }} - {{- if regexMatch "^/(api|a2a|assets|health|env-config\\.js|index\\.html|mockServiceWorker\\.js)(/|$)" . }}{{ fail "ui.basePath cannot start with a path nginx serves, such as /api or /assets" }}{{ end }} - - name: KAGENT_UI_BASE_PATH - value: {{ . | quote }} - {{- end }} - {{- if .Values.ui.auth }} - - name: KAGENT_SSO_REDIRECT_PATH - value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }} - {{- end }} - {{- if .Values.ui.streamTimeoutSeconds }} - - name: KAGENT_STREAM_TIMEOUT_MS - value: {{ mul (int .Values.ui.streamTimeoutSeconds) 1000 | quote }} - {{- end }} - {{- with .Values.ui.env }} - {{- toYaml . | nindent 12 }} - {{- end }} + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "kagent.fullname" . }}-ui ports: - name: http containerPort: {{ .Values.ui.service.ports.targetPort }} diff --git a/helm/kagent/tests/controller-deployment_test.yaml b/helm/kagent/tests/controller-deployment_test.yaml index 43aaca0bf1..9f4d6a2016 100644 --- a/helm/kagent/tests/controller-deployment_test.yaml +++ b/helm/kagent/tests/controller-deployment_test.yaml @@ -4,6 +4,30 @@ templates: - controller-configmap.yaml - postgresql-secret.yaml tests: + - it: keeps chart defaults in the ConfigMap and pod identity and credentials in env + template: controller-deployment.yaml + set: + controller.substrate.enabled: true + controller.metrics.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].envFrom + content: + configMapRef: + name: RELEASE-NAME-controller + - matchRegex: + path: spec.template.spec.containers[0].env[*].name + pattern: "^(KAGENT_NAMESPACE|K8S_POD_NAME|K8S_POD_UID|K8S_NODE_NAME|OTEL_RESOURCE_ATTRIBUTES|POSTGRES_PASSWORD|KAGENT_POSTGRES_DATABASE_URL)$" + - notExists: + path: data.KAGENT_POSTGRES_DATABASE_URL + template: controller-configmap.yaml + - notExists: + path: data.POSTGRES_PASSWORD + template: controller-configmap.yaml + - notExists: + path: data.OTEL_RESOURCE_ATTRIBUTES + template: controller-configmap.yaml + - it: should render controller deployment with default values template: controller-deployment.yaml asserts: @@ -78,13 +102,11 @@ tests: value: 8083 - it: should disable gRPC reflection by default - template: controller-deployment.yaml + template: controller-configmap.yaml asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_GRPC_REFLECTION - value: "false" + - equal: + path: data.KAGENT_GRPC_REFLECTION + value: "false" - it: should set global annotations on deployment template: controller-deployment.yaml @@ -123,21 +145,18 @@ tests: enabled: true ateApiEndpoint: "dns:///api.ate-system.svc:443" asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_SUBSTRATE_ATE_API_ENDPOINT - value: "dns:///api.ate-system.svc:443" - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_SUBSTRATE_ATE_API_CA_FILE - value: /run/substrate-servicedns/trust-bundle.pem - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE - value: /run/substrate-podidentity/credential-bundle.pem + - equal: + path: data.KAGENT_SUBSTRATE_ATE_API_ENDPOINT + value: "dns:///api.ate-system.svc:443" + template: controller-configmap.yaml + - equal: + path: data.KAGENT_SUBSTRATE_ATE_API_CA_FILE + value: /run/substrate-servicedns/trust-bundle.pem + template: controller-configmap.yaml + - equal: + path: data.KAGENT_SUBSTRATE_ATE_API_CLIENT_CERT_FILE + value: /run/substrate-podidentity/credential-bundle.pem + template: controller-configmap.yaml - equal: path: spec.template.spec.volumes[0].name value: substrate-servicedns @@ -601,11 +620,10 @@ tests: postgres: urlFile: "/var/secrets/db-url" asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_POSTGRES_DATABASE_URL_FILE - value: "/var/secrets/db-url" + - equal: + path: data.KAGENT_POSTGRES_DATABASE_URL_FILE + value: "/var/secrets/db-url" + template: controller-configmap.yaml - notContains: path: spec.template.spec.containers[0].env content: @@ -708,11 +726,10 @@ tests: bundled: enabled: true asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_POSTGRES_DATABASE_URL_FILE - value: "/var/secrets/db-url" + - equal: + path: data.KAGENT_POSTGRES_DATABASE_URL_FILE + value: "/var/secrets/db-url" + template: controller-configmap.yaml - notContains: path: spec.template.spec.containers[0].env content: @@ -825,18 +842,12 @@ tests: any: true - it: should not set metrics env vars by default - template: controller-deployment.yaml + template: controller-configmap.yaml asserts: - - notContains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_BIND_ADDRESS - any: true - - notContains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_SECURE - any: true + - notExists: + path: data.KAGENT_METRICS_BIND_ADDRESS + - notExists: + path: data.KAGENT_METRICS_SECURE - it: should expose metrics container port when enabled template: controller-deployment.yaml @@ -851,32 +862,26 @@ tests: protocol: TCP - it: should set metrics env vars when enabled - template: controller-deployment.yaml + template: controller-configmap.yaml set: controller.metrics.enabled: true asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_BIND_ADDRESS - value: ":8443" - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_SECURE - value: "true" + - equal: + path: data.KAGENT_METRICS_BIND_ADDRESS + value: ":8443" + - equal: + path: data.KAGENT_METRICS_SECURE + value: "true" - it: should reflect insecure serving in env - template: controller-deployment.yaml + template: controller-configmap.yaml set: controller.metrics.enabled: true controller.metrics.secureServing: false asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_SECURE - value: "false" + - equal: + path: data.KAGENT_METRICS_SECURE + value: "false" - it: should derive metrics container port from bindAddress template: controller-deployment.yaml @@ -884,11 +889,10 @@ tests: controller.metrics.enabled: true controller.metrics.bindAddress: ":9443" asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_BIND_ADDRESS - value: ":9443" + - equal: + path: data.KAGENT_METRICS_BIND_ADDRESS + value: ":9443" + template: controller-configmap.yaml - contains: path: spec.template.spec.containers[0].ports content: @@ -928,16 +932,12 @@ tests: controller.metrics.enabled: true controller.metrics.bindAddress: "0" asserts: - - notContains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_BIND_ADDRESS - any: true - - notContains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_SECURE - any: true + - notExists: + path: data.KAGENT_METRICS_BIND_ADDRESS + template: controller-configmap.yaml + - notExists: + path: data.KAGENT_METRICS_SECURE + template: controller-configmap.yaml - notContains: path: spec.template.spec.containers[0].ports content: @@ -950,16 +950,12 @@ tests: controller.metrics.enabled: true controller.metrics.bindAddress: "" asserts: - - notContains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_BIND_ADDRESS - any: true - - notContains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_SECURE - any: true + - notExists: + path: data.KAGENT_METRICS_BIND_ADDRESS + template: controller-configmap.yaml + - notExists: + path: data.KAGENT_METRICS_SECURE + template: controller-configmap.yaml - notContains: path: spec.template.spec.containers[0].ports content: @@ -974,11 +970,10 @@ tests: - name: KAGENT_METRICS_BIND_ADDRESS value: "0" asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_METRICS_BIND_ADDRESS - value: ":8443" + - equal: + path: data.KAGENT_METRICS_BIND_ADDRESS + value: ":8443" + template: controller-configmap.yaml - contains: path: spec.template.spec.containers[0].env content: diff --git a/helm/kagent/tests/global-values_test.yaml b/helm/kagent/tests/global-values_test.yaml index da360e223d..8552615b1a 100644 --- a/helm/kagent/tests/global-values_test.yaml +++ b/helm/kagent/tests/global-values_test.yaml @@ -5,9 +5,10 @@ templates: - controller-configmap.yaml - rbac/getter-role.yaml - substrate-workerpool.yaml - # ui-deployment.yaml includes ui-nginx-configmap.yaml, and helm-unittest can - # only resolve cross-template includes when both are listed here. + # Helm-unittest requires the UI's ConfigMaps to be listed here so it can + # resolve the Deployment's checksum includes. - ui-deployment.yaml + - ui-configmap.yaml - ui-nginx-configmap.yaml tests: - it: should override every image registry from global.imageRegistry diff --git a/helm/kagent/tests/pod-labels_test.yaml b/helm/kagent/tests/pod-labels_test.yaml index edc5c03b6e..cd26707e82 100644 --- a/helm/kagent/tests/pod-labels_test.yaml +++ b/helm/kagent/tests/pod-labels_test.yaml @@ -3,8 +3,10 @@ templates: - controller-deployment.yaml - controller-configmap.yaml - postgresql.yaml + - postgresql-configmap.yaml - postgresql-secret.yaml - ui-deployment.yaml + - ui-configmap.yaml - ui-nginx-configmap.yaml tests: - it: should not add extra pod labels by default (controller) diff --git a/helm/kagent/tests/postgresql_test.yaml b/helm/kagent/tests/postgresql_test.yaml index 5d6d40a018..4d4c088d36 100644 --- a/helm/kagent/tests/postgresql_test.yaml +++ b/helm/kagent/tests/postgresql_test.yaml @@ -1,8 +1,39 @@ suite: test postgresql templates: + - postgresql-configmap.yaml - postgresql.yaml - postgresql-secret.yaml tests: + - it: loads non-secret database settings from the ConfigMap + template: postgresql.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].envFrom + content: + configMapRef: + name: RELEASE-NAME-postgresql + documentIndex: 2 + - lengthEqual: + path: spec.template.spec.containers[0].env + count: 1 + documentIndex: 2 + - matchRegex: + path: spec.template.metadata.annotations["checksum/configmap"] + pattern: "^[a-f0-9]{64}$" + documentIndex: 2 + - notExists: + path: data.POSTGRES_PASSWORD + template: postgresql-configmap.yaml + + - it: omits the database ConfigMap when bundled postgres is disabled + template: postgresql-configmap.yaml + set: + database.postgres.bundled.enabled: false + database.postgres.url: postgres://user:pass@external-host:5432/db + asserts: + - hasDocuments: + count: 0 + # ============================================================================= # bundled mode (default — url and urlFile both empty, bundled.enabled true) # ============================================================================= @@ -153,29 +184,21 @@ tests: key: POSTGRES_PASSWORD - it: should set POSTGRES_DB and POSTGRES_USER to hardcoded values - template: postgresql.yaml - documentIndex: 2 + template: postgresql-configmap.yaml asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: POSTGRES_DB - value: "kagent" - - contains: - path: spec.template.spec.containers[0].env - content: - name: POSTGRES_USER - value: "kagent" + - equal: + path: data.POSTGRES_DB + value: "kagent" + - equal: + path: data.POSTGRES_USER + value: "kagent" - it: should set PGDATA env var - template: postgresql.yaml - documentIndex: 2 + template: postgresql-configmap.yaml asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: PGDATA - value: /var/lib/postgresql/data/pgdata + - equal: + path: data.PGDATA + value: /var/lib/postgresql/data/pgdata - it: should have liveness and readiness probes template: postgresql.yaml diff --git a/helm/kagent/tests/security-context_test.yaml b/helm/kagent/tests/security-context_test.yaml index fc654afdfc..b3583142a7 100644 --- a/helm/kagent/tests/security-context_test.yaml +++ b/helm/kagent/tests/security-context_test.yaml @@ -5,6 +5,7 @@ templates: - controller-deployment.yaml - postgresql-secret.yaml - ui-deployment.yaml + - ui-configmap.yaml - ui-nginx-configmap.yaml tests: # ============================================================================= diff --git a/helm/kagent/tests/ui-deployment_test.yaml b/helm/kagent/tests/ui-deployment_test.yaml index 3dc248cac0..01766e2157 100644 --- a/helm/kagent/tests/ui-deployment_test.yaml +++ b/helm/kagent/tests/ui-deployment_test.yaml @@ -1,8 +1,53 @@ suite: test ui deployment templates: + - ui-configmap.yaml - ui-deployment.yaml - ui-nginx-configmap.yaml tests: + - it: loads browser settings from the ConfigMap and rolls out on changes + template: ui-deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].envFrom + content: + configMapRef: + name: RELEASE-NAME-ui + - notExists: + path: spec.template.spec.containers[0].env + - matchRegex: + path: spec.template.metadata.annotations["checksum/configmap"] + pattern: "^[a-f0-9]{64}$" + + - it: preserves explicit env overrides and Secret references + template: ui-deployment.yaml + set: + ui.env: + - name: KAGENT_API_BASE_URL + value: /custom-api + - name: CUSTOM_TOKEN + valueFrom: + secretKeyRef: + name: ui-token + key: token + asserts: + - equal: + path: spec.template.spec.containers[0].env + value: + - name: KAGENT_API_BASE_URL + value: /custom-api + - name: CUSTOM_TOKEN + valueFrom: + secretKeyRef: + name: ui-token + key: token + - equal: + path: data.KAGENT_API_BASE_URL + value: /api + template: ui-configmap.yaml + - notExists: + path: data.CUSTOM_TOKEN + template: ui-configmap.yaml + - it: should render ui deployment with default values template: ui-deployment.yaml asserts: @@ -303,68 +348,53 @@ tests: path: spec.template.spec.topologySpreadConstraints - it: should pass runtime browser config to init.sh as env - template: ui-deployment.yaml + template: ui-configmap.yaml asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_API_BASE_URL - value: "/api" - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_SSO_REDIRECT_PATH - value: "/oauth2/start" - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_STREAM_TIMEOUT_MS - value: "1800000" + - equal: + path: data.KAGENT_API_BASE_URL + value: "/api" + - equal: + path: data.KAGENT_SSO_REDIRECT_PATH + value: "/oauth2/start" + - equal: + path: data.KAGENT_STREAM_TIMEOUT_MS + value: "1800000" - it: should carry an overridden publicBackendUrl through to the browser config - template: ui-deployment.yaml + template: ui-configmap.yaml set: ui: publicBackendUrl: "https://api.example.com/api" asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_API_BASE_URL - value: "https://api.example.com/api" + - equal: + path: data.KAGENT_API_BASE_URL + value: "https://api.example.com/api" - it: should fall back to /api when publicBackendUrl is blanked - template: ui-deployment.yaml + template: ui-configmap.yaml set: ui: publicBackendUrl: "" asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_API_BASE_URL - value: "/api" + - equal: + path: data.KAGENT_API_BASE_URL + value: "/api" - it: should pass ui.basePath through to the browser config only when set - template: ui-deployment.yaml + template: ui-configmap.yaml set: ui: basePath: /ui asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_UI_BASE_PATH - value: "/ui" + - equal: + path: data.KAGENT_UI_BASE_PATH + value: "/ui" - it: should omit KAGENT_UI_BASE_PATH by default - template: ui-deployment.yaml + template: ui-configmap.yaml asserts: - - notContains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_UI_BASE_PATH - any: true + - notExists: + path: data.KAGENT_UI_BASE_PATH - it: should reject a ui.basePath that is not a path template: ui-deployment.yaml @@ -394,60 +424,29 @@ tests: errorMessage: ui.basePath cannot start with a path nginx serves, such as /api or /assets - it: should accept a ui.basePath that only shares a prefix with a served path - template: ui-deployment.yaml + template: ui-configmap.yaml set: ui: basePath: /apis asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_UI_BASE_PATH - value: "/apis" + - equal: + path: data.KAGENT_UI_BASE_PATH + value: "/apis" - it: should reflect overridden auth and stream settings - template: ui-deployment.yaml + template: ui-configmap.yaml set: ui: auth: ssoRedirectPath: /custom/start streamTimeoutSeconds: 90 asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_SSO_REDIRECT_PATH - value: "/custom/start" - - contains: - path: spec.template.spec.containers[0].env - content: - name: KAGENT_STREAM_TIMEOUT_MS - value: "90000" - - - it: should not set env vars that only the removed node server consumed - template: ui-deployment.yaml - asserts: - - notContains: - path: spec.template.spec.containers[0].env - content: - name: NEXT_PUBLIC_BACKEND_URL - value: "/api" - - notContains: - path: spec.template.spec.containers[0].env - content: - name: NEXT_PUBLIC_SSO_REDIRECT_PATH - value: "/oauth2/start" - - notContains: - path: spec.template.spec.containers[0].env - content: - name: BACKEND_INTERNAL_URL - value: "http://RELEASE-NAME-controller.NAMESPACE.svc:8083/api" - # The browser reaches gRPC over the same origin, through nginx, so there is - # no separate gRPC URL for anything to be configured with. - - notContains: - path: spec.template.spec.containers[0].env - content: - name: BACKEND_GRPC_URL + - equal: + path: data.KAGENT_SSO_REDIRECT_PATH + value: "/custom/start" + - equal: + path: data.KAGENT_STREAM_TIMEOUT_MS + value: "90000" - it: should not mount a nextjs build cache template: ui-deployment.yaml diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 9b7934b566..3c84f827d0 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -397,7 +397,8 @@ controller: grpc: reflection: false - # Extra controller environment variables. + # Extra controller environment variables, applied after ConfigMap defaults. + # Kept in the Deployment to support valueFrom and $(VAR_NAME) expansion. env: [] # Standalone workspaces, enabled by default. @@ -525,9 +526,10 @@ ui: proxyReadTimeout: 1800s # -- proxy_send_timeout: max time between two successive writes to the upstream. proxySendTimeout: 1800s - # -- Extra environment variables for the UI container: a list of `{name, value}` - # entries, spliced into its `env:` verbatim. An installed app extension's own - # settings go here, named `EXTENSION_*`; the container's startup script copies + # -- Extra environment variables for the UI container, applied after ConfigMap + # defaults. Entries stay in the Deployment to support valueFrom and $(VAR_NAME) + # expansion. An installed app extension's settings go here, named + # `KAGENT_EXTENSION_*`; the container's startup script copies # those onto `window.environmentVariables` for the browser to read. env: [] # -- Identity headers the UI's nginx proxy will forward to the backend on From fb256fecd58285e2bfb0ad9fed3d564d75d24013 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:16:24 +0000 Subject: [PATCH 07/14] fix: include env package in sandbox guest image build Signed-off-by: Eitan Yarmush --- go/sandbox/guest/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/go/sandbox/guest/Dockerfile b/go/sandbox/guest/Dockerfile index cbf30e9dd7..7f32e0544a 100644 --- a/go/sandbox/guest/Dockerfile +++ b/go/sandbox/guest/Dockerfile @@ -5,6 +5,7 @@ ARG TARGETARCH WORKDIR /workspace COPY go.mod go.sum ./ +COPY core/pkg/env/ core/pkg/env/ COPY pkg/logging/ pkg/logging/ COPY sandbox/guest/cmd/ sandbox/guest/cmd/ From 7ff69637d3115cd238b51e1bea0ca74a7c1446e0 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:18:59 +0000 Subject: [PATCH 08/14] ci: check env docs in the existing manifests job Signed-off-by: Eitan Yarmush --- .github/workflows/ci.yaml | 15 +++++++++++---- .github/workflows/env-docs.yaml | 27 --------------------------- 2 files changed, 11 insertions(+), 31 deletions(-) delete mode 100644 .github/workflows/env-docs.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f15c857bfa..62ef12b73e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,12 +3,16 @@ name: CI Build on: push: branches: [main, "release/**"] - paths-ignore: - - "**/*.md" + paths: + - "**" + - "!**/*.md" + - "docs/env.md" pull_request: branches: [main, "release/**"] - paths-ignore: - - "**/*.md" + paths: + - "**" + - "!**/*.md" + - "docs/env.md" merge_group: workflow_dispatch: inputs: @@ -607,6 +611,9 @@ jobs: cache: true cache-dependency-path: go/go.sum + - name: Check environment documentation + run: make env-docs-check + - name: Generate controller manifests run: make controller-manifests diff --git a/.github/workflows/env-docs.yaml b/.github/workflows/env-docs.yaml deleted file mode 100644 index c1dec42791..0000000000 --- a/.github/workflows/env-docs.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Environment Documentation - -on: - push: - branches: [main, "release/**"] - pull_request: - branches: [main, "release/**"] - merge_group: - workflow_dispatch: - -permissions: - contents: read - -jobs: - env-docs-check: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Set up Go - uses: actions/setup-go@v7 - with: - go-version-file: go/go.mod - cache: true - cache-dependency-path: go/go.sum - - name: Check environment documentation - run: make env-docs-check From c6eb24ce9b683373256efd499275195e5090a6d2 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:34:01 +0000 Subject: [PATCH 09/14] refactor: read Go environment settings through the registry Signed-off-by: Eitan Yarmush --- STYLE.md | 13 ++++ docs/env.md | 14 ++-- go/.golangci.yaml | 7 ++ go/adk/cmd/main.go | 8 +- go/adk/examples/byo/main.go | 2 +- go/adk/pkg/a2a/server/server.go | 8 +- go/adk/pkg/a2a/server/server_test.go | 7 +- go/adk/pkg/agent/agent.go | 29 +++---- go/adk/pkg/app/app.go | 6 +- go/adk/pkg/app/app_test.go | 8 ++ go/adk/pkg/config/config_materialize.go | 4 +- go/adk/pkg/embedding/embedding.go | 20 ++--- go/adk/pkg/internal/azureai/azureai.go | 23 ++---- go/adk/pkg/internal/azureai/azureai_test.go | 21 +++--- go/adk/pkg/models/anthropic.go | 4 +- go/adk/pkg/models/foundry.go | 6 +- go/adk/pkg/models/mistral.go | 6 +- go/adk/pkg/models/ollama.go | 4 +- go/adk/pkg/models/openai.go | 10 +-- go/adk/pkg/models/sapaicore.go | 6 +- go/adk/pkg/runner/adapter.go | 10 +-- go/adk/pkg/tools/skills.go | 30 +------- go/adk/pkg/tools/skills_test.go | 16 ---- go/core/cli/internal/commands/const.go | 45 +++-------- go/core/cli/internal/commands/db/db.go | 13 ++-- go/core/cli/internal/commands/db/db_test.go | 31 ++++++++ go/core/cli/internal/commands/install.go | 27 ++++--- go/core/cli/internal/db/migrate/migrate.go | 9 ++- .../cli/internal/db/migrate/migrate_test.go | 23 ++++-- go/core/hack/mockllm/main.go | 5 +- go/core/internal/translator/otel.go | 75 ++++++++++--------- go/core/internal/translator/otel_test.go | 55 ++++++++++++++ go/core/pkg/env/kagent.go | 9 +-- go/core/pkg/env/otel.go | 42 +++++------ go/core/pkg/env/providers.go | 6 +- go/core/pkg/env/runtime.go | 16 ++-- go/core/pkg/env/var.go | 59 ++++++++------- go/core/pkg/env/var_test.go | 75 +++++++++++++++++++ go/harness/claude/cmd/main.go | 2 +- go/harness/codex/cmd/main.go | 2 +- go/pkg/telemetry/telemetry.go | 22 +++--- go/pkg/telemetry/telemetry_test.go | 2 +- 42 files changed, 455 insertions(+), 325 deletions(-) create mode 100644 go/core/cli/internal/commands/db/db_test.go diff --git a/STYLE.md b/STYLE.md index d6c354fc10..0def3d733d 100644 --- a/STYLE.md +++ b/STYLE.md @@ -205,6 +205,19 @@ Go code lives in the `go/` workspace (`go/api`, `go/core`, `go/adk`). Run HTTP calls use `http.NewRequestWithContext`. Never drop a `cancel` function. - Context keys use unexported struct types, never strings. +### Environment variables + +- Read user-configurable settings through registered accessors in `core/pkg/env`. + Register each setting once, listing every consuming component. +- Use `Get()` for the registered default and `Lookup()` when a caller must + distinguish unset values. Use boolean/integer `LookupWithError()` when invalid + input needs a diagnostic; it distinguishes invalid input from unset or empty. +- Registered booleans accept `true`, `t`, `1`, `false`, `f`, and `0`, ignoring case + and surrounding whitespace. Upstream settings with a different grammar use + string registrations and their own parser. +- Direct process reads require an explained lint exception, such as resolving + controller-generated payloads or arbitrary credential placeholders. + ### Logging - Use standard-library `log/slog`. Binaries write JSON to stderr and read the diff --git a/docs/env.md b/docs/env.md index 1e939c482c..56f42c951c 100644 --- a/docs/env.md +++ b/docs/env.md @@ -37,7 +37,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_API_URL` | String | `(none)` | Base URL for kagent control-plane API calls. Required by Python runtimes and supplied by the controller in managed runtimes; also used as the E2E test URL when KAGENT_E2E_API_URL is unset. | | `KAGENT_BASH_VENV_PATH` | String | `(none)` | Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set. | | `KAGENT_CONFIG_DIR` | String | `/config` | Go ADK configuration directory; --filepath takes precedence. | -| `KAGENT_ENABLE_FILE_SEARCH_TOOLS` | Boolean | `false` | When true, enables the list_files and grep_file skills tools, which let an agent enumerate and search the filesystem under its session/skills roots without a shell. Disabled by default; set in Harness env to opt in. | +| `KAGENT_ENABLE_FILE_SEARCH_TOOLS` | Boolean | `false` | When true, t, or 1 (case-insensitive), enables the list_files and grep_file skills tools, which let an agent enumerate and search the filesystem under its session/skills roots without a shell. Disabled by default; set in Harness env to opt in. | | `KAGENT_GATEWAY_URL` | String | `(none)` | Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value. | | `KAGENT_LOG_LEVEL` | String | `info` | Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels. | | `KAGENT_NAME` | String | `(none)` | Agent name for standalone runtimes. Required by Python runtimes; supplied by the controller in managed runtimes. | @@ -77,7 +77,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `OTEL_METRICS_EXPORTER` | String | `(none)` | Metric exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | | `OTEL_PROPAGATORS` | String | `tracecontext` | SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes. | | `OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents. | -| `OTEL_SDK_DISABLED` | Boolean | `false` | Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes. | +| `OTEL_SDK_DISABLED` | String | `false` | Disable SDK telemetry and forwarding to managed runtimes when true (case-insensitive). Other values are treated as false. | | `OTEL_SEMCONV_STABILITY_OPT_IN` | String | `gen_ai_latest_experimental` | Python Google ADK semantic-convention opt-in; set by kagent when absent. | | `OTEL_SERVICE_NAME` | String | `(none)` | SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes. | | `OTEL_TRACES_EXPORTER` | String | `(none)` | Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | @@ -92,7 +92,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `AZURE_OPENAI_API_KEY` | String | `(none)` | API key for Azure OpenAI. | | `GEMINI_API_KEY` | String | `(none)` | Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs. | | `GOOGLE_API_KEY` | String | `(none)` | API key for Google Gemini. | -| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | +| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. The controller defaults to false. When unset in the CLI, migrations read the controller ConfigMap and fall back to true if it is unavailable. | | `KAGENT_DEFAULT_MODEL_PROVIDER` | String | `openAI` | Default LLM provider for agents (e.g. openAI, anthropic, ollama, azureOpenAI). | | `KAGENT_HELM_EXTRA_ARGS` | String | `(none)` | Additional arguments to pass to Helm commands. | | `KAGENT_HELM_REPO` | String | `oci://ghcr.io/kagent-dev/kagent/helm/` | Helm repository URL for kagent charts. | @@ -108,7 +108,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | Variable | Type | Default | Description | |----------|------|---------|-------------| | `KAGENT_CONTROLLER_NAME` | String | `kagent-controller` | Name of the kagent controller service. | -| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | +| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. The controller defaults to false. When unset in the CLI, migrations read the controller ConfigMap and fall back to true if it is unavailable. | | `KAGENT_GATEWAY_URL` | String | `(none)` | Base URL for A2A and MCP traffic. The controller falls back to http://127.0.0.1:8083; Python runtimes require a value. | | `KAGENT_GRPC_REFLECTION` | Boolean | `false` | Enable gRPC server reflection on the controller. | | `KAGENT_HTTP_BIND_ADDRESS` | String | `:8083` | Listen address for the controller HTTP, gRPC, A2A, and MCP server. | @@ -117,7 +117,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_METRICS_BIND_ADDRESS` | String | `0` | Address the controller-runtime metrics server binds to, e.g. :8080. "0" (the default) serves no metrics, so an installation that does not set this is unchanged. The Helm chart renders this variable, and its ServiceMonitor, from controller.metrics. | | `KAGENT_METRICS_SECURE` | Boolean | `false` | Serve the metrics endpoint over HTTPS with authentication and authorization. A scraper then needs a token bound to the metrics-reader ClusterRole. | | `KAGENT_NAMESPACE` | String | `kagent` | Kubernetes namespace where kagent resources are deployed. The controller injects the agent namespace into runtimes; Python runtimes require it. | -| `KAGENT_OTEL_CAPTURE_RAW_API_BODIES` | Boolean | `false` | Set to true to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data. | +| `KAGENT_OTEL_CAPTURE_RAW_API_BODIES` | Boolean | `false` | Set to true, t, or 1 (case-insensitive) to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data. | | `KAGENT_OTEL_MAX_CAPTURE_BYTES` | Integer | `16384` | Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384. | | `KAGENT_OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Resource attributes, as key=value pairs, added to every agent runtime. | | `KAGENT_POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | @@ -153,7 +153,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `OTEL_METRICS_EXPORTER` | String | `(none)` | Metric exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | | `OTEL_PROPAGATORS` | String | `tracecontext` | SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes. | | `OTEL_RESOURCE_ATTRIBUTES` | String | `(none)` | Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents. | -| `OTEL_SDK_DISABLED` | Boolean | `false` | Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes. | +| `OTEL_SDK_DISABLED` | String | `false` | Disable SDK telemetry and forwarding to managed runtimes when true (case-insensitive). Other values are treated as false. | | `OTEL_SERVICE_NAME` | String | `(none)` | SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes. | | `OTEL_TRACES_EXPORTER` | String | `(none)` | Trace exporter, otlp or none. Managed runtime export requires explicit otlp and an endpoint; unset disables forwarding. Standalone SDKs may default to otlp. | @@ -161,7 +161,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | Variable | Type | Default | Description | |----------|------|---------|-------------| -| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. | +| `KAGENT_DATABASE_VECTOR_ENABLED` | Boolean | `false` | Enable vector database migrations and vector-backed database functionality. The controller defaults to false. When unset in the CLI, migrations read the controller ConfigMap and fall back to true if it is unavailable. | | `KAGENT_POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | | `KAGENT_POSTGRES_DATABASE_URL_FILE` | String | `(none)` | File containing the PostgreSQL connection URL; takes precedence over KAGENT_POSTGRES_DATABASE_URL in the controller. | | `KAGENT_SKIP_MIGRATIONS` | Boolean | `false` | Verify required database migrations at startup without applying them. | diff --git a/go/.golangci.yaml b/go/.golangci.yaml index 453041b96f..91f48dd5a4 100644 --- a/go/.golangci.yaml +++ b/go/.golangci.yaml @@ -90,9 +90,12 @@ linters: - shadow enable-all: true forbidigo: + analyze-types: true forbid: - pattern: ^log\.(Fatal|Fatalf|Fatalln|Print|Printf|Println|Panic|Panicf|Panicln)$ msg: Use log/slog with the shared logging package + - pattern: ^os\.(Getenv|LookupEnv)$ + msg: Use a registered accessor from core/pkg/env for configurable settings importas: alias: - pkg: k8s.io/api/core/v1 @@ -157,6 +160,10 @@ linters: - zz_generated.*\.go$ - .*conversion.*\.go$ rules: + - linters: + - forbidigo + text: Use a registered accessor from core/pkg/env + path: '(^|/)core/pkg/env/|_test\.go$' - linters: - kubeapilinter path-except: "api/v1alpha" diff --git a/go/adk/cmd/main.go b/go/adk/cmd/main.go index de1ab1dc7e..1555411bd9 100644 --- a/go/adk/cmd/main.go +++ b/go/adk/cmd/main.go @@ -46,13 +46,7 @@ func main() { slog.SetDefault(logger) logger.Info("logger initialized", "level", *logLevel) - configDir := *filepathFlag - if configDir == "" { - configDir = os.Getenv("KAGENT_CONFIG_DIR") - } - if configDir == "" { - configDir = "/config" - } + configDir := cmp.Or(*filepathFlag, env.KagentConfigDir.Get(), env.KagentConfigDir.DefaultValue()) if err := run(logger, *host, *portFlag, configDir); err != nil { logger.Error("agent stopped", "error", err) diff --git a/go/adk/examples/byo/main.go b/go/adk/examples/byo/main.go index fc3759c37e..5dc3c08a83 100644 --- a/go/adk/examples/byo/main.go +++ b/go/adk/examples/byo/main.go @@ -47,7 +47,7 @@ func main() { logger, _ := logging.New(os.Stderr, "info") slog.SetDefault(logger) - modelName := os.Getenv("MODEL_NAME") + modelName := os.Getenv("MODEL_NAME") //nolint:forbidigo // Application-specific setting in this standalone BYO example. if modelName == "" { modelName = "gpt-4o-mini" } diff --git a/go/adk/pkg/a2a/server/server.go b/go/adk/pkg/a2a/server/server.go index 719eb86d77..b3f012d141 100644 --- a/go/adk/pkg/a2a/server/server.go +++ b/go/adk/pkg/a2a/server/server.go @@ -19,6 +19,7 @@ import ( a2agrpc "github.com/a2aproject/a2a-go/v2/a2agrpc/v1" a2apb "github.com/a2aproject/a2a-go/v2/a2apb/v1" "github.com/a2aproject/a2a-go/v2/a2asrv" + "github.com/kagent-dev/kagent/go/core/pkg/env" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -32,8 +33,7 @@ import ( ) const ( - a2aMaxContentLengthEnvVar = "KAGENT_A2A_MAX_CONTENT_LENGTH" - defaultMaxContentLength = int64(10 * 1024 * 1024) + defaultMaxContentLength = int64(10 * 1024 * 1024) ) // ServerConfig holds configuration for the A2A server. @@ -201,7 +201,7 @@ func (h rpcEndSignal) HandleRPC(ctx context.Context, rpcStats stats.RPCStats) { } func getMaxContentLength(logger *slog.Logger) *int64 { - value, ok := os.LookupEnv(a2aMaxContentLengthEnvVar) + value, ok := env.KagentA2AMaxContentLength.Lookup() if !ok { maxContentLength := defaultMaxContentLength return &maxContentLength @@ -217,7 +217,7 @@ func getMaxContentLength(logger *slog.Logger) *int64 { if err != nil || maxContentLength < 0 { logger.Info( "invalid A2A request size limit, using default", - "environment_variable", a2aMaxContentLengthEnvVar, + "environment_variable", env.KagentA2AMaxContentLength.Name(), "value", value, "default", defaultMaxContentLength, ) diff --git a/go/adk/pkg/a2a/server/server_test.go b/go/adk/pkg/a2a/server/server_test.go index 1ab5a706a6..8fafe92338 100644 --- a/go/adk/pkg/a2a/server/server_test.go +++ b/go/adk/pkg/a2a/server/server_test.go @@ -18,6 +18,7 @@ import ( a2apb "github.com/a2aproject/a2a-go/v2/a2apb/v1" "github.com/a2aproject/a2a-go/v2/a2apb/v1/pbconv" "github.com/a2aproject/a2a-go/v2/a2asrv" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/telemetry" "go.opentelemetry.io/otel" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -305,7 +306,7 @@ func TestA2ARequestSizeLimit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv(a2aMaxContentLengthEnvVar, "5") + t.Setenv(env.KagentA2AMaxContentLength.Name(), "5") srv, err := NewA2AServer(a2atype.AgentCard{}, substrateExecutor{}, slog.New(slog.DiscardHandler), ServerConfig{Port: "0"}) if err != nil { t.Fatalf("NewA2AServer: %v", err) @@ -328,7 +329,7 @@ func TestA2ARequestSizeLimit(t *testing.T) { } func TestA2ARequestSizeLimitDisabled(t *testing.T) { - t.Setenv(a2aMaxContentLengthEnvVar, "unlimited") + t.Setenv(env.KagentA2AMaxContentLength.Name(), "unlimited") srv, err := NewA2AServer(a2atype.AgentCard{}, substrateExecutor{}, slog.New(slog.DiscardHandler), ServerConfig{Port: "0"}) if err != nil { t.Fatalf("NewA2AServer: %v", err) @@ -378,7 +379,7 @@ func TestGetMaxContentLength(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv(a2aMaxContentLengthEnvVar, tt.value) + t.Setenv(env.KagentA2AMaxContentLength.Name(), tt.value) got := getMaxContentLength(slog.New(slog.DiscardHandler)) if tt.unlimited { if got != nil { diff --git a/go/adk/pkg/agent/agent.go b/go/adk/pkg/agent/agent.go index c7f02b20d7..9fc6b3f002 100644 --- a/go/adk/pkg/agent/agent.go +++ b/go/adk/pkg/agent/agent.go @@ -15,6 +15,7 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/sts" "github.com/kagent-dev/kagent/go/adk/pkg/tools" "github.com/kagent-dev/kagent/go/api/adk" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" "google.golang.org/adk/v2/agent" "google.golang.org/adk/v2/agent/llmagent" @@ -51,7 +52,7 @@ func createGoogleADKAgent(ctx context.Context, agentConfig *adk.AgentConfig, age return nil, fmt.Errorf("agent config is required") } - propagateToken := strings.ToLower(os.Getenv("KAGENT_PROPAGATE_TOKEN")) == "true" + propagateToken := strings.EqualFold(strings.TrimSpace(env.KagentPropagateToken.Get()), "true") var dynamicHeaderProvider mcp.DynamicHeaderProvider if stsPlugin != nil { dynamicHeaderProvider = stsPlugin.HeaderProvider @@ -59,7 +60,9 @@ func createGoogleADKAgent(ctx context.Context, agentConfig *adk.AgentConfig, age toolsets := mcp.CreateToolsets(ctx, agentConfig.HttpTools, agentConfig.SseTools, agentConfig.StdioTools, propagateToken, dynamicHeaderProvider) skillsDirectory := agentConfig.SkillsDirectory if skillsDirectory == "" && legacySkillsEnv { - skillsDirectory = strings.TrimSpace(os.Getenv("KAGENT_SKILLS_FOLDER")) + if folder, set := env.KagentSkillsFolder.Lookup(); set { + skillsDirectory = strings.TrimSpace(folder) + } } if skillsDirectory != "" { skillsSource := skill.NewFileSystemSource(os.DirFS(skillsDirectory)) @@ -282,9 +285,9 @@ func CreateLLM(ctx context.Context, m adk.Model) (adkmodel.LLM, error) { return models.NewAzureOpenAIModel(ctx, cfg) case *adk.Gemini: - apiKey := os.Getenv("GOOGLE_API_KEY") + apiKey := env.GoogleAPIKey.Get() if apiKey == "" { - apiKey = os.Getenv("GEMINI_API_KEY") + apiKey = env.GeminiAPIKey.Get() } if apiKey == "" { return nil, fmt.Errorf("gemini model requires GOOGLE_API_KEY or GEMINI_API_KEY environment variable") @@ -303,10 +306,10 @@ func CreateLLM(ctx context.Context, m adk.Model) (adkmodel.LLM, error) { }) case *adk.GeminiVertexAI: - project := os.Getenv("GOOGLE_CLOUD_PROJECT") - location := os.Getenv("GOOGLE_CLOUD_LOCATION") + project := env.GoogleCloudProject.Get() + location := env.GoogleCloudLocation.Get() if location == "" { - location = os.Getenv("GOOGLE_CLOUD_REGION") + location = env.GoogleCloudRegion.Get() } if project == "" || location == "" { return nil, fmt.Errorf("GeminiVertexAI requires GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION (or GOOGLE_CLOUD_REGION) environment variables") @@ -354,7 +357,7 @@ func CreateLLM(ctx context.Context, m adk.Model) (adkmodel.LLM, error) { // then applies the cloud/local routing. Defaulting it to localhost here // would look like an operator-chosen endpoint and pin every cloud model // to the local daemon. - baseURL := os.Getenv("OLLAMA_API_BASE") + baseURL := env.OllamaAPIBase.Get() modelName := m.Model if modelName == "" { modelName = DefaultOllamaModel @@ -365,7 +368,7 @@ func CreateLLM(ctx context.Context, m adk.Model) (adkmodel.LLM, error) { Model: modelName, Host: baseURL, // The environment holds only the gateway credential placeholder. - APIKey: os.Getenv("OLLAMA_API_KEY"), + APIKey: env.OllamaAPIKey.Get(), Options: m.Options, } return models.NewOllamaModel(ctx, cfg) @@ -373,7 +376,7 @@ func CreateLLM(ctx context.Context, m adk.Model) (adkmodel.LLM, error) { case *adk.Bedrock: region := m.Region if region == "" { - region = os.Getenv("AWS_REGION") + region = env.AWSRegion.Get() } if region == "" { return nil, fmt.Errorf("bedrock requires AWS_REGION environment variable or region in model config") @@ -406,10 +409,10 @@ func CreateLLM(ctx context.Context, m adk.Model) (adkmodel.LLM, error) { case *adk.GeminiAnthropic: // GeminiAnthropic = Claude models accessed through Google Cloud Vertex AI. // Uses the Anthropic SDK's built-in Vertex AI support with Application Default Credentials. - project := os.Getenv("GOOGLE_CLOUD_PROJECT") - region := os.Getenv("GOOGLE_CLOUD_LOCATION") + project := env.GoogleCloudProject.Get() + region := env.GoogleCloudLocation.Get() if region == "" { - region = os.Getenv("GOOGLE_CLOUD_REGION") + region = env.GoogleCloudRegion.Get() } if project == "" || region == "" { return nil, fmt.Errorf("GeminiAnthropic (Anthropic on Vertex AI) requires GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables") diff --git a/go/adk/pkg/app/app.go b/go/adk/pkg/app/app.go index f658531b05..eaac8a2fca 100644 --- a/go/adk/pkg/app/app.go +++ b/go/adk/pkg/app/app.go @@ -209,10 +209,10 @@ func applyDefaults(cfg AppConfig) AppConfig { // buildAppName derives the app name from environment variables or agent card, // following the same convention as the Python KAgentConfig. func buildAppName(agentCard *a2atype.AgentCard) string { - kagentName := os.Getenv("KAGENT_NAME") - kagentNamespace := os.Getenv("KAGENT_NAMESPACE") + kagentName := env.KagentName.Get() + kagentNamespace, namespaceSet := env.KagentNamespace.Lookup() - if kagentNamespace != "" && kagentName != "" { + if namespaceSet && kagentNamespace != "" && kagentName != "" { namespace := strings.ReplaceAll(kagentNamespace, "-", "_") name := strings.ReplaceAll(kagentName, "-", "_") return namespace + "__NS__" + name diff --git a/go/adk/pkg/app/app_test.go b/go/adk/pkg/app/app_test.go index 62e13fbab8..489dc30ca0 100644 --- a/go/adk/pkg/app/app_test.go +++ b/go/adk/pkg/app/app_test.go @@ -3,6 +3,7 @@ package app import ( "context" "iter" + "os" "slices" "testing" "time" @@ -131,6 +132,13 @@ func TestBuildAppName_FromAgentCard(t *testing.T) { } } +func TestBuildAppName_UnsetNamespace(t *testing.T) { + t.Setenv(env.KagentName.Name(), "my-agent") + t.Setenv(env.KagentNamespace.Name(), "") + require.NoError(t, os.Unsetenv(env.KagentNamespace.Name())) + require.Equal(t, "card-name", buildAppName(&a2atype.AgentCard{Name: "card-name"})) +} + func TestBuildAppName_Default(t *testing.T) { t.Setenv("KAGENT_NAME", "") t.Setenv("KAGENT_NAMESPACE", "") diff --git a/go/adk/pkg/config/config_materialize.go b/go/adk/pkg/config/config_materialize.go index cd62680cf8..f744b77a7e 100644 --- a/go/adk/pkg/config/config_materialize.go +++ b/go/adk/pkg/config/config_materialize.go @@ -32,7 +32,7 @@ func MaterializeFromEnv(configDir string) error { } func materializeEnvToFile(envKey, path string) error { - value := strings.TrimSpace(os.Getenv(envKey)) + value := strings.TrimSpace(os.Getenv(envKey)) //nolint:forbidigo // Controller-generated payloads and credentials, not user configuration. if value == "" { return nil } @@ -67,7 +67,7 @@ func expandConfigEnv(raw string) (string, error) { const prefix, suffix = "__KAGENT_ENV[", "]__" if strings.HasPrefix(value, prefix) && strings.HasSuffix(value, suffix) { name := strings.TrimSuffix(strings.TrimPrefix(value, prefix), suffix) - resolved, ok := os.LookupEnv(name) + resolved, ok := os.LookupEnv(name) //nolint:forbidigo // Resolve arbitrary credential placeholders in compiled configuration. if !ok { return nil, fmt.Errorf("required environment variable %s is not set", name) } diff --git a/go/adk/pkg/embedding/embedding.go b/go/adk/pkg/embedding/embedding.go index dad54c3d17..576956e040 100644 --- a/go/adk/pkg/embedding/embedding.go +++ b/go/adk/pkg/embedding/embedding.go @@ -7,7 +7,6 @@ import ( "math" "net/http" "net/url" - "os" "sync" "time" @@ -19,6 +18,7 @@ import ( "github.com/kagent-dev/kagent/go/adk/pkg/internal/azureai" "github.com/kagent-dev/kagent/go/adk/pkg/models" "github.com/kagent-dev/kagent/go/api/adk" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" "github.com/ollama/ollama/api" "github.com/openai/openai-go/v3" @@ -99,7 +99,7 @@ type openAIProvider struct { } func newOpenAIProvider(cfg *adk.EmbeddingConfig) (*openAIProvider, error) { - apiKey := os.Getenv("OPENAI_API_KEY") + apiKey := env.OpenAIAPIKey.Get() httpClient, err := embeddingHTTPClient(cfg) if err != nil { return nil, err @@ -167,7 +167,7 @@ type azureOpenAIProvider struct { func newAzureOpenAIProvider(cfg *adk.EmbeddingConfig, cred azureai.TokenCredential) (*azureOpenAIProvider, error) { apiVersion := cfg.APIVersion if apiVersion == "" { - apiVersion = os.Getenv("OPENAI_API_VERSION") + apiVersion = env.OpenAIAPIVersion.Get() } if apiVersion == "" { apiVersion = "2024-02-15-preview" @@ -178,7 +178,7 @@ func newAzureOpenAIProvider(cfg *adk.EmbeddingConfig, cred azureai.TokenCredenti endpoint = cfg.BaseUrl } if endpoint == "" { - endpoint = os.Getenv("AZURE_OPENAI_ENDPOINT") + endpoint = env.AzureOpenAIEndpoint.Get() } if endpoint == "" { return nil, fmt.Errorf("Azure OpenAI endpoint must be set via endpoint, base_url, or AZURE_OPENAI_ENDPOINT env var") //nolint:staticcheck // ST1005: keep product name readable @@ -203,7 +203,7 @@ func newAzureOpenAIProvider(cfg *adk.EmbeddingConfig, cred azureai.TokenCredenti // token when APIKeyPassthrough is enabled (a placeholder Api-Key is // overwritten per request by embeddingPassthroughOpts), otherwise the // AZURE_OPENAI_API_KEY Api-Key header, otherwise DefaultAzureCredential. - apiKey := os.Getenv("AZURE_OPENAI_API_KEY") + apiKey := env.AzureOpenAIAPIKey.Get() if cfg.APIKeyPassthrough { apiKey = "passthrough" } @@ -236,7 +236,7 @@ type ollamaProvider struct { func newOllamaProvider(cfg *adk.EmbeddingConfig) (*ollamaProvider, error) { host := cfg.BaseUrl if host == "" { - host = os.Getenv("OLLAMA_API_BASE") + host = env.OllamaAPIBase.Get() } if host == "" { host = "http://localhost:11434" @@ -282,7 +282,7 @@ func (p *geminiProvider) generate(ctx context.Context, texts []string) ([][]floa p.once.Do(func() { client, err := genai.NewClient(ctx, &genai.ClientConfig{ - APIKey: os.Getenv("GOOGLE_API_KEY"), + APIKey: env.GoogleAPIKey.Get(), }) if err != nil { p.initErr = fmt.Errorf("failed to create genai client: %w", err) @@ -325,9 +325,9 @@ type bedrockProvider struct { func (p *bedrockProvider) generate(ctx context.Context, texts []string) ([][]float32, error) { log := logging.FromContext(ctx) - region := os.Getenv("AWS_DEFAULT_REGION") + region := env.AWSDefaultRegion.Get() if region == "" { - region = os.Getenv("AWS_REGION") + region = env.AWSRegion.Get() } if region == "" { region = "us-east-1" @@ -460,7 +460,7 @@ func newFoundryProvider(cfg *adk.EmbeddingConfig, cred azureai.TokenCredential) } // See newAzureOpenAIProvider - the passthrough placeholder short-circuits // past DefaultAzureCredential resolution the same way it does for chat. - apiKey := os.Getenv(azureai.FoundryAPIKeyEnvVar) + apiKey := env.FoundryAPIKey.Get() if cfg.APIKeyPassthrough { apiKey = "passthrough" } diff --git a/go/adk/pkg/internal/azureai/azureai.go b/go/adk/pkg/internal/azureai/azureai.go index 8d91d80798..0a7e3c66eb 100644 --- a/go/adk/pkg/internal/azureai/azureai.go +++ b/go/adk/pkg/internal/azureai/azureai.go @@ -11,7 +11,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -19,6 +18,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/anthropics/anthropic-sdk-go" anthropicoption "github.com/anthropics/anthropic-sdk-go/option" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" ) @@ -41,37 +41,24 @@ func resolveScope(scope string) string { return scope } -// Foundry-specific configuration conventions. These are the environment variables -// the controller injects for a Foundry ModelConfig plus the default api-version. -const ( - FoundryEndpointEnvVar = "FOUNDRY_ENDPOINT" - FoundryDeploymentEnvVar = "FOUNDRY_DEPLOYMENT" - FoundryAPIVersionEnvVar = "FOUNDRY_API_VERSION" - FoundryAPIKeyEnvVar = "FOUNDRY_API_KEY" -) - -// FoundryDefaultAPIVersion is the Foundry OpenAI-compatible data-plane API -// version used when none is configured. -const FoundryDefaultAPIVersion = "2024-10-21" - // ResolveFoundry applies FOUNDRY_* environment-variable fallbacks and the default // api-version. Empty endpoint/deployment are returned as-is so callers can // produce context-specific validation errors. func ResolveFoundry(endpoint, deployment, apiVersion string) (ep, dep, ver string) { ep = endpoint if ep == "" { - ep = os.Getenv(FoundryEndpointEnvVar) + ep = env.FoundryEndpoint.Get() } dep = deployment if dep == "" { - dep = os.Getenv(FoundryDeploymentEnvVar) + dep = env.FoundryDeployment.Get() } ver = apiVersion if ver == "" { - ver = os.Getenv(FoundryAPIVersionEnvVar) + ver = env.FoundryAPIVersion.Get() } if ver == "" { - ver = FoundryDefaultAPIVersion + ver = env.FoundryAPIVersion.DefaultValue() } return ep, dep, ver } diff --git a/go/adk/pkg/internal/azureai/azureai_test.go b/go/adk/pkg/internal/azureai/azureai_test.go index 7db13465c5..c9c989699f 100644 --- a/go/adk/pkg/internal/azureai/azureai_test.go +++ b/go/adk/pkg/internal/azureai/azureai_test.go @@ -11,6 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/anthropics/anthropic-sdk-go" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/openai/openai-go/v3" ) @@ -245,9 +246,9 @@ func TestNewAnthropicClientWorkloadIdentity(t *testing.T) { } func TestResolveFoundryUsesProvidedValues(t *testing.T) { - t.Setenv(FoundryEndpointEnvVar, "env-endpoint") - t.Setenv(FoundryDeploymentEnvVar, "env-deployment") - t.Setenv(FoundryAPIVersionEnvVar, "env-version") + t.Setenv(env.FoundryEndpoint.Name(), "env-endpoint") + t.Setenv(env.FoundryDeployment.Name(), "env-deployment") + t.Setenv(env.FoundryAPIVersion.Name(), "env-version") ep, dep, ver := ResolveFoundry("cfg-endpoint", "cfg-deployment", "cfg-version") if ep != "cfg-endpoint" || dep != "cfg-deployment" || ver != "cfg-version" { @@ -256,9 +257,9 @@ func TestResolveFoundryUsesProvidedValues(t *testing.T) { } func TestResolveFoundryFallsBackToEnv(t *testing.T) { - t.Setenv(FoundryEndpointEnvVar, "env-endpoint") - t.Setenv(FoundryDeploymentEnvVar, "env-deployment") - t.Setenv(FoundryAPIVersionEnvVar, "env-version") + t.Setenv(env.FoundryEndpoint.Name(), "env-endpoint") + t.Setenv(env.FoundryDeployment.Name(), "env-deployment") + t.Setenv(env.FoundryAPIVersion.Name(), "env-version") ep, dep, ver := ResolveFoundry("", "", "") if ep != "env-endpoint" || dep != "env-deployment" || ver != "env-version" { @@ -267,12 +268,12 @@ func TestResolveFoundryFallsBackToEnv(t *testing.T) { } func TestResolveFoundryDefaultsAPIVersion(t *testing.T) { - t.Setenv(FoundryEndpointEnvVar, "") - t.Setenv(FoundryDeploymentEnvVar, "") - t.Setenv(FoundryAPIVersionEnvVar, "") + t.Setenv(env.FoundryEndpoint.Name(), "") + t.Setenv(env.FoundryDeployment.Name(), "") + t.Setenv(env.FoundryAPIVersion.Name(), "") ep, dep, ver := ResolveFoundry("e", "d", "") - if ep != "e" || dep != "d" || ver != FoundryDefaultAPIVersion { + if ep != "e" || dep != "d" || ver != env.FoundryAPIVersion.DefaultValue() { t.Fatalf("ResolveFoundry() = (%q, %q, %q), want default api-version", ep, dep, ver) } } diff --git a/go/adk/pkg/models/anthropic.go b/go/adk/pkg/models/anthropic.go index 231a2089f8..50b8bf11a5 100644 --- a/go/adk/pkg/models/anthropic.go +++ b/go/adk/pkg/models/anthropic.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "log/slog" - "os" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/bedrock" "github.com/anthropics/anthropic-sdk-go/option" "github.com/anthropics/anthropic-sdk-go/vertex" awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" ) @@ -47,7 +47,7 @@ type AnthropicModel struct { func NewAnthropicModel(ctx context.Context, config *AnthropicConfig) (*AnthropicModel, error) { apiKey := "passthrough" // placeholder; real auth set per-request by transport if !config.APIKeyPassthrough { - apiKey = os.Getenv("ANTHROPIC_API_KEY") + apiKey = env.AnthropicAPIKey.Get() if apiKey == "" { return nil, fmt.Errorf("ANTHROPIC_API_KEY environment variable is not set") } diff --git a/go/adk/pkg/models/foundry.go b/go/adk/pkg/models/foundry.go index 520f847de0..679c3416f7 100644 --- a/go/adk/pkg/models/foundry.go +++ b/go/adk/pkg/models/foundry.go @@ -3,10 +3,10 @@ package models import ( "context" "fmt" - "os" "strings" "github.com/kagent-dev/kagent/go/adk/pkg/internal/azureai" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" ) @@ -68,7 +68,7 @@ func NewFoundryModel(ctx context.Context, config *FoundryConfig) (*OpenAIModel, // otherwise the API key when provided, otherwise DefaultAzureCredential // (Workload Identity in-cluster, az CLI in dev), eagerly probed so a // misconfigured identity fails readiness at startup. - apiKey := os.Getenv(azureai.FoundryAPIKeyEnvVar) + apiKey := env.FoundryAPIKey.Get() if config.APIKeyPassthrough { apiKey = "passthrough" } @@ -141,7 +141,7 @@ func NewFoundryAnthropicModel(ctx context.Context, config *AnthropicConfig, endp HTTPClient: httpClient, } - apiKey := os.Getenv(azureai.FoundryAPIKeyEnvVar) + apiKey := env.FoundryAPIKey.Get() if config.APIKeyPassthrough { // Placeholder x-api-key overwritten per request by anthropicPassthroughOpts. apiKey = "passthrough" diff --git a/go/adk/pkg/models/mistral.go b/go/adk/pkg/models/mistral.go index e84a0a6481..ab9e0711f2 100644 --- a/go/adk/pkg/models/mistral.go +++ b/go/adk/pkg/models/mistral.go @@ -4,8 +4,8 @@ import ( "context" "fmt" "log/slog" - "os" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" @@ -45,7 +45,7 @@ func NewMistralModel(ctx context.Context, config *MistralConfig) (*MistralModel, logger := logging.FromContext(ctx) apiKey := "passthrough" // placeholder; real auth set per-request by transport if !config.APIKeyPassthrough { - apiKey = os.Getenv("MISTRAL_API_KEY") + apiKey = env.MistralAPIKey.Get() if apiKey == "" { return nil, fmt.Errorf("MISTRAL_API_KEY environment variable is not set") } @@ -53,7 +53,7 @@ func NewMistralModel(ctx context.Context, config *MistralConfig) (*MistralModel, baseURL := config.BaseUrl if baseURL == "" { - baseURL = os.Getenv("MISTRAL_API_BASE") + baseURL = env.MistralAPIBase.Get() } if baseURL == "" { baseURL = DefaultMistralBaseURL diff --git a/go/adk/pkg/models/ollama.go b/go/adk/pkg/models/ollama.go index 8389c2abea..424e7929df 100644 --- a/go/adk/pkg/models/ollama.go +++ b/go/adk/pkg/models/ollama.go @@ -5,10 +5,10 @@ import ( "fmt" "log/slog" "net/url" - "os" "strconv" "strings" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" "github.com/ollama/ollama/api" ) @@ -288,7 +288,7 @@ func NewOllamaModel(ctx context.Context, config *OllamaConfig) (*OllamaModel, er logger := logging.FromContext(ctx) host := config.Host if host == "" { - host = os.Getenv("OLLAMA_API_BASE") + host = env.OllamaAPIBase.Get() } host = resolveOllamaEndpoint(config, host) diff --git a/go/adk/pkg/models/openai.go b/go/adk/pkg/models/openai.go index 58d6ca70f6..bb0cd2e106 100644 --- a/go/adk/pkg/models/openai.go +++ b/go/adk/pkg/models/openai.go @@ -4,9 +4,9 @@ import ( "context" "fmt" "log/slog" - "os" "github.com/kagent-dev/kagent/go/adk/pkg/internal/azureai" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" @@ -107,7 +107,7 @@ func resolveOpenAIAPIKey(ctx context.Context, config *OpenAIConfig) (string, err if config.APIKey != "" { return config.APIKey, nil } - if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" { + if apiKey := env.OpenAIAPIKey.Get(); apiKey != "" { return apiKey, nil } if config.BaseUrl == "" { @@ -135,7 +135,7 @@ func NewAzureOpenAIModel(ctx context.Context, config *AzureOpenAIConfig) (*OpenA logger := logging.FromContext(ctx) endpoint := config.Endpoint if endpoint == "" { - endpoint = os.Getenv("AZURE_OPENAI_ENDPOINT") + endpoint = env.AzureOpenAIEndpoint.Get() } if endpoint == "" { return nil, fmt.Errorf("AZURE_OPENAI_ENDPOINT environment variable is not set") @@ -143,7 +143,7 @@ func NewAzureOpenAIModel(ctx context.Context, config *AzureOpenAIConfig) (*OpenA apiVersion := config.APIVersion if apiVersion == "" { - apiVersion = os.Getenv("OPENAI_API_VERSION") + apiVersion = env.OpenAIAPIVersion.Get() } if apiVersion == "" { apiVersion = "2024-02-15-preview" @@ -170,7 +170,7 @@ func NewAzureOpenAIModel(ctx context.Context, config *AzureOpenAIConfig) (*OpenA // (a placeholder Api-Key is overwritten per request by openAIPassthroughOpts), // otherwise the AZURE_OPENAI_API_KEY Api-Key header, otherwise // DefaultAzureCredential (Workload Identity), eagerly probed for readiness. - apiKey := os.Getenv("AZURE_OPENAI_API_KEY") + apiKey := env.AzureOpenAIAPIKey.Get() if config.APIKeyPassthrough { apiKey = "passthrough" } diff --git a/go/adk/pkg/models/sapaicore.go b/go/adk/pkg/models/sapaicore.go index ea30154021..dc149a2d5a 100644 --- a/go/adk/pkg/models/sapaicore.go +++ b/go/adk/pkg/models/sapaicore.go @@ -6,13 +6,13 @@ import ( "fmt" "net/http" "net/url" - "os" "strings" "sync" "time" "log/slog" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" ) @@ -58,8 +58,8 @@ func (m *SAPAICoreModel) ensureToken(ctx context.Context) (string, error) { return m.token, nil } - clientID := os.Getenv("SAP_AI_CORE_CLIENT_ID") - clientSecret := os.Getenv("SAP_AI_CORE_CLIENT_SECRET") + clientID := env.SAPAICoreClientID.Get() + clientSecret := env.SAPAICoreClientSecret.Get() if m.Config.AuthUrl == "" || clientID == "" || clientSecret == "" { return "", fmt.Errorf("SAP AI Core requires auth_url + SAP_AI_CORE_CLIENT_ID/SECRET env vars") } diff --git a/go/adk/pkg/runner/adapter.go b/go/adk/pkg/runner/adapter.go index 71c40dac00..1d32161fcc 100644 --- a/go/adk/pkg/runner/adapter.go +++ b/go/adk/pkg/runner/adapter.go @@ -3,7 +3,6 @@ package runner import ( "context" "fmt" - "os" "strings" "log/slog" @@ -13,6 +12,7 @@ import ( kagentmemory "github.com/kagent-dev/kagent/go/adk/pkg/memory" "github.com/kagent-dev/kagent/go/adk/pkg/sts" "github.com/kagent-dev/kagent/go/api/adk" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" adkmemory "google.golang.org/adk/v2/memory" adkplugin "google.golang.org/adk/v2/plugin" @@ -107,8 +107,8 @@ func CreateRunnerConfig( } func buildTokenPropagationPlugin(ctx context.Context, log *slog.Logger) (*sts.TokenPropagationPlugin, error) { - propagateToken := strings.EqualFold(strings.TrimSpace(os.Getenv("KAGENT_PROPAGATE_TOKEN")), "true") - stsWellKnownURI := strings.TrimSpace(os.Getenv("KAGENT_STS_WELL_KNOWN_URI")) + propagateToken := strings.EqualFold(strings.TrimSpace(env.KagentPropagateToken.Get()), "true") + stsWellKnownURI := strings.TrimSpace(env.StsWellKnownURI.Get()) if !propagateToken && stsWellKnownURI == "" { return nil, nil } @@ -135,8 +135,8 @@ func buildTokenPropagationPlugin(ctx context.Context, log *slog.Logger) (*sts.To // RFC 8707 resource / RFC 8693 audience scope the exchanged token to a // backend. Both are repeatable; empty values are omitted from the request. - resource := splitCSV(os.Getenv("KAGENT_STS_RESOURCE")) - audience := splitCSV(os.Getenv("KAGENT_STS_AUDIENCE")) + resource := splitCSV(env.KagentSTSResource.Get()) + audience := splitCSV(env.KagentSTSAudience.Get()) log.InfoContext(ctx, "enabling STS token propagation plugin", "well_known_uri", stsWellKnownURI) return sts.NewTokenPropagationPlugin(integration, log, resource, audience), nil diff --git a/go/adk/pkg/tools/skills.go b/go/adk/pkg/tools/skills.go index ab77bab733..bb77690d76 100644 --- a/go/adk/pkg/tools/skills.go +++ b/go/adk/pkg/tools/skills.go @@ -6,38 +6,12 @@ import ( "path/filepath" "strings" + "github.com/kagent-dev/kagent/go/core/pkg/env" adkagent "google.golang.org/adk/v2/agent" "google.golang.org/adk/v2/tool" "google.golang.org/adk/v2/tool/functiontool" ) -// enableFileSearchToolsEnv gates the list_files and grep_file tools, which -// are opt-in (disabled by default): they let an agent enumerate and search -// the filesystem under its session/skills roots without invoking a shell, so -// deployments that want to grant that visibility deliberately can, rather -// than having it enabled implicitly. -// -// Also registered (separately, for `kagent env` CLI discoverability only, -// not read here) as KagentEnableFileSearchTools in go/core/pkg/env/kagent.go. -// TestEnableFileSearchToolsEnvMatchesRegistry pins the two literals together -// so they cannot drift. -const enableFileSearchToolsEnv = "KAGENT_ENABLE_FILE_SEARCH_TOOLS" - -// fileSearchToolsEnabled accepts the same case-insensitive true-values as -// Python's file_search_tools_enabled() (kagent-skills/shell.py), so the -// same literal env var value behaves identically in either runtime rather -// than relying on Go's strconv.ParseBool grammar, which Python doesn't -// replicate exactly (e.g. ParseBool requires the exact casing "True", not -// "tRue"). -func fileSearchToolsEnabled() bool { - switch strings.ToLower(strings.TrimSpace(os.Getenv(enableFileSearchToolsEnv))) { - case "1", "t", "true": - return true - default: - return false - } -} - const ( readFileDescription = `Reads a file from the filesystem with line numbers. @@ -233,7 +207,7 @@ func NewSkillExecutionTools(skillsDirectory string) ([]tool.Tool, error) { // list_files/grep_file are opt-in: they give an agent broad filesystem // visibility, so deployments enable them deliberately. Note this gate is // theirs alone -- bash below is always registered. - fileSearchEnabled := fileSearchToolsEnabled() + fileSearchEnabled := env.KagentEnableFileSearchTools.Get() if fileSearchEnabled { listFilesTool, err := functiontool.New(functiontool.Config{ Name: "list_files", diff --git a/go/adk/pkg/tools/skills_test.go b/go/adk/pkg/tools/skills_test.go index a0b20442b4..c97c4299d1 100644 --- a/go/adk/pkg/tools/skills_test.go +++ b/go/adk/pkg/tools/skills_test.go @@ -8,27 +8,11 @@ import ( "testing" "unicode/utf8" - "github.com/kagent-dev/kagent/go/core/pkg/env" adkagent "google.golang.org/adk/v2/agent" "google.golang.org/adk/v2/tool" "google.golang.org/adk/v2/tool/toolconfirmation" ) -// TestEnableFileSearchToolsEnvMatchesRegistry pins this package's env var -// literal to the `kagent env` registry entry in go/core/pkg/env. The name is -// declared twice on purpose -- the runtime reads the raw variable here, while -// the registry exists solely so `kagent env` can document it -- so without -// this assertion nothing would catch the two drifting apart. -// -// This is a test-only import: it does not put a dependency on the -// control-plane module into the shipped agent runtime. -func TestEnableFileSearchToolsEnvMatchesRegistry(t *testing.T) { - if got, want := enableFileSearchToolsEnv, env.KagentEnableFileSearchTools.Name(); got != want { - t.Fatalf("env var name drift: %s has %q, go/core/pkg/env registry has %q", - "go/adk/pkg/tools/skills.go", got, want) - } -} - // fakeToolContext is a minimal agent.Context for directly invoking tools in // tests, bypassing the full ADK flow engine. Embeds StrictContextMock (an // ADK test double) and overrides only the methods functiontool.Run() calls. diff --git a/go/core/cli/internal/commands/const.go b/go/core/cli/internal/commands/const.go index 09b16935a1..7cda463f8b 100644 --- a/go/core/cli/internal/commands/const.go +++ b/go/core/cli/internal/commands/const.go @@ -1,17 +1,13 @@ package commands import ( - "os" "strings" "github.com/kagent-dev/kagent/go/api/v1alpha3" "github.com/kagent-dev/kagent/go/core/pkg/env" ) -const ( - DefaultModelProvider = v1alpha3.ModelProviderOpenAI - DefaultHelmOciRegistry = "oci://ghcr.io/kagent-dev/kagent/helm/" -) +const DefaultModelProvider = v1alpha3.ModelProviderOpenAI // GetModelProvider returns the model provider from KAGENT_DEFAULT_MODEL_PROVIDER environment variable func GetModelProvider() v1alpha3.ModelProvider { @@ -50,41 +46,24 @@ func GetModelProviderHelmValuesKey(provider v1alpha3.ModelProvider) string { return helmKey } -// GetProviderAPIKey returns the env var name for the provider's API key. -// Returns "" for providers that use cloud credentials instead of an API key -// (Bedrock, GeminiVertexAI, AnthropicVertexAI). Ollama returns OLLAMA_API_KEY: -// it is needed only for Ollama Cloud, so callers treat it as optional. -func GetProviderAPIKey(provider v1alpha3.ModelProvider) string { +// providerAPIKey selects the registered API key for a provider. Cloud credential +// providers have no API-key setting. +func providerAPIKey(provider v1alpha3.ModelProvider) (env.StringVar, bool) { switch provider { case v1alpha3.ModelProviderOpenAI: - return env.OpenAIAPIKey.Name() + return env.OpenAIAPIKey, true case v1alpha3.ModelProviderAnthropic: - return env.AnthropicAPIKey.Name() + return env.AnthropicAPIKey, true case v1alpha3.ModelProviderAzureOpenAI: - return env.AzureOpenAIAPIKey.Name() + return env.AzureOpenAIAPIKey, true case v1alpha3.ModelProviderOllama: - // Ollama Cloud authenticates with OLLAMA_API_KEY. A local daemon needs - // none, so the key is optional here rather than required; the runtime - // falls back to the daemon when it is absent. - return env.OllamaAPIKey.Name() + return env.OllamaAPIKey, true case v1alpha3.ModelProviderGemini: - // Prefer GOOGLE_API_KEY, fall back to GEMINI_API_KEY to match the - // runtime behaviour in go/adk/pkg/agent/agent.go. - if _, ok := os.LookupEnv(env.GoogleAPIKey.Name()); ok { - return env.GoogleAPIKey.Name() + if _, set := env.GoogleAPIKey.Lookup(); set { + return env.GoogleAPIKey, true } - return "GEMINI_API_KEY" + return env.GeminiAPIKey, true default: - // Bedrock, GeminiVertexAI, AnthropicVertexAI use cloud credentials - // rather than a simple API key, so no check is needed. - return "" - } -} - -// GetEnvVarWithDefault returns the value of the environment variable if it exists, otherwise returns the default value -func GetEnvVarWithDefault(envVar, defaultValue string) string { - if value, exists := os.LookupEnv(envVar); exists { - return value + return env.StringVar{}, false } - return defaultValue } diff --git a/go/core/cli/internal/commands/db/db.go b/go/core/cli/internal/commands/db/db.go index f906d12887..8d53798fa5 100644 --- a/go/core/cli/internal/commands/db/db.go +++ b/go/core/cli/internal/commands/db/db.go @@ -48,17 +48,14 @@ func NewDBCmd() *cobra.Command { // precedence, on: the KAGENT_DATABASE_VECTOR_ENABLED env var in the CLI's own // environment (explicit operator intent, works without a cluster), the // controller's configmap on the live cluster (the same value the server -// reads), and finally the controller's default (enabled). +// reads), and finally the CLI's fallback (enabled). func migrationSources(namespace *string) dbmigrate.SourcesFunc { return func(ctx context.Context) ([]migrations.Source, error) { vectorEnabled := true - if v := os.Getenv(vectorEnabledKey); v != "" { - b, err := strconv.ParseBool(v) - if err != nil { - fmt.Fprintf(os.Stderr, "warning: invalid %s=%q; assuming true\n", vectorEnabledKey, v) - } else { - vectorEnabled = b - } + if b, set, err := kagentenv.DatabaseVectorEnabled.LookupWithError(); err != nil { + fmt.Fprintf(os.Stderr, "warning: %v; assuming true\n", err) + } else if set { + vectorEnabled = b } else if b, ok := clusterVectorEnabled(ctx, *namespace); ok { vectorEnabled = b } diff --git a/go/core/cli/internal/commands/db/db_test.go b/go/core/cli/internal/commands/db/db_test.go new file mode 100644 index 0000000000..4992c0d3f7 --- /dev/null +++ b/go/core/cli/internal/commands/db/db_test.go @@ -0,0 +1,31 @@ +package db + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/core/pkg/env" + "github.com/stretchr/testify/require" +) + +func TestMigrationSourcesFromEnvironment(t *testing.T) { + for _, tt := range []struct { + input string + want []string + }{ + {input: "false", want: []string{"core"}}, + {input: " TrUe ", want: []string{"core", "vector"}}, + {input: "invalid", want: []string{"core", "vector"}}, + } { + t.Run(tt.input, func(t *testing.T) { + t.Setenv(env.DatabaseVectorEnabled.Name(), tt.input) + namespace := "unused" + sources, err := migrationSources(&namespace)(t.Context()) + require.NoError(t, err) + var names []string + for _, source := range sources { + names = append(names, source.Name) + } + require.Equal(t, tt.want, names) + }) + } +} diff --git a/go/core/cli/internal/commands/install.go b/go/core/cli/internal/commands/install.go index 15b0d53eaf..c6afca4feb 100644 --- a/go/core/cli/internal/commands/install.go +++ b/go/core/cli/internal/commands/install.go @@ -78,15 +78,15 @@ func runInstall(ctx context.Context, options connection.Options, cfg *InstallCfg // get model provider from KAGENT_DEFAULT_MODEL_PROVIDER environment variable or use DefaultModelProvider modelProvider := GetModelProvider() - // If model provider is openai, check if the API key is set - apiKeyName := GetProviderAPIKey(modelProvider) - apiKeyValue := os.Getenv(apiKeyName) - - if apiKeyName != "" && apiKeyValue == "" { - fmt.Fprintf(os.Stderr, "%s is not set\n", apiKeyName) - fmt.Fprintf(os.Stderr, "Please set the %s environment variable\n", apiKeyName) - fmt.Fprintf(os.Stderr, "To use a different provider set KAGENT_DEFAULT_MODEL_PROVIDER (e.g. ollama, anthropic, gemini)\n") - return nil + apiKeyValue := "" + if apiKey, ok := providerAPIKey(modelProvider); ok { + apiKeyValue = apiKey.Get() + if apiKeyValue == "" { + fmt.Fprintf(os.Stderr, "%s is not set\n", apiKey.Name()) + fmt.Fprintf(os.Stderr, "Please set the %s environment variable\n", apiKey.Name()) + fmt.Fprintf(os.Stderr, "To use a different provider set KAGENT_DEFAULT_MODEL_PROVIDER (e.g. ollama, anthropic, gemini)\n") + return nil + } } helmConfig := setupHelmConfig(modelProvider, apiKeyValue) @@ -125,9 +125,12 @@ func setupHelmConfig(modelProvider v1alpha3.ModelProvider, apiKeyValue string) h } // allow user to set the helm registry and version - helmRegistry := GetEnvVarWithDefault(env.KagentHelmRepo.Name(), DefaultHelmOciRegistry) - helmVersion := GetEnvVarWithDefault(env.KagentHelmVersion.Name(), version.Version) - helmExtraArgs := GetEnvVarWithDefault(env.KagentHelmExtraArgs.Name(), "") + helmRegistry := env.KagentHelmRepo.Get() + helmVersion, versionSet := env.KagentHelmVersion.Lookup() + if !versionSet { + helmVersion = version.Version + } + helmExtraArgs := env.KagentHelmExtraArgs.Get() // split helmExtraArgs by "--set" to get additional values extraValues := strings.Split(helmExtraArgs, "--set") diff --git a/go/core/cli/internal/db/migrate/migrate.go b/go/core/cli/internal/db/migrate/migrate.go index 028bf637a0..614836b8c7 100644 --- a/go/core/cli/internal/db/migrate/migrate.go +++ b/go/core/cli/internal/db/migrate/migrate.go @@ -8,12 +8,12 @@ import ( "fmt" "io" "io/fs" - "os" "regexp" "slices" "strconv" "strings" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/pressly/goose/v3" "github.com/spf13/cobra" @@ -21,7 +21,6 @@ import ( ) const ( - dbURLEnv = "KAGENT_POSTGRES_DATABASE_URL" sourceFlag = "source" ) @@ -108,10 +107,12 @@ The command reads KAGENT_POSTGRES_DATABASE_URL when --db-url is empty.`, func (s *commandState) resolveDSN() (string, error) { dsn := strings.TrimSpace(s.dbURL) if dsn == "" { - dsn = os.Getenv(dbURLEnv) + if value, set := env.PostgresDatabaseURL.Lookup(); set { + dsn = value + } } if dsn == "" { - return "", fmt.Errorf("set the database URL with --db-url or %s", dbURLEnv) + return "", fmt.Errorf("set the database URL with --db-url or %s", env.PostgresDatabaseURL.Name()) } return dsn, nil } diff --git a/go/core/cli/internal/db/migrate/migrate_test.go b/go/core/cli/internal/db/migrate/migrate_test.go index fd7a885f09..c07fe90f50 100644 --- a/go/core/cli/internal/db/migrate/migrate_test.go +++ b/go/core/cli/internal/db/migrate/migrate_test.go @@ -6,11 +6,13 @@ import ( "encoding/json" "errors" "io" + "os" "strings" "testing" "testing/fstest" "github.com/kagent-dev/kagent/go/core/internal/dbtest" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/core/pkg/migrations" ) @@ -90,19 +92,26 @@ func TestNewCommandAcceptsHyphenatedNames(t *testing.T) { func TestResolveDSN(t *testing.T) { tests := []struct { - name string - flag string - env string - want string - wantErr bool + name string + flag string + env string + want string + wantErr bool + unsetEnv bool }{ {name: "flag wins over env", flag: "postgres://flag", env: "postgres://env", want: "postgres://flag"}, {name: "env fallback", env: "postgres://env", want: "postgres://env"}, {name: "neither set", wantErr: true}, + {name: "unset env does not use controller default", unsetEnv: true, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv(dbURLEnv, tt.env) + t.Setenv(env.PostgresDatabaseURL.Name(), tt.env) + if tt.unsetEnv { + if err := os.Unsetenv(env.PostgresDatabaseURL.Name()); err != nil { + t.Fatal(err) + } + } s := &commandState{dbURL: tt.flag} got, err := s.resolveDSN() if (err != nil) != tt.wantErr { @@ -175,7 +184,7 @@ func TestArgValidation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv(dbURLEnv, "") + t.Setenv(env.PostgresDatabaseURL.Name(), "") _, _, err := runCLI(t, testSources(), tt.args...) if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %v, want containing %q", err, tt.wantErr) diff --git a/go/core/hack/mockllm/main.go b/go/core/hack/mockllm/main.go index 7fad59f7a0..50886d9e77 100644 --- a/go/core/hack/mockllm/main.go +++ b/go/core/hack/mockllm/main.go @@ -7,6 +7,7 @@ import ( "os" "strconv" + "github.com/kagent-dev/kagent/go/core/pkg/env" e2emocks "github.com/kagent-dev/kagent/go/core/test/e2e/mocks" "github.com/kagent-dev/kagent/go/pkg/logging" "github.com/kagent-dev/mockllm" @@ -21,7 +22,7 @@ func main() { ctx := logging.IntoContext(context.Background(), logger) agentServiceAccount := "system:serviceaccount:kagent:test-sts" stsPort := 8091 - if port := os.Getenv("KAGENT_STS_PORT"); port != "" { + if port := env.STSPort.Get(); port != "" { stsPort, _ = strconv.Atoi(port) } stsServer := e2emocks.NewMockSTSServer(agentServiceAccount, uint16(stsPort)) @@ -40,7 +41,7 @@ func main() { return } mockllmCfg.ListenAddr = ":8090" - if port := os.Getenv("KAGENT_LLM_PORT"); port != "" { + if port := env.LLMPort.Get(); port != "" { mockllmCfg.ListenAddr = ":" + port } server := mockllm.NewServer(mockllmCfg) diff --git a/go/core/internal/translator/otel.go b/go/core/internal/translator/otel.go index dec034ed4d..7008da0db3 100644 --- a/go/core/internal/translator/otel.go +++ b/go/core/internal/translator/otel.go @@ -3,11 +3,11 @@ package translator import ( "fmt" "net/url" - "os" "strconv" "strings" "github.com/kagent-dev/kagent/go/api/v1alpha3" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/telemetry" "github.com/kagent-dev/kagent/go/pkg/telemetry/conv" "github.com/kagent-dev/kagent/go/pkg/tracing" @@ -17,14 +17,9 @@ import ( ) const ( - otelSDKDisabled = "OTEL_SDK_DISABLED" - otelServiceName = "OTEL_SERVICE_NAME" - otelResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES" - otelExporterOTLPEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT" - otelExporterOTLPProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL" - otelExporterOTLPTimeout = "OTEL_EXPORTER_OTLP_TIMEOUT" - otelCaptureRawAPIBodies = "KAGENT_OTEL_CAPTURE_RAW_API_BODIES" - otelMaxCaptureBytes = "KAGENT_OTEL_MAX_CAPTURE_BYTES" + otelServiceName = "OTEL_SERVICE_NAME" + otelResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES" + // OperatorResourceAttributesVariable carries the operator's resource // attributes. The controller's own OTEL_RESOURCE_ATTRIBUTES also names its // pod, which no runtime may inherit. @@ -44,16 +39,24 @@ const ( var signals = []Signal{SignalTraces, SignalMetrics, SignalLogs} +var signalVariables = map[Signal]struct { + exporter, endpoint, protocol env.StringVar +}{ + SignalTraces: {env.OtelTracesExporter, env.OtelExporterOTLPTracesEndpoint, env.OtelExporterOTLPTracesProtocol}, + SignalMetrics: {env.OtelMetricsExporter, env.OtelExporterOTLPMetricsEndpoint, env.OtelExporterOTLPMetricsProtocol}, + SignalLogs: {env.OtelLogsExporter, env.OtelExporterOTLPLogsEndpoint, env.OtelExporterOTLPLogsProtocol}, +} + func (s Signal) exporterVariable() string { - return "OTEL_" + strings.ToUpper(string(s)) + "_EXPORTER" + return signalVariables[s].exporter.Name() } func (s Signal) endpointVariable() string { - return "OTEL_EXPORTER_OTLP_" + strings.ToUpper(string(s)) + "_ENDPOINT" + return signalVariables[s].endpoint.Name() } func (s Signal) protocolVariable() string { - return "OTEL_EXPORTER_OTLP_" + strings.ToUpper(string(s)) + "_PROTOCOL" + return signalVariables[s].protocol.Name() } // TelemetryConfig is the controller-owned telemetry configuration compiled @@ -90,12 +93,12 @@ type SignalConfig struct { // observability configuration cannot invalidate AgentTemplates. func TelemetryConfigFromProcess() (TelemetryConfig, []error) { config := TelemetryConfig{ - Endpoint: strings.TrimSpace(os.Getenv(otelExporterOTLPEndpoint)), - Protocol: strings.ToLower(strings.TrimSpace(os.Getenv(otelExporterOTLPProtocol))), - CaptureRawAPIBodies: strings.EqualFold(strings.TrimSpace(os.Getenv(otelCaptureRawAPIBodies)), "true"), + Endpoint: strings.TrimSpace(env.OtelExporterOTLPEndpoint.Get()), + Protocol: strings.ToLower(strings.TrimSpace(env.OtelExporterOTLPProtocol.Get())), + CaptureRawAPIBodies: env.OtelCaptureRawAPIBodies.Get(), } var warnings []error - disabled := strings.EqualFold(strings.TrimSpace(os.Getenv(otelSDKDisabled)), "true") + disabled := strings.EqualFold(strings.TrimSpace(env.OtelSDKDisabled.Get()), "true") for _, signal := range signals { resolved, err := config.signalFromProcess(signal, disabled) if err != nil { @@ -106,9 +109,9 @@ func TelemetryConfigFromProcess() (TelemetryConfig, []error) { if config.Protocol == "" { config.Protocol = defaultOTLPProtocol } - if raw := strings.TrimSpace(os.Getenv(otelExporterOTLPTimeout)); raw != "" { + if raw := strings.TrimSpace(env.OtelExporterOTLPTimeout.Get()); raw != "" { if value, err := strconv.Atoi(raw); err != nil || value <= 0 { - warnings = append(warnings, fmt.Errorf("%s must be a positive number of milliseconds", otelExporterOTLPTimeout)) + warnings = append(warnings, fmt.Errorf("%s must be a positive number of milliseconds", env.OtelExporterOTLPTimeout.Name())) } else { config.Timeout = raw } @@ -118,7 +121,7 @@ func TelemetryConfigFromProcess() (TelemetryConfig, []error) { warnings = append(warnings, err) } config.ResourceAttributes = attributes - switch capture := strings.TrimSpace(os.Getenv(tracing.CaptureContentEnvironmentVariable)); capture { + switch capture := strings.TrimSpace(env.OtelCaptureMessageContent.Get()); capture { case tracing.CaptureContentSpanOnly: config.CaptureSensitiveContent = true case "", "NO_CONTENT", tracing.CaptureContentDisabled: @@ -147,15 +150,15 @@ func (c *TelemetryConfig) signal(signal Signal) *SignalConfig { // signalFromProcess resolves one signal. Only an explicit otlp exporter is // forwarded; the chart always renders one. func (c TelemetryConfig) signalFromProcess(signal Signal, disabled bool) (SignalConfig, error) { - switch exporter := strings.TrimSpace(os.Getenv(signal.exporterVariable())); { + switch exporter := strings.TrimSpace(signalVariables[signal].exporter.Get()); { case disabled || exporter == "" || exporter == "none": return SignalConfig{}, nil case exporter != "otlp": return SignalConfig{}, fmt.Errorf("%s must be otlp or none, not %q", signal.exporterVariable(), exporter) } resolved := SignalConfig{ - EndpointOverride: strings.TrimSpace(os.Getenv(signal.endpointVariable())), - ProtocolOverride: strings.ToLower(strings.TrimSpace(os.Getenv(signal.protocolVariable()))), + EndpointOverride: strings.TrimSpace(signalVariables[signal].endpoint.Get()), + ProtocolOverride: strings.ToLower(strings.TrimSpace(signalVariables[signal].protocol.Get())), } endpoint := resolved.EndpointOverride if endpoint == "" { @@ -186,7 +189,7 @@ func (c TelemetryConfig) signalFromProcess(signal Signal, disabled bool) (Signal } func resourceAttributesFromProcess() (string, error) { - raw := strings.TrimSpace(os.Getenv(OperatorResourceAttributesVariable)) + raw := strings.TrimSpace(env.OtelResourceAttributes.Get()) if raw == "" { return "", nil } @@ -210,13 +213,15 @@ func resourceAttributesFromProcess() (string, error) { // value is reported and replaced by the default so an observability setting // cannot invalidate AgentTemplates. func maxCaptureBytesFromProcess() (int, error) { - raw := strings.TrimSpace(os.Getenv(otelMaxCaptureBytes)) - if raw == "" { + value, set, err := env.OtelMaxCaptureBytes.LookupWithError() + if err != nil { + return 0, err + } + if !set { return 0, nil } - value, err := strconv.Atoi(raw) - if err != nil || value <= 0 || value > tracing.MaxCaptureBytes { - return 0, fmt.Errorf("%s must be a positive integer of at most %d bytes", otelMaxCaptureBytes, tracing.MaxCaptureBytes) + if value <= 0 || value > tracing.MaxCaptureBytes { + return 0, fmt.Errorf("%s must be a positive integer of at most %d bytes", env.OtelMaxCaptureBytes.Name(), tracing.MaxCaptureBytes) } return value, nil } @@ -302,8 +307,8 @@ func IsResourceAttributesVariable(name string) bool { // runtime revisions. Other OTEL variables remain available for harness tuning. func OwnsTelemetryEnvironment(name string) bool { switch name { - case otelSDKDisabled, otelServiceName, - otelExporterOTLPEndpoint, otelExporterOTLPProtocol, otelExporterOTLPTimeout, + case env.OtelSDKDisabled.Name(), otelServiceName, + env.OtelExporterOTLPEndpoint.Name(), env.OtelExporterOTLPProtocol.Name(), env.OtelExporterOTLPTimeout.Name(), tracing.CaptureContentEnvironmentVariable: return true } @@ -329,7 +334,7 @@ func (c TelemetryConfig) TelemetryEnvironment(identity tracing.RuntimeTelemetry, strings.Join([]string{c.ResourceAttributes, harnessAttributes}, ","), resourceIdentity(identity))} if !c.Enabled() { environment := []corev1.EnvVar{ - {Name: otelSDKDisabled, Value: "true"}, + {Name: env.OtelSDKDisabled.Name(), Value: "true"}, {Name: SignalTraces.exporterVariable(), Value: "none"}, {Name: SignalMetrics.exporterVariable(), Value: "none"}, {Name: SignalLogs.exporterVariable(), Value: "none"}, @@ -350,9 +355,9 @@ func (c TelemetryConfig) TelemetryEnvironment(identity tracing.RuntimeTelemetry, } sharedEndpoint, sharedProtocol := c.sharedExporter() if sharedEndpoint { - environment = append(environment, corev1.EnvVar{Name: otelExporterOTLPEndpoint, Value: c.Endpoint}) + environment = append(environment, corev1.EnvVar{Name: env.OtelExporterOTLPEndpoint.Name(), Value: c.Endpoint}) } - environment = append(environment, corev1.EnvVar{Name: otelExporterOTLPProtocol, Value: sharedProtocol}) + environment = append(environment, corev1.EnvVar{Name: env.OtelExporterOTLPProtocol.Name(), Value: sharedProtocol}) for _, signal := range signals { resolved := c.signal(signal) if !resolved.Enabled { @@ -366,7 +371,7 @@ func (c TelemetryConfig) TelemetryEnvironment(identity tracing.RuntimeTelemetry, } } if c.Timeout != "" { - environment = append(environment, corev1.EnvVar{Name: otelExporterOTLPTimeout, Value: c.Timeout}) + environment = append(environment, corev1.EnvVar{Name: env.OtelExporterOTLPTimeout.Name(), Value: c.Timeout}) } return append(environment, corev1.EnvVar{Name: otelServiceName, Value: identity.AgentName}, resource, capture) } @@ -399,7 +404,7 @@ func (c TelemetryConfig) sharedExporter() (bool, string) { func DefaultsEnvironment() []corev1.EnvVar { environment := make([]corev1.EnvVar, 0, len(telemetry.Defaults)) for _, value := range telemetry.Defaults { - environment = append(environment, corev1.EnvVar{Name: value.Name, Value: value.Value}) + environment = append(environment, corev1.EnvVar{Name: value.Name(), Value: value.DefaultValue()}) } return environment } diff --git a/go/core/internal/translator/otel_test.go b/go/core/internal/translator/otel_test.go index d1d786d2aa..41866f7063 100644 --- a/go/core/internal/translator/otel_test.go +++ b/go/core/internal/translator/otel_test.go @@ -201,6 +201,61 @@ func TestTelemetryConfigFromProcessReportsInvalidOperatorSettings(t *testing.T) } } +func TestTelemetryCaptureBudgetValidation(t *testing.T) { + for _, tt := range []struct { + input string + want int + wantWarning bool + }{ + {input: ""}, + {input: " "}, + {input: " 4096 ", want: 4096}, + {input: "65536", want: 65536}, + {input: "0", wantWarning: true}, + {input: "-1", wantWarning: true}, + {input: "65537", wantWarning: true}, + {input: "garbage", wantWarning: true}, + {input: "99999999999999999999999999", wantWarning: true}, + } { + t.Run(tt.input, func(t *testing.T) { + clearTelemetryEnvironment(t) + t.Setenv("KAGENT_OTEL_MAX_CAPTURE_BYTES", tt.input) + got, warnings := translator.TelemetryConfigFromProcess() + if got.MaxCaptureBytes != tt.want || (len(warnings) > 0) != tt.wantWarning { + t.Fatalf("MaxCaptureBytes = %d, warnings = %v", got.MaxCaptureBytes, warnings) + } + if tt.wantWarning && !strings.Contains(warnings[0].Error(), "KAGENT_OTEL_MAX_CAPTURE_BYTES") { + t.Fatalf("warning does not identify the setting: %v", warnings) + } + }) + } +} + +func TestTelemetryBooleanGrammars(t *testing.T) { + for _, tt := range []struct { + input string + disabled, capture bool + }{ + {input: " TrUe ", disabled: true, capture: true}, + {input: "1", capture: true}, + {input: "t", capture: true}, + {input: "false"}, + {input: "invalid"}, + } { + t.Run(tt.input, func(t *testing.T) { + clearTelemetryEnvironment(t) + t.Setenv("OTEL_TRACES_EXPORTER", "otlp") + t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317") + t.Setenv("OTEL_SDK_DISABLED", tt.input) + t.Setenv("KAGENT_OTEL_CAPTURE_RAW_API_BODIES", tt.input) + got, warnings := translator.TelemetryConfigFromProcess() + if len(warnings) != 0 || got.Traces.Enabled == tt.disabled || got.CaptureRawAPIBodies != tt.capture { + t.Fatalf("telemetry = %#v, warnings = %v", got, warnings) + } + }) + } +} + func TestOwnsTelemetryEnvironment(t *testing.T) { for _, name := range []string{ "OTEL_SDK_DISABLED", diff --git a/go/core/pkg/env/kagent.go b/go/core/pkg/env/kagent.go index ccbcaa081d..b5b9ce65c4 100644 --- a/go/core/pkg/env/kagent.go +++ b/go/core/pkg/env/kagent.go @@ -78,15 +78,10 @@ var ( ComponentAgentRuntime, ) - // Registered here for `kagent env` CLI discoverability only -- the - // actual gate is read independently (raw os.Getenv, not via this var) - // in go/adk/pkg/tools/skills.go's enableFileSearchToolsEnv. The two - // literals are pinned together by that package's - // TestEnableFileSearchToolsEnvMatchesRegistry. KagentEnableFileSearchTools = RegisterBoolVar( "KAGENT_ENABLE_FILE_SEARCH_TOOLS", false, - "When true, enables the list_files and grep_file skills tools, which let an agent "+ + "When true, t, or 1 (case-insensitive), enables the list_files and grep_file skills tools, which let an agent "+ "enumerate and search the filesystem under its session/skills roots without a "+ "shell. Disabled by default; set in Harness env to opt in.", ComponentAgentRuntime, @@ -116,7 +111,7 @@ var ( DatabaseVectorEnabled = RegisterBoolVar( "KAGENT_DATABASE_VECTOR_ENABLED", false, - "Enable vector database migrations and vector-backed database functionality.", + "Enable vector database migrations and vector-backed database functionality. The controller defaults to false. When unset in the CLI, migrations read the controller ConfigMap and fall back to true if it is unavailable.", ComponentDatabase, ComponentController, ComponentCLI, ) diff --git a/go/core/pkg/env/otel.go b/go/core/pkg/env/otel.go index 37f7d02cf2..537fc34b01 100644 --- a/go/core/pkg/env/otel.go +++ b/go/core/pkg/env/otel.go @@ -54,25 +54,25 @@ var ( ) var ( - _ = RegisterBoolVar("OTEL_SDK_DISABLED", false, "Set to true to disable SDK telemetry and forwarding of telemetry signals to managed runtimes.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_SERVICE_NAME", "", "SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_RESOURCE_ATTRIBUTES", "", "Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TIMEOUT", "", "OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms).", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "", "Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "", "Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "", "Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "", "Trace protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "", "Metric protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "", "Log protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "", "Trace SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "", "Metric SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "", "Log SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_PROPAGATORS", "tracecontext", "SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip", "OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "base2_exponential_bucket_histogram", "Default SDK histogram aggregation applied by kagent and supplied to managed runtimes.", ComponentController, ComponentAgentRuntime) - _ = RegisterBoolVar("KAGENT_OTEL_CAPTURE_RAW_API_BODIES", false, "Set to true to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data.", ComponentController) - _ = RegisterIntVar("KAGENT_OTEL_MAX_CAPTURE_BYTES", 16384, "Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384.", ComponentController) - _ = RegisterStringVar("ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN", "2", "Python Google ADK telemetry schema version; set by kagent when absent.", ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental", "Python Google ADK semantic-convention opt-in; set by kagent when absent.", ComponentAgentRuntime) - _ = RegisterStringVar("ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS", "", "Python Google ADK span content capture. When absent, derived from OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (true for SPAN_ONLY or SPAN_AND_EVENT, false otherwise).", ComponentAgentRuntime) + OtelSDKDisabled = RegisterStringVar("OTEL_SDK_DISABLED", "false", "Disable SDK telemetry and forwarding to managed runtimes when true (case-insensitive). Other values are treated as false.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_SERVICE_NAME", "", "SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_RESOURCE_ATTRIBUTES", "", "Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPTimeout = RegisterStringVar("OTEL_EXPORTER_OTLP_TIMEOUT", "", "OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms).", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPTracesEndpoint = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "", "Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPMetricsEndpoint = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "", "Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPLogsEndpoint = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "", "Log endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPTracesProtocol = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "", "Trace protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPMetricsProtocol = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", "", "Metric protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPLogsProtocol = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL", "", "Log protocol override: grpc or http/protobuf. Falls back to OTEL_EXPORTER_OTLP_PROTOCOL.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", "", "Trace SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT", "", "Metric SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_EXPORTER_OTLP_LOGS_TIMEOUT", "", "Log SDK timeout in milliseconds, overriding OTEL_EXPORTER_OTLP_TIMEOUT. Not forwarded by the controller.", ComponentController, ComponentAgentRuntime) + OtelPropagators = RegisterStringVar("OTEL_PROPAGATORS", "tracecontext", "SDK trace propagators. Kagent defaults to W3C tracecontext without baggage and supplies that default to managed runtimes.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPCompression = RegisterStringVar("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip", "OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip.", ComponentController, ComponentAgentRuntime) + OtelExporterOTLPMetricsDefaultHistogramAggregation = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "base2_exponential_bucket_histogram", "Default SDK histogram aggregation applied by kagent and supplied to managed runtimes.", ComponentController, ComponentAgentRuntime) + OtelCaptureRawAPIBodies = RegisterBoolVar("KAGENT_OTEL_CAPTURE_RAW_API_BODIES", false, "Set to true, t, or 1 (case-insensitive) to enable native Claude raw API body logging when log export is enabled. Independent of span content capture; bodies may contain sensitive data.", ComponentController) + OtelMaxCaptureBytes = RegisterIntVar("KAGENT_OTEL_MAX_CAPTURE_BYTES", 16384, "Per-input/output content capture budget in bytes when capture is enabled. Valid values are 1 through 65536; absent or invalid values use 16384.", ComponentController) + _ = RegisterStringVar("ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN", "2", "Python Google ADK telemetry schema version; set by kagent when absent.", ComponentAgentRuntime) + _ = RegisterStringVar("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental", "Python Google ADK semantic-convention opt-in; set by kagent when absent.", ComponentAgentRuntime) + _ = RegisterStringVar("ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS", "", "Python Google ADK span content capture. When absent, derived from OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (true for SPAN_ONLY or SPAN_AND_EVENT, false otherwise).", ComponentAgentRuntime) ) diff --git a/go/core/pkg/env/providers.go b/go/core/pkg/env/providers.go index 69a8dc3c37..752d44d8f7 100644 --- a/go/core/pkg/env/providers.go +++ b/go/core/pkg/env/providers.go @@ -164,9 +164,9 @@ var ( // Provider aliases. var ( - _ = RegisterStringVar("GEMINI_API_KEY", "", "Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs.", ComponentAgentRuntime, ComponentCLI) - _ = RegisterStringVar("GOOGLE_CLOUD_REGION", "", "Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset.", ComponentAgentRuntime) - _ = RegisterStringVar("AWS_DEFAULT_REGION", "", "Preferred region for Python Bedrock models and Go/Python Bedrock embeddings, before AWS_REGION and the us-east-1 fallback.", ComponentAgentRuntime) + GeminiAPIKey = RegisterStringVar("GEMINI_API_KEY", "", "Fallback Gemini API key when GOOGLE_API_KEY is unset; supported by the CLI and Go/Python ADKs.", ComponentAgentRuntime, ComponentCLI) + GoogleCloudRegion = RegisterStringVar("GOOGLE_CLOUD_REGION", "", "Go ADK Vertex AI region fallback when GOOGLE_CLOUD_LOCATION is unset.", ComponentAgentRuntime) + AWSDefaultRegion = RegisterStringVar("AWS_DEFAULT_REGION", "", "Preferred region for Python Bedrock models and Go/Python Bedrock embeddings, before AWS_REGION and the us-east-1 fallback.", ComponentAgentRuntime) ) // SAP AI Core diff --git a/go/core/pkg/env/runtime.go b/go/core/pkg/env/runtime.go index 87c201ce94..db6987a216 100644 --- a/go/core/pkg/env/runtime.go +++ b/go/core/pkg/env/runtime.go @@ -1,13 +1,13 @@ package env // Registrations for user-configurable runtime settings. -// Keep their defaults aligned with the Go/Python readers; importing this package -// is not required for a runtime or an upstream SDK to read its environment. +// Go runtimes read these accessors. Metadata-only entries describe settings +// consumed by Python runtimes or upstream SDKs. var ( - KagentPort = RegisterStringVar("KAGENT_PORT", "", "ADK A2A listen port: the Go HTTP/gRPC listener defaults to 8080; the Python gRPC listener defaults to 80. Explicit Go --port/AppConfig.Port or Python a2a_grpc_address takes precedence. The controller sets 80 for managed kagent runtimes. Python's HTTP --port is separate.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) - _ = RegisterStringVar("KAGENT_BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) - _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) - _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) + KagentPort = RegisterStringVar("KAGENT_PORT", "", "ADK A2A listen port: the Go HTTP/gRPC listener defaults to 8080; the Python gRPC listener defaults to 80. Explicit Go --port/AppConfig.Port or Python a2a_grpc_address takes precedence. The controller sets 80 for managed kagent runtimes. Python's HTTP --port is separate.", ComponentAgentRuntime) + KagentConfigDir = RegisterStringVar("KAGENT_CONFIG_DIR", "/config", "Go ADK configuration directory; --filepath takes precedence.", ComponentAgentRuntime) + KagentA2AMaxContentLength = RegisterStringVar("KAGENT_A2A_MAX_CONTENT_LENGTH", "10485760", "Maximum A2A request size in bytes for Go/Python servers. 0, none, or unlimited disables the limit; invalid values use the default.", ComponentAgentRuntime) + _ = RegisterStringVar("KAGENT_BASH_VENV_PATH", "", "Virtual environment used for Python skills shell commands; its bin directory is prepended to PATH and VIRTUAL_ENV is set.", ComponentAgentRuntime) + _ = RegisterBoolVar("KAGENT_OPENAI_AGENTS_NATIVE_TRACING", false, "Keep the OpenAI Agents SDK native tracing processor alongside kagent OpenTelemetry export in the Python OpenAI runtime.", ComponentAgentRuntime) + _ = RegisterBoolVar("OPENAI_AGENTS_DISABLE_TRACING", false, "Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1.", ComponentAgentRuntime) ) diff --git a/go/core/pkg/env/var.go b/go/core/pkg/env/var.go index 0daf49bda8..47b1381291 100644 --- a/go/core/pkg/env/var.go +++ b/go/core/pkg/env/var.go @@ -192,26 +192,31 @@ func RegisterBoolVar(name string, defaultValue bool, description string, compone // Get returns the current value of the environment variable, or the default. func (b BoolVar) Get() bool { - if val, ok := os.LookupEnv(b.v.Name); ok { - parsed, err := strconv.ParseBool(val) - if err == nil { - return parsed - } - } - return b.defaultValue + value, _, _ := b.LookupWithError() // Invalid input uses the registered default. + return value } -// Lookup returns the value and whether the variable was set. +// Lookup returns the value and whether a nonempty, valid value was set. func (b BoolVar) Lookup() (bool, bool) { + value, set, err := b.LookupWithError() + return value, set && err == nil +} + +// LookupWithError distinguishes invalid input from an unset or empty value. +// Boolean values are case-insensitive and ignore surrounding whitespace. +// Unset, empty, and invalid values return the registered default; invalid input +// additionally returns set=true and a parsing error. +func (b BoolVar) LookupWithError() (value, set bool, err error) { val, ok := os.LookupEnv(b.v.Name) - if !ok { - return b.defaultValue, false + val = strings.TrimSpace(val) + if !ok || val == "" { + return b.defaultValue, false, nil } - parsed, err := strconv.ParseBool(val) + parsed, err := strconv.ParseBool(strings.ToLower(val)) if err != nil { - return b.defaultValue, false + return b.defaultValue, true, fmt.Errorf("failed to parse %s as a boolean: %w", b.v.Name, err) } - return parsed, true + return parsed, true, nil } // Name returns the environment variable name. @@ -240,26 +245,30 @@ func RegisterIntVar(name string, defaultValue int, description string, component // Get returns the current value of the environment variable, or the default. func (i IntVar) Get() int { - if val, ok := os.LookupEnv(i.v.Name); ok { - parsed, err := strconv.Atoi(val) - if err == nil { - return parsed - } - } - return i.defaultValue + value, _, _ := i.LookupWithError() // Invalid input uses the registered default. + return value } -// Lookup returns the value and whether the variable was set. +// Lookup returns the value and whether a nonempty, valid value was set. func (i IntVar) Lookup() (int, bool) { + value, set, err := i.LookupWithError() + return value, set && err == nil +} + +// LookupWithError distinguishes invalid input from an unset or empty value. +// Integer values ignore surrounding whitespace. Unset, empty, and invalid values +// return the registered default; invalid input also returns set=true and an error. +func (i IntVar) LookupWithError() (value int, set bool, err error) { val, ok := os.LookupEnv(i.v.Name) - if !ok { - return i.defaultValue, false + val = strings.TrimSpace(val) + if !ok || val == "" { + return i.defaultValue, false, nil } parsed, err := strconv.Atoi(val) if err != nil { - return i.defaultValue, false + return i.defaultValue, true, fmt.Errorf("failed to parse %s as an integer: %w", i.v.Name, err) } - return parsed, true + return parsed, true, nil } // Name returns the environment variable name. diff --git a/go/core/pkg/env/var_test.go b/go/core/pkg/env/var_test.go index 20d9117651..c3ac4354d5 100644 --- a/go/core/pkg/env/var_test.go +++ b/go/core/pkg/env/var_test.go @@ -72,6 +72,81 @@ func TestRegisterBoolVar(t *testing.T) { } } +func TestBoolLookupWithError(t *testing.T) { + v := RegisterBoolVar("TEST_BOOL_LOOKUP_ERROR", true, "test", ComponentTesting) + for _, tt := range []struct { + name, input string + unset, want, set, wantErr bool + }{ + {name: "unset", unset: true, want: true}, + {name: "empty", want: true}, + {name: "whitespace", input: " \t", want: true}, + {name: "mixed case", input: " tRuE ", want: true, set: true}, + {name: "short true", input: " T ", want: true, set: true}, + {name: "one", input: "1", want: true, set: true}, + {name: "false", input: " FaLsE ", set: true}, + {name: "short false", input: "F", set: true}, + {name: "zero", input: "0", set: true}, + {name: "invalid", input: "yes", want: true, set: true, wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(v.Name(), tt.input) + if tt.unset { + require.NoError(t, os.Unsetenv(v.Name())) + } + got, set, err := v.LookupWithError() + require.Equal(t, tt.want, got) + require.Equal(t, tt.set, set) + if tt.wantErr { + require.ErrorContains(t, err, v.Name()) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.want, v.Get()) + got, valid := v.Lookup() + require.Equal(t, tt.want, got) + require.Equal(t, tt.set && !tt.wantErr, valid) + }) + } +} + +func TestIntLookupWithError(t *testing.T) { + v := RegisterIntVar("TEST_INT_LOOKUP_ERROR", 42, "test", ComponentTesting) + for _, tt := range []struct { + name, input string + want int + unset, set, wantErr bool + }{ + {name: "unset", unset: true, want: 42}, + {name: "empty", want: 42}, + {name: "whitespace", input: " \t", want: 42}, + {name: "valid", input: " 4096 ", want: 4096, set: true}, + {name: "zero", input: "0", set: true}, + {name: "negative", input: "-1", want: -1, set: true}, + {name: "invalid", input: "many", want: 42, set: true, wantErr: true}, + {name: "overflow", input: "99999999999999999999999999", want: 42, set: true, wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(v.Name(), tt.input) + if tt.unset { + require.NoError(t, os.Unsetenv(v.Name())) + } + got, set, err := v.LookupWithError() + require.Equal(t, tt.want, got) + require.Equal(t, tt.set, set) + if tt.wantErr { + require.ErrorContains(t, err, v.Name()) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.want, v.Get()) + got, valid := v.Lookup() + require.Equal(t, tt.want, got) + require.Equal(t, tt.set && !tt.wantErr, valid) + }) + } +} + func TestRegisterIntVar(t *testing.T) { allVars = make(map[string]Var) diff --git a/go/harness/claude/cmd/main.go b/go/harness/claude/cmd/main.go index 8e45566875..21db0c8dbf 100644 --- a/go/harness/claude/cmd/main.go +++ b/go/harness/claude/cmd/main.go @@ -34,7 +34,7 @@ func main() { os.Exit(1) } ctx := logging.IntoContext(context.Background(), logger) - if err := run(ctx, *check, os.Getenv, os.Environ()); err != nil { + if err := run(ctx, *check, os.Getenv, os.Environ()); err != nil { //nolint:forbidigo // Reads only controller-generated config and agent-card payloads. logger.ErrorContext(ctx, "claude harness stopped", "error", err) os.Exit(1) } diff --git a/go/harness/codex/cmd/main.go b/go/harness/codex/cmd/main.go index d041ffb380..0a2a7d6109 100644 --- a/go/harness/codex/cmd/main.go +++ b/go/harness/codex/cmd/main.go @@ -34,7 +34,7 @@ func main() { os.Exit(1) } ctx := logging.IntoContext(context.Background(), logger) - if err := run(ctx, *check, os.Getenv, os.Environ()); err != nil { + if err := run(ctx, *check, os.Getenv, os.Environ()); err != nil { //nolint:forbidigo // Reads only controller-generated config and agent-card payloads. logger.ErrorContext(ctx, "codex harness stopped", "error", err) os.Exit(1) } diff --git a/go/pkg/telemetry/telemetry.go b/go/pkg/telemetry/telemetry.go index adbdac74ab..5bff26f0eb 100644 --- a/go/pkg/telemetry/telemetry.go +++ b/go/pkg/telemetry/telemetry.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "github.com/kagent-dev/kagent/go/core/pkg/env" "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/contrib/propagators/autoprop" "go.opentelemetry.io/otel" @@ -28,15 +29,12 @@ import ( // FlushTimeout bounds one ForceFlush. const FlushTimeout = 3 * time.Second -// Default is an SDK setting kagent applies when the environment leaves it unset. -type Default struct{ Name, Value string } - // Defaults keep baggage from leaking to tools and model providers, compress // exports, and give every runtime the same histogram buckets. -var Defaults = []Default{ - {Name: "OTEL_PROPAGATORS", Value: "tracecontext"}, - {Name: "OTEL_EXPORTER_OTLP_COMPRESSION", Value: "gzip"}, - {Name: "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", Value: "base2_exponential_bucket_histogram"}, +var Defaults = []env.StringVar{ + env.OtelPropagators, + env.OtelExporterOTLPCompression, + env.OtelExporterOTLPMetricsDefaultHistogramAggregation, } // WithDefaults adds each default that a child process environment leaves unset. @@ -44,10 +42,10 @@ func WithDefaults(environment []string) []string { result := slices.Clone(environment) for _, value := range Defaults { if !slices.ContainsFunc(environment, func(variable string) bool { - current, found := strings.CutPrefix(variable, value.Name+"=") + current, found := strings.CutPrefix(variable, value.Name()+"=") return found && current != "" }) { - result = append(result, value.Name+"="+value.Value) + result = append(result, value.Name()+"="+value.DefaultValue()) } } return result @@ -77,13 +75,13 @@ func Init(ctx context.Context, opts Options) (*Providers, error) { slog.Default().ErrorContext(context.Background(), "opentelemetry", "error", err) })) for _, value := range Defaults { - if os.Getenv(value.Name) == "" { - _ = os.Setenv(value.Name, value.Value) + if current, set := value.Lookup(); !set || current == "" { + _ = os.Setenv(value.Name(), value.DefaultValue()) } } otel.SetTextMapPropagator(autoprop.NewTextMapPropagator()) providers := &Providers{} - if strings.EqualFold(strings.TrimSpace(os.Getenv("OTEL_SDK_DISABLED")), "true") { + if strings.EqualFold(strings.TrimSpace(env.OtelSDKDisabled.Get()), "true") { return providers, nil } var errs []error diff --git a/go/pkg/telemetry/telemetry_test.go b/go/pkg/telemetry/telemetry_test.go index 5151cbe5e1..bd0ba654ba 100644 --- a/go/pkg/telemetry/telemetry_test.go +++ b/go/pkg/telemetry/telemetry_test.go @@ -138,7 +138,7 @@ func TestInitAppliesDefaultsWithoutBaggage(t *testing.T) { restoreGlobals(t) setExporters(t, "none", "none", "none") for _, value := range Defaults { - t.Setenv(value.Name, "") + t.Setenv(value.Name(), "") } t.Setenv("OTEL_EXPORTER_OTLP_COMPRESSION", "none") if _, err := Init(t.Context(), Options{}); err != nil { From f3e8b45a8f87ccf8b3f9d58b22c0f0fcb457b86e Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:38:14 +0000 Subject: [PATCH 10/14] refactor: reuse registered telemetry names and defaults Signed-off-by: Eitan Yarmush --- go/core/internal/translator/otel.go | 38 ++++++++++-------------- go/core/internal/translator/otel_test.go | 5 ++-- go/core/pkg/env/otel.go | 4 +-- go/pkg/tracing/runtime.go | 10 ++----- 4 files changed, 22 insertions(+), 35 deletions(-) diff --git a/go/core/internal/translator/otel.go b/go/core/internal/translator/otel.go index 7008da0db3..35d20306c9 100644 --- a/go/core/internal/translator/otel.go +++ b/go/core/internal/translator/otel.go @@ -16,17 +16,6 @@ import ( corev1 "k8s.io/api/core/v1" ) -const ( - otelServiceName = "OTEL_SERVICE_NAME" - otelResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES" - - // OperatorResourceAttributesVariable carries the operator's resource - // attributes. The controller's own OTEL_RESOURCE_ATTRIBUTES also names its - // pod, which no runtime may inherit. - OperatorResourceAttributesVariable = "KAGENT_OTEL_RESOURCE_ATTRIBUTES" - defaultOTLPProtocol = "grpc" -) - // Signal is one OpenTelemetry signal the controller configures. type Signal string @@ -107,7 +96,7 @@ func TelemetryConfigFromProcess() (TelemetryConfig, []error) { *config.signal(signal) = resolved } if config.Protocol == "" { - config.Protocol = defaultOTLPProtocol + config.Protocol = env.OtelExporterOTLPProtocol.DefaultValue() } if raw := strings.TrimSpace(env.OtelExporterOTLPTimeout.Get()); raw != "" { if value, err := strconv.Atoi(raw); err != nil || value <= 0 { @@ -126,7 +115,7 @@ func TelemetryConfigFromProcess() (TelemetryConfig, []error) { config.CaptureSensitiveContent = true case "", "NO_CONTENT", tracing.CaptureContentDisabled: default: - warnings = append(warnings, fmt.Errorf("%s must be SPAN_ONLY or NO_CONTENT, not %q", tracing.CaptureContentEnvironmentVariable, capture)) + warnings = append(warnings, fmt.Errorf("%s must be SPAN_ONLY or NO_CONTENT, not %q", env.OtelCaptureMessageContent.Name(), capture)) } maxCaptureBytes, err := maxCaptureBytesFromProcess() if err != nil { @@ -172,7 +161,7 @@ func (c TelemetryConfig) signalFromProcess(signal Signal, disabled bool) (Signal protocol = c.Protocol } if protocol == "" { - protocol = defaultOTLPProtocol + protocol = env.OtelExporterOTLPProtocol.DefaultValue() } if protocol != "grpc" && protocol != "http/protobuf" { return SignalConfig{}, fmt.Errorf("unsupported OTLP %s protocol %q", signal, protocol) @@ -188,6 +177,9 @@ func (c TelemetryConfig) signalFromProcess(signal Signal, disabled bool) (Signal return resolved, nil } +// resourceAttributesFromProcess reads the operator's shared runtime attributes. +// The controller's own OTEL_RESOURCE_ATTRIBUTES includes its pod identity, which +// must not be inherited by agent runtimes. func resourceAttributesFromProcess() (string, error) { raw := strings.TrimSpace(env.OtelResourceAttributes.Get()) if raw == "" { @@ -204,7 +196,7 @@ func resourceAttributesFromProcess() (string, error) { entries = append(entries, strings.TrimSpace(key)+"="+strings.TrimSpace(value)) } if len(invalid) != 0 { - return strings.Join(entries, ","), fmt.Errorf("%s ignores entries that are not key=value: %q", OperatorResourceAttributesVariable, invalid) + return strings.Join(entries, ","), fmt.Errorf("%s ignores entries that are not key=value: %q", env.OtelResourceAttributes.Name(), invalid) } return strings.Join(entries, ","), nil } @@ -286,11 +278,11 @@ func ProviderName(provider v1alpha3.ModelProvider) string { // sets. The rendered value keeps its entries under the agent identity. func HarnessResourceAttributes(harness *HarnessConfiguration) (string, error) { for _, variable := range harness.Spec.Env { - if variable.Name != otelResourceAttributes { + if variable.Name != env.OtelSDKResourceAttributes.Name() { continue } if variable.Value == nil { - return "", NewValidationError("Harness env %q must be a literal value", otelResourceAttributes) + return "", NewValidationError("Harness env %q must be a literal value", env.OtelSDKResourceAttributes.Name()) } return *variable.Value, nil } @@ -300,16 +292,16 @@ func HarnessResourceAttributes(harness *HarnessConfiguration) (string, error) { // IsResourceAttributesVariable reports whether a Harness variable is the one // TelemetryEnvironment merges instead of copying. func IsResourceAttributesVariable(name string) bool { - return name == otelResourceAttributes + return name == env.OtelSDKResourceAttributes.Name() } // OwnsTelemetryEnvironment reports whether Kagent compiles the variable into // runtime revisions. Other OTEL variables remain available for harness tuning. func OwnsTelemetryEnvironment(name string) bool { switch name { - case env.OtelSDKDisabled.Name(), otelServiceName, + case env.OtelSDKDisabled.Name(), env.OtelServiceName.Name(), env.OtelExporterOTLPEndpoint.Name(), env.OtelExporterOTLPProtocol.Name(), env.OtelExporterOTLPTimeout.Name(), - tracing.CaptureContentEnvironmentVariable: + env.OtelCaptureMessageContent.Name(): return true } for _, signal := range signals { @@ -326,11 +318,11 @@ func OwnsTelemetryEnvironment(name string) bool { // capture decision is always rendered, so a runtime that reaches a collector // through settings the controller did not render still follows it. func (c TelemetryConfig) TelemetryEnvironment(identity tracing.RuntimeTelemetry, harnessAttributes string) []corev1.EnvVar { - capture := corev1.EnvVar{Name: tracing.CaptureContentEnvironmentVariable, Value: tracing.CaptureContentDisabled} + capture := corev1.EnvVar{Name: env.OtelCaptureMessageContent.Name(), Value: tracing.CaptureContentDisabled} if c.CaptureSensitiveContent { capture.Value = tracing.CaptureContentSpanOnly } - resource := corev1.EnvVar{Name: otelResourceAttributes, Value: tracing.MergeResourceAttributes( + resource := corev1.EnvVar{Name: env.OtelSDKResourceAttributes.Name(), Value: tracing.MergeResourceAttributes( strings.Join([]string{c.ResourceAttributes, harnessAttributes}, ","), resourceIdentity(identity))} if !c.Enabled() { environment := []corev1.EnvVar{ @@ -373,7 +365,7 @@ func (c TelemetryConfig) TelemetryEnvironment(identity tracing.RuntimeTelemetry, if c.Timeout != "" { environment = append(environment, corev1.EnvVar{Name: env.OtelExporterOTLPTimeout.Name(), Value: c.Timeout}) } - return append(environment, corev1.EnvVar{Name: otelServiceName, Value: identity.AgentName}, resource, capture) + return append(environment, corev1.EnvVar{Name: env.OtelServiceName.Name(), Value: identity.AgentName}, resource, capture) } // sharedExporter reports whether an enabled signal exports to the shared diff --git a/go/core/internal/translator/otel_test.go b/go/core/internal/translator/otel_test.go index 41866f7063..4336df3eb3 100644 --- a/go/core/internal/translator/otel_test.go +++ b/go/core/internal/translator/otel_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/kagent-dev/kagent/go/core/internal/translator" + "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/tracing" corev1 "k8s.io/api/core/v1" ) @@ -129,7 +130,7 @@ func TestTelemetryEnvironmentKeepsIdentityOverOperatorAndHarnessAttributes(t *te clearTelemetryEnvironment(t) t.Setenv("OTEL_TRACES_EXPORTER", "otlp") t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317") - t.Setenv(translator.OperatorResourceAttributesVariable, "deployment.environment.name=prod, service.namespace=forged,k8s.cluster.name=east") + t.Setenv(env.OtelResourceAttributes.Name(), "deployment.environment.name=prod, service.namespace=forged,k8s.cluster.name=east") got, warnings := translator.TelemetryConfigFromProcess() if len(warnings) != 0 { @@ -193,7 +194,7 @@ func TestTelemetryConfigFromProcessRejectsInvalidSignalConfiguration(t *testing. func TestTelemetryConfigFromProcessReportsInvalidOperatorSettings(t *testing.T) { clearTelemetryEnvironment(t) t.Setenv("OTEL_EXPORTER_OTLP_TIMEOUT", "15s") - t.Setenv(translator.OperatorResourceAttributesVariable, "deployment.environment.name=prod,broken") + t.Setenv(env.OtelResourceAttributes.Name(), "deployment.environment.name=prod,broken") got, warnings := translator.TelemetryConfigFromProcess() if len(warnings) != 2 || got.Timeout != "" || got.ResourceAttributes != "deployment.environment.name=prod" { diff --git a/go/core/pkg/env/otel.go b/go/core/pkg/env/otel.go index 537fc34b01..4844cdfbe5 100644 --- a/go/core/pkg/env/otel.go +++ b/go/core/pkg/env/otel.go @@ -55,8 +55,8 @@ var ( var ( OtelSDKDisabled = RegisterStringVar("OTEL_SDK_DISABLED", "false", "Disable SDK telemetry and forwarding to managed runtimes when true (case-insensitive). Other values are treated as false.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_SERVICE_NAME", "", "SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes.", ComponentController, ComponentAgentRuntime) - _ = RegisterStringVar("OTEL_RESOURCE_ATTRIBUTES", "", "Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents.", ComponentController, ComponentAgentRuntime) + OtelServiceName = RegisterStringVar("OTEL_SERVICE_NAME", "", "SDK service name for the current process. Defaults to kagent-controller in the controller; the controller supplies the agent name to managed runtimes.", ComponentController, ComponentAgentRuntime) + OtelSDKResourceAttributes = RegisterStringVar("OTEL_RESOURCE_ATTRIBUTES", "", "Comma-separated SDK resource attributes for the current process. Helm injects controller identity; kagent constructs runtime identity separately. Use KAGENT_OTEL_RESOURCE_ATTRIBUTES for attributes shared with managed agents.", ComponentController, ComponentAgentRuntime) OtelExporterOTLPTimeout = RegisterStringVar("OTEL_EXPORTER_OTLP_TIMEOUT", "", "OTLP export timeout in milliseconds. The controller forwards positive integers; absent values use each SDK's default (normally 10000 ms).", ComponentController, ComponentAgentRuntime) OtelExporterOTLPTracesEndpoint = RegisterStringVar("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "", "Trace endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) OtelExporterOTLPMetricsEndpoint = RegisterStringVar("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "", "Metric endpoint override. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT; an HTTP override must include its signal path.", ComponentController, ComponentAgentRuntime) diff --git a/go/pkg/tracing/runtime.go b/go/pkg/tracing/runtime.go index c9e13e2cf1..b3b68062d2 100644 --- a/go/pkg/tracing/runtime.go +++ b/go/pkg/tracing/runtime.go @@ -130,14 +130,8 @@ func (r Runtime) NativeHarness() bool { return r == RuntimeClaude || r == RuntimeCodex } -// CaptureContentEnvironmentVariable is the OpenTelemetry GenAI instrumentation -// switch for recording prompts and responses. The controller renders it into -// every runtime from one setting, so the runtimes that read it directly and -// the ones that carry the decision in their compiled configuration agree. -const CaptureContentEnvironmentVariable = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" - -// Values the controller renders for CaptureContentEnvironmentVariable. The ADK -// runtimes read the variable as a mode and treat a plain true as log records +// Values the controller renders for OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. +// The ADK runtimes read the variable as a mode and treat a plain true as log records // only, so the span form is spelled out; false is what every runtime, // including kagent's own ADK payload capture, reads as off. const ( From 75b9c416f2185dbab16c0f5e034d7148d6880c2f Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:42:35 +0000 Subject: [PATCH 11/14] refactor: scope testing and UI environment prefixes Signed-off-by: Eitan Yarmush --- .github/workflows/ci.yaml | 2 +- DEVELOPMENT.md | 4 +- Makefile | 10 ++--- STYLE.md | 2 +- .../architecture/oidc-proxy-authentication.md | 2 +- docs/env.md | 38 +++++++++---------- go/Makefile | 2 +- go/core/pkg/env/testing.go | 20 +++++----- go/core/pkg/env/ui.go | 16 ++++---- go/core/test/e2e/README.md | 2 +- go/core/test/e2e/interaction_test.go | 4 +- go/core/test/upgrade/invoke_e2e_test.go | 6 +-- go/core/test/upgrade/rolling_upgrade_test.go | 4 +- go/core/test/upgrade/upgrade_test.go | 4 +- helm/kagent/templates/ui-configmap.yaml | 6 +-- helm/kagent/tests/ui-deployment_test.yaml | 20 +++++----- helm/kagent/values.yaml | 2 +- scripts/setup-cluster/README.md | 4 +- scripts/setup-cluster/setup-cluster.sh | 2 +- ui/.env.example | 20 +++++----- ui/README.md | 14 +++---- ui/docs/app-extensions.md | 20 +++++----- ui/package.json | 2 +- ui/playwright.config.ts | 26 ++++++------- ui/playwright/README.md | 14 +++---- ui/playwright/globalSetup.ts | 2 +- ui/scripts/init.sh | 14 +++---- ui/src/api/config.ts | 6 +-- ui/src/appExtensions/activeExtensions.ts | 4 +- ui/src/env.test.ts | 6 +-- ui/src/env.ts | 10 ++--- ui/src/mocks/startMockBackend.ts | 2 +- ui/src/vite-env.d.ts | 4 +- ui/vite.config.ts | 18 ++++----- 34 files changed, 156 insertions(+), 156 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 62ef12b73e..3929878095 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -195,7 +195,7 @@ jobs: export KAGENT_E2E_CLI="$PWD/core/bin/kagent-local" envsubst < core/test/e2e/manifests/tracing.yaml.tmpl | kubectl apply -f - export KAGENT_E2E_API_URL="$KAGENT_API_URL" - export KAGENT_LOCAL_HOST=172.17.0.1 + export KAGENT_E2E_LOCAL_HOST=172.17.0.1 # TestMain keeps the receiver alive for every runtime's response flush. export KAGENT_E2E_OTLP_LISTEN_ADDRESS=:14317 # Concurrent actors can push an earlier failure out of the final log tail. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 358b28574f..ecd10cf4ca 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -32,8 +32,8 @@ defaults remain `8080` for Go's shared HTTP/gRPC listener and `80` for Python's gRPC listener; Python's HTTP port is configured separately with `--port`. UI containers and Vite development use the same `KAGENT_*` inputs, documented in -`ui/.env.example`. UI build-time switches use `KAGENT_VITE_*`; extension settings -use `KAGENT_EXTENSION_*`. Other `KAGENT_*` settings are not exposed to the browser. +`ui/.env.example`. UI build-time switches use `KAGENT_UI_VITE_*`; extension settings +use `KAGENT_UI_EXTENSION_*`. Other `KAGENT_*` settings are not exposed to the browser. To understand how to develop for kagent, it's important to understand the architecture of the project. Please refer to the [README.md](README.md#architecture) file for an overview of the project. diff --git a/Makefile b/Makefile index 8f7ceffc01..f89de343ae 100644 --- a/Makefile +++ b/Makefile @@ -582,7 +582,7 @@ install-previous-release: ## Install the previous released kagent + kagent-crds # Later Goose releases test previous-release behavior after the target migrations, # data survival, schema equality, previous/current controller startup, and a # complete application and schema rollback to the previous release. -# KAGENT_LOCAL_HOST lets the agent reach the local mock LLM. +# KAGENT_E2E_LOCAL_HOST lets the agent reach the local mock LLM. # Prerequisite (provided by CI as a separate step; run it locally first): a kind # cluster (make create-kind-cluster). .PHONY: announce-upgrade-from @@ -594,11 +594,11 @@ run-upgrade-tests: announce-upgrade-from build install-previous-release ## Test @echo "=== Upgrade test: $(UPGRADE_FROM_VERSION) -> $(VERSION) (registry=$(DOCKER_REGISTRY)) ===" @set -e; \ kind_gw="$$($(CONTAINER_RUNTIME) network inspect kind -f '{{range .IPAM.Config}}{{if .Gateway}}{{.Gateway}}{{"\n"}}{{end}}{{end}}' | grep -E '^[0-9]+\.' | head -1)"; \ - echo "kind gateway (KAGENT_LOCAL_HOST): $$kind_gw"; \ + echo "kind gateway (KAGENT_E2E_LOCAL_HOST): $$kind_gw"; \ cd go && \ - KAGENT_RUN_UPGRADE_TESTS=true \ + KAGENT_E2E_RUN_UPGRADE_TESTS=true \ REPO_ROOT=$(CURDIR) \ - KAGENT_LOCAL_HOST="$$kind_gw" \ + KAGENT_E2E_LOCAL_HOST="$$kind_gw" \ UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ VERSION=$(VERSION) \ DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ @@ -614,7 +614,7 @@ run-rolling-upgrade-tests: UPGRADE_PREV_EXTRA_ARGS = --set controller.replicas=2 run-rolling-upgrade-tests: announce-upgrade-from build install-previous-release ## Install the previous release with 2 controller replicas, build the current images, and run the rolling upgrade e2e test @echo "=== Rolling upgrade test: $(UPGRADE_FROM_VERSION) -> $(VERSION) (registry=$(DOCKER_REGISTRY)) ===" cd go && \ - KAGENT_RUN_ROLLING_UPGRADE_TESTS=true \ + KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS=true \ REPO_ROOT=$(CURDIR) \ UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ VERSION=$(VERSION) \ diff --git a/STYLE.md b/STYLE.md index 0def3d733d..a333a72cba 100644 --- a/STYLE.md +++ b/STYLE.md @@ -328,7 +328,7 @@ All new CRD API surface goes in `v1alpha3`. Side effects and async logic belong in the reconciler (or a dedicated controller). - Every behavioral change to a translator **SHOULD** come with golden tests - (`KAGENT_UPDATE_GOLDEN=true go test ...` to regenerate). + (`KAGENT_TEST_UPDATE_GOLDEN=true go test ...` to regenerate). ### Database diff --git a/docs/architecture/oidc-proxy-authentication.md b/docs/architecture/oidc-proxy-authentication.md index b8ea1b88d0..20cd7c2477 100644 --- a/docs/architecture/oidc-proxy-authentication.md +++ b/docs/architecture/oidc-proxy-authentication.md @@ -126,7 +126,7 @@ integration must supply its own `auth.AuthProvider` through The UI reads `SSO_REDIRECT_PATH` from `window.environmentVariables` at runtime and applies its configured base path. Helm supplies that value from -`ui.auth.ssoRedirectPath` through `KAGENT_SSO_REDIRECT_PATH`. The chart allows +`ui.auth.ssoRedirectPath` through `KAGENT_UI_SSO_REDIRECT_PATH`. The chart allows unauthenticated access to the login page, health checks, and the assets required to render the login page. diff --git a/docs/env.md b/docs/env.md index 56f42c951c..1f279e2f80 100644 --- a/docs/env.md +++ b/docs/env.md @@ -176,35 +176,35 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_E2E_CLAUDE_IMAGE` | String | `(none)` | Digest-pinned Claude harness image substituted into E2E lifecycle manifests. | | `KAGENT_E2E_CLI` | String | `(none)` | CLI executable for catalog lifecycle E2E tests; make -C go e2e builds and supplies it. | | `KAGENT_E2E_CODEX_IMAGE` | String | `(none)` | Digest-pinned Codex harness image substituted into E2E lifecycle manifests. | +| `KAGENT_E2E_LOCAL_HOST` | String | `(none)` | Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems. | | `KAGENT_E2E_OTLP_LISTEN_ADDRESS` | String | `(none)` | Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver. | | `KAGENT_E2E_REQUIRE_TRACING` | Boolean | `false` | Fail instead of skip when expected native harness tracing support is unavailable. | | `KAGENT_E2E_RUNTIME_IMAGE` | String | `(none)` | Required digest-pinned Go ADK image for sandbox E2E tests and lifecycle manifests. | +| `KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS` | Boolean | `false` | Run rolling upgrade integration tests when exactly true. | +| `KAGENT_E2E_RUN_UPGRADE_TESTS` | Boolean | `false` | Run upgrade integration tests when exactly true. | | `KAGENT_E2E_SANDBOX_NAMESPACE` | String | `kagent` | Namespace for sandbox E2E resources. | | `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | -| `KAGENT_LLM_PORT` | String | `8090` | Port for the mock LLM server. | -| `KAGENT_LOCAL_HOST` | String | `(none)` | Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems. | -| `KAGENT_RUN_ROLLING_UPGRADE_TESTS` | Boolean | `false` | Run rolling upgrade integration tests when exactly true. | -| `KAGENT_RUN_UPGRADE_TESTS` | Boolean | `false` | Run upgrade integration tests when exactly true. | -| `KAGENT_STS_PORT` | String | `8091` | Port for the mock STS (Security Token Service) server. | +| `KAGENT_E2E_UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50. | +| `KAGENT_E2E_UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | +| `KAGENT_E2E_UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | +| `KAGENT_E2E_UI_LOOP_PORT` | String | `8001` | Vite development/preview port and UI browser-test port. | +| `KAGENT_TEST_LLM_PORT` | String | `8090` | Port for the mock LLM server. | | `KAGENT_TEST_PYTHON` | String | `(none)` | Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset. | -| `KAGENT_UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to KAGENT_UI_LOOP_PORT plus 50. | -| `KAGENT_UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | -| `KAGENT_UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | -| `KAGENT_UI_LOOP_PORT` | String | `8001` | Vite development/preview port and UI browser-test port. | -| `KAGENT_UPDATE_GOLDEN` | Boolean | `false` | When true, update golden test files instead of comparing. | -| `KAGENT_VITE_API_MODE` | String | `(none)` | Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_ENABLE_MOCK_UI; leave unset for normal development. | +| `KAGENT_TEST_STS_PORT` | String | `8091` | Port for the mock STS (Security Token Service) server. | +| `KAGENT_TEST_UPDATE_GOLDEN` | Boolean | `false` | When true, update golden test files instead of comparing. | +| `KAGENT_UI_VITE_API_MODE` | String | `(none)` | Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_UI_ENABLE_MOCK; leave unset for normal development. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | ## ui | Variable | Type | Default | Description | |----------|------|---------|-------------| -| `KAGENT_API_BASE_URL` | String | `/api` | Browser API base URL for UI containers and Vite development. | -| `KAGENT_DEV_CONTROLLER_URL` | String | `http://127.0.0.1:8083` | Vite development proxy target for /api and /a2a; not sent to the browser. | -| `KAGENT_ENABLE_MOCK_UI` | Boolean | `false` | Serve the development UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Release bundles do not include the mock backend. | -| `KAGENT_EXTENSION_` | String | `(none)` | UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public. | -| `KAGENT_SSO_REDIRECT_PATH` | String | `/oauth2/start` | UI path used by Sign in with SSO. | -| `KAGENT_STREAM_TIMEOUT_MS` | String | `1800000` | UI chat stream inactivity timeout in milliseconds. 0 disables the timeout. Applies in containers and Vite development. | +| `KAGENT_UI_API_BASE_URL` | String | `/api` | Browser API base URL for UI containers and Vite development. | | `KAGENT_UI_BASE_PATH` | String | `(none)` | UI public path prefix, such as /ui; empty serves at the root. Applies in containers and Vite development; the container falls back to the root for invalid or reserved prefixes. | -| `KAGENT_VITE_API_MODE` | String | `(none)` | Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_ENABLE_MOCK_UI; leave unset for normal development. | -| `KAGENT_VITE_EXAMPLE_EXTENSION` | Boolean | `false` | Build-time switch enabling the bundled example UI extension. | +| `KAGENT_UI_DEV_CONTROLLER_URL` | String | `http://127.0.0.1:8083` | Vite development proxy target for /api and /a2a; not sent to the browser. | +| `KAGENT_UI_ENABLE_MOCK` | Boolean | `false` | Serve the development UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Release bundles do not include the mock backend. | +| `KAGENT_UI_EXTENSION_` | String | `(none)` | UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public. | +| `KAGENT_UI_SSO_REDIRECT_PATH` | String | `/oauth2/start` | UI path used by Sign in with SSO. | +| `KAGENT_UI_STREAM_TIMEOUT_MS` | String | `1800000` | UI chat stream inactivity timeout in milliseconds. 0 disables the timeout. Applies in containers and Vite development. | +| `KAGENT_UI_VITE_API_MODE` | String | `(none)` | Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_UI_ENABLE_MOCK; leave unset for normal development. | +| `KAGENT_UI_VITE_EXAMPLE_EXTENSION` | Boolean | `false` | Build-time switch enabling the bundled example UI extension. | diff --git a/go/Makefile b/go/Makefile index 80132282eb..b54e5569e5 100644 --- a/go/Makefile +++ b/go/Makefile @@ -128,7 +128,7 @@ run: fmt vet ## Run a controller from your host. .PHONY: test test: ## Run all unit tests. - KAGENT_UPDATE_GOLDEN=$(KAGENT_UPDATE_GOLDEN) go test -race -skip 'TestE2E.*' -v ./... + KAGENT_TEST_UPDATE_GOLDEN=$(KAGENT_TEST_UPDATE_GOLDEN) go test -race -skip 'TestE2E.*' -v ./... # Pin e2e tests to the kind cluster so a stray current-context (or an # ambient KUBECONFIG) can't leak resources onto a remote cluster. Override diff --git a/go/core/pkg/env/testing.go b/go/core/pkg/env/testing.go index d2bd2458d2..96fd8247b0 100644 --- a/go/core/pkg/env/testing.go +++ b/go/core/pkg/env/testing.go @@ -3,28 +3,28 @@ package env // Testing environment variables used in E2E tests and mock services. var ( KagentLocalHost = RegisterStringVar( - "KAGENT_LOCAL_HOST", + "KAGENT_E2E_LOCAL_HOST", "", "Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems.", ComponentTesting, ) UpdateGolden = RegisterBoolVar( - "KAGENT_UPDATE_GOLDEN", + "KAGENT_TEST_UPDATE_GOLDEN", false, "When true, update golden test files instead of comparing.", ComponentTesting, ) STSPort = RegisterStringVar( - "KAGENT_STS_PORT", + "KAGENT_TEST_STS_PORT", "8091", "Port for the mock STS (Security Token Service) server.", ComponentTesting, ) LLMPort = RegisterStringVar( - "KAGENT_LLM_PORT", + "KAGENT_TEST_LLM_PORT", "8090", "Port for the mock LLM server.", ComponentTesting, @@ -43,10 +43,10 @@ var ( _ = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) _ = RegisterBoolVar("KAGENT_E2E_REQUIRE_TRACING", false, "Fail instead of skip when expected native harness tracing support is unavailable.", ComponentTesting) - _ = RegisterBoolVar("KAGENT_RUN_UPGRADE_TESTS", false, "Run upgrade integration tests when exactly true.", ComponentTesting) - _ = RegisterBoolVar("KAGENT_RUN_ROLLING_UPGRADE_TESTS", false, "Run rolling upgrade integration tests when exactly true.", ComponentTesting) - _ = RegisterStringVar("KAGENT_UI_LOOP_PORT", "8001", "Vite development/preview port and UI browser-test port.", ComponentTesting) - _ = RegisterStringVar("KAGENT_UI_LOOP_EXTENSION_PORT", "", "Example-extension browser-test port. Defaults to KAGENT_UI_LOOP_PORT plus 50.", ComponentTesting) - _ = RegisterBoolVar("KAGENT_UI_LOOP_LIVE", false, "Run UI browser tests against a real cluster when exactly true.", ComponentTesting) - _ = RegisterStringVar("KAGENT_UI_LOOP_LIVE_PORT", "8301", "Live-cluster UI browser-test port.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_E2E_RUN_UPGRADE_TESTS", false, "Run upgrade integration tests when exactly true.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS", false, "Run rolling upgrade integration tests when exactly true.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_PORT", "8001", "Vite development/preview port and UI browser-test port.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_EXTENSION_PORT", "", "Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_E2E_UI_LOOP_LIVE", false, "Run UI browser tests against a real cluster when exactly true.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_LIVE_PORT", "8301", "Live-cluster UI browser-test port.", ComponentTesting) ) diff --git a/go/core/pkg/env/ui.go b/go/core/pkg/env/ui.go index 07e3d174d2..5ace7d0e72 100644 --- a/go/core/pkg/env/ui.go +++ b/go/core/pkg/env/ui.go @@ -2,13 +2,13 @@ package env // The container entrypoint and Vite map the same deployment names to browser keys. var ( - _ = RegisterStringVar("KAGENT_API_BASE_URL", "/api", "Browser API base URL for UI containers and Vite development.", ComponentUI) - _ = RegisterStringVar("KAGENT_STREAM_TIMEOUT_MS", "1800000", "UI chat stream inactivity timeout in milliseconds. 0 disables the timeout. Applies in containers and Vite development.", ComponentUI) + _ = RegisterStringVar("KAGENT_UI_API_BASE_URL", "/api", "Browser API base URL for UI containers and Vite development.", ComponentUI) + _ = RegisterStringVar("KAGENT_UI_STREAM_TIMEOUT_MS", "1800000", "UI chat stream inactivity timeout in milliseconds. 0 disables the timeout. Applies in containers and Vite development.", ComponentUI) _ = RegisterStringVar("KAGENT_UI_BASE_PATH", "", "UI public path prefix, such as /ui; empty serves at the root. Applies in containers and Vite development; the container falls back to the root for invalid or reserved prefixes.", ComponentUI) - _ = RegisterStringVar("KAGENT_SSO_REDIRECT_PATH", "/oauth2/start", "UI path used by Sign in with SSO.", ComponentUI) - _ = RegisterBoolVar("KAGENT_ENABLE_MOCK_UI", false, "Serve the development UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Release bundles do not include the mock backend.", ComponentUI) - _ = RegisterStringVar("KAGENT_EXTENSION_", "", "UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public.", ComponentUI) - _ = RegisterStringVar("KAGENT_DEV_CONTROLLER_URL", "http://127.0.0.1:8083", "Vite development proxy target for /api and /a2a; not sent to the browser.", ComponentUI) - _ = RegisterBoolVar("KAGENT_VITE_EXAMPLE_EXTENSION", false, "Build-time switch enabling the bundled example UI extension.", ComponentUI) - _ = RegisterStringVar("KAGENT_VITE_API_MODE", "", "Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_ENABLE_MOCK_UI; leave unset for normal development.", ComponentUI, ComponentTesting) + _ = RegisterStringVar("KAGENT_UI_SSO_REDIRECT_PATH", "/oauth2/start", "UI path used by Sign in with SSO.", ComponentUI) + _ = RegisterBoolVar("KAGENT_UI_ENABLE_MOCK", false, "Serve the development UI from in-browser fixtures when true. Overrides backend settings; no user is signed in. Release bundles do not include the mock backend.", ComponentUI) + _ = RegisterStringVar("KAGENT_UI_EXTENSION_", "", "UI extension settings forwarded to the browser at runtime. Each installed extension owns its keys and defaults; these values are public.", ComponentUI) + _ = RegisterStringVar("KAGENT_UI_DEV_CONTROLLER_URL", "http://127.0.0.1:8083", "Vite development proxy target for /api and /a2a; not sent to the browser.", ComponentUI) + _ = RegisterBoolVar("KAGENT_UI_VITE_EXAMPLE_EXTENSION", false, "Build-time switch enabling the bundled example UI extension.", ComponentUI) + _ = RegisterStringVar("KAGENT_UI_VITE_API_MODE", "", "Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_UI_ENABLE_MOCK; leave unset for normal development.", ComponentUI, ComponentTesting) ) diff --git a/go/core/test/e2e/README.md b/go/core/test/e2e/README.md index b95fc37fa2..b628c25136 100644 --- a/go/core/test/e2e/README.md +++ b/go/core/test/e2e/README.md @@ -135,7 +135,7 @@ KAGENT_E2E_API_URL=http://:8083 make -C go e2e `TestSessionInteraction` starts the deterministic mock LLM on the test host and translates its listener to the host address reachable from the cluster (`172.17.0.1` on Linux and `host.docker.internal` on macOS). Set -`KAGENT_LOCAL_HOST` when the cluster uses a different host address. +`KAGENT_E2E_LOCAL_HOST` when the cluster uses a different host address. `TestMCPInteraction` starts `mockmcp` on the same reachable host, registers it as a `RemoteMCPServer`, and verifies an actual `tools/call` request. diff --git a/go/core/test/e2e/interaction_test.go b/go/core/test/e2e/interaction_test.go index 8380ed3bfa..1e776a69ce 100644 --- a/go/core/test/e2e/interaction_test.go +++ b/go/core/test/e2e/interaction_test.go @@ -942,7 +942,7 @@ func reachableServerURL(t *testing.T, baseURL, path string) string { if err != nil { t.Fatalf("parse mock LLM address: %v", err) } - host := os.Getenv("KAGENT_LOCAL_HOST") + host := os.Getenv("KAGENT_E2E_LOCAL_HOST") if host == "" { switch goruntime.GOOS { case "darwin": @@ -950,7 +950,7 @@ func reachableServerURL(t *testing.T, baseURL, path string) string { case "linux": host = "172.17.0.1" default: - t.Fatalf("KAGENT_LOCAL_HOST is required on %s", goruntime.GOOS) + t.Fatalf("KAGENT_E2E_LOCAL_HOST is required on %s", goruntime.GOOS) } } if net.ParseIP(host) != nil { diff --git a/go/core/test/upgrade/invoke_e2e_test.go b/go/core/test/upgrade/invoke_e2e_test.go index ec5e89563b..692e7e6ea4 100644 --- a/go/core/test/upgrade/invoke_e2e_test.go +++ b/go/core/test/upgrade/invoke_e2e_test.go @@ -44,7 +44,7 @@ func checkoutPreviousRelease(t *testing.T, env upgradeEnv) string { // matching-version tree (repo root for HEAD, the worktree for the prior // release). It port-forwards the controller for KAGENT_API_URL — re-established per // state, so it survives the controller being reinstalled between states — and -// relies on KAGENT_LOCAL_HOST (kind gateway IP, set by the make target) for the +// relies on KAGENT_E2E_LOCAL_HOST (kind gateway IP, set by the make target) for the // agent→host mock-LLM callback. label identifies the state in messages. func runInvokeE2E(t *testing.T, env upgradeEnv, treeGoDir, label string) { t.Helper() @@ -83,8 +83,8 @@ func upgradeE2EEnv(t *testing.T, env upgradeEnv, port int) []string { func requireInvokeEnvironment(t *testing.T, treeGoDir, label string) { t.Helper() - if os.Getenv("KAGENT_LOCAL_HOST") == "" { - t.Skipf("[%s] KAGENT_LOCAL_HOST is not set; run via make run-upgrade-tests", label) + if os.Getenv("KAGENT_E2E_LOCAL_HOST") == "" { + t.Skipf("[%s] KAGENT_E2E_LOCAL_HOST is not set; run via make run-upgrade-tests", label) } if _, err := os.Stat(filepath.Join(treeGoDir, "go.mod")); err != nil { t.Fatalf("[%s] test tree %q is not usable: %v", label, treeGoDir, err) diff --git a/go/core/test/upgrade/rolling_upgrade_test.go b/go/core/test/upgrade/rolling_upgrade_test.go index adc33afbe6..d16fe2c8fb 100644 --- a/go/core/test/upgrade/rolling_upgrade_test.go +++ b/go/core/test/upgrade/rolling_upgrade_test.go @@ -12,8 +12,8 @@ import ( ) func TestRollingUpgradeCompatibility(t *testing.T) { - if os.Getenv("KAGENT_RUN_ROLLING_UPGRADE_TESTS") != "true" { - t.Skip("set KAGENT_RUN_ROLLING_UPGRADE_TESTS=true to run rolling upgrade tests") + if os.Getenv("KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS") != "true" { + t.Skip("set KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS=true to run rolling upgrade tests") } env := loadUpgradeEnv(t) diff --git a/go/core/test/upgrade/upgrade_test.go b/go/core/test/upgrade/upgrade_test.go index 304d653bf6..4daa466c09 100644 --- a/go/core/test/upgrade/upgrade_test.go +++ b/go/core/test/upgrade/upgrade_test.go @@ -45,8 +45,8 @@ type postgresMigrationState struct { } func TestUpgrade(t *testing.T) { - if os.Getenv("KAGENT_RUN_UPGRADE_TESTS") != "true" { - t.Skip("set KAGENT_RUN_UPGRADE_TESTS=true to run upgrade tests") + if os.Getenv("KAGENT_E2E_RUN_UPGRADE_TESTS") != "true" { + t.Skip("set KAGENT_E2E_RUN_UPGRADE_TESTS=true to run upgrade tests") } env := loadUpgradeEnv(t) diff --git a/helm/kagent/templates/ui-configmap.yaml b/helm/kagent/templates/ui-configmap.yaml index b26ac89908..8613a25071 100644 --- a/helm/kagent/templates/ui-configmap.yaml +++ b/helm/kagent/templates/ui-configmap.yaml @@ -6,7 +6,7 @@ metadata: labels: {{- include "kagent.labels" . | nindent 4 }} data: - KAGENT_API_BASE_URL: {{ .Values.ui.publicBackendUrl | default "/api" | quote }} + KAGENT_UI_API_BASE_URL: {{ .Values.ui.publicBackendUrl | default "/api" | quote }} {{- with .Values.ui.basePath }} {{- /* Keep in sync with ui/scripts/init.sh. nginx strips the prefix, so it must not start with a path nginx serves. */}} {{- if or (not (regexMatch "^(/[A-Za-z0-9._~-]+)*/?$" .)) (regexMatch "(^|/)\\.\\.?(/|$)" .) }}{{ fail "ui.basePath must be a path like /ui" }}{{ end }} @@ -14,8 +14,8 @@ data: KAGENT_UI_BASE_PATH: {{ . | quote }} {{- end }} {{- if .Values.ui.auth }} - KAGENT_SSO_REDIRECT_PATH: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }} + KAGENT_UI_SSO_REDIRECT_PATH: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }} {{- end }} {{- if .Values.ui.streamTimeoutSeconds }} - KAGENT_STREAM_TIMEOUT_MS: {{ mul (int .Values.ui.streamTimeoutSeconds) 1000 | quote }} + KAGENT_UI_STREAM_TIMEOUT_MS: {{ mul (int .Values.ui.streamTimeoutSeconds) 1000 | quote }} {{- end }} diff --git a/helm/kagent/tests/ui-deployment_test.yaml b/helm/kagent/tests/ui-deployment_test.yaml index 01766e2157..5240f707ca 100644 --- a/helm/kagent/tests/ui-deployment_test.yaml +++ b/helm/kagent/tests/ui-deployment_test.yaml @@ -22,7 +22,7 @@ tests: template: ui-deployment.yaml set: ui.env: - - name: KAGENT_API_BASE_URL + - name: KAGENT_UI_API_BASE_URL value: /custom-api - name: CUSTOM_TOKEN valueFrom: @@ -33,7 +33,7 @@ tests: - equal: path: spec.template.spec.containers[0].env value: - - name: KAGENT_API_BASE_URL + - name: KAGENT_UI_API_BASE_URL value: /custom-api - name: CUSTOM_TOKEN valueFrom: @@ -41,7 +41,7 @@ tests: name: ui-token key: token - equal: - path: data.KAGENT_API_BASE_URL + path: data.KAGENT_UI_API_BASE_URL value: /api template: ui-configmap.yaml - notExists: @@ -351,13 +351,13 @@ tests: template: ui-configmap.yaml asserts: - equal: - path: data.KAGENT_API_BASE_URL + path: data.KAGENT_UI_API_BASE_URL value: "/api" - equal: - path: data.KAGENT_SSO_REDIRECT_PATH + path: data.KAGENT_UI_SSO_REDIRECT_PATH value: "/oauth2/start" - equal: - path: data.KAGENT_STREAM_TIMEOUT_MS + path: data.KAGENT_UI_STREAM_TIMEOUT_MS value: "1800000" - it: should carry an overridden publicBackendUrl through to the browser config @@ -367,7 +367,7 @@ tests: publicBackendUrl: "https://api.example.com/api" asserts: - equal: - path: data.KAGENT_API_BASE_URL + path: data.KAGENT_UI_API_BASE_URL value: "https://api.example.com/api" - it: should fall back to /api when publicBackendUrl is blanked @@ -377,7 +377,7 @@ tests: publicBackendUrl: "" asserts: - equal: - path: data.KAGENT_API_BASE_URL + path: data.KAGENT_UI_API_BASE_URL value: "/api" - it: should pass ui.basePath through to the browser config only when set @@ -442,10 +442,10 @@ tests: streamTimeoutSeconds: 90 asserts: - equal: - path: data.KAGENT_SSO_REDIRECT_PATH + path: data.KAGENT_UI_SSO_REDIRECT_PATH value: "/custom/start" - equal: - path: data.KAGENT_STREAM_TIMEOUT_MS + path: data.KAGENT_UI_STREAM_TIMEOUT_MS value: "90000" - it: should not mount a nextjs build cache diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 3c84f827d0..d1bdb79cea 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -529,7 +529,7 @@ ui: # -- Extra environment variables for the UI container, applied after ConfigMap # defaults. Entries stay in the Deployment to support valueFrom and $(VAR_NAME) # expansion. An installed app extension's settings go here, named - # `KAGENT_EXTENSION_*`; the container's startup script copies + # `KAGENT_UI_EXTENSION_*`; the container's startup script copies # those onto `window.environmentVariables` for the browser to read. env: [] # -- Identity headers the UI's nginx proxy will forward to the backend on diff --git a/scripts/setup-cluster/README.md b/scripts/setup-cluster/README.md index 844de82055..62ff661fb3 100644 --- a/scripts/setup-cluster/README.md +++ b/scripts/setup-cluster/README.md @@ -37,7 +37,7 @@ yarn dev That is the whole reason this script holds two forwards open rather than one. It used to forward only the UI, and running the dev server then needed -`KAGENT_DEV_CONTROLLER_URL=http://127.0.0.1:8080` in `ui/.env` — a line whose absence +`KAGENT_UI_DEV_CONTROLLER_URL=http://127.0.0.1:8080` in `ui/.env` — a line whose absence looked like a broken backend rather than a missing forward, since the page loaded fine and every read failed with `ECONNREFUSED 127.0.0.1:8083`. @@ -61,7 +61,7 @@ If you would rather not build one, the UI runs entirely on in-browser fixtures: ```sh cd ui -KAGENT_ENABLE_MOCK_UI=true yarn dev +KAGENT_UI_ENABLE_MOCK=true yarn dev ``` Every page works and says on the page that the data is not real. `?mock=empty`, diff --git a/scripts/setup-cluster/setup-cluster.sh b/scripts/setup-cluster/setup-cluster.sh index d1a31f49b1..f350be0fa5 100755 --- a/scripts/setup-cluster/setup-cluster.sh +++ b/scripts/setup-cluster/setup-cluster.sh @@ -170,7 +170,7 @@ kubectl get pods -n kagent # not a dev server. 8083 is the controller, which `yarn dev` proxies to by default. # # The second one is here so that default is true. With only the UI forwarded, running -# the dev server needed KAGENT_DEV_CONTROLLER_URL pointed at 8080 in `ui/.env`, and +# the dev server needed KAGENT_UI_DEV_CONTROLLER_URL pointed at 8080 in `ui/.env`, and # without that line every read failed with `ECONNREFUSED 127.0.0.1:8083` on a page that # otherwise loaded -- which reads as a broken backend rather than a missing forward. A # second `kubectl` is cheaper than a setting every reader has to be told about. diff --git a/ui/.env.example b/ui/.env.example index 94a997166f..357d46ddfa 100644 --- a/ui/.env.example +++ b/ui/.env.example @@ -5,8 +5,8 @@ # so a change needs no restart but does need a full page reload. # # Only the keys below reach the browser: `CORE_ENV_VARS` in `src/env.ts` for the -# application's own, and anything named `KAGENT_EXTENSION_*` for an installed extension's. -# The `KAGENT_VITE_` keys below use a different mechanism — see their own section. +# application's own, and anything named `KAGENT_UI_EXTENSION_*` for an installed extension's. +# The `KAGENT_UI_VITE_` keys below use a different mechanism — see their own section. # --- Settings the application reads --- @@ -18,23 +18,23 @@ # # `?mock=ok|empty|error|slow` picks which scenario the fixtures play, and does nothing # unless this is already on. -# KAGENT_ENABLE_MOCK_UI=true +# KAGENT_UI_ENABLE_MOCK=true # Where the browser calls the API. Relative by default, which the dev server proxies. -# KAGENT_API_BASE_URL=/api +# KAGENT_UI_API_BASE_URL=/api # Where the dev server sends those calls. Read by `vite.config.ts`, so it never reaches # the browser. Defaults to the controller on 8083, which is what # `scripts/setup-cluster/setup-cluster.sh` forwards — a cluster from that script needs # nothing here. Point it at 8080 to go through the UI pod's nginx instead, which is the # hop a deployment takes. -# KAGENT_DEV_CONTROLLER_URL=http://127.0.0.1:8080 +# KAGENT_UI_DEV_CONTROLLER_URL=http://127.0.0.1:8080 # Chat stream inactivity timeout, in milliseconds. -# KAGENT_STREAM_TIMEOUT_MS=1800000 +# KAGENT_UI_STREAM_TIMEOUT_MS=1800000 # Where "Sign in with SSO" sends the browser. -# KAGENT_SSO_REDIRECT_PATH=/oauth2/start +# KAGENT_UI_SSO_REDIRECT_PATH=/oauth2/start # Public path prefix when serving below a sub-path. # KAGENT_UI_BASE_PATH=/ui @@ -43,19 +43,19 @@ # --- Build-time switches --- # # Read by the bundler through `import.meta.env`, not from the browser's runtime -# config. Vite is configured to expose only the `KAGENT_VITE_` prefix. +# config. Vite is configured to expose only the `KAGENT_UI_VITE_` prefix. # Changing one needs the dev server restarted, not just a reload. # Install the bundled Example App Extension, which is off by default. A switch for # that one example: it names no extension and cannot, because an extension has to be # imported to be in the bundle at all. Which extensions a build installs is the array # in `src/appExtensions/activeExtensions.ts`. -# KAGENT_VITE_EXAMPLE_EXTENSION=true +# KAGENT_UI_VITE_EXAMPLE_EXTENSION=true # --- Settings an installed extension reads --- # -# Anything named `KAGENT_EXTENSION_*` is passed through untouched. The application reads none +# Anything named `KAGENT_UI_EXTENSION_*` is passed through untouched. The application reads none # of them and does not know what they mean; the extension that reads them documents # them. Nothing needs adding here for an extension to gain one. diff --git a/ui/README.md b/ui/README.md index 3104d8e65a..65660c4067 100644 --- a/ui/README.md +++ b/ui/README.md @@ -56,9 +56,9 @@ kubectl -n kagent port-forward svc/kagent-controller 8083:8083 ``` Either way `8083` is where the proxy looks, so neither case needs a `.env` entry. Two -settings change that when you want them to: `KAGENT_DEV_CONTROLLER_URL=http://127.0.0.1:8080` +settings change that when you want them to: `KAGENT_UI_DEV_CONTROLLER_URL=http://127.0.0.1:8080` sends the proxy through the UI pod's nginx instead, which is the hop a deployed build -takes; and `KAGENT_API_BASE_URL` calls a backend directly, bypassing the proxy. +takes; and `KAGENT_UI_API_BASE_URL` calls a backend directly, bypassing the proxy. Which backend is serving is decided in exactly one place, `src/api/config.ts`. Nothing above the data layer knows or cares. @@ -79,12 +79,12 @@ to have landed. | Setting | Effect | | --- | --- | -| `KAGENT_API_BASE_URL` | where the browser calls the API (default `/api`) | -| `KAGENT_ENABLE_MOCK_UI` | `true` serves the whole API from in-browser fixtures | -| `KAGENT_SSO_REDIRECT_PATH` | where "Sign in with SSO" sends the browser | -| `KAGENT_STREAM_TIMEOUT_MS` | chat stream inactivity timeout | +| `KAGENT_UI_API_BASE_URL` | where the browser calls the API (default `/api`) | +| `KAGENT_UI_ENABLE_MOCK` | `true` serves the whole API from in-browser fixtures | +| `KAGENT_UI_SSO_REDIRECT_PATH` | where "Sign in with SSO" sends the browser | +| `KAGENT_UI_STREAM_TIMEOUT_MS` | chat stream inactivity timeout | -`KAGENT_ENABLE_MOCK_UI` is a development setting. The release image deliberately ships no +`KAGENT_UI_ENABLE_MOCK` is a development setting. The release image deliberately ships no mock backend, so a built bundle logs a warning and uses the real API rather than entering a mode it cannot serve. diff --git a/ui/docs/app-extensions.md b/ui/docs/app-extensions.md index 5898ca2ebb..0fc8fc73d9 100644 --- a/ui/docs/app-extensions.md +++ b/ui/docs/app-extensions.md @@ -17,8 +17,8 @@ That is the whole integration surface. Installing a second extension is one more entry; nothing else in the application changes. Nothing is installed by default, including the bundled example. To see the example -running without editing the file, set `KAGENT_VITE_EXAMPLE_EXTENSION=true` — in `ui/.env`, -or on the command line as `KAGENT_VITE_EXAMPLE_EXTENSION=true yarn dev`. +running without editing the file, set `KAGENT_UI_VITE_EXAMPLE_EXTENSION=true` — in `ui/.env`, +or on the command line as `KAGENT_UI_VITE_EXAMPLE_EXTENSION=true yarn dev`. --- @@ -83,10 +83,10 @@ installed, so a default build renders only what this project itself provides. Switch it on with: ```bash -KAGENT_VITE_EXAMPLE_EXTENSION=true yarn dev +KAGENT_UI_VITE_EXAMPLE_EXTENSION=true yarn dev ``` -or by putting `KAGENT_VITE_EXAMPLE_EXTENSION=true` in `ui/.env`, which `.env.example` +or by putting `KAGENT_UI_VITE_EXAMPLE_EXTENSION=true` in `ui/.env`, which `.env.example` documents. It is read by the bundler rather than at runtime, so changing it needs the dev server restarted. @@ -567,7 +567,7 @@ rule applies one level up: the earlier extension's providers wrap the later one' ## Settings an extension reads An extension that needs configuring — an API root, a feature flag, an account id — -names a variable `KAGENT_EXTENSION_*`. Three steps, and none of them touch this +names a variable `KAGENT_UI_EXTENSION_*`. Three steps, and none of them touch this application. **Read it** with `readEnv`, which takes any key and a fallback: @@ -575,7 +575,7 @@ application. ```ts import { readEnv } from "@/appExtensions"; -const apiUrl = readEnv("KAGENT_EXTENSION_EXAMPLE_API_URL", "https://api.example.test"); +const apiUrl = readEnv("KAGENT_UI_EXTENSION_EXAMPLE_API_URL", "https://api.example.test"); ``` **Set it in a deployment** through `ui.env` in the `kagent` chart, which is passed @@ -585,7 +585,7 @@ through to the UI deployment verbatim: # values.yaml ui: env: - - name: KAGENT_EXTENSION_EXAMPLE_API_URL + - name: KAGENT_UI_EXTENSION_EXAMPLE_API_URL value: https://api.example.test ``` @@ -594,11 +594,11 @@ section at the bottom for exactly this, so a branch that installs an extension appends rather than editing the application's own settings above it: ```sh -KAGENT_EXTENSION_EXAMPLE_API_URL=https://api.example.test +KAGENT_UI_EXTENSION_EXAMPLE_API_URL=https://api.example.test ``` That is the whole mechanism, and nothing needs registering. The container's -`scripts/init.sh` copies every `KAGENT_EXTENSION_*` variable out of the pod's environment +`scripts/init.sh` copies every `KAGENT_UI_EXTENSION_*` variable out of the pod's environment **by prefix**, and the dev server does the same from `.env` and your shell. Neither enumerates the keys, so this application never learns what your settings mean and needs no change when your extension grows one. @@ -618,7 +618,7 @@ two consequences of it are worth knowing: **Do not add a `VITE_` prefix.** That prefix is Vite's `envPrefix` filter, which decides what `import.meta.env` exposes to bundled code; it applies to build-time variables and to nothing else. A prefixed name would no longer match the -`KAGENT_EXTENSION_` prefix the init script and the dev server select on, so the value +`KAGENT_UI_EXTENSION_` prefix the init script and the dev server select on, so the value would never arrive and `readEnv` would quietly return the fallback. --- diff --git a/ui/package.json b/ui/package.json index 08189f47fc..b7bdc06149 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,7 +17,7 @@ "test:watch": "vitest", "test:pw": "playwright test", "test:e2e": "yarn test:pw", - "test:pw:live": "KAGENT_UI_LOOP_LIVE=true playwright test" + "test:pw:live": "KAGENT_E2E_UI_LOOP_LIVE=true playwright test" }, "dependencies": { "@bufbuild/protobuf": "2.15.0", diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index d13e82b0c9..a1e5f79ca0 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -6,7 +6,7 @@ import { defineConfig, devices } from "@playwright/test"; * main process and again in every worker, so anything varying per process gives * each worker a different base URL from the one the servers were started on. */ -const PORT = Number(process.env.KAGENT_UI_LOOP_PORT ?? 8001); +const PORT = Number(process.env.KAGENT_E2E_UI_LOOP_PORT ?? 8001); const BASE_URL = `http://localhost:${PORT}`; /** @@ -21,7 +21,7 @@ const BASE_URL = `http://localhost:${PORT}`; // Deliberately not PORT + 1: Vite falls forward to the next free port when the // one it is told to use is busy, so adjacent ports let a slow-to-die server from // a previous run push one app onto the other's port. -const EXTENSION_PORT = Number(process.env.KAGENT_UI_LOOP_EXTENSION_PORT ?? PORT + 50); +const EXTENSION_PORT = Number(process.env.KAGENT_E2E_UI_LOOP_EXTENSION_PORT ?? PORT + 50); const EXTENSION_BASE_URL = `http://localhost:${EXTENSION_PORT}`; /** Specs that need an extension installed opt in by filename. */ @@ -30,10 +30,10 @@ const EXTENSION_SPECS = /\.withExtension\.spec\.ts$/; /** * The suite is the acceptance bar, so what it runs against cannot depend on the * shell it was started from: both servers are pinned to the in-browser mock - * backend. An inherited KAGENT_VITE_API_MODE=live would otherwise point a whole run at + * backend. An inherited KAGENT_UI_VITE_API_MODE=live would otherwise point a whole run at * a real cluster. */ -const MOCK_BACKEND = { KAGENT_VITE_API_MODE: "mock" }; +const MOCK_BACKEND = { KAGENT_UI_VITE_API_MODE: "mock" }; /** * What each of the three servers is pinned to. @@ -42,13 +42,13 @@ const MOCK_BACKEND = { KAGENT_VITE_API_MODE: "mock" }; * stated in one place — and so a branch that installs an extension changes a * value instead of restructuring the `projects`/`webServer` blocks. * - * `KAGENT_VITE_EXAMPLE_EXTENSION` is pinned on the bare server for the same reason - * `KAGENT_VITE_API_MODE` is: an inherited value must not be able to decide what a run + * `KAGENT_UI_VITE_EXAMPLE_EXTENSION` is pinned on the bare server for the same reason + * `KAGENT_UI_VITE_API_MODE` is: an inherited value must not be able to decide what a run * measures. Left unpinned, the bare project measures whatever the shell happened * to export. */ -const BARE_APP = { ...MOCK_BACKEND, KAGENT_VITE_EXAMPLE_EXTENSION: "false" }; -const EXAMPLE_APP = { ...MOCK_BACKEND, KAGENT_VITE_EXAMPLE_EXTENSION: "true" }; +const BARE_APP = { ...MOCK_BACKEND, KAGENT_UI_VITE_EXAMPLE_EXTENSION: "false" }; +const EXAMPLE_APP = { ...MOCK_BACKEND, KAGENT_UI_VITE_EXAMPLE_EXTENSION: "true" }; /** * The third mode: one app, wired to a real backend. @@ -67,7 +67,7 @@ const EXAMPLE_APP = { ...MOCK_BACKEND, KAGENT_VITE_EXAMPLE_EXTENSION: "true" }; * So `LIVE` swaps the whole `projects`/`webServer` pair rather than appending to * it. `yarn test:pw` and `yarn test:pw:live` are two disjoint runs. */ -const LIVE = process.env.KAGENT_UI_LOOP_LIVE === "true"; +const LIVE = process.env.KAGENT_E2E_UI_LOOP_LIVE === "true"; /** * Its own port, far from the mock servers' 8001/8051, for the same reason those @@ -75,7 +75,7 @@ const LIVE = process.env.KAGENT_UI_LOOP_LIVE === "true"; * told to use is busy, so a live run must not be able to land on a port a mock * server is about to want, or vice versa. */ -const LIVE_PORT = Number(process.env.KAGENT_UI_LOOP_LIVE_PORT ?? 8301); +const LIVE_PORT = Number(process.env.KAGENT_E2E_UI_LOOP_LIVE_PORT ?? 8301); const LIVE_BASE_URL = `http://localhost:${LIVE_PORT}`; /** Read by `playwright/globalSetup.ts` to decide what to verify about a server. */ @@ -86,11 +86,11 @@ export const LIVE_PROJECT = "chromium-live"; * build reaches it through nginx — so the app uses the same relative URLs either * way and this mode tests the addressing a real deployment uses. * - * `KAGENT_VITE_API_MODE` is pinned as well as the runtime flag: the build-time pin is + * `KAGENT_UI_VITE_API_MODE` is pinned as well as the runtime flag: the build-time pin is * the one thing an inherited `.env` cannot override, and a live suite that * silently answered from fixtures would be worse than a red one. */ -const LIVE_APP = { KAGENT_VITE_API_MODE: "live", KAGENT_ENABLE_MOCK_UI: "false" }; +const LIVE_APP = { KAGENT_UI_VITE_API_MODE: "live", KAGENT_UI_ENABLE_MOCK: "false" }; /** * How the live server is started. @@ -197,7 +197,7 @@ export default defineConfig({ // extension specs then fail looking for contributions that were never installed. // That was an intermittent failure whose frequency depended only on whether // something happened to linger. Refusing to adopt makes an occupied port a - // loud startup error instead; set KAGENT_UI_LOOP_PORT / KAGENT_UI_LOOP_EXTENSION_PORT to run + // loud startup error instead; set KAGENT_E2E_UI_LOOP_PORT / KAGENT_E2E_UI_LOOP_EXTENSION_PORT to run // alongside a dev server you want to keep. webServer: LIVE ? [ diff --git a/ui/playwright/README.md b/ui/playwright/README.md index ba5de4165b..2a4b01bb12 100644 --- a/ui/playwright/README.md +++ b/ui/playwright/README.md @@ -8,7 +8,7 @@ rewrite is done when the same general set of journeys still passes. ```bash cd ui yarn test:pw # or: yarn test:e2e -KAGENT_UI_LOOP_PORT=8012 yarn test:pw # when something else owns the default port +KAGENT_E2E_UI_LOOP_PORT=8012 yarn test:pw # when something else owns the default port ``` Nothing else is needed — no cluster, no port-forward, no provider key. @@ -38,7 +38,7 @@ here. ## What it runs against The suite runs against the in-browser mock backend (`src/mocks/`), pinned by the -`webServer` block in `playwright.config.ts` so an inherited `KAGENT_VITE_API_MODE=live` +`webServer` block in `playwright.config.ts` so an inherited `KAGENT_UI_VITE_API_MODE=live` cannot silently point a run at a real cluster. That buys three things the old kind-cluster setup could not offer: the data is fixed, so a spec can assert exact rows; the run takes seconds; and failure is a first-class state rather than @@ -90,8 +90,8 @@ Which extensions a build installs is decided at build time, so "installed" and | Project | Server | Specs | |---|---|---| -| `chromium` | bare — no extension, on `KAGENT_UI_LOOP_PORT` | everything not matching `*.withExtension.spec.ts` | -| `chromium-with-extension` | `KAGENT_VITE_EXAMPLE_EXTENSION=true`, on `KAGENT_UI_LOOP_PORT + 50` | `*.withExtension.spec.ts` | +| `chromium` | bare — no extension, on `KAGENT_E2E_UI_LOOP_PORT` | everything not matching `*.withExtension.spec.ts` | +| `chromium-with-extension` | `KAGENT_UI_VITE_EXAMPLE_EXTENSION=true`, on `KAGENT_E2E_UI_LOOP_PORT + 50` | `*.withExtension.spec.ts` | A spec opts into the extension-installed app by being named `*.withExtension.spec.ts`. @@ -211,7 +211,7 @@ conventions below: the shared fixture import, and antd's class names. ```bash cd ui yarn test:pw:live -KAGENT_UI_LOOP_LIVE_PORT=8312 yarn test:pw:live # to run beside something on 8301 +KAGENT_E2E_UI_LOOP_LIVE_PORT=8312 yarn test:pw:live # to run beside something on 8301 ``` Unlike `yarn test:pw`, this one **does** need a cluster, with the controller @@ -222,7 +222,7 @@ A live run reaches the controller through Vite's proxy, exactly as a deployed build reaches it through nginx, so the app uses the same relative URLs either way and this mode tests the addressing a real deployment uses. -**Why a separate mode rather than a third project.** `KAGENT_UI_LOOP_LIVE=true` swaps the +**Why a separate mode rather than a third project.** `KAGENT_E2E_UI_LOOP_LIVE=true` swaps the whole `projects`/`webServer` pair in `playwright.config.ts` instead of appending to it, because the two modes' requirements are mutually exclusive. A live project in the default list would make `yarn test:pw` — which is meant to need nothing but a @@ -232,7 +232,7 @@ would cost every live run the time to boot Vite twice for nothing. The two runs are disjoint. The live project also gets its own port, 8301, far from the mock servers' 8001/8051 for the same reason those two are 50 apart. -**A green live run has to have been live.** `KAGENT_VITE_API_MODE` is pinned at build +**A green live run has to have been live.** `KAGENT_UI_VITE_API_MODE` is pinned at build time as well as at runtime, because a build-time pin is the one thing an inherited `.env` cannot override — and a live suite that quietly answered from fixtures would be worse than a red one, since a green one gets taken as evidence the diff --git a/ui/playwright/globalSetup.ts b/ui/playwright/globalSetup.ts index da14bc041e..46f7df64af 100644 --- a/ui/playwright/globalSetup.ts +++ b/ui/playwright/globalSetup.ts @@ -53,7 +53,7 @@ export default async function globalSetup(config: FullConfig): Promise { `${baseUrl} was expected to serve the app with the example extension ` + `installed (project "${project.name}"), but no extension points are ` + `mounted. The server on that port came up without ` + - `KAGENT_VITE_EXAMPLE_EXTENSION=true.`, + `KAGENT_UI_VITE_EXAMPLE_EXTENSION=true.`, ); } if (!wantsExtension && slots > 0) { diff --git a/ui/scripts/init.sh b/ui/scripts/init.sh index 0d4c856c41..b7268985df 100644 --- a/ui/scripts/init.sh +++ b/ui/scripts/init.sh @@ -25,10 +25,10 @@ mkdir -p /tmp/nginx/client_temp \ # awaited fetch to have landed. Keep the keys in sync with `ui/src/env.ts`. CONFIG_PATH=/tmp/kagent/env-config.js -API_BASE_URL="${KAGENT_API_BASE_URL:-/api}" -SSO_REDIRECT_PATH="${KAGENT_SSO_REDIRECT_PATH:-/oauth2/start}" -STREAM_TIMEOUT_MS="${KAGENT_STREAM_TIMEOUT_MS:-1800000}" -ENABLE_MOCK_UI="${KAGENT_ENABLE_MOCK_UI:-false}" +API_BASE_URL="${KAGENT_UI_API_BASE_URL:-/api}" +SSO_REDIRECT_PATH="${KAGENT_UI_SSO_REDIRECT_PATH:-/oauth2/start}" +STREAM_TIMEOUT_MS="${KAGENT_UI_STREAM_TIMEOUT_MS:-1800000}" +ENABLE_MOCK_UI="${KAGENT_UI_ENABLE_MOCK:-false}" # Public path prefix when a reverse proxy serves the UI under a sub-path, e.g. /ui. BASE_PATH="${KAGENT_UI_BASE_PATH:-}" BASE_PATH="${BASE_PATH%/}" @@ -42,7 +42,7 @@ fi # A non-numeric timeout would abort every chat stream immediately, which looks # like the backend hanging up rather than like a bad value. Fall back instead. if ! [[ "$STREAM_TIMEOUT_MS" =~ ^[0-9]+$ ]]; then - echo "init.sh: KAGENT_STREAM_TIMEOUT_MS='${STREAM_TIMEOUT_MS}' is not a number; using 1800000" >&2 + echo "init.sh: KAGENT_UI_STREAM_TIMEOUT_MS='${STREAM_TIMEOUT_MS}' is not a number; using 1800000" >&2 STREAM_TIMEOUT_MS=1800000 fi @@ -57,7 +57,7 @@ json_escape() { } # Anything an installed extension reads, passed through verbatim and by name rather -# than by list: whatever the chart sets as `KAGENT_EXTENSION_*` arrives, so this image needs +# than by list: whatever the chart sets as `KAGENT_UI_EXTENSION_*` arrives, so this image needs # no change when an extension grows a setting, and this repository does not enumerate # another product's configuration. The application ignores keys it has no use for. # @@ -66,7 +66,7 @@ extension_json="" while IFS='=' read -r name value; do extension_json+=" \"$(json_escape "$name")\": \"$(json_escape "$value")\", " -done < <(env | grep '^KAGENT_EXTENSION_' | sort) +done < <(env | grep '^KAGENT_UI_EXTENSION_' | sort) cat > "$CONFIG_PATH" < { describe("readEnv", () => { it("reads a key the application knows nothing about", () => { - window.environmentVariables = { KAGENT_EXTENSION_ANYTHING: "a-value" }; - expect(readEnv("KAGENT_EXTENSION_ANYTHING", "a-fallback")).toBe("a-value"); + window.environmentVariables = { KAGENT_UI_EXTENSION_ANYTHING: "a-value" }; + expect(readEnv("KAGENT_UI_EXTENSION_ANYTHING", "a-fallback")).toBe("a-value"); }); it("uses the caller's fallback when it is absent", () => { - expect(readEnv("KAGENT_EXTENSION_ANYTHING", "a-fallback")).toBe("a-fallback"); + expect(readEnv("KAGENT_UI_EXTENSION_ANYTHING", "a-fallback")).toBe("a-fallback"); }); }); diff --git a/ui/src/env.ts b/ui/src/env.ts index 217bb228a1..e4bdf2017b 100644 --- a/ui/src/env.ts +++ b/ui/src/env.ts @@ -36,15 +36,15 @@ export interface EnvironmentVariables { * publish every credential on the developer's machine into the HTML. * * An extension's own keys are not listed here, and are not listed anywhere: anything - * named `KAGENT_EXTENSION_*` is passed through by the dev server and by `scripts/init.sh`, + * named `KAGENT_UI_EXTENSION_*` is passed through by the dev server and by `scripts/init.sh`, * and read back with `readEnv`, which takes any key. This application does not know * what an extension's settings are and does not need to. */ export const CORE_ENV_VARS = { - API_BASE_URL: "KAGENT_API_BASE_URL", - SSO_REDIRECT_PATH: "KAGENT_SSO_REDIRECT_PATH", - STREAM_TIMEOUT_MS: "KAGENT_STREAM_TIMEOUT_MS", - ENABLE_MOCK_UI: "KAGENT_ENABLE_MOCK_UI", + API_BASE_URL: "KAGENT_UI_API_BASE_URL", + SSO_REDIRECT_PATH: "KAGENT_UI_SSO_REDIRECT_PATH", + STREAM_TIMEOUT_MS: "KAGENT_UI_STREAM_TIMEOUT_MS", + ENABLE_MOCK_UI: "KAGENT_UI_ENABLE_MOCK", BASE_PATH: "KAGENT_UI_BASE_PATH", } as const satisfies Record; diff --git a/ui/src/mocks/startMockBackend.ts b/ui/src/mocks/startMockBackend.ts index f242a3b745..335e61ac7b 100644 --- a/ui/src/mocks/startMockBackend.ts +++ b/ui/src/mocks/startMockBackend.ts @@ -9,7 +9,7 @@ * * Order matters: the transport is installed before anything can call an * operation, which is why this is awaited in `main.tsx` before the app renders. - * Only `main.tsx` calls it, and only when `KAGENT_VITE_API_MODE` is not `"live"` — a page + * Only `main.tsx` calls it, and only when `KAGENT_UI_VITE_API_MODE` is not `"live"` — a page * that quietly served fixtures because the backend was unreachable would look * healthy while showing data that was never real. */ diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts index dd434a0eb4..47fe7590a6 100644 --- a/ui/src/vite-env.d.ts +++ b/ui/src/vite-env.d.ts @@ -2,7 +2,7 @@ interface ImportMetaEnv { /** Optional build-time override; normal development uses runtime configuration. */ - readonly KAGENT_VITE_API_MODE?: "mock" | "live"; + readonly KAGENT_UI_VITE_API_MODE?: "mock" | "live"; /** * `"true"` installs the bundled Example App Extension. * @@ -10,7 +10,7 @@ interface ImportMetaEnv { * extension has to be imported to be in the bundle at all, so which ones are * installed is the array in `appExtensions/activeExtensions.ts`. */ - readonly KAGENT_VITE_EXAMPLE_EXTENSION?: "true" | "false"; + readonly KAGENT_UI_VITE_EXAMPLE_EXTENSION?: "true" | "false"; } interface ImportMeta { diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 4cd915d0f7..817b8317da 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -21,8 +21,8 @@ import { CORE_ENV_VARS, ENV_DEFAULTS } from "./src/env.ts"; function devProxy(mode: string) { const fromFiles = loadEnv(mode, import.meta.dirname, ""); const target = - process.env.KAGENT_DEV_CONTROLLER_URL || - fromFiles.KAGENT_DEV_CONTROLLER_URL || + process.env.KAGENT_UI_DEV_CONTROLLER_URL || + fromFiles.KAGENT_UI_DEV_CONTROLLER_URL || "http://127.0.0.1:8083"; return { @@ -42,17 +42,17 @@ function devProxy(mode: string) { * * A prefix instead. `readEnv` already takes any key and `window.environmentVariables` * is already an open record, so nothing in the application changes when an extension - * adds a setting: it names it `KAGENT_EXTENSION_SOMETHING` and it arrives. + * adds a setting: it names it `KAGENT_UI_EXTENSION_SOMETHING` and it arrives. * * Still bounded, which is the point of `CORE_ENV_VARS` being an explicit map: the dev * server inlines these into the document, so a wholesale copy of the environment would * publish every credential on the machine into the HTML. A variable has to be * deliberately named for an extension to be copied — but only that deliberately, so a - * stray `KAGENT_EXTENSION_` variable in a shell will be inlined. It is a development server + * stray `KAGENT_UI_EXTENSION_` variable in a shell will be inlined. It is a development server * rendering a page for the developer who started it; the container path below is the * one that faces a cluster, and there the environment is the chart's own. */ -const EXTENSION_ENV_PREFIX = "KAGENT_EXTENSION_"; +const EXTENSION_ENV_PREFIX = "KAGENT_UI_EXTENSION_"; /** * Serves the deployment configuration the way the container does. @@ -119,7 +119,7 @@ function devEnvConfig(mode: string): Plugin { export default defineConfig(({ mode }) => ({ // Only explicit build-time settings are exposed through import.meta.env. - envPrefix: "KAGENT_VITE_", + envPrefix: "KAGENT_UI_VITE_", // Relative asset URLs, resolved against the `` in index.html. base: "./", // JSX is transformed by oxc, which routes the factory at @emotion/react — @@ -148,7 +148,7 @@ export default defineConfig(({ mode }) => ({ alias: { "@": path.resolve(import.meta.dirname, "./src") }, }, server: { - port: Number(process.env.KAGENT_UI_LOOP_PORT ?? 8001), + port: Number(process.env.KAGENT_E2E_UI_LOOP_PORT ?? 8001), host: "0.0.0.0", // Stands in for what nginx does in a deployed cluster, so a dev server // talking to a real controller uses the same relative URLs as production. @@ -157,7 +157,7 @@ export default defineConfig(({ mode }) => ({ proxy: devProxy(mode), }, preview: { - port: Number(process.env.KAGENT_UI_LOOP_PORT ?? 8001), + port: Number(process.env.KAGENT_E2E_UI_LOOP_PORT ?? 8001), host: "0.0.0.0", }, test: { @@ -172,6 +172,6 @@ export default defineConfig(({ mode }) => ({ // mode is stated rather than inherited. It used to come free from the dev // default; that default is gone, because a page that quietly serves fixtures // when the backend is down is worse than one that says so. - env: { KAGENT_VITE_API_MODE: "mock" }, + env: { KAGENT_UI_VITE_API_MODE: "mock" }, }, })); From 9f3b8f8d3776a4a3423210b746a8b4bc25039127 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 13:51:21 +0000 Subject: [PATCH 12/14] refactor: read E2E settings through the environment registry Signed-off-by: Eitan Yarmush --- Makefile | 20 +++---- docs/env.md | 18 ++++-- go/.golangci.yaml | 7 ++- .../internal/grpcserver/task_store_test.go | 3 +- go/core/pkg/env/controller.go | 4 +- go/core/pkg/env/providers.go | 4 +- go/core/pkg/env/testing.go | 46 +++++++++------ .../test/e2e/cli_catalog_lifecycle_test.go | 5 +- go/core/test/e2e/config_test.go | 32 +++++++++++ go/core/test/e2e/interaction_test.go | 8 +-- go/core/test/e2e/main_test.go | 3 +- go/core/test/e2e/sandbox_agent_test.go | 4 +- go/core/test/e2e/sandbox_test.go | 12 ++-- go/core/test/e2e/tracing_test.go | 6 +- go/core/test/upgrade/config_test.go | 56 +++++++++++++++++++ go/core/test/upgrade/invoke_e2e_test.go | 3 +- go/core/test/upgrade/rolling_upgrade_test.go | 4 +- go/core/test/upgrade/upgrade_test.go | 35 ++++++------ 18 files changed, 194 insertions(+), 76 deletions(-) create mode 100644 go/core/test/e2e/config_test.go create mode 100644 go/core/test/upgrade/config_test.go diff --git a/Makefile b/Makefile index f89de343ae..bcd49bee65 100644 --- a/Makefile +++ b/Makefile @@ -597,12 +597,12 @@ run-upgrade-tests: announce-upgrade-from build install-previous-release ## Test echo "kind gateway (KAGENT_E2E_LOCAL_HOST): $$kind_gw"; \ cd go && \ KAGENT_E2E_RUN_UPGRADE_TESTS=true \ - REPO_ROOT=$(CURDIR) \ + KAGENT_E2E_REPO_ROOT=$(CURDIR) \ KAGENT_E2E_LOCAL_HOST="$$kind_gw" \ - UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ - VERSION=$(VERSION) \ - DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ - KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) \ + KAGENT_E2E_UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ + KAGENT_E2E_VERSION=$(VERSION) \ + KAGENT_E2E_DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ + KAGENT_E2E_KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) \ OPENAI_API_KEY="$${OPENAI_API_KEY:-test}" \ go test ./core/test/upgrade -run TestUpgrade -count=1 -timeout=45m -v @@ -615,11 +615,11 @@ run-rolling-upgrade-tests: announce-upgrade-from build install-previous-release @echo "=== Rolling upgrade test: $(UPGRADE_FROM_VERSION) -> $(VERSION) (registry=$(DOCKER_REGISTRY)) ===" cd go && \ KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS=true \ - REPO_ROOT=$(CURDIR) \ - UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ - VERSION=$(VERSION) \ - DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ - KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) \ + KAGENT_E2E_REPO_ROOT=$(CURDIR) \ + KAGENT_E2E_UPGRADE_FROM_VERSION=$(UPGRADE_FROM_VERSION) \ + KAGENT_E2E_VERSION=$(VERSION) \ + KAGENT_E2E_DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ + KAGENT_E2E_KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) \ OPENAI_API_KEY="$${OPENAI_API_KEY:-test}" \ go test ./core/test/upgrade -run TestRollingUpgradeCompatibility -count=1 -timeout=20m -v diff --git a/docs/env.md b/docs/env.md index 1f279e2f80..80caae1368 100644 --- a/docs/env.md +++ b/docs/env.md @@ -55,7 +55,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `OLLAMA_API_KEY` | String | `(none)` | API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly. | | `OPENAI_AGENTS_DISABLE_TRACING` | Boolean | `false` | Disable OpenAI Agents SDK tracing, including the kagent bridge. The Python OpenAI runtime accepts true or 1. | | `OPENAI_API_BASE` | String | `(none)` | Custom base URL for the OpenAI API. | -| `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. | +| `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. Upgrade tests fall back to a placeholder when unset or empty. | | `OPENAI_API_VERSION` | String | `(none)` | Azure OpenAI API version. The Go and Python ADKs fall back to 2024-02-15-preview when model configuration and this variable are unset. | | `OPENAI_ORGANIZATION` | String | `(none)` | OpenAI organization identifier. | | `OTEL_EXPORTER_OTLP_COMPRESSION` | String | `gzip` | OTLP compression default applied by kagent. The native Codex process has this variable removed because its exporter does not support gzip. | @@ -101,7 +101,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_POSTGRES_DATABASE_URL` | String | `postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres` | PostgreSQL connection URL. The default applies only to the controller; kagent db requires this variable or --db-url. Helm supplies its configured connection URL. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | | `OLLAMA_API_KEY` | String | `(none)` | API key for Ollama Cloud. When set, a cloud-tagged model reaches api.ollama.com directly. | -| `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. | +| `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. Upgrade tests fall back to a placeholder when unset or empty. | ## controller @@ -176,24 +176,32 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_E2E_CLAUDE_IMAGE` | String | `(none)` | Digest-pinned Claude harness image substituted into E2E lifecycle manifests. | | `KAGENT_E2E_CLI` | String | `(none)` | CLI executable for catalog lifecycle E2E tests; make -C go e2e builds and supplies it. | | `KAGENT_E2E_CODEX_IMAGE` | String | `(none)` | Digest-pinned Codex harness image substituted into E2E lifecycle manifests. | +| `KAGENT_E2E_DOCKER_REGISTRY` | String | `localhost:5001` | Image registry for upgrade tests; supplied by make from DOCKER_REGISTRY. | +| `KAGENT_E2E_KIND_CLUSTER_NAME` | String | `kagent` | Kind cluster used by upgrade tests; supplied by make from KIND_CLUSTER_NAME. | +| `KAGENT_E2E_KUBE_CONTEXT` | String | `(none)` | Kubernetes context for upgrade tests. Defaults to kind- followed by KAGENT_E2E_KIND_CLUSTER_NAME. | | `KAGENT_E2E_LOCAL_HOST` | String | `(none)` | Host reachable from E2E runtimes for local mock servers. Defaults to 172.17.0.1 on Linux and host.docker.internal on macOS; required on other systems. | +| `KAGENT_E2E_NAMESPACE` | String | `kagent` | Kubernetes namespace used by upgrade tests. | | `KAGENT_E2E_OTLP_LISTEN_ADDRESS` | String | `(none)` | Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver. | -| `KAGENT_E2E_REQUIRE_TRACING` | Boolean | `false` | Fail instead of skip when expected native harness tracing support is unavailable. | +| `KAGENT_E2E_REPO_ROOT` | String | `(none)` | Repository root for upgrade tests. Falls back to git rev-parse --show-toplevel. | +| `KAGENT_E2E_REQUIRE_TRACING` | String | `false` | Fail instead of skip when expected native harness tracing support is unavailable. Enabled by true, ignoring case and surrounding whitespace. | | `KAGENT_E2E_RUNTIME_IMAGE` | String | `(none)` | Required digest-pinned Go ADK image for sandbox E2E tests and lifecycle manifests. | -| `KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS` | Boolean | `false` | Run rolling upgrade integration tests when exactly true. | -| `KAGENT_E2E_RUN_UPGRADE_TESTS` | Boolean | `false` | Run upgrade integration tests when exactly true. | +| `KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS` | String | `false` | Run rolling upgrade integration tests when exactly true. | +| `KAGENT_E2E_RUN_UPGRADE_TESTS` | String | `false` | Run upgrade integration tests when exactly true. | | `KAGENT_E2E_SANDBOX_NAMESPACE` | String | `kagent` | Namespace for sandbox E2E resources. | | `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | | `KAGENT_E2E_UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50. | | `KAGENT_E2E_UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | | `KAGENT_E2E_UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | | `KAGENT_E2E_UI_LOOP_PORT` | String | `8001` | Vite development/preview port and UI browser-test port. | +| `KAGENT_E2E_UPGRADE_FROM_VERSION` | String | `(none)` | Required previous release version for upgrade tests; supplied by make from UPGRADE_FROM_VERSION. | +| `KAGENT_E2E_VERSION` | String | `(none)` | Required current image tag for upgrade tests; supplied by make from VERSION. | | `KAGENT_TEST_LLM_PORT` | String | `8090` | Port for the mock LLM server. | | `KAGENT_TEST_PYTHON` | String | `(none)` | Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset. | | `KAGENT_TEST_STS_PORT` | String | `8091` | Port for the mock STS (Security Token Service) server. | | `KAGENT_TEST_UPDATE_GOLDEN` | Boolean | `false` | When true, update golden test files instead of comparing. | | `KAGENT_UI_VITE_API_MODE` | String | `(none)` | Build-time API mode override, mock or live, used by UI tests. Overrides KAGENT_UI_ENABLE_MOCK; leave unset for normal development. | | `KUBECONFIG` | String | `(none)` | Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery. | +| `OPENAI_API_KEY` | String | `(none)` | API key for OpenAI. Upgrade tests fall back to a placeholder when unset or empty. | ## ui diff --git a/go/.golangci.yaml b/go/.golangci.yaml index 91f48dd5a4..afa9b29986 100644 --- a/go/.golangci.yaml +++ b/go/.golangci.yaml @@ -163,7 +163,12 @@ linters: - linters: - forbidigo text: Use a registered accessor from core/pkg/env - path: '(^|/)core/pkg/env/|_test\.go$' + path: '(^|/)core/pkg/env/' + - linters: + - forbidigo + text: Use a registered accessor from core/pkg/env + # Unit tests may inspect process state; core/test E2E settings use the registry. + path: '(^|/)(api|adk|harness|pkg|sandbox|core/(cli|internal|pkg))/.*_test\.go$' - linters: - kubeapilinter path-except: "api/v1alpha" diff --git a/go/core/internal/grpcserver/task_store_test.go b/go/core/internal/grpcserver/task_store_test.go index c7fce7db0f..36416969bd 100644 --- a/go/core/internal/grpcserver/task_store_test.go +++ b/go/core/internal/grpcserver/task_store_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/kagent-dev/kagent/go/core/internal/substrate" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "google.golang.org/protobuf/proto" "github.com/a2aproject/a2a-go/v2/a2a" @@ -447,7 +448,7 @@ func TestRuntimeTaskStoreThroughGRPC(t *testing.T) { runtimeServer.Stop() _, err = second.GetTask(publicCtx, &a2apb.GetTaskRequest{Tenant: "team-a/assistant", Id: taskID}) require.NoError(t, err) - if python := os.Getenv("KAGENT_TEST_PYTHON"); python != "" { + if python := kagentenv.TestPython.Get(); python != "" { t.Run("python SDK with PostgreSQL", func(t *testing.T) { // Run the Python adapter against this same API and PostgreSQL session. // Only native work is a controlled fixture. Enable with the repository diff --git a/go/core/pkg/env/controller.go b/go/core/pkg/env/controller.go index d75d870b58..f186c4a575 100644 --- a/go/core/pkg/env/controller.go +++ b/go/core/pkg/env/controller.go @@ -17,6 +17,6 @@ var ( // Shared settings read by logging and Kubernetes libraries. var ( - LogLevel = RegisterStringVar("KAGENT_LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) - _ = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController, ComponentCLI, ComponentTesting) + LogLevel = RegisterStringVar("KAGENT_LOG_LEVEL", "info", "Logging level for the controller, CLI, and Go/Python runtimes, including the Python ADK HTTP server: debug, info, warn, or error. Python also accepts standard Python logging levels.", ComponentController, ComponentCLI, ComponentAgentRuntime) + Kubeconfig = RegisterStringVar("KUBECONFIG", "", "Kubernetes client configuration file list for the controller, CLI Kubernetes operations, and tests. When unset, client-go uses its normal in-cluster or user kubeconfig discovery.", ComponentController, ComponentCLI, ComponentTesting) ) diff --git a/go/core/pkg/env/providers.go b/go/core/pkg/env/providers.go index 752d44d8f7..ea050c1a9b 100644 --- a/go/core/pkg/env/providers.go +++ b/go/core/pkg/env/providers.go @@ -9,8 +9,8 @@ var ( OpenAIAPIKey = RegisterStringVar( "OPENAI_API_KEY", "", - "API key for OpenAI.", - ComponentAgentRuntime, ComponentCLI, + "API key for OpenAI. Upgrade tests fall back to a placeholder when unset or empty.", + ComponentAgentRuntime, ComponentCLI, ComponentTesting, ) OpenAIOrganization = RegisterStringVar( diff --git a/go/core/pkg/env/testing.go b/go/core/pkg/env/testing.go index 96fd8247b0..0fd85a2010 100644 --- a/go/core/pkg/env/testing.go +++ b/go/core/pkg/env/testing.go @@ -32,21 +32,33 @@ var ( ) var ( - _ = RegisterStringVar("KAGENT_TEST_PYTHON", "", "Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_API_URL", "", "Controller URL for Go E2E tests. Falls back to KAGENT_API_URL; tests skip when neither is set.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_CLI", "", "CLI executable for catalog lifecycle E2E tests; make -C go e2e builds and supplies it.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_RUNTIME_IMAGE", "", "Required digest-pinned Go ADK image for sandbox E2E tests and lifecycle manifests.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_BYO_IMAGE", "", "Digest-pinned BYO image substituted into E2E lifecycle manifests.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_CLAUDE_IMAGE", "", "Digest-pinned Claude harness image substituted into E2E lifecycle manifests.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_CODEX_IMAGE", "", "Digest-pinned Codex harness image substituted into E2E lifecycle manifests.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_SANDBOX_NAMESPACE", "kagent", "Namespace for sandbox E2E resources.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) - _ = RegisterBoolVar("KAGENT_E2E_REQUIRE_TRACING", false, "Fail instead of skip when expected native harness tracing support is unavailable.", ComponentTesting) - _ = RegisterBoolVar("KAGENT_E2E_RUN_UPGRADE_TESTS", false, "Run upgrade integration tests when exactly true.", ComponentTesting) - _ = RegisterBoolVar("KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS", false, "Run rolling upgrade integration tests when exactly true.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_PORT", "8001", "Vite development/preview port and UI browser-test port.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_EXTENSION_PORT", "", "Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50.", ComponentTesting) - _ = RegisterBoolVar("KAGENT_E2E_UI_LOOP_LIVE", false, "Run UI browser tests against a real cluster when exactly true.", ComponentTesting) - _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_LIVE_PORT", "8301", "Live-cluster UI browser-test port.", ComponentTesting) + TestPython = RegisterStringVar("KAGENT_TEST_PYTHON", "", "Python executable enabling the Python TaskStore/PostgreSQL interoperability test. That subtest is not run when unset.", ComponentTesting) + E2EAPIURL = RegisterStringVar("KAGENT_E2E_API_URL", "", "Controller URL for Go E2E tests. Falls back to KAGENT_API_URL; tests skip when neither is set.", ComponentTesting) + E2ECLI = RegisterStringVar("KAGENT_E2E_CLI", "", "CLI executable for catalog lifecycle E2E tests; make -C go e2e builds and supplies it.", ComponentTesting) + E2ERuntimeImage = RegisterStringVar("KAGENT_E2E_RUNTIME_IMAGE", "", "Required digest-pinned Go ADK image for sandbox E2E tests and lifecycle manifests.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_BYO_IMAGE", "", "Digest-pinned BYO image substituted into E2E lifecycle manifests.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_CLAUDE_IMAGE", "", "Digest-pinned Claude harness image substituted into E2E lifecycle manifests.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_CODEX_IMAGE", "", "Digest-pinned Codex harness image substituted into E2E lifecycle manifests.", ComponentTesting) + E2ESandboxNamespace = RegisterStringVar("KAGENT_E2E_SANDBOX_NAMESPACE", "kagent", "Namespace for sandbox E2E resources.", ComponentTesting) + E2ESandboxWorkerPool = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) + E2EOTLPListenAddress = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) + E2ERequireTracing = RegisterStringVar("KAGENT_E2E_REQUIRE_TRACING", "false", "Fail instead of skip when expected native harness tracing support is unavailable. Enabled by true, ignoring case and surrounding whitespace.", ComponentTesting) + E2ERunUpgradeTests = RegisterStringVar("KAGENT_E2E_RUN_UPGRADE_TESTS", "false", "Run upgrade integration tests when exactly true.", ComponentTesting) + E2ERunRollingUpgradeTests = RegisterStringVar("KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS", "false", "Run rolling upgrade integration tests when exactly true.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_PORT", "8001", "Vite development/preview port and UI browser-test port.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_EXTENSION_PORT", "", "Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50.", ComponentTesting) + _ = RegisterBoolVar("KAGENT_E2E_UI_LOOP_LIVE", false, "Run UI browser tests against a real cluster when exactly true.", ComponentTesting) + _ = RegisterStringVar("KAGENT_E2E_UI_LOOP_LIVE_PORT", "8301", "Live-cluster UI browser-test port.", ComponentTesting) +) + +// Upgrade test inputs supplied by the root Make targets. Their Make variable +// names remain shared with build tooling; the Go tests consume these scoped names. +var ( + E2ERepoRoot = RegisterStringVar("KAGENT_E2E_REPO_ROOT", "", "Repository root for upgrade tests. Falls back to git rev-parse --show-toplevel.", ComponentTesting) + E2EUpgradeFromVersion = RegisterStringVar("KAGENT_E2E_UPGRADE_FROM_VERSION", "", "Required previous release version for upgrade tests; supplied by make from UPGRADE_FROM_VERSION.", ComponentTesting) + E2EVersion = RegisterStringVar("KAGENT_E2E_VERSION", "", "Required current image tag for upgrade tests; supplied by make from VERSION.", ComponentTesting) + E2EDockerRegistry = RegisterStringVar("KAGENT_E2E_DOCKER_REGISTRY", "localhost:5001", "Image registry for upgrade tests; supplied by make from DOCKER_REGISTRY.", ComponentTesting) + E2EKindClusterName = RegisterStringVar("KAGENT_E2E_KIND_CLUSTER_NAME", "kagent", "Kind cluster used by upgrade tests; supplied by make from KIND_CLUSTER_NAME.", ComponentTesting) + E2ENamespace = RegisterStringVar("KAGENT_E2E_NAMESPACE", "kagent", "Kubernetes namespace used by upgrade tests.", ComponentTesting) + E2EKubeContext = RegisterStringVar("KAGENT_E2E_KUBE_CONTEXT", "", "Kubernetes context for upgrade tests. Defaults to kind- followed by KAGENT_E2E_KIND_CLUSTER_NAME.", ComponentTesting) ) diff --git a/go/core/test/e2e/cli_catalog_lifecycle_test.go b/go/core/test/e2e/cli_catalog_lifecycle_test.go index 52c30d4c76..18566ed467 100644 --- a/go/core/test/e2e/cli_catalog_lifecycle_test.go +++ b/go/core/test/e2e/cli_catalog_lifecycle_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "google.golang.org/protobuf/encoding/protojson" "k8s.io/client-go/tools/clientcmd" ) @@ -161,7 +162,7 @@ func TestE2ECLISessionDiscoveryAndInvoke(t *testing.T) { func runKagentCLI(t *testing.T, ctx context.Context, binary string, args ...string) string { t.Helper() command := exec.CommandContext(ctx, binary, args...) - kubeconfig := os.Getenv(clientcmd.RecommendedConfigPathEnvVar) + kubeconfig := kagentenv.Kubeconfig.Get() if kubeconfig == "" { kubeconfig = clientcmd.RecommendedHomeFile } @@ -180,7 +181,7 @@ func runKagentCLI(t *testing.T, ctx context.Context, binary string, args ...stri func kagentCLI(t *testing.T) string { t.Helper() - binary := os.Getenv("KAGENT_E2E_CLI") + binary := kagentenv.E2ECLI.Get() if binary == "" { t.Fatal("KAGENT_E2E_CLI is not set; run E2E tests through `make -C go e2e`") } diff --git a/go/core/test/e2e/config_test.go b/go/core/test/e2e/config_test.go new file mode 100644 index 0000000000..74583d9d34 --- /dev/null +++ b/go/core/test/e2e/config_test.go @@ -0,0 +1,32 @@ +package e2e_test + +import ( + "testing" + + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" + "github.com/stretchr/testify/require" +) + +func TestInteractionTargetConfiguration(t *testing.T) { + for _, tc := range []struct { + name string + e2eURL string + apiURL string + want string + }{ + {name: "explicit E2E URL", e2eURL: "http://e2e.example:8083", want: "e2e.example:8083"}, + {name: "E2E URL takes precedence", e2eURL: "http://e2e.example:8083", apiURL: "http://runtime.example:8084", want: "e2e.example:8083"}, + {name: "empty E2E URL falls back", apiURL: "http://runtime.example:8084", want: "runtime.example:8084"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(kagentenv.E2EAPIURL.Name(), tc.e2eURL) + t.Setenv(kagentenv.KagentAPIURL.Name(), tc.apiURL) + require.Equal(t, tc.want, interactionTarget(t)) + }) + } +} + +func TestReachableServerURLConfiguredHost(t *testing.T) { + t.Setenv(kagentenv.KagentLocalHost.Name(), "mock.example") + require.Equal(t, "http://mock.example:8090/v1", reachableServerURL(t, "http://127.0.0.1:8090/", "/v1")) +} diff --git a/go/core/test/e2e/interaction_test.go b/go/core/test/e2e/interaction_test.go index 1e776a69ce..507c63901d 100644 --- a/go/core/test/e2e/interaction_test.go +++ b/go/core/test/e2e/interaction_test.go @@ -13,7 +13,6 @@ import ( "net/http/httptest" "net/http/httputil" "net/url" - "os" goruntime "runtime" "strconv" "strings" @@ -30,6 +29,7 @@ import ( kagenta2a "github.com/kagent-dev/kagent/go/api/a2a" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" "github.com/kagent-dev/kagent/go/api/v1alpha3" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/mockllm" "github.com/kagent-dev/mockmcp" "google.golang.org/genproto/googleapis/rpc/errdetails" @@ -628,9 +628,9 @@ type sharedInteractionFixture struct { func interactionTarget(t *testing.T) string { t.Helper() - rawURL := os.Getenv("KAGENT_E2E_API_URL") + rawURL := kagentenv.E2EAPIURL.Get() if rawURL == "" { - rawURL = os.Getenv("KAGENT_API_URL") + rawURL = kagentenv.KagentAPIURL.Get() } if rawURL == "" { t.Skip("KAGENT_E2E_API_URL is not set") @@ -942,7 +942,7 @@ func reachableServerURL(t *testing.T, baseURL, path string) string { if err != nil { t.Fatalf("parse mock LLM address: %v", err) } - host := os.Getenv("KAGENT_E2E_LOCAL_HOST") + host := kagentenv.KagentLocalHost.Get() if host == "" { switch goruntime.GOOS { case "darwin": diff --git a/go/core/test/e2e/main_test.go b/go/core/test/e2e/main_test.go index dc04c02ea9..83b57d19ea 100644 --- a/go/core/test/e2e/main_test.go +++ b/go/core/test/e2e/main_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/logging" ctrl "sigs.k8s.io/controller-runtime" ) @@ -15,7 +16,7 @@ var suiteTraceReceiver *otlpTraceReceiver func TestMain(m *testing.M) { ctrl.SetLogger(logging.AsLogr(slog.Default())) - address := os.Getenv("KAGENT_E2E_OTLP_LISTEN_ADDRESS") + address := kagentenv.E2EOTLPListenAddress.Get() if address == "" { os.Exit(m.Run()) } diff --git a/go/core/test/e2e/sandbox_agent_test.go b/go/core/test/e2e/sandbox_agent_test.go index e0441236eb..914e038c87 100644 --- a/go/core/test/e2e/sandbox_agent_test.go +++ b/go/core/test/e2e/sandbox_agent_test.go @@ -3,7 +3,6 @@ package e2e_test import ( "context" "encoding/json" - "os" "testing" "time" @@ -12,6 +11,7 @@ import ( "github.com/google/uuid" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" "github.com/kagent-dev/kagent/go/api/v1alpha3" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/mockllm" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -23,7 +23,7 @@ import ( func TestSandboxAgentMCP(t *testing.T) { target := interactionTarget(t) - image := os.Getenv("KAGENT_E2E_RUNTIME_IMAGE") + image := kagentenv.E2ERuntimeImage.Get() require.NotEmpty(t, image, "KAGENT_E2E_RUNTIME_IMAGE must be set to a digest-pinned Go ADK image") f := newSandboxFixture(t) kube := interactionKubeClient(t) diff --git a/go/core/test/e2e/sandbox_test.go b/go/core/test/e2e/sandbox_test.go index 9f1009b2b9..a6c00fcb6c 100644 --- a/go/core/test/e2e/sandbox_test.go +++ b/go/core/test/e2e/sandbox_test.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "encoding/json" "io" - "os" "testing" "time" @@ -16,6 +15,7 @@ import ( sandboxapi "github.com/kagent-dev/kagent/go/api/sandbox" "github.com/kagent-dev/kagent/go/api/v1alpha3" "github.com/kagent-dev/kagent/go/core/internal/substrate" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -44,15 +44,15 @@ type sandboxFixture struct { func newSandboxFixture(t *testing.T) *sandboxFixture { t.Helper() target := interactionTarget(t) - image := os.Getenv("KAGENT_E2E_RUNTIME_IMAGE") + image := kagentenv.E2ERuntimeImage.Get() require.NotEmpty(t, image, "KAGENT_E2E_RUNTIME_IMAGE must be set to a digest-pinned Go ADK image") - namespace := os.Getenv("KAGENT_E2E_SANDBOX_NAMESPACE") + namespace := kagentenv.E2ESandboxNamespace.Get() if namespace == "" { - namespace = "kagent" + namespace = kagentenv.E2ESandboxNamespace.DefaultValue() } - pool := os.Getenv("KAGENT_E2E_SANDBOX_WORKER_POOL") + pool := kagentenv.E2ESandboxWorkerPool.Get() if pool == "" { - pool = "kagent-default" + pool = kagentenv.E2ESandboxWorkerPool.DefaultValue() } conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) diff --git a/go/core/test/e2e/tracing_test.go b/go/core/test/e2e/tracing_test.go index 8d301fc24b..38747b3622 100644 --- a/go/core/test/e2e/tracing_test.go +++ b/go/core/test/e2e/tracing_test.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net" - "os" "strings" "sync" "testing" @@ -19,6 +18,7 @@ import ( "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/kagent-dev/kagent/go/api/v1alpha3" "github.com/kagent-dev/kagent/go/core/internal/substrate" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/kagent-dev/kagent/go/pkg/telemetry" "github.com/kagent-dev/kagent/go/pkg/tracing" "github.com/stretchr/testify/require" @@ -72,7 +72,7 @@ func startOTLPTraceReceiver(t *testing.T) *otlpTraceReceiver { if suiteTraceReceiver != nil { return suiteTraceReceiver } - address := os.Getenv("KAGENT_E2E_OTLP_LISTEN_ADDRESS") + address := kagentenv.E2EOTLPListenAddress.Get() if address == "" { address = ":14317" } @@ -392,7 +392,7 @@ func requireTracingHarnesses(t *testing.T) { if err := kube.Get(t.Context(), ctrlclient.ObjectKey{Namespace: "kagent", Name: name}, &harness); apierrors.IsNotFound(err) { // A job dedicated to tracing must fail rather than silently skip both // cases when its fixtures are missing. - if strings.EqualFold(strings.TrimSpace(os.Getenv("KAGENT_E2E_REQUIRE_TRACING")), "true") { + if strings.EqualFold(strings.TrimSpace(kagentenv.E2ERequireTracing.Get()), "true") { t.Fatalf("tracing Harness %s is not installed", name) } t.Skip("dedicated tracing Harnesses are not installed") diff --git a/go/core/test/upgrade/config_test.go b/go/core/test/upgrade/config_test.go new file mode 100644 index 0000000000..ecac62b6ac --- /dev/null +++ b/go/core/test/upgrade/config_test.go @@ -0,0 +1,56 @@ +package upgrade + +import ( + "os" + "path/filepath" + "testing" + + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" + "github.com/stretchr/testify/require" +) + +func TestLoadUpgradeEnv(t *testing.T) { + repoRoot := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "Makefile"), nil, 0o600)) + + for _, tc := range []struct { + name string + values map[string]string + want upgradeEnv + }{ + { + name: "empty settings use defaults", + want: upgradeEnv{dockerRegistry: "localhost:5001", kindClusterName: "kagent", namespace: "kagent", kubeContext: "kind-kagent", openAIAPIKey: "fake"}, + }, + { + name: "context follows cluster name", + values: map[string]string{kagentenv.E2EKindClusterName.Name(): "custom"}, + want: upgradeEnv{dockerRegistry: "localhost:5001", kindClusterName: "custom", namespace: "kagent", kubeContext: "kind-custom", openAIAPIKey: "fake"}, + }, + { + name: "explicit settings override defaults", + values: map[string]string{ + kagentenv.E2EDockerRegistry.Name(): "registry.example", + kagentenv.E2EKindClusterName.Name(): "custom", + kagentenv.E2ENamespace.Name(): "agents", + kagentenv.E2EKubeContext.Name(): "explicit-context", + kagentenv.OpenAIAPIKey.Name(): "test-key", + }, + want: upgradeEnv{dockerRegistry: "registry.example", kindClusterName: "custom", namespace: "agents", kubeContext: "explicit-context", openAIAPIKey: "test-key"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(kagentenv.E2ERepoRoot.Name(), repoRoot) + t.Setenv(kagentenv.E2EUpgradeFromVersion.Name(), "1.0.0") + t.Setenv(kagentenv.E2EVersion.Name(), "1.1.0") + for _, variable := range []kagentenv.StringVar{kagentenv.E2EDockerRegistry, kagentenv.E2EKindClusterName, kagentenv.E2ENamespace, kagentenv.E2EKubeContext, kagentenv.OpenAIAPIKey} { + t.Setenv(variable.Name(), tc.values[variable.Name()]) + } + want := tc.want + want.repoRoot = repoRoot + want.upgradeFromVersion = "1.0.0" + want.version = "1.1.0" + require.Equal(t, want, loadUpgradeEnv(t)) + }) + } +} diff --git a/go/core/test/upgrade/invoke_e2e_test.go b/go/core/test/upgrade/invoke_e2e_test.go index 692e7e6ea4..f4ab7f25ad 100644 --- a/go/core/test/upgrade/invoke_e2e_test.go +++ b/go/core/test/upgrade/invoke_e2e_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/stretchr/testify/require" ) @@ -83,7 +84,7 @@ func upgradeE2EEnv(t *testing.T, env upgradeEnv, port int) []string { func requireInvokeEnvironment(t *testing.T, treeGoDir, label string) { t.Helper() - if os.Getenv("KAGENT_E2E_LOCAL_HOST") == "" { + if kagentenv.KagentLocalHost.Get() == "" { t.Skipf("[%s] KAGENT_E2E_LOCAL_HOST is not set; run via make run-upgrade-tests", label) } if _, err := os.Stat(filepath.Join(treeGoDir, "go.mod")); err != nil { diff --git a/go/core/test/upgrade/rolling_upgrade_test.go b/go/core/test/upgrade/rolling_upgrade_test.go index d16fe2c8fb..4754a9149e 100644 --- a/go/core/test/upgrade/rolling_upgrade_test.go +++ b/go/core/test/upgrade/rolling_upgrade_test.go @@ -3,16 +3,16 @@ package upgrade import ( "context" "fmt" - "os" "strings" "testing" "time" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" "github.com/stretchr/testify/require" ) func TestRollingUpgradeCompatibility(t *testing.T) { - if os.Getenv("KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS") != "true" { + if kagentenv.E2ERunRollingUpgradeTests.Get() != "true" { t.Skip("set KAGENT_E2E_RUN_ROLLING_UPGRADE_TESTS=true to run rolling upgrade tests") } diff --git a/go/core/test/upgrade/upgrade_test.go b/go/core/test/upgrade/upgrade_test.go index 4daa466c09..d65a781db0 100644 --- a/go/core/test/upgrade/upgrade_test.go +++ b/go/core/test/upgrade/upgrade_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" migrations "github.com/kagent-dev/kagent/go/core/pkg/migrations" "github.com/stretchr/testify/require" ) @@ -45,7 +46,7 @@ type postgresMigrationState struct { } func TestUpgrade(t *testing.T) { - if os.Getenv("KAGENT_E2E_RUN_UPGRADE_TESTS") != "true" { + if kagentenv.E2ERunUpgradeTests.Get() != "true" { t.Skip("set KAGENT_E2E_RUN_UPGRADE_TESTS=true to run upgrade tests") } @@ -310,45 +311,45 @@ func installPreviousReleaseCommand(ctx context.Context, env upgradeEnv) *exec.Cm func loadUpgradeEnv(t *testing.T) upgradeEnv { t.Helper() - // Resolve the repo root. Prefer an explicit REPO_ROOT (set by the make + // Resolve the repo root. Prefer an explicit KAGENT_E2E_REPO_ROOT (set by the make // targets), otherwise ask git: this is location-independent, so moving this // file cannot silently point make at the wrong root, and git is already a hard // dependency of this flow (the make targets derive versions from git tags). - repoRoot := os.Getenv("REPO_ROOT") + repoRoot := kagentenv.E2ERepoRoot.Get() if repoRoot == "" { out, err := exec.CommandContext(t.Context(), "git", "rev-parse", "--show-toplevel").Output() - require.NoError(t, err, "resolve repo root via `git rev-parse --show-toplevel`; set REPO_ROOT to override") + require.NoError(t, err, "resolve repo root via `git rev-parse --show-toplevel`; set KAGENT_E2E_REPO_ROOT to override") repoRoot = strings.TrimSpace(string(out)) } // Fail clearly here rather than letting `make -C ...` fail with a // confusing "no rule to make target" if the root resolved wrong. _, err := os.Stat(filepath.Join(repoRoot, "Makefile")) - require.NoError(t, err, "resolved repo root %q has no Makefile; set REPO_ROOT", repoRoot) + require.NoError(t, err, "resolved repo root %q has no Makefile; set KAGENT_E2E_REPO_ROOT", repoRoot) - clusterName := envOrDefault("KIND_CLUSTER_NAME", "kagent") + clusterName := envOrDefault(kagentenv.E2EKindClusterName, kagentenv.E2EKindClusterName.DefaultValue()) return upgradeEnv{ repoRoot: repoRoot, - upgradeFromVersion: requireEnv(t, "UPGRADE_FROM_VERSION"), - version: requireEnv(t, "VERSION"), - dockerRegistry: envOrDefault("DOCKER_REGISTRY", "localhost:5001"), + upgradeFromVersion: requireEnv(t, kagentenv.E2EUpgradeFromVersion), + version: requireEnv(t, kagentenv.E2EVersion), + dockerRegistry: envOrDefault(kagentenv.E2EDockerRegistry, kagentenv.E2EDockerRegistry.DefaultValue()), kindClusterName: clusterName, - namespace: envOrDefault("NAMESPACE", "kagent"), - kubeContext: envOrDefault("KUBE_CONTEXT", "kind-"+clusterName), - openAIAPIKey: envOrDefault("OPENAI_API_KEY", "fake"), + namespace: envOrDefault(kagentenv.E2ENamespace, kagentenv.E2ENamespace.DefaultValue()), + kubeContext: envOrDefault(kagentenv.E2EKubeContext, "kind-"+clusterName), + openAIAPIKey: envOrDefault(kagentenv.OpenAIAPIKey, "fake"), } } -func requireEnv(t *testing.T, key string) string { +func requireEnv(t *testing.T, variable kagentenv.StringVar) string { t.Helper() - val := os.Getenv(key) - require.NotEmpty(t, val, "%s must be set", key) + val := variable.Get() + require.NotEmpty(t, val, "%s must be set", variable.Name()) return val } -func envOrDefault(key, fallback string) string { - if val := os.Getenv(key); val != "" { +func envOrDefault(variable kagentenv.StringVar, fallback string) string { + if val := variable.Get(); val != "" { return val } return fallback From 6ed23a7ff117ac5ce90c1fa3c6e151ad2ca256f0 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 27 Sep 2026 14:07:48 +0000 Subject: [PATCH 13/14] ci: consume image inspection output before closing the pipe Signed-off-by: Eitan Yarmush --- .github/workflows/ci.yaml | 11 ++++++----- scripts/setup-cluster/setup-cluster.sh | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3929878095..24a570f7bf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -157,7 +157,7 @@ jobs: ' _ # Resolve the guest on the runner: localhost:5001 is not reachable # from the controller pod. Substrate rewrites registry pulls on workers. - GUEST_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/sandbox-guest:${VERSION}" | awk '$1 == "Digest:" { print $2; exit }') + GUEST_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/sandbox-guest:${VERSION}" | awk '$1 == "Digest:" { print $2 }') test -n "$GUEST_DIGEST" export KAGENT_HELM_EXTRA_ARGS="${KAGENT_HELM_EXTRA_ARGS} --set-string controller.sandbox.guestImage.digest=${GUEST_DIGEST}" make helm-install-provider @@ -169,16 +169,17 @@ jobs: working-directory: go shell: bash run: | - RUNTIME_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/golang-adk:${VERSION}" | awk '$1 == "Digest:" { print $2; exit }') + # Read all output so Docker does not get a broken pipe under pipefail. + RUNTIME_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/golang-adk:${VERSION}" | awk '$1 == "Digest:" { print $2 }') test -n "$RUNTIME_DIGEST" export KAGENT_E2E_RUNTIME_IMAGE="localhost:5001/kagent-dev/kagent/golang-adk@${RUNTIME_DIGEST}" - BYO_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/byo-a2a:${VERSION}" | awk '$1 == "Digest:" { print $2; exit }') + BYO_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/byo-a2a:${VERSION}" | awk '$1 == "Digest:" { print $2 }') test -n "$BYO_DIGEST" export KAGENT_E2E_BYO_IMAGE="localhost:5001/kagent-dev/kagent/byo-a2a@${BYO_DIGEST}" - CLAUDE_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/claude-harness:${VERSION}" | awk '$1 == "Digest:" { print $2; exit }') + CLAUDE_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/claude-harness:${VERSION}" | awk '$1 == "Digest:" { print $2 }') test -n "$CLAUDE_DIGEST" export KAGENT_E2E_CLAUDE_IMAGE="localhost:5001/kagent-dev/kagent/claude-harness@${CLAUDE_DIGEST}" - CODEX_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/codex-harness:${VERSION}" | awk '$1 == "Digest:" { print $2; exit }') + CODEX_DIGEST=$(docker buildx imagetools inspect "localhost:5001/kagent-dev/kagent/codex-harness:${VERSION}" | awk '$1 == "Digest:" { print $2 }') test -n "$CODEX_DIGEST" export KAGENT_E2E_CODEX_IMAGE="localhost:5001/kagent-dev/kagent/codex-harness@${CODEX_DIGEST}" envsubst < core/test/e2e/manifests/lifecycle.yaml.tmpl | kubectl apply -f - diff --git a/scripts/setup-cluster/setup-cluster.sh b/scripts/setup-cluster/setup-cluster.sh index f350be0fa5..822d09a57f 100755 --- a/scripts/setup-cluster/setup-cluster.sh +++ b/scripts/setup-cluster/setup-cluster.sh @@ -120,7 +120,7 @@ docker buildx build --push --platform "linux/${ARCH}" \ --build-arg BUILD_PACKAGE=adk/cmd/main.go \ -t localhost:5001/kagent-dev/kagent/golang-adk:dev -f go/Dockerfile ./go HARNESS_DIGEST="$(docker buildx imagetools inspect localhost:5001/kagent-dev/kagent/golang-adk:dev \ - | awk '/^Digest:/{print $2; exit}')" + | awk '/^Digest:/{print $2}')" step "9/10 An Agent with inline template and Harness" kubectl apply -f - < Date: Sun, 27 Sep 2026 14:46:08 +0000 Subject: [PATCH 14/14] fix: use the registered port in the BYO E2E fixture Signed-off-by: Eitan Yarmush --- go/core/test/e2e/manifests/lifecycle.yaml.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/core/test/e2e/manifests/lifecycle.yaml.tmpl b/go/core/test/e2e/manifests/lifecycle.yaml.tmpl index 94c95d5453..20780682cf 100644 --- a/go/core/test/e2e/manifests/lifecycle.yaml.tmpl +++ b/go/core/test/e2e/manifests/lifecycle.yaml.tmpl @@ -55,7 +55,7 @@ spec: image: ${KAGENT_E2E_RUNTIME_IMAGE} command: ["/app"] env: - - name: PORT + - name: KAGENT_PORT value: "80" substrate: workerPoolRef: