From aa30a32feaebd13c8266da44f06da6dc31b8292b Mon Sep 17 00:00:00 2001 From: Shreyas Nagaraj Date: Tue, 28 Jul 2026 15:41:24 +0530 Subject: [PATCH 1/3] fix: default gen_ai payload capture to off Flip gen_ai.payload_capture_enabled default from true to false so prompt/ response content is only captured on explicit opt-in. Unset or empty HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED now resolves to capture-off, matching the privacy-control intent (disable wins unless explicitly enabled). Update default-config test and docs to reflect the new default. Co-authored-by: Cursor --- AGENTS.md | 2 +- docs/sdk-quickstart.md | 2 +- src/harness_sdk/config/default.py | 2 +- test/config/test_config.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ad162e7..a157c3f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Key variables: | `HARNESS_OBSERVABILITY_PLUGINS` | Comma-separated observability plugin names | | `HARNESS_ENABLE_CONSOLE_SPAN_EXPORTER` | Set to any value to dump spans to stdout | | `HARNESS_CONFIG_FILE` | Path to YAML config file (overrides env) | -| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | Capture LLM prompt/response payloads | +| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | Capture LLM prompt/response payloads (default: off) | | `HARNESS_GEN_AI_PAYLOAD_EVALUATION_ENABLED` | Run control plugins on GenAI spans | ### Instrumentation opt-in (strict `HARNESS_` prefix, no legacy aliases) diff --git a/docs/sdk-quickstart.md b/docs/sdk-quickstart.md index 461157a..5e6ad95 100644 --- a/docs/sdk-quickstart.md +++ b/docs/sdk-quickstart.md @@ -145,7 +145,7 @@ Agent().instrument(skip_libraries=["requests"]) | Variable | Effect | |---|---| -| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | `false` to omit prompt/response bodies from spans | +| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | `true` to capture prompt/response bodies in spans (default: off) | | `HARNESS_SPAN_ATTRIBUTES` | extra attributes on every span, e.g. `env=prod,team=ai` | | `HARNESS_OBSERVABILITY_PLUGINS` | `builtin_span_attributes` to keep instrumentation but disable the SDK's own OTLP exporter (if you already export spans yourself) | | `HARNESS_ENABLED` | `false` to disable the SDK entirely (no code change) | diff --git a/src/harness_sdk/config/default.py b/src/harness_sdk/config/default.py index 561510f..00f6379 100644 --- a/src/harness_sdk/config/default.py +++ b/src/harness_sdk/config/default.py @@ -43,7 +43,7 @@ }, 'gen_ai': { 'enabled_frameworks': [], - 'payload_capture_enabled': True, + 'payload_capture_enabled': False, 'payload_evaluation_enabled': True, }, 'plugins': { diff --git a/test/config/test_config.py b/test/config/test_config.py index 04c45f3..7a807d8 100644 --- a/test/config/test_config.py +++ b/test/config/test_config.py @@ -65,7 +65,7 @@ def test_sdk_default_config(): gen_ai = traceable_config.gen_ai assert Config().enabled_ai_frameworks == [] assert gen_ai.enabled.value is False - assert gen_ai.payload_capture_enabled.value is True + assert gen_ai.payload_capture_enabled.value is False assert gen_ai.payload_evaluation_enabled.value is True assert len(traceable_config.span_attributes) == 0 From 66415a065bdf40b555394444157c675174317521 Mon Sep 17 00:00:00 2001 From: Shreyas Nagaraj Date: Tue, 28 Jul 2026 15:54:08 +0530 Subject: [PATCH 2/3] feat: accept HA_ alias for instrumentation opt-in flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HARNESS_ENABLE_API and HARNESS_ENABLE_AI_* now resolve under both HARNESS_ and HA_ prefixes (HARNESS_ wins when both are set), matching the HA_/HARNESS_ parity of the other SDK settings. AT_/TA_ aliases remain unsupported — the flags never existed under those prefixes. Co-authored-by: Cursor --- AGENTS.md | 2 +- docs/sdk-quickstart.md | 2 ++ src/harness_sdk/env.py | 29 ++++++++++++++++--- .../instrumentation_definitions.py | 4 +-- test/instrumentation/test_opt_in_gating.py | 23 +++++++++++++-- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a157c3f..4ab9c6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ Key variables: | `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | Capture LLM prompt/response payloads (default: off) | | `HARNESS_GEN_AI_PAYLOAD_EVALUATION_ENABLED` | Run control plugins on GenAI spans | -### Instrumentation opt-in (strict `HARNESS_` prefix, no legacy aliases) +### Instrumentation opt-in (`HARNESS_` or `HA_` prefix; `HARNESS_` wins — no `AT_`/`TA_` aliases) Instrumentation is opt-in: `Agent().instrument()` instruments nothing unless a flag below is set to `true`. Categorization and gating live in `src/harness_sdk/instrumentation/instrumentation_definitions.py` (`is_library_enabled`, `is_api_instrumentation_enabled`, `any_ai_provider_enabled`), enforced in `Agent.instrument()`. diff --git a/docs/sdk-quickstart.md b/docs/sdk-quickstart.md index 5e6ad95..28107c0 100644 --- a/docs/sdk-quickstart.md +++ b/docs/sdk-quickstart.md @@ -62,6 +62,8 @@ flag is on only when its value is `true`. | `HARNESS_ENABLE_AI_GOOGLE_GENAI` | Google GenAI (Gemini / Vertex AI) | | `HARNESS_ENABLE_AI_MCP` | Model Context Protocol | +Each flag also accepts the `HA_` alias (e.g. `HA_ENABLE_API=true`); when both are set, the `HARNESS_` value wins. `AT_`/`TA_` aliases are not supported for these flags. + ```bash # Example: HTTP/API instrumentation plus LiteLLM export HARNESS_ENABLE_API=true diff --git a/src/harness_sdk/env.py b/src/harness_sdk/env.py index e84d53f..de3b171 100644 --- a/src/harness_sdk/env.py +++ b/src/harness_sdk/env.py @@ -20,9 +20,30 @@ def is_env_var_present(target_key): def is_harness_flag_enabled(env_var_name): - """Strict opt-in flag under the HARNESS_ prefix only (no legacy aliases). + """Opt-in flag under HARNESS_ or HA_ (HARNESS_ wins when both are set). - Returns True only when the value is present and case-insensitively 'true'. + Enable flags never existed under AT_/TA_, so only those two prefixes are + honored. Returns True only when the resolved value is present and + case-insensitively 'true'. """ - value = os.environ.get(env_var_name) - return value is not None and value.strip().lower() == "true" + for key in _flag_keys(env_var_name): + if key in os.environ: + return os.environ[key].strip().lower() == "true" + return False + + +def is_enable_flag_present(env_var_name): + """Presence check for an opt-in flag under HARNESS_ or HA_.""" + return any(key in os.environ for key in _flag_keys(env_var_name)) + + +_FLAG_PREFIXES = ("HARNESS_", "HA_") + + +def _flag_keys(env_var_name): + suffix = ( + env_var_name[len("HARNESS_"):] + if env_var_name.startswith("HARNESS_") + else env_var_name + ) + return [f"{prefix}{suffix}" for prefix in _FLAG_PREFIXES] diff --git a/src/harness_sdk/instrumentation/instrumentation_definitions.py b/src/harness_sdk/instrumentation/instrumentation_definitions.py index e6fddbb..57b7419 100644 --- a/src/harness_sdk/instrumentation/instrumentation_definitions.py +++ b/src/harness_sdk/instrumentation/instrumentation_definitions.py @@ -4,7 +4,7 @@ from importlib import metadata as importlib_metadata from harness_sdk.custom_logger import get_custom_logger -from harness_sdk.env import is_harness_flag_enabled +from harness_sdk.env import is_enable_flag_present, is_harness_flag_enabled FLASK_KEY = 'flask' DJANGO_KEY = 'django' @@ -75,7 +75,7 @@ def is_library_enabled(library_key, enabled_ai_frameworks=None): """Decide whether a supported library should be instrumented based on opt-in env flags.""" if library_key in AI_LIBRARY_ENV_FLAGS: env_flag = AI_LIBRARY_ENV_FLAGS[library_key] - if env_flag in os.environ: + if is_enable_flag_present(env_flag): return is_harness_flag_enabled(env_flag) configured_frameworks = { _normalize_library_name(name) diff --git a/test/instrumentation/test_opt_in_gating.py b/test/instrumentation/test_opt_in_gating.py index 90a59f7..df5f71d 100644 --- a/test/instrumentation/test_opt_in_gating.py +++ b/test/instrumentation/test_opt_in_gating.py @@ -82,8 +82,19 @@ def test_flag_requires_exact_true_value(): assert is_api_instrumentation_enabled() is True -def test_ai_flags_have_no_legacy_aliases(): +def test_ai_flags_accept_ha_alias(): os.environ["HA_ENABLE_AI_OPENAI"] = "true" + assert is_library_enabled(OPENAI_KEY) is True + assert any_ai_provider_enabled() is True + + +def test_ai_flag_harness_prefix_wins_over_ha(): + os.environ["HARNESS_ENABLE_AI_OPENAI"] = "false" + os.environ["HA_ENABLE_AI_OPENAI"] = "true" + assert is_library_enabled(OPENAI_KEY) is False + + +def test_ai_flags_have_no_at_ta_aliases(): os.environ["AT_ENABLE_AI_OPENAI"] = "true" os.environ["TA_ENABLE_AI_OPENAI"] = "true" try: @@ -93,13 +104,19 @@ def test_ai_flags_have_no_legacy_aliases(): os.environ.pop(legacy, None) -def test_api_flag_has_no_legacy_aliases(): +def test_api_flag_accepts_ha_alias(): os.environ["HA_ENABLE_API"] = "true" + assert is_api_instrumentation_enabled() is True + + +def test_api_flag_has_no_at_ta_aliases(): os.environ["AT_ENABLE_API"] = "true" + os.environ["TA_ENABLE_API"] = "true" try: assert is_api_instrumentation_enabled() is False finally: - os.environ.pop("AT_ENABLE_API", None) + for legacy in ("AT_ENABLE_API", "TA_ENABLE_API"): + os.environ.pop(legacy, None) # --------------------------------------------------------------------------- # From 34fdc985fc72f28d6e31a3b07390cda26efe3908 Mon Sep 17 00:00:00 2001 From: Shreyas Nagaraj Date: Tue, 28 Jul 2026 16:28:59 +0530 Subject: [PATCH 3/3] fix: honor explicit false for ENABLE_CONSOLE_SPAN_EXPORTER The console span exporter was gated on env var presence, so HARNESS_ENABLE_CONSOLE_SPAN_EXPORTER=false (or any value) still enabled it. Switch to strict boolean resolution via the new is_env_flag_enabled helper: only the case-insensitive value 'true' enables the exporter, resolved with the usual HARNESS_ > HA_ > AT_ > TA_ precedence. Remove the now-unused is_env_var_present presence-check helper. Co-authored-by: Cursor --- AGENTS.md | 4 +- src/harness_sdk/agent_init.py | 4 +- src/harness_sdk/env.py | 10 ++-- src/harness_sdk/plugins/builtin/pipeline.py | 4 +- test/env_test.py | 57 +++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 test/env_test.py diff --git a/AGENTS.md b/AGENTS.md index 4ab9c6e..5a86d70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ RUN_SDK_INTEGRATION_TESTS=1 ./scripts/run-unit-tests.sh ## Environment variable naming -All SDK config uses the `HARNESS_` prefix. `HA_`, `AT_`, and `TA_` are legacy aliases accepted for backwards compatibility. When a setting is defined under multiple prefixes, precedence is `HARNESS_` > `HA_` > `AT_` > `TA_`. Resolution lives in `src/harness_sdk/env.py` (`get_env_value`, `is_env_var_present`, `is_harness_flag_enabled`). +All SDK config uses the `HARNESS_` prefix. `HA_`, `AT_`, and `TA_` are legacy aliases accepted for backwards compatibility. When a setting is defined under multiple prefixes, precedence is `HARNESS_` > `HA_` > `AT_` > `TA_`. Resolution lives in `src/harness_sdk/env.py` (`get_env_value`, `is_env_flag_enabled`, `is_harness_flag_enabled`). Key variables: | Variable | Purpose | @@ -34,7 +34,7 @@ Key variables: | `HARNESS_REPORTING_COMPRESSION` | `gzip` or empty | | `HARNESS_CONTROL_PLUGINS` | Comma-separated control plugin names | | `HARNESS_OBSERVABILITY_PLUGINS` | Comma-separated observability plugin names | -| `HARNESS_ENABLE_CONSOLE_SPAN_EXPORTER` | Set to any value to dump spans to stdout | +| `HARNESS_ENABLE_CONSOLE_SPAN_EXPORTER` | `true` to dump spans to stdout (default: off) | | `HARNESS_CONFIG_FILE` | Path to YAML config file (overrides env) | | `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | Capture LLM prompt/response payloads (default: off) | | `HARNESS_GEN_AI_PAYLOAD_EVALUATION_ENABLED` | Run control plugins on GenAI spans | diff --git a/src/harness_sdk/agent_init.py b/src/harness_sdk/agent_init.py index 8ff94b6..c1f51f1 100644 --- a/src/harness_sdk/agent_init.py +++ b/src/harness_sdk/agent_init.py @@ -17,7 +17,7 @@ from opentelemetry.sdk.resources import Resource from harness_sdk import constants from harness_sdk.config import config_pb2 -from harness_sdk.env import is_env_var_present +from harness_sdk.env import is_env_flag_enabled from harness_sdk.otlp_reporting import ( compression_type_to_otlp_grpc, compression_type_to_otlp_http, @@ -43,7 +43,7 @@ def apply_config(self, agent_config): self._config = agent_config self.init_trace_provider() self.init_propagation() - if is_env_var_present("ENABLE_CONSOLE_SPAN_EXPORTER"): + if is_env_flag_enabled("ENABLE_CONSOLE_SPAN_EXPORTER"): self.set_console_span_processor() def init_trace_provider(self) -> None: diff --git a/src/harness_sdk/env.py b/src/harness_sdk/env.py index de3b171..e179847 100644 --- a/src/harness_sdk/env.py +++ b/src/harness_sdk/env.py @@ -14,9 +14,13 @@ def get_env_value(target_key): return None -def is_env_var_present(target_key): - """Return True if the key is set under any supported prefix (presence check).""" - return any(f"{prefix}{target_key}" in os.environ for prefix in _PREFIXES) +def is_env_flag_enabled(target_key): + """Boolean SDK flag under any supported prefix (HARNESS_ > HA_ > AT_ > TA_). + + Returns True only when the resolved value is case-insensitively 'true'. + """ + value = get_env_value(target_key) + return value is not None and value.strip().lower() == "true" def is_harness_flag_enabled(env_var_name): diff --git a/src/harness_sdk/plugins/builtin/pipeline.py b/src/harness_sdk/plugins/builtin/pipeline.py index 6149060..e665500 100644 --- a/src/harness_sdk/plugins/builtin/pipeline.py +++ b/src/harness_sdk/plugins/builtin/pipeline.py @@ -6,7 +6,7 @@ from harness_sdk.agent_init import AgentInit from harness_sdk.custom_logger import get_custom_logger -from harness_sdk.env import is_env_var_present +from harness_sdk.env import is_env_flag_enabled from harness_sdk.excluded_by_attribute_span_processor import ExcludeByAttributeSpanProcessor from harness_sdk.db_control_span_processor import DbControlSpanProcessor from harness_sdk.gen_ai_payload_scrub_span_processor import GenAiPayloadScrubSpanProcessor @@ -25,7 +25,7 @@ def on_init(self, config: Any) -> None: self._agent_init = AgentInit(config) def create_span_processors(self, config: Any) -> List[SpanProcessor]: - if is_env_var_present("ENABLE_CONSOLE_SPAN_EXPORTER"): + if is_env_flag_enabled("ENABLE_CONSOLE_SPAN_EXPORTER"): self._agent_init.set_console_span_processor() return [] diff --git a/test/env_test.py b/test/env_test.py new file mode 100644 index 0000000..fe23574 --- /dev/null +++ b/test/env_test.py @@ -0,0 +1,57 @@ +"""Boolean SDK flag resolution: is_env_flag_enabled across prefix aliases.""" +import os + +import pytest + +from harness_sdk.env import is_env_flag_enabled + +_KEY = "ENABLE_CONSOLE_SPAN_EXPORTER" +_PREFIXES = ("HARNESS_", "HA_", "AT_", "TA_") + + +@pytest.fixture(autouse=True) +def clear_flag(): + for prefix in _PREFIXES: + os.environ.pop(f"{prefix}{_KEY}", None) + yield + for prefix in _PREFIXES: + os.environ.pop(f"{prefix}{_KEY}", None) + + +def test_unset_flag_is_disabled(): + assert is_env_flag_enabled(_KEY) is False + + +def test_true_enables_flag(): + os.environ[f"HARNESS_{_KEY}"] = "true" + assert is_env_flag_enabled(_KEY) is True + + +def test_explicit_false_disables_flag(): + os.environ[f"HARNESS_{_KEY}"] = "false" + assert is_env_flag_enabled(_KEY) is False + + +def test_arbitrary_value_does_not_enable_flag(): + os.environ[f"HARNESS_{_KEY}"] = "1" + assert is_env_flag_enabled(_KEY) is False + os.environ[f"HARNESS_{_KEY}"] = "yes" + assert is_env_flag_enabled(_KEY) is False + + +def test_empty_value_does_not_enable_flag(): + os.environ[f"HARNESS_{_KEY}"] = "" + assert is_env_flag_enabled(_KEY) is False + + +def test_legacy_aliases_enable_flag(): + for prefix in ("HA_", "AT_", "TA_"): + os.environ[f"{prefix}{_KEY}"] = "true" + assert is_env_flag_enabled(_KEY) is True + del os.environ[f"{prefix}{_KEY}"] + + +def test_harness_prefix_wins_over_legacy_alias(): + os.environ[f"HARNESS_{_KEY}"] = "false" + os.environ[f"HA_{_KEY}"] = "true" + assert is_env_flag_enabled(_KEY) is False