# Changelog

All notable changes to the MCA SDK will be documented in this file.

## [Unreleased]

## [0.9.3] - 2026-07-08

### Added
- **`mca_sdk.ai_assist` subpackage** — public API for end-user data
  scientists to program against the AI-assistant surface. Exposes
  `get_guide_path()`, `read_guide()`, `render_prompt(variant, **fields)`,
  `list_assistants()`, and `install(assistant, dest, force=False)`.
- **`%mca_instrument` / `%mca_prompt` IPython magics** —
  `%load_ext mca_sdk.ai_assist.jupyter` registers a notebook-native
  onboarding path. `%mca_instrument` reuses the same `_auto_init` /
  `_shutdown_with_timeout` bootstrap as `mca-instrument` / `mca-run`, so
  env-var semantics are identical across surfaces. IPython is optional
  (`pip install "mca-sdk[jupyter]"`); the module raises a clear error if
  IPython is missing.
- **`mca-init ai-assist` subcommand** — scaffolds per-assistant config
  files into the caller's repo at each assistant's canonical location:
  Claude Code (`.claude/skills/mca-instrument.md`), Cursor
  (`.cursor/rules/mca-instrument.mdc`), Copilot
  (`.github/copilot-instructions.md`), Gemini CLI (`GEMINI.md`). Also
  supports `mca-init ai-assist prompt --model-id ... --type ...` to
  render a fully-filled Variant A/B paste-in prompt to stdout. Every
  template LINKS to `FOR_AI_ASSISTANTS.md` inside the wheel — none
  duplicate pattern content.
- Notebook starter template shipped in the wheel at
  `mca_sdk/ai_assist/templates/notebook/mca_starter.ipynb`.

### Changed
- **`FOR_AI_ASSISTANTS.md`** received three rounds of post-0.9.2
  corrections from live instrumentation observations: six additional
  pattern gaps closed (association-id join keys, batch-vs-per-instance
  `record_*` rules, and related edge cases); `model_types` values
  corrected to the canonical set; and a new section covering MLOps
  pipeline usage and dependency-installation guidance.

### Fixed
- **OTel SDK 1.35–1.38 import compatibility** — `mca_sdk` no longer
  raises `ImportError: cannot import name 'ReadableLogRecord' from
  'opentelemetry.sdk._logs'` when installed against `opentelemetry-sdk`
  releases in the 1.35 through 1.38 window (where the symbol did not
  exist). `mca_sdk/exporters/gcp_logging.py` and
  `mca_sdk/buffering/exporters.py` now use try/except fallback chains
  to import `LogRecord`, `LogRecordExporter`, and
  `LogRecordExportResult` from whichever module path the installed
  OTel version exposes them at (`_logs`, `_logs._internal`, or the
  older `LogExporter` / `LogExportResult` aliases). Verified against
  OTel 1.35.0, 1.36.0, and 1.43.0. Type annotations changed from
  `Sequence[ReadableLogRecord]` to `Sequence[LogRecord]`; runtime
  behavior is unchanged.
- **MANIFEST.in** now includes `FOR_AI_ASSISTANTS.md` and all
  `ai_assist/templates/*` files in sdists. The guide previously shipped
  only in wheels via `setup.py package_data`; sdist consumers
  (`pip install --no-binary`) lost it.
- Lint and mypy strict-mode errors resolved across
  `mca_sdk/ai_assist/cli_install.py`, `mca_sdk/ai_assist/jupyter.py`,
  `mca_sdk/buffering/exporters.py`, and
  `mca_sdk/exporters/gcp_logging.py`. No behavior change.
- **Sphinx docs build** — `IPython` is now mocked in `docs/conf.py`
  so the `mca_sdk.ai_assist.jupyter` autosummary page builds cleanly
  in docs environments without IPython installed.

### Security
- **cryptography** upgraded to 48.0.1. Expired CVE-2026-4539 entry
  removed from `.pip-audit-allowlist.txt`.

### Notes
- The `mca-init <project_dir>` invocation is unchanged. The new
  `ai-assist` action is pre-dispatched in `main()` before argparse sees
  the positional arg, so no argparse restructure was needed.
- **PHI is still the application's responsibility.** Every template
  body carries the "warn, do not auto-mask" language from
  FOR_AI_ASSISTANTS.md §6.

## [0.9.2] - 2026-06-30

### Changed
- **Docs (`FOR_AI_ASSISTANTS.md`)**: rewrote the in-wheel AI-assistant
  instrumentation guide. Now covers `autolog()` (stories 8-2, 9-1, 9-2),
  agentic multi-step instrumentation (story 1-11), Vertex AI Gemini direct
  via `record_llm_generation` context manager, FastAPI `lifespan` startup,
  `RegistryClient` with `use_gcp_auth=True` + `token_audience`, vendor
  bridge, direct OpenAI/Anthropic patchers, LangChain / LlamaIndex
  callbacks, batch-vs-per-instance `record_*` rules, `association_id`
  join-key guidance, and PHI opt-in (`MCA_ENABLE_PHI_FILTER`). Corrects
  the env-var name from `MCA_COLLECTOR_ENDPOINT` to the canonical
  `MCA_OTEL_ENDPOINT` (deployment-only override retained for backwards
  compat). Adds owner-stated and fallback prompt templates so the
  assistant cannot guess model type / shape (stories 1-6, 1-7, 1-8).

## [0.9.1] - 2026-06-12

### Changed
- **CI (story 6-14)**: scan-before-push DAG over kaniko + crane against
  GitLab Container Registry. Four jobs, parallel matrix over the 4
  sdk-examples (internal-model, internal-genai, internal-agentic,
  vendor-bridge):
  - `docker:build` (build stage) — kaniko `--no-push --tar-path` produces
    an OCI tarball; remote layer cache via `--cache-repo` so pipelines
    after the first benefit from cache hits. Auth via the
    `DOCKER_AUTH_CONFIG` env var (kaniko reads natively — no destructive
    `cat >` overwrite of `/kaniko/.docker/config.json`).
  - `docker:scan` (security stage) — Trivy 0.50.0 scans the local tarball
    (`trivy image --input image-${EXAMPLE}.tar`) so nothing reaches the
    registry until findings clear the gate. GitLab `cache:` keyed
    `trivy-db-v0.50.0` reuses the vulnerability DB across pipelines.
    HIGH gate retains `--ignore-unfixed`; **CRITICAL gate runs without
    `--ignore-unfixed`** so unpatchable critical CVEs still fail the
    pipeline (suppression goes through `.trivyignore`). Report uploaded
    via `--format template --template "@contrib/gitlab.tpl"` so the
    GitLab MR Container Scanning widget actually renders findings (the
    previous `--format json` was the Aqua schema and was silently dropped).
  - `docker:push` (build stage) — `crane push` with destinations built as
    a bash array and quoted-expanded (`"${DESTS[@]}"`); only runs after
    `docker:scan` passes. Tags emitted: `$CI_COMMIT_SHA` (always),
    `:dev` (development), `:latest` (master), `$CI_COMMIT_TAG` (releases).
  - `docker:smoke-test` (new `docker-verify` stage) — runs the just-pushed
    image as the job container against an OTel Collector services-sidecar,
    invokes the example entrypoint with `timeout 30`, and asserts a clean
    exit (0) or timeout-kill (124, expected for long-running services).
    `allow_failure: false`. **AC3 is now AUTO-VERIFIED on every CI run**.
  HEALTHCHECK directives added to the three batch-job Dockerfiles
  (internal-model, internal-genai, internal-agentic) — PID 1 liveness
  probe via `os.path.exists('/proc/1/status')`, no procps install
  required. vendor-bridge keeps its existing curl-based healthcheck.
  Closes story 6-13 AC5. Local debug procedure in
  `mca-prototype/sdk-examples/DOCKER.md` rewritten to reuse the existing
  `docker-compose.yml` collector service instead of an ad-hoc `mca-verify`
  network.
- **CI**: added `python:integration-test` GitLab CI job running the `integration`
  marker against a live OTel-Collector sidecar; raised `python:unit-test`
  coverage gate to `--cov-fail-under=85` (`allow_failure: true` pending
  story 6-65). AC5 (SDK Docker image + commit-hash tag) deferred to story
  6-14. Story 6-13.

### Fixed
- **CI hardening (story 6-13 review)**: replaced flaky `sleep 1` mock-collector
  startup with a port-readiness wait loop; removed the `python:integration-test`
  phantom `--config=/etc/otelcol-contrib/config.yaml` override and added a
  sidecar-readiness wait; added `--disable-socket` and `--timeout=` flags so
  `pytest-socket` / `pytest-timeout` actually enforce; flipped
  `security:gitleaks` to `allow_failure: false` with `--redact` (AC3 secret-leak
  enforcement); removed the dead commented-out Teams webhook stanza in
  `security:pip-audit`.

### Security
- **CI security finding closeout (story 6-13 final remediation)**:
  - **pip-audit CVE allowlist externalised** to
    `mca-prototype/.pip-audit-allowlist.txt`. Each entry now carries the
    affected package, justification, expiry/review date, and tracking
    ticket. The `security:pip-audit` GitLab CI job loads the file via
    shell expansion (no extra container deps) and echoes the loaded
    entries before running `pip-audit --strict`. The job's `rules: changes:`
    arrays now include the allowlist path so allowlist edits retrigger the
    scan. Replaces the previous hardcoded `--ignore-vuln CVE-2026-0994
    --ignore-vuln CVE-2026-4539` flags.
  - **bandit SAST scope expanded** from `mca-prototype/mca_sdk/` only to
    `bandit -c .bandit -r mca-prototype/`, picking up `sdk-examples/**/*.py`
    and the top-level `mca-prototype/*.py` dev-tooling scripts. The repo-root
    `.bandit` config is now actually loaded; its `exclude_dirs` was rewritten
    from leading-slash patterns to bandit's documented substring form, and
    three `*non_instrumented.py` demo baselines were added to `exclude_dirs`.
    `bandit-report.json` is uploaded as an artifact with `reports.sast` so
    the GitLab MR widget surfaces the SAST report.
### Fixed
- **Story 1-21 adversarial-review remediations** — addresses all 10 findings from
  the post-implementation review of `mca.sdk.version` resource attribute support
  (`mca_sdk/core/providers.py`, `mca_sdk/version.py`,
  `tests/unit/test_providers.py`,
  `tests/integration/test_sdk_version_in_telemetry_payload.py`,
  `tests/unit/test_version_check.py`):
  - **Reserved-key collision guard now also covers `**kwargs`** — previously
    `extra_resource` was filtered against `_SDK_RESERVED_RESOURCE_KEYS` while
    `attributes.update(kwargs)` ran unguarded, allowing callers to override
    `service.name`, `model.id`, `mca.sdk.version`, etc. The new
    `_merge_external_attributes` helper enforces the same guard chain on both
    inputs (Findings 1, 3).
  - **Prometheus normalization-evasion vectors blocked** — variants such as
    `mca_sdk_version`, `MCA.SDK.VERSION`, and `Mca.Sdk.Version` previously
    bypassed the reserved-key check and would have produced duplicate-label
    crashes in the Prometheus exporter at first scrape. The merge helper now
    normalizes via `_prom_normalize` before comparing (Finding 8).
  - **Internal collisions across `extra_resource` and `**kwargs` deduped** — a
    shared `accepted_normalized` map is threaded through both calls so a
    registry-supplied label cannot be silently overwritten by a kwargs vector
    pointing at the same Prometheus bucket. Registry config wins; kwargs is
    dropped with a structured WARNING (Finding 10).
  - **Non-primitive attribute values dropped with WARNING** — `None`, `dict`,
    `list`, and other non-`str/int/float/bool` values are no longer forwarded
    to the OTel SDK (which silently rejects them), avoiding telemetry
    truncation surprises. `bool` continues to be accepted as the `int`
    subclass it is (Finding 3).
  - **Reserved-keys docstring grouped into SET vs PROTECTED ONLY** — the
    comment block above `_SDK_RESERVED_RESOURCE_KEYS` previously claimed the
    SDK set every entry, which was misleading for the `telemetry.sdk.*`
    family. Those keys are still protected, but documented as auto-populated
    by `OTELResourceDetector` (Finding 4).
  - **Tautological version asserts replaced with structural regex** — unit
    and integration tests previously did `attrs["mca.sdk.version"] ==
    __version__`, which trivially passes when both sides resolve to
    `"unknown"` in dev environments. The new `_VERSION_RE` regex accepts
    either the documented `"unknown"` fallback or a real semver-ish string,
    and `telemetry.sdk.version` is asserted only by presence (Findings 5, 6).
  - **WARNING emitted when `mca-sdk` version falls back to `"unknown"`** —
    `mca_sdk/version.py` now logs a WARNING immediately before assigning
    `__version__ = "unknown"`, so editable / mis-packaged installs surface
    a diagnostic instead of silently emitting telemetry tagged
    `mca.sdk.version="unknown"`. A code comment also pins the
    `version("mca-sdk")` literal to `setup.py name=` to prevent drift
    (Findings 7, 9).
  - **New unit tests** in `TestCreateResourceAttributes` cover normalization
    evasion for both `mca.sdk.version` and `telemetry.sdk.version`, core-arg
    hijack (`service.name`, `model.id`), internal collision dedup,
    cross-source collisions between `extra_resource` and `**kwargs`,
    None/dict/list rejection, primitive acceptance, and structured WARNING
    content (Findings 5, 6, 8, 10 — test side).

### Behavior change
- **`telemetry.sdk.version` is no longer overwritten with the MCA SDK
  version.** Story 1-21 originally assigned `attributes["telemetry.sdk.version"]
  = __version__` in `create_resource_attributes`, which violated OTel
  semantic conventions (`telemetry.sdk.*` is reserved for the OpenTelemetry
  SDK itself) and broke downstream OTel pipeline analysis that keys on the
  real SDK version. The assignment is removed; `OTELResourceDetector`
  populates `telemetry.sdk.{name,version,language}` from the actual
  `opentelemetry-sdk` package. Downstream consumers reading
  `telemetry.sdk.version` as MCA's SDK version must migrate to
  `mca.sdk.version`, which remains MCA-controlled and tamper-resistant
  (Finding 2).

### Added
- **GenAI vendor auto-instrumentation suite (Stories 9-1 through 9-7)** —
  four new auto-instrumentation modules and a unified entrypoint. All four
  emit MCA-standard `genai.*` metrics (`genai.requests_total`,
  `genai.requests_failed_total`, `genai.tokens_total`, `genai.cost_usd`,
  `genai.latency_seconds`) and request/usage spans matching the
  `_genai_attrs.py` contract. PHI capture is opt-in per-callback via the
  `capture_enabled` parameter; defaults are fail-closed (no
  prompt/completion text captured unless explicitly enabled). The new
  `autoinstrument()` function is re-exported from `mca_sdk` as the single
  entrypoint that activates whichever providers are installed.
  - **Story 9-1 — OpenAI autologger** (`mca_sdk/integrations/autolog/_openai.py`,
    `847c2a91`). Patches `openai.ChatCompletion`, `openai.Embedding`, and
    the streaming variants. Handles sync + async paths, captures
    request/response shapes, token counts, latency, and cost estimation
    with graceful fallback when the SDK shape changes.
  - **Story 9-2 — Anthropic autologger** (`mca_sdk/integrations/autolog/_anthropic.py`,
    `42b388ab` + `97c2144f`). Patches `messages.create` and the
    `client.messages.stream()` context manager so streamed responses are
    aggregated correctly into a single span/usage record.
  - **Story 9-3 — LangChain callback handler** (`MCALangChainCallback`,
    `a91c81c0`). Captures chain / agent / tool / LLM events including
    streaming and async; uses `ContextVars` for thread-safe concurrent
    tracking; respects local `capture_enabled` independent of the global
    config.
  - **Story 9-4 — LlamaIndex callback handler** (`MCALlamaIndexCallback`,
    `6f4210b6`). Tracks RAG queries, retrievals, LLM synthesis, and
    embeddings; uses contextvars for thread/async safety; FIFO-bounded
    in-memory state to prevent unbounded growth on long-running services.
  - **Story 9-5 — `autoinstrument()` unified entrypoint** (`670772a4`,
    `d4ded980`). Single function call that detects installed providers
    and activates the relevant patchers/callbacks. Now re-exported from
    `mca_sdk` for the documented public API.
  - **Story 9-7 hardening (`1e28fafa`, `7ffdefb2`)**:
    - `capture_enabled` parameter threaded through `capture_input_safe()`
      and `capture_output_safe()` (17 call sites across the LangChain and
      LlamaIndex callbacks). Restores fail-closed PHI protection for
      callback-based instrumentation; previously the callbacks ignored
      local capture flags and only honored the global config.
    - Performance test fixtures fixed to use `model_type="vendor"` with
      `strict_validation=False`; `setup_method` / `teardown_method`
      added across all 4 provider test classes for proper isolation.
    - Integration test suites added for OpenAI (chat + embeddings) and
      Anthropic (messages + streaming) end-to-end telemetry.
    - Coverage-test mock-patch repair: `test_autoinstrument_coverage.py`
      switched to `patch.object(autolog_module, ...)` and four
      unmeasurable `sys.modules`-mocked tests removed from
      `test_autolog_openai_coverage.py`.
  - **New `sdk-examples/direct-llm/`** with runnable examples for OpenAI,
    Anthropic, LangChain, and LlamaIndex; each performs
    `check_collector_available()` socket detection and falls back to
    `MCA_ENV=staging` when the collector is offline so the examples run
    without infrastructure.
  - **Documentation**: new `tests/README.md` documenting test strategy
    and the 85% coverage target.

### Security
- **Story 8-9 F1–F10 credential / span-lifecycle remediations** for
  agentic-AI goal tracking and credential sanitization
  (`mca_sdk/core/client.py`, `mca_sdk/integrations/_genai_attrs.py`,
  `mca_sdk/utils/serialization.py`, `mca_sdk/utils/redaction.py`,
  `mca_sdk/integrations/decorators.py`, plus a 32-test regression suite):
  - **F1 — short-token credential leak** (`536feabe`). `sanitize_token`
    previously passed tokens shorter than `MIN_SANITIZABLE_TOKEN_LENGTH = 8`
    through unchanged, leaking 100% of the secret into logs. Sub-threshold
    tokens are now replaced with `[REDACTED_TOKEN]`; the last-4-character
    `***XXXX` masking is preserved for `>= 8`-char tokens.
  - **F2 — substring false-positives in credential matcher** (`3eae1c46`).
    The previous `cred_key in key_lower` substring scan flagged legitimate
    keys (`author_id` matched `auth`, `tokenizer_config` matched `token`,
    `passwordless_sso` matched `password`), silently corrupting user data
    with `[REDACTED]`. The matcher now splits keys on `_ / - / . /
    whitespace` and only matches a credential name when its tokenized form
    appears as a contiguous subsequence of the key tokens. The credential
    set is promoted to a module-level `FrozenSet` and pre-tokenized once.
  - **F3 / F4 — structural credential redaction regardless of value
    shape** (`0cc93e0d`). `_sanitize_json_credentials` previously only
    replaced credential-keyed values when they were strings of length
    `>= 8`; short tokens, numeric values, and credential-keyed dict /
    list subtrees passed through verbatim or were only partially scrubbed.
    The new `_redact_credential_value` helper recursively replaces every
    leaf in a credential-keyed subtree with `[REDACTED]` regardless of
    value type or length. Two unit tests in `test_client_helpers.py` that
    codified the previous leak (short-string and numeric pass-through)
    were updated to assert the new structural contract.
  - **F5 / F6 — cross-thread OTel context detach in goal lifecycle**
    (`1b35fc8b`). `_active_goals` now stores `attach_thread_id` alongside
    the OTel context token (5-tuple). `record_goal_completed` and
    `_cleanup_stale_goals` route detach through `_safe_detach_context`,
    which no-ops when the detaching thread differs from the attaching
    thread. Per-goal cleanup is wrapped in `try/except` so a single
    cross-thread failure no longer halts the stale-goal sweep.
  - **F7 — sub-millisecond timestamp precision and ns-scale rejection**
    (`82ecbfac`, `574b45fd`). `int(timestamp * 1e9)` silently dropped
    sub-millisecond precision (float64 carries only ~15–17 significant
    decimal digits; a current epoch-seconds value consumes ~10), so
    adjacent events differing by `< 1 ms` could collide or invert in
    OTel span-event ordering. Replaced with integer arithmetic that
    splits whole seconds and fractional nanoseconds:
    `whole_s * 1_000_000_000 + frac_ns`. Applied to both the
    `human_intervention` span-event timestamp and the synthetic
    `wait_time` `start_time` computation. Validator guard tightened to
    `1e9 <= timestamp < 1e11` so ns-scale `time.time_ns()` (~1.7e18)
    inputs are rejected with the existing actionable message — previously
    only values `< 1e9` (year 2001) were caught.
  - **F8 — unconditional span end / context detach on goal completion**
    (`14212504`). `record_goal_completed` previously executed
    `set_attribute` / `set_status` / `span.end()` /
    `_safe_detach_context()` as a flat statement sequence; an exception
    raised by `span.set_attribute()` (e.g. invalid attribute type passed
    via `**attributes`) skipped both `span.end()` and the context detach,
    leaking the span and stranding the OTel context token in the active
    goal's thread. Wrapped in `try/finally` with an inner `try/finally`
    around the detach so `span.end()` always runs after attribute setup
    and `_safe_detach_context()` always runs after `end()`, regardless
    of failure mode.
  - **F9 — `_sanitize_json_credentials` widened to `Any -> Any`**
    (`cd4ccf74`). The previous `Dict[str, Any] -> Dict[str, Any]`
    signature combined with an `assert isinstance(result, dict)` at one
    call site crashed on valid non-object JSON roots (lists, strings,
    numbers, booleans, `None`) and was elided entirely under
    `python -O`, removing the only guard against silent type confusion.
    Non-`dict` inputs are now handled directly: lists / tuples recurse
    with the same cycle guard; scalars pass through.
  - **F10 — orphan-span cleanup on goal-start failure** (`df6c089b`).
    `record_goal_started()` previously only guarded the `_active_goals`
    dict insert with the goals lock. An exception from
    `otel_context.attach()` or `trace.set_span_in_context()` left the
    span created but never tracked or ended, leaking it indefinitely.
    Both calls are now wrapped in `try/except BaseException`; on
    failure, the context token is detached (if captured) and the span
    is ended before re-raising.
- **Story 9-6 PHI-safe data capture remediations** across the GenAI
  integrations (`83b7fc6c`):
  - Catastrophic fail-open vulnerability fixed: user-hook exceptions
    no longer cause raw inputs/outputs to be emitted unfiltered.
  - `_anthropic.py` and `_openai.py` async handlers refactored to use
    the shared capture utilities instead of duplicated inline logic.
  - Unsafe inline fallback logic removed from the LangChain and
    LlamaIndex callbacks.
  - 1024-character OpenTelemetry attribute limit enforced on captured
    payload attributes.
  - Blind string truncation removed (it produced malformed JSON when
    a payload was cut mid-escape sequence).

### Fixed
- **Story 1-18 input/output tracking adversarial-review remediations**
  (`a9e31202` — closes `1-18-adversarial-review-2`). Addresses all 10
  findings from the post-implementation review of GenAI input/output
  capture:
  - Internal credential masker now runs in a deterministic order
    relative to user hooks.
  - Recursive credential masking is applied **before** payload truncation
    limits, so a credential split across the truncation boundary cannot
    leak.
  - Oversized payloads are recorded as span events instead of attributes,
    preventing silent OTel-side rejection of values above the
    1024-character attribute limit.
  - Cycle-safe traversal guards against silent OOM and `RecursionError`
    on self-referential inputs.
  - LiteLLM and core-client hook input formats unified so user hooks
    receive the same shape regardless of which integration path
    invoked them.
  - 11 new tests provide full coverage for the edge cases above.
- **Pipeline-stability fixes** (`3237c7a8`):
  - Black auto-format pass across 19 files that failed
    `black --check` in the `python:lint` job.
  - Missing test fixture `tests/fixtures/agent/advisories/p0_25h.json`
    added.
  - State-persistence test no longer hits a 60s timeout: explicit
    `vendor_bridge_poll_interval=0.01` override (default 30s).
  - `docker:push` script rewritten in POSIX `sh` (no bash arrays);
    `crane:debug` uses busybox `sh`, which does not support
    `DESTS=( ... )`.
  - `python:integration-test` and `docker:smoke-test` services switched
    from `otel/opentelemetry-collector-contrib:0.115.0` (docker.io
    image not found on the runner) to the `ghcr.io` mirror.
  - Bandit findings closed: `raw_model.py` flipped `verify=False` →
    `verify=True` (B501 HIGH) and removed the `urllib3
    InsecureRequestWarning` suppression; `lockfile_diff_gate.py` got
    `# nosec B310` (project-controlled URL); `instrumented_model.py`
    got `# nosec B301` (SDK-generated artifact);
    `mock_vendor_api.py` got `# nosec B104` (intentional demo
    binding).
  - `jaraco.context >= 6.1.0` floor pin added to fix CVE-2026-23949
    (HIGH); `.trivyignore` carries the same CVE as belt-and-suspenders;
    `--ignorefile .trivyignore` passed to all `trivy` commands in the
    `docker:scan` matrix.

## [0.8.33] - 2026-05-20

### Added
- **`MCAClient.record_retrieval(...)`** — context-managed retrieval-observation
  API that emits a `rag.retrieval` span matching `docs/api-contracts.md` §9.2
  (`mca_sdk/core/client.py`, `mca_sdk/integrations/_genai_attrs.py`). Enforces
  the 50-document cap, min-max score normalization, and parallel-array length
  agreement contract. PHI sanitization remains the caller's responsibility.
  Closes the deferred ACs #1/#2/#6 from `tk_a0c83cb04d774b89`.
- **Configurable histogram-boundary env vars**
  (`MCA_LATENCY_SHORT_TAIL_BOUNDARIES`, `MCA_LATENCY_LONG_TAIL_BOUNDARIES`)
  resolved at import via `_resolve_latency_boundaries`
  (`mca_sdk/core/providers.py`). Parse failures log a WARNING and fall back
  to the default tuples — the resolver does not raise. Closes DP-16 round-2
  finding #9.
- **Caller-supplied `views` now take precedence** over
  `DEFAULT_HISTOGRAM_BOUNDARY_VIEWS` in `setup_meter_provider`. The SDK
  filters its default views to skip any instrument name already targeted by
  a caller `View`, eliminating the OTel duplicate-view runtime warning.
  Closes DP-16 round-2 finding #11.
- **`LATENCY_SHORT_TAIL_BOUNDARIES`, `LATENCY_LONG_TAIL_BOUNDARIES`,
  `DEFAULT_HISTOGRAM_BOUNDARY_VIEWS` re-exported from `mca_sdk`** so operators
  composing custom Views can reuse the SDK defaults verbatim.

### Fixed
- **`generative model_id` adversarial-review remediations**
  (`mca_sdk/integrations/litellm_callback.py`,
  `mca_sdk/integrations/_genai_attrs.py`, `mca_sdk/core/client.py`):
  falsy override `0` is no longer discarded; span-side `mca.model_id` and
  legacy `model_id` keys are emitted by `build_request_attributes` and
  `build_usage_attributes`; broad `except Exception:` narrowed to
  `(AttributeError, TypeError)`; new reserved sentinel `model_id="error"`
  distinguishes SDK-defect from configuration-gap; label cardinality bounded
  at 64 characters; whitespace-only and complex-object overrides rejected;
  bound default cached on the callback to eliminate hot-path `getattr`.
- **Metric-temporality adversarial-review remediations** for the 6-56 / KE-705
  delta fix (`mca_sdk/core/providers.py`, `mca_sdk/buffering/exporters.py`):
  histograms reverted to cumulative (PromQL `histogram_quantile()`
  requirement); E2E test now wires `preferred_temporality` to the **exporter**
  matching production; `lowmemory` map includes `ObservableCounter` /
  `ObservableUpDownCounter`; resolver narrows `except Exception:` to
  `(OSError, KeyError, ValueError, TypeError, AttributeError)`; cumulative
  branch returns explicit `_TEMPORALITY_CUMULATIVE_FULL` map; capability
  probe added for older `OTLPMetricExporter` releases; invalid-value log
  severity escalated to WARNING.
- **`setup.py` duplicate `docs` extras_require entry removed** — the
  duplicate would silently overwrite the first list, causing pip to install
  whichever variant happened to be parsed last. Fixes the build-tooling
  drift introduced earlier in the 0.8.32 cycle.
- **`tests/unit/test_providers.py` temporality assertions aligned** with
  `bea06d97` so the suite reflects the cumulative-histogram + delta-counter
  contract rather than the brief delta-histogram window.
- **`mca_sdk/security/phi_filter.py` mypy `--strict` arg-type errors resolved**
  (`c58eefc8`). `is_enabled()` previously passed `enable_raw` / `legacy_raw`
  (typed `str | None`) into `_parse_bool(name: str, raw: str)` guarded by
  separately-bound `enable_set` / `legacy_set` booleans that mypy could not
  use to narrow the `str | None` to `str`. Replaced both call sites with
  direct `is not None` checks so the narrowing is local to the conditional
  expression. No runtime behavior change — purely a type-checker correctness
  fix that unblocks the `python:lint` CI job.
- **Black reformatting pass across 33 files**
  (`mca_sdk/core/providers.py`, `mca_sdk/security/phi_filter.py`,
  `mca_sdk/security/secret_filter.py`, plus tests; commit `c58eefc8`).
  Reformat with `black --line-length 100 --target-version py310` to clear
  `python:lint` failures on the development branch. Cosmetic only —
  no semantic changes; included in the wheel because the touched
  `mca_sdk/` files are part of the published package.

## [0.8.32] - 2026-05-13

### BREAKING

- **PHI attribute filter is now disabled by default** (`mca_sdk/security/phi_filter.py`). The defense-in-depth filter introduced by story 6-31 no longer runs on `record_*` calls, the audit drain, the disk-spillover serializer, or the synchronous `record_error` path unless the caller explicitly opts in. Per CLAUDE.md ("PHI Masking is Application Responsibility"), application-level sanitization is the only layer between caller-supplied attribute values and downstream telemetry sinks. Because this changes the default behavior of a security-relevant safety net, the next release MUST be a major version bump (`1.0.0`).
  - **Opt-in:** set `MCA_ENABLE_PHI_FILTER=true` (canonical positive name; also accepts `1` / `yes` / `on`, case-insensitive).
  - **Legacy alias:** `MCA_DISABLE_PHI_FILTER=false` is still accepted but emits a `DeprecationWarning` on first read and will be removed in a future major release.
  - **Strict parsing:** unrecognized env-var values (e.g. `enable`, `maybe`) raise `ConfigurationError` rather than being silently treated as falsy. This prevents the foot-gun where a misspelled opt-in silently leaves the filter off.
  - **Operator notification:** `MCAClient.__init__` emits a one-shot `WARNING` log line per process when the filter is off, so operators learn at startup that PHI redaction is the caller's responsibility.
  - **Migration required before upgrading:** if your model container relied on SDK-side PHI redaction, you must either (a) set `MCA_ENABLE_PHI_FILTER=true` to keep the previous behavior, or (b) audit your application-side sanitization to confirm SSN/MRN/DOB strings are scrubbed before they reach `record_*` calls. The new `tests/security/test_attribute_phi_leakage.py::TestAttributePHILeakageWhenFilterDisabled` suite documents exactly which code paths now leak PHI by default.

### Added
- **Histograms reverted to `AggregationTemporality.CUMULATIVE`** — partial rollback of the 6-56 / KE-705 default. The previous change swept Histogram into the DELTA map alongside the four sum-type instruments; the duplicate-`(metric, label-set)` rejection from Cloud Monitoring's `WriteTimeSeries` is exclusively a counter problem, and Prometheus `histogram_quantile()` (referenced in `docs/api-contracts.md` §9.1) mathematically requires cumulative buckets. Counter / UpDownCounter / ObservableCounter / ObservableUpDownCounter remain DELTA. **Behavior change for any consumer that built dashboards on the brief delta-histogram window:** `*_bucket` series now revert to cumulative; existing PromQL `histogram_quantile()` queries that were broken by the prior change are restored. See `docs/api-contracts.md` §9.4 for the full rationale.
- **`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` is now honored** by `_resolve_temporality_preference` in `mca_sdk/core/providers.py`. Precedence: proprietary `MCA_OTEL_TEMPORALITY` wins when set; otherwise the OTel standard variable is honored; otherwise default `delta`. When both are set with conflicting values, the proprietary value wins and a WARNING is logged naming both variables. The proprietary variable is now formally deprecated and slated for removal one minor release after this dual-read window — operators should migrate to the standard variable. Closes adversarial-review finding #2.
- **`OTLPMetricExporter` capability probe** in `BufferedOTLPMetricExporter` (`mca_sdk/buffering/exporters.py`). On module import, `inspect.signature(OTLPMetricExporter.__init__)` is checked for `preferred_temporality` (or a `**kwargs` pass-through). When unsupported, the kwarg is dropped at construction time, a single WARNING is logged with the installed `opentelemetry-exporter-otlp` version and remediation guidance (`upgrade to opentelemetry-exporter-otlp>=1.20.0`), and the SDK still starts. Prevents startup crashes against older exporter releases. Closes adversarial-review finding #7.
- `association_id` forwarding on `instrument_model` and `instrument_async_model` (`mca_sdk/integrations/decorators.py`), closing the API-parity gap from `[0.8.28]` on the two sibling decorators not updated at the time. Backward compatible: no behavior change when `association_id` is omitted.
- **RAG Metric Contract specification** (`docs/api-contracts.md` section 9.2). Pins the SDK-emitted retrieval-context attribute names (`gen_ai.rag.query`, `gen_ai.rag.documents.count`, `gen_ai.rag.documents.ids`, `gen_ai.rag.documents.scores`, optional `gen_ai.rag.documents.relevance`) as parallel arrays with a 50-document cap and a `gen_ai.rag.documents.truncated` warning attribute. Indexed-key emission (e.g. `gen_ai.rag.documents.0.id`) is forbidden because each unique index value would create a new attribute key and blow up backend schema cardinality. Future RAG emission code (e.g. story 1-20) MUST follow this contract; no SDK code currently emits divergent names so no migration is needed.
- **`MCAClient.record_retrieval(...)`** — context-managed retrieval-observation API matching `docs/api-contracts.md` §9.2 (`mca_sdk/core/client.py`, `mca_sdk/integrations/_genai_attrs.py::build_retrieval_attributes`). Emits a `rag.retrieval` span with `langfuse.observation.type="retrieval"`; enforces the 50-doc cap (top-by-score with `gen_ai.rag.documents.truncated=true`), min-max score normalization to `[0, 1]`, and parallel-array length agreement (mismatched `relevance` raises `ValidationError`). Constant-score document sets emit all-1.0 with an SDK-side debug log. PHI sanitization remains the caller's responsibility per `sdk/CLAUDE.md`. Closes `tk_a0c83cb04d774b89` ACs #1 / #2 / #6 (Langfuse retrieval observation type and sampling-rate sentinel) that were deferred when the original ticket landed as docs-only in `7eb136fc`.
- **`gen_ai.rag.documents.source_system`, `gen_ai.rag.documents.score_metric`, `gen_ai.rag.sampling_rate`, `gen_ai.rag.parent_retrieval_id`** added to the §9.2 attribute allow-list (`tests/unit/test_rag_contract.py::_ALLOWED_RAG_ATTRIBUTE_NAMES`). Hybrid retrieval (dense + sparse + graph + reranker) is now expressed as multiple sibling `rag.retrieval` spans sharing a caller-supplied `parent_retrieval_id` UUID; each sibling carries its own `source_system` enum value. The `score_metric` enum (`cosine` | `dot_product` | `bm25` | `reciprocal_rank` | `llm_logprob` | `unknown`) lets evaluators recover original score semantics after min-max normalization. `sampling_rate` is a sentinel — the SDK never drops spans on its own; downstream sub-sampling is owned by the LLM Evaluator (recommended default 0.2 per `feedback.md` Q1).
- **Histogram Threshold Semantics decision** (`docs/api-contracts.md` section 9.1). Pins **p95** as the chosen percentile semantics for `agent.goal_duration_seconds` and `agent.intervention_wait_time_seconds`, matching the existing `latency_p95` API column convention. Includes a mandatory consumer-visible caveat: the long-tail boundary set widens above 1h (1h → 2h → 6h → 24h), so consumers MUST corroborate p95 against per-call BigQuery exemplars for tail-heavy populations rather than trust Prometheus interpolation alone.
- **`MCA_OTEL_TEMPORALITY` env var** (values: `delta` / `cumulative` / `lowmemory`). Controls the OTLP metric exporter's per-instrument temporality preference resolved by `_resolve_temporality_preference` in `mca_sdk/core/providers.py`. Default `delta` flips the four sum-type instruments to `AggregationTemporality.DELTA` (Histogram remains cumulative; see Added entry above). `cumulative` returns an explicit cumulative map for every instrument (no longer relies on OTel implicit default — finding #6). `lowmemory` is functionally an alias of `delta` after histogram was dropped from both maps; retained for parity with the OTel preset. Unrecognized values fall back to `delta` and emit a single WARNING log (severity escalated from INFO — finding #9). The resolver MUST NOT raise — `BaseException` propagates, but realistic failure modes for `os.environ.get` are caught, logged at WARNING, and fall back to the default delta map (finding #5). Documented in `docs/api-contracts.md` §9.4.

### Fixed
- **Adversarial-review remediations for the 6-56 / KE-705 delta-temporality fix** (`mca_sdk/core/providers.py`, `mca_sdk/buffering/exporters.py`, `tests/unit/test_temporality_resolver.py`, `tests/unit/test_temporality_e2e.py`, `tests/unit/test_buffering_exporters.py`, `docs/api-contracts.md` §9.4):
  - Histograms no longer forced to DELTA — see Added entry. (#4 / #8)
  - E2E test rewritten to wire `preferred_temporality` to the **exporter** (matching production), not the reader. The previous test validated a configuration shape production never exercised. (#1)
  - `lowmemory` map now includes `ObservableCounter` and `ObservableUpDownCounter`. The previous omission re-introduced the duplicate-TimeSeries problem for any async-counter user under that profile. (#3)
  - Resolver narrowed `except Exception:` to `(OSError, KeyError, ValueError, TypeError, AttributeError)` and emits a WARNING before falling back. `BaseException` subclasses propagate. (#5)
  - Cumulative branch returns an explicit `_TEMPORALITY_CUMULATIVE_FULL` map instead of `None` (no longer relies on OTel's library default forever being cumulative). (#6)
  - Capability probe added to `BufferedOTLPMetricExporter` — see Added entry. (#7)
  - `MCA_OTEL_TEMPORALITY` invalid-value log severity escalated from INFO to WARNING. (#9)
  - Multi-language parity (Java/Go/Node.js SDKs) tracked via parallel tickets in their respective repos; defensive `cumulativetodelta` collector workaround documented in §9.4. Out of scope for this Python repo. (#10)
- **Counter metric datapoints now export with delta temporality** (`AggregationTemporality.DELTA`) by default (KE-705), fixing the OTel Collector `googlecloud` exporter's `Duplicate TimeSeries encountered` rejection cascade. Each export carries a delta unique per `(metric, label-set)` per `WriteTimeSeries` request, so collector batch coalescing can no longer create duplicates that fail Google's per-series-per-request contract. Affects the four sum-type instruments only — Counter, UpDownCounter, ObservableCounter, ObservableUpDownCounter. Histograms remain cumulative (PromQL `histogram_quantile()` requirement; see §9.1). `ObservableGauge` is intentionally absent from the temporality map — gauges have no temporality concept per the OTel spec.
  - **Migration impact:** Consumers reading `_total` counters via Prometheus `rate()` or `increase()` continue to work unchanged — those PromQL functions reconstruct rates from deltas natively. Consumers reading raw cumulative values for sum-type counters from BigQuery (`emms_metrics.metric_value`) will now see per-export delta values, not running totals; `SUM(metric_value) GROUP BY metric_name, label_set, time_bucket` reconstructs the prior view. To roll back to cumulative for sum-type counters, set `MCA_OTEL_TEMPORALITY=cumulative` (or `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative`). The proprietary variable will be removed in v0.X+2 once downstream consumers confirm dashboard parity.
- `instrument_model` / `instrument_async_model` generator wrappers now route mid-iteration exceptions to `client.record_error` instead of silently emitting `record_prediction` with `status.code = "OK"` from a `finally` block. Errors inside a wrapped generator are now counted as errors.
- `instrument_model` no longer attempts to iterate an async generator with a synchronous `for` loop. The `inspect.isasyncgen(result)` branch was removed from the sync wrapper; async generators returned from sync-decorated functions now fall through to the standard success-path recording.
- **DP-16 round-2 adversarial-review remediations** (`mca_sdk/core/providers.py`, `mca_sdk/__init__.py`, `tests/unit/test_histogram_bucket_views.py`):
  - **`MCA_LATENCY_SHORT_TAIL_BOUNDARIES` and `MCA_LATENCY_LONG_TAIL_BOUNDARIES` env vars** now override the hardcoded boundary tuples for operators whose latency profiles deviate from the SDK defaults. Values parsed as comma-separated floats; must contain at least 2 strictly ascending finite values. Any parse / validation failure logs a WARNING and falls back to the default tuple — the resolver MUST NOT raise. Resolved at module import via `_resolve_latency_boundaries`, mirroring the contract of `_resolve_temporality_preference`. Closes finding #9.
  - **Caller-supplied views now take precedence over SDK boundary defaults** in `setup_meter_provider`. OTel Python evaluates ALL matching views with no built-in precedence (two views targeting the same instrument name produce conflicting metric streams plus a runtime warning), so the SDK now filters `DEFAULT_HISTOGRAM_BOUNDARY_VIEWS` to skip any instrument name already targeted by a caller-supplied `View`. Pass `views=[]` (default) to inherit SDK shapes; pass a `View(instrument_name="model.latency_seconds", ...)` to override that single name without forking the per-call sites. Closes finding #11.
  - **`LATENCY_SHORT_TAIL_BOUNDARIES`, `LATENCY_LONG_TAIL_BOUNDARIES`, `DEFAULT_HISTOGRAM_BOUNDARY_VIEWS` are now public** (re-exported from `mca_sdk`), so operators composing custom Views can reuse the SDK defaults verbatim instead of duplicating tuple literals. The leading underscores were dropped during this round-2 cleanup.
- **DP-16: Seconds-scale histogram bucket boundaries** now configured via OpenTelemetry Views on the MeterProvider (`mca_sdk/core/providers.py`). The enumerated SDK-managed short-tail instruments — `model.latency_seconds`, `genai.latency_seconds`, `agentic.latency_seconds`, `vendor.latency_seconds`, `<prefix>.embedding_latency_seconds` for the same four prefixes, `agent.tool_latency_seconds`, and `mca.registry.refresh_latency_seconds` — take short-tail boundaries `(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10)`; `agent.goal_duration_seconds` and `agent.intervention_wait_time_seconds` take long-tail boundaries `(1, 5, 30, 60, 300, 600, 1800, 3600, 7200, 21600, 86400)` extending to 24h. Previously every realistic ML latency (50ms–2s) collapsed into the single `[0, 5)` bucket because the SDK records seconds while OTel's defaults are millisecond-tuned, making p95 unreconstructable. The non-time histograms `model.mae` and `model.rmse` intentionally retain default boundaries. **Custom user histograms registered via `MCAClient.record_metric` also retain OTel default boundaries** — only the enumerated SDK-managed names above are reshaped (round-2 fix; round-1's `*latency_seconds` wildcard was rejected as it would silently overwrite caller-defined custom metrics). **Migration impact:** Prometheus consumers (Grafana dashboards, alert rules using `histogram_quantile()` over the old default boundaries) MUST update their queries to the new boundary set; the `le` labels emitted on `*_bucket` time-series have changed. Operators with in-flight dashboards can set `MCA_DISABLE_HISTOGRAM_BOUNDARY_VIEWS=true` to fall back to OTel default boundaries for one release as a migration buffer; the env var will be removed in the next minor.
- **Generative `model_id` attribute parity** (`mca_sdk/integrations/litellm_callback.py`, `mca_sdk/integrations/_genai_attrs.py`, `mca_sdk/core/client.py`). Every `genai.*` metric datapoint — `genai.requests_total`, `genai.requests_failed_total`, `genai.tokens_total`, `genai.cost_usd`, `genai.latency_seconds` — now carries the BHSF-registered `model_id` on both the success path (`log_success_event`) and the failure path (`log_failure_event`). The same resolved value is now also attached to request and usage spans under `mca.model_id` (SDK-internal namespace, matches `mca.sdk.version`) plus the legacy unprefixed `model_id` key for trace-to-metric correlation; this closes the span-side parity gap that broke the trace-to-metric join. Resolution order is per-call `kwargs["metadata"]["mca_model_id"]` override → `client.config.model_id` (cached at callback `__init__` per the identity-immutability contract — no per-call reflection on the hot path) → the literal string `"unspecified"` (never empty string, since Prometheus / GMP drop or coalesce empty-string label values). Required because this repo's collector disables `resource_to_telemetry_conversion`, so resource-level attributes do not surface as Prometheus labels — `model_id` must live on the datapoint itself for alert-policy-sync's per-model error-rate routing to work.
  - **Adversarial-review remediations** (against commit `61e5d7ec`):
    - Falsy override `0` is no longer discarded — resolver gate is `is not None`, sanitizer accepts `int` / `float` / `bool` and emits the stringified value (e.g. `"0"`). (#1)
    - Span-side `mca.model_id` + legacy `model_id` now emitted by `build_request_attributes` and `build_usage_attributes` — both `MCALiteLLMCallback` and `MCAClient.record_llm_generation` thread the resolved value through. (#2 / #3)
    - Broad `except Exception:` replaced with targeted `except (AttributeError, TypeError)` around the metadata-access boundary; logic bugs (`NameError`, typos) propagate to the public callback's existing exception boundary instead of being silently swallowed. A deduplicated `WARNING` is logged once per callback instance the first time the targeted handler fires. (#4)
    - New reserved sentinel `model_id="error"` (distinct from `"unspecified"`) returned only when the targeted handler fires, so operators can tell an SDK defect / buggy caller apart from a configuration gap. Documented in `docs/api-contracts.md` §9.1. (#5)
    - Label cardinality bounded at 64 characters after `.strip()` (stricter than the SDK-wide `MAX_ATTRIBUTE_LENGTH = 1024` because `model_id` is also a Prometheus label dimension; labels above ~63 chars degrade the backend). (#6)
    - Whitespace-only overrides (e.g., `"   "`) are now rejected and fall through to the cached default. (#7)
    - Complex objects passed as `mca_model_id` are rejected by an explicit `isinstance(value, (str, int, float, bool))` allow-list — no more `<Obj at 0x...>` memory-address labels. (#8)
    - Span attributes namespaced under `mca.model_id` (matches the existing `mca.*` SDK convention); the unstable `gen_ai.request.model_id` was deliberately NOT used. The unprefixed `model_id` key is retained on spans for trace-to-metric correlation parity. (#9)
    - Bound default cached on the callback in `__init__` as `self._bound_model_id_default`; per-call resolution short-circuits to it when no override is present, eliminating the hot-path `getattr` chain. (#10)
    - Redundant `isinstance(kwargs, dict)` removed; the resolver honors the public method's `Dict[str, Any]` type contract and lets `AttributeError` propagate to the public callback's `try/except` boundary, surfacing upstream type-contract violations instead of masking them as `"unspecified"`. (#11)

## [0.8.31] - 2026-05-05

### Security
- **TelemetryQueue now fails closed on unencrypted persistence in production.** When `persist=True` and no encryption is configured (`encryption_enabled=False` or `encryption=None`), the constructor raises `BufferingError` in detected production environments instead of only logging a HIPAA warning. Dev/test environments retain the warning-only path. Use `QueueEncryption` + `encryption_enabled=True` for production, or set `MCA_ALLOW_ENV_KEYS_IN_PROD=true` as a documented dev/test escape hatch (never for real production deployments). Closes a documented HIPAA gap; converting a warning that already said the behavior was non-compliant to an enforced error is a fix.
- **DeadLetterQueue now blocks `MCA_SDK_ENCRYPTION_KEY` in production** (parity with `TelemetryQueue`'s `MCA_QUEUE_ENCRYPTION_KEY` policy). Environment-loaded keys are rejected in detected production environments with an actionable `BufferingError`. Explicit keys passed via the `encryption_key` kwarg (recommended path: secret manager) are always allowed. The same `MCA_ALLOW_ENV_KEYS_IN_PROD=true` override applies.

### Added
- **`StaleConfigError` exception** raised by `RegistryClient._get_cached` / `fetch_model_config` / `fetch_deployment_config` when a cached entry's age exceeds `registry_max_stale_secs`. Enables fail-closed behavior for clinical/safety-critical models when the Registration API has been unreachable for an extended period. Stale entries are intentionally preserved in the cache for operator forensics; a subsequent successful refresh clears the stale state automatically. Inherits from `RegistryError` so existing broad catch blocks still work.
- **`MCAConfig.registry_max_stale_secs`** (int seconds) and the `MCA_REGISTRY_MAX_STALE_SECS` environment variable. Default `0` disables the cap (backward compatible). When set, must be `>= refresh_interval_secs` (validated in `__post_init__`). Recommended production value is ~6x `refresh_interval_secs` (e.g., 3600s with the default 600s refresh).
- **`mca.registry.stale_config_errors_total` counter metric** (attributes: `model_id`, `config_type`) fires each time `_get_cached` fails closed on the cap. Use as an early-warning alert.
- **`mca_sdk.utils.environment.is_production_environment()`** shared helper. Consolidates the production-detection heuristic previously duplicated on `TelemetryQueue` so `TelemetryQueue` and `DeadLetterQueue` cannot drift. Honors the same `MCA_ALLOW_ENV_KEYS_IN_PROD=true` override.

### Changed
- **`CacheEntry` now tracks `fetched_at`** in addition to `expires_at` so staleness can be measured independently of TTL. Field defaults to `0.0` for backward compatibility with any code that directly instantiates `CacheEntry`.
- **`RegistryClient.__init__` accepts `max_stale_secs`** (default 0 = disabled). `MCAClient` forwards `MCAConfig.registry_max_stale_secs`.
- **`RegistryClient._refresh_loop`** catches `StaleConfigError` explicitly (ordered before the broader `RegistryError` handler, because `StaleConfigError` inherits from it) so operators see a CRITICAL log when the cache is stale during a refresh attempt, and the loop continues to the next interval.
- **`TelemetryQueue._is_production_environment`** now delegates to the shared `mca_sdk.utils.environment.is_production_environment()` helper (behavior unchanged).
- **`get_encryption_key_from_env()` docstring** updated to document that callers (e.g., `DeadLetterQueue`) enforce production gating; the helper itself only reads the env var.

## [0.8.30] - 2026-05-04

### Fixed
- **Fixed Cloud Monitoring "Duplicate TimeSeries" rejections** by increasing default `metric_export_interval_ms` from 5000ms to 30000ms, eliminating batch-window collision with the OTel Collector's 5s batch timeout.
- **Fixed thread-safety bugs in concurrent client instantiation** by introducing reference-counted `_MCA_PROVIDER_REGISTRY` (WeakKeyDictionary) and Event-based double-checked locking with `_PROVIDER_INIT_LOCKS`.
- **Fixed "murder-suicide" bug** where one client's `shutdown()` killed the shared provider used by other clients, causing silent metric loss.
- **Fixed Prometheus HTTP server thread leak** by invoking `prometheus_shutdown()` callback when provider refcount reaches 0.
- **Fixed global state mismatch post-shutdown** by clearing OTel's `_METER_PROVIDER` in `unregister_provider()` to allow fresh initialization.
- **Fixed race condition in double-checked locking** where waiting threads fell through on initialization failure; now raises explicit `ConfigurationError` with root cause.
- **Fixed race condition during registry assignment** by moving `metrics.set_meter_provider()` inside `_MCA_PROVIDER_REGISTRY_LOCK` to ensure atomic registry-then-global-state update.
- **Fixed exception shadowing in init lock cleanup** by storing initialization failures in `_PROVIDER_INIT_ERRORS` dict and propagating to waiting threads.

### Changed
- **Downgraded conflicting-config behavior from crash to warning** (`ProviderAlreadySetWarning`) and provider reuse, preventing breakage in multi-module setups.
- **Refactored `setup_meter_provider` to eliminate monkey-patching**, using `_MCA_PROVIDER_REGISTRY` and reference counting instead of tampering with OTel internal state.
- **Expanded `config_key` tuple** to include all provider-affecting parameters (Prometheus, persist_queue, retry_policy, views) to prevent silent feature loss on reuse.
- **Added `OTEL_METRIC_EXPORT_INTERVAL` env var support** as fallback for `METRIC_EXPORT_INTERVAL_MS`, following OpenTelemetry standard conventions.
- **Replaced `logger.warning()` with `warnings.warn()`** for structural config conflicts to guarantee stderr visibility even when logging is unconfigured.
- **Updated test suite** to use MCA SDK's own `unregister_provider()` flow in `conftest.py` instead of manually mutating `_internal` APIs.

## [0.8.29] - 2026-05-01

### Added
- **`MCAClient.record_llm_generation()` context manager for Langfuse `GENERATION` observations**
  - New helper at `mca_sdk/core/client.py` that emits an OTel span shaped to the `gen_ai.*` semantic conventions Langfuse keys off to mint a `GENERATION` observation (with computed `latency_ms`) instead of a generic `SPAN`.
  - Closes the latency-coverage gap for direct-Vertex AI call sites (`internal-genai-vertex`, `internal-agentic-vertex`) that bypass the LiteLLM proxy and therefore never exercised `MCALiteLLMCallback` in production. Prior to this change those paths landed in Langfuse as `SPAN` observations with no `startTime`/`endTime`-derived latency, so the AI Command Center reported 0% latency coverage for `medical-research-assistant` / `agent.goal` and the genai `clinical-notes-summarizer-v2` cohort.
  - Context-manager shape: `with client.record_llm_generation(model=..., provider=..., operation="chat", prompt=messages, temperature=..., max_tokens=...) as gen: ...; gen.set_completion(...); gen.set_usage(...); gen.set_cost(...)`.
  - Span is named `llm.{model}` to match the existing `MCALiteLLMCallback` convention so a single Langfuse series covers both call paths.
  - Sets `gen_ai.system`, `gen_ai.request.model`, `gen_ai.response.model`, `gen_ai.operation.name`, `langfuse.observation.type="generation"`, `gen_ai.request.{temperature,max_tokens,top_p}`, `gen_ai.usage.{prompt_tokens,completion_tokens,total_tokens,cost_usd}`, `gen_ai.prompt`, `gen_ai.completion`, and `gen_ai.latency_seconds`. Legacy `llm.*` / `genai.*` keys are also emitted for backward compatibility with existing dashboards.
  - Exceptions inside the `with` block close the span with `StatusCode.ERROR`, `error=True`, and `error.type` set to the exception class name, then re-raise.
  - Provider is inferred from the model name when not passed explicitly (e.g. `gemini-*` → `google`, `gpt-*` → `openai`, `claude-*` → `anthropic`).

### Changed
- **Factored shared `gen_ai.*` attribute builder out of `MCALiteLLMCallback`**
  - New module `mca_sdk/integrations/_genai_attrs.py` exposes `extract_provider`, `build_request_attributes`, and `build_usage_attributes`. Both `MCALiteLLMCallback` and the new `record_llm_generation` helper delegate to it so attribute names, reserved keys, and provider inference stay identical across the two paths.
- **`internal-genai-vertex/main.py` and `internal-agentic-vertex/agent_instrumented.py` now use `record_llm_generation`** on the direct Vertex AI path. Redundant `record_counter("genai.tokens.*")` / `record_gauge("genai.cost_usd")` calls were removed — the same information is now on the generation span as `gen_ai.usage.*` attributes.

### Notes (out of scope for this release)
- The predictive-model gap (`emms_model_latency_p95` time series missing) requires a `ClusterRules` recording rule and an `etl-metrics` METRICS-list entry in the `platform/` repo. That fix is **not** in this SDK release; see the plan at `.claude/plans/got-this-when-looking-wiggly-petal.md`.
- PHI sanitization of `gen_ai.prompt` / `gen_ai.completion` remains the application's responsibility per `sdk/CLAUDE.md`. The helper's docstring reminds callers of that obligation.

## [0.8.28] - 2026-04-26

### Added
- **`association_id` parameter for `record_prediction()` and `record_error()`**
  - New optional keyword-only parameter `association_id: Optional[str] = None` on both methods
  - Links a prediction or error event to a downstream business outcome, time-series entity, or external record via the `ml.association.id` OpenTelemetry span attribute and structured log field
  - Reserved from user-supplied metric attributes to prevent spoofing
  - Keyword-only (`*` separator) ensures full backward compatibility — existing call sites are unaffected
  - The `@predict()` decorator forwards `association_id` via `kwargs.get()` (not `pop`) so user functions that also accept the parameter can consume it

## [0.8.28] - 2026-04-28

### Breaking changes
- **Queue-encryption internals removed from top-level `mca_sdk` namespace** (Story 8.10)
  - `QueueEncryption`, `EncryptionManager`, `migrate_queue_to_encrypted`, and `verify_encrypted_queue` are no longer re-exported from `mca_sdk`.
  - `from mca_sdk import QueueEncryption` now raises `ImportError`.
  - Operators with advanced needs (key rotation, offline verification, plaintext-to-encrypted migration) can still import these via their submodules: `from mca_sdk.security.encryption import QueueEncryption` and `from mca_sdk.security.migration import migrate_queue_to_encrypted, verify_encrypted_queue`.
  - Rationale: these are internal building blocks; they polluted `help(mca_sdk)` and tab-completion and invited misuse.

## [0.8.27] - 2026-04-23

### Fixed
- **CRITICAL: Cloud Monitoring "Duplicate TimeSeries" rejections from `googlecloud` exporter**
  - Observed in `otel-collector-dev` starting 2026-04-23T16:35Z: `rpc error: code = InvalidArgument ... resource.type=generic_node, resource.labels={node_id:"", namespace:"bhsf-emms", location:"global"}: Duplicate TimeSeries encountered. Only one point can be written per TimeSeries per request.` Affected `model.predictions_total`, `model.latency_seconds`, and every `agent.*` / `genai.*` / `vendor.*` metric.
  - **Root Cause**: The SDK never populated `service.instance.id` on the OpenTelemetry `Resource`. With only `service.name` / `service.namespace` present, the `googlecloud` exporter falls back to the `generic_node` monitored resource, which has no attribute that distinguishes producers, so every workload collapses to the identical `(namespace=bhsf-emms, location=global, node_id="")` resource key. Cloud Monitoring's `CreateTimeSeries` RPC requires at most one datapoint per unique `(metric.type, metric.labels, resource)` tuple per call, so the very first scrape after two workloads produced the same counter is rejected.
  - **Fix**: `create_resource_attributes()` in `mca_sdk/core/providers.py` now force-sets `service.instance.id` to a module-level `_SDK_INSTANCE_ID = f"{socket.gethostname()}:{os.getpid()}:{uuid.uuid4().hex[:8]}"` captured once per process. `service.instance.id` is also added to `_SDK_RESERVED_RESOURCE_KEYS` so registry-supplied `extra_resource` entries cannot override the SDK-owned identity. With this attribute present, the `googlecloud` exporter promotes the resource to `generic_task`, which uses `service.name` / `service.namespace` / `service.instance.id` as `job` / `namespace` / `task_id` — each process gets a distinct resource key and datapoints no longer collide.
  - **Why `host:pid:uuid`**: `socket.gethostname()` resolves to the pod name on GKE (readable `task_id` for operators), `os.getpid()` disambiguates sidecars or multi-process workers, and the 8-char UUID guarantees uniqueness if a pod is recreated with the same name before the old timeseries window closes.
  - **Not fixed by this release**: workloads still running 0.8.23 / 0.8.24 / 0.8.25 will keep triggering "Duplicate TimeSeries" until they upgrade. A collector-side fallback (a `transform` processor that synthesizes `service.instance.id` from `k8s.pod.name` when missing) would cover them without a redeploy and is tracked separately.
  - **Out of scope**: the unrelated `vendor.tokens_total` DISTRIBUTION-vs-INT64 descriptor mismatch on `ai-ops-agent`.

## [0.8.25] - 2026-04-22

### Fixed
- **CRITICAL: Prometheus exporter drop of `emms_model_predictions_total` and `emms_model_latency_seconds`**
  - Observed in prod (`otel-collector-dev`, namespace `monitoring`) for model `patient-readmission-classifier-v1` (model id `7a6fd3b1-3d8c-4768-9100-a8628d1f6237`): `failed to convert metric model.predictions_total: duplicate label names in constant and variable labels for metric "emms_model_predictions_total"`.
  - **Root Cause**: Two *resource* attributes — `model.version` (set by the SDK from `MCAConfig.model_version`) and `model_version` (injected by the registry's `extra_resource` payload) — both normalize to the Prometheus label name `model_version` after `[^a-zA-Z0-9_]` → `_` substitution. With `resource_to_telemetry_conversion.enabled: true`, the Prometheus exporter emits both as labels on the same metric, and rejects the sample.
  - **Why v0.8.24 missed it**: `_filter_reserved_attributes()` guards *data-point* attributes passed to `record_prediction()` / `record_error()` against the resource. It does not inspect the Resource itself for intra-resource collisions between SDK-set keys and `extra_resource` keys.
  - **Fix**: `create_resource_attributes()` in `mca_sdk/core/providers.py` now computes the Prometheus-normalized form of every SDK-reserved resource key (`service.name`, `service.namespace`, `model.version`, `model.category`, `model.type`, `model.id`, `team.name`, `mca.sdk.version`, `telemetry.sdk.{name,version,language}`) and drops any `extra_resource` entry whose normalized name collides. Intra-`extra_resource` collisions use first-wins. A WARNING is logged per dropped key.
  - **Defense-in-depth**: `RegistryClient._parse_model_config` logs a WARNING at ingest time when registry `extra_resource` contains keys that would be dropped downstream, naming the colliding reserved key so operators can correct the registry entry.
  - **Follow-up for registry owners**: the registration UI / API should reject `extra_resource` keys that duplicate canonical SDK resource fields (e.g., `model_version` when `model.version` is the SDK field) so the value is not silently lost.

## [0.8.24] - 2026-04-21

### Fixed
- **CRITICAL: Duplicate Label Names Error in Prometheus Export**
  - Fixed "duplicate label names in constant and variable labels for metric" errors that occurred when:
    - OTel Collector's Prometheus exporter has `resource_to_telemetry_conversion: enabled: true`
    - User passes metric attributes (via `record_prediction()` kwargs) that match resource attribute keys
    - Registry's `extra_resource` JSONB field contains keys that users commonly pass (e.g., `batch_size`, `version`, etc.)
  - **Root Cause**: Prometheus exporter converts ALL resource attributes to constant labels. When the same key appears as both a resource attribute (constant label) and a metric data-point attribute (variable label), Prometheus rejects the metric with duplicate label error.
  - **Solution**: Enhanced `_filter_reserved_attributes()` to dynamically extract ALL resource attribute keys from the configured OpenTelemetry Resource and filter them from metric attributes. Previous implementation used a hardcoded list that missed registry `extra_resource` keys and other dynamic attributes.
  - **Impact**: 
    - Prevents collector-level metric drops for `emms_model_predictions_total` and `emms_model_latency_seconds`
    - Protects against arbitrary collisions from registry-supplied resource attributes
    - Ensures predictive models work correctly even when registry configuration changes
  - **Affected Methods**: `record_prediction()`, `record_error()`, `record_metric()`
  - **Backward Compatible**: Filtering is transparent to users; no API changes

### Changed
- **Dynamic Resource Attribute Filtering**: `_filter_reserved_attributes()` now queries actual resource attributes instead of using hardcoded list
  - Automatically protects against duplicates from: SDK resource attrs, registry `extra_resource`, collector-injected attrs (environment, gcp.region), and threshold resource attrs
  - Adds both dotted (`service.name`) and underscored (`service_name`) versions to catch Prometheus normalization
  - Preserves defensive filtering of high-cardinality IDs (`prediction_id`, `user_id`, `session_id`)

## [0.8.23] - 2026-04-21

### Added
- **Status Label for Prediction Metrics**: Added `status` label to `model.predictions_total` counter and `model.latency_seconds` histogram
  - `status="success"` for successful predictions (via `record_prediction()`)
  - `status="error"` for failed predictions (via `record_error()`)
  - **Benefits**:
    - Clean error rate calculations: `rate({status="error"}) / rate(total)`
    - Easy filtering in Grafana dashboards and Cloud Monitoring
    - Simpler Prometheus alerting rules
    - Better observability of prediction outcomes
  - **Backward Compatible**: Additive change only, existing queries continue to work
  - **Visibility**: Status label appears in GCP Cloud Monitoring Metrics Explorer as a filterable dimension
  - **Use Case**: Enables queries like `sum(rate(model_predictions_total{status="error"}[5m])) / sum(rate(model_predictions_total[5m]))` for error rates

### Changed
- **Metric Label Consistency**: All prediction metrics now include explicit status labels for uniform querying

## [0.8.22] - 2026-04-20

### Added
- **Status Label for Prediction Metrics**: Added `status` label to `model.predictions_total` counter and `model.latency_seconds` histogram
  - `status="success"` for successful predictions (via `record_prediction()`)
  - `status="error"` for failed predictions (via `record_error()`)
  - **Benefits**:
    - Clean error rate calculations: `rate({status='error'}) / rate(total)`
    - Easy filtering in Grafana dashboards
    - Simpler Prometheus alerting rules
    - Better observability of prediction outcomes
  - **Backward Compatible**: Additive change only, existing queries continue to work
  - **Use Case**: Enables queries like `sum(rate(model_predictions_total{status="error"}[5m])) / sum(rate(model_predictions_total[5m]))` for error rates

### Fixed
- **Prometheus Metrics Export**: Fixed duplicate label names error in Prometheus metrics export by preventing duplicate label names from causing export failures
- **Test Infrastructure**: Updated test patches to use correct import path for serialize_for_telemetry
- **Code Quality**: Applied black formatting with line-length 100 to match CI configuration
- **Linting**: Removed unused serialize_for_telemetry import to pass ruff linting
- **Test Coverage**: Updated tests to reflect auth error fallback behavior and added missing test parameters
- **Agentic AI**: Fixed prediction endpoint for agentic AI use case
- **Documentation**: Streamlined SDK instrumentation documentation
- **Configuration**: Modified code to use REGISTRY_TOKEN_AUDIENCE environment variable

### Security
- **Dependency Updates**: 
  - Upgraded cryptography from 46.0.6 to 46.0.7 to fix CVE-2026-39892
  - Security upgrade fastapi from 0.103.2 to 0.109.1 (Snyk)
  - Security upgrade axios from 1.14.0 to 1.15.0 (Snyk)
  - Aligned security dependency versions across package configs
  - Reduced vulnerabilities in requirements-security.txt

## [0.8.20] - 2026-04-09

### Added
- **GCP Token Audience Override**: Added `gcp_token_audience` configuration option to `MCAConfig` and `token_audience` parameter to `RegistryClient`. This allows overriding the GCP ID token audience when the registry URL is an IP address but the token audience must match the service DNS name (e.g., `MCA_GCP_TOKEN_AUDIENCE=https://emms-registration-api-dev.internal`). Fixes GCP ID token audience mismatch errors in deployed environments.

### Fixed
- **Environment Variable**: Updated deployed examples to use `MCA_TLS_INSECURE_REGISTRY` environment variable with consistent naming.

## [0.8.19] - 2026-04-08

### Fixed
- **Registry Validation**: Updated `MCAClient` to allow unauthenticated registry connections when `tls_insecure_registry=True` is enabled. This permits connection to internal development endpoints that do not require GCP authentication or bearer tokens.

## [0.8.18] - 2026-04-08

### Added
- **API Consistency**: Updated `RegistryClient` to explicitly accept `tls_insecure_registry` as a keyword argument, matching the naming convention in `MCAConfig`. This prevents `TypeError` when passing configuration properties directly to the registry client.

## [0.8.17] - 2026-04-07

### Added
- **tls_insecure_registry**: Added `tls_insecure_registry=True` (and the `TLS_INSECURE_REGISTRY` environment variable) to `MCAConfig` to allow bypassing strict SSL certificate verification when connecting to HTTPS registry endpoints with self-signed certificates during local development.

## [0.8.13] - 2026-04-06

### Fixed
- **RegistryClient**: Properly restored the `version` parameter across all internal methods and `MCAClient` hydration logic. This ensures that the version parameter provided during SDK initialization (including the default `0.3.0`) is correctly handled and logged, while still maintaining compatibility with legacy registry endpoints that do not yet support explicit version parameters.

## [0.8.12] - 2026-04-04

### Fixed
- **RegistryClient**: Restored the `version: Optional[str] = None` parameter to `fetch_model_config()` and `_fetch_model_config_internal()`. This parameter was mistakenly removed in a previous refactor, causing compatibility issues with `MCAConfig.model_version` defaults and internal `MCAClient` hydration logic, resulting in 404 errors for deployed models trying to resolve the `0.3.0` version string. (Note: This release was missing some file updates due to a commit error, fixed in 0.8.13).

## [0.8.11] - 2026-04-03

### Fixed
- **RegistryClient**: Reverted the unsupported `version` parameter usage for legacy registry API endpoint (`/api/v1/configs/{model_id}`). The version parameter is now ignored and a warning is logged, preventing 404/rejection errors when using legacy registry endpoints.

## [0.8.10] - 2026-04-02

### Fixed
- **Registry API Endpoint**: Reverted to `/api/v1/configs/{model_id}` from `/models/{model_id}`
  - **Root Cause**: The new `/models/{model_id}` endpoint (introduced in v0.8.6) returns 404 for all registered models
  - **Solution**: Reverted to the old `/api/v1/configs/{model_id}` endpoint which still works and has model data
  - **Impact**: Fixes RegistryConfigNotFoundError for all models including patient-readmission-classifier-v1, medical-research-assistant, and clinical-notes-summarizer-v2
  - **Future**: Will re-migrate to new endpoint once Registry team completes data migration to `/models/` endpoint

## [0.8.9] - 2026-04-01

### Fixed
- **Reverted Registry API version=latest behavior**: Reverted the change introduced in v0.8.6 that explicitly sent `version=latest` when fetching model configurations without a specified version.
  - **Root Cause**: The Registry API backend does not support the literal string "latest" and treating it as a semantic version caused `404 Not Found` errors.
  - **Solution**: The SDK now omits the `version` parameter entirely when requesting the latest version, matching pre-0.8.6 behavior.
  - **API Contract Update**: The API contract documentation has been updated to explicitly state that omitting the parameter is the standard way to request the latest version, and "latest" is not a supported literal string.

## [0.8.8] - 2026-03-27

### Fixed
- **CRITICAL: Counter Metric Instrumentation Bug** - Fixed `record_counter()` and `record_gauge()` methods creating incorrect instrument types
  - **Root Cause**: `record_counter()` was calling `record_metric()` without specifying `instrument_type="counter"`, causing counters to be created as Histograms
  - **Impact**: Counter metrics like `emms_genai_requests_total` were recorded as Histograms, making them invisible to aggregation queries that expect counter semantics (e.g., `counter-increase` CTEs in BigQuery)
  - **Affected Metrics**: All metrics recorded via `record_counter()` including:
    - `emms_genai_requests_total` (GenAI request counter)
    - `emms_model_predictions_total` (prediction counter)
    - Any custom counters recorded by applications
  - **Symptoms**: Aggregation ETL queries finding latency metrics but no request counters; traces present but counter data missing
  - **Fix**: Added explicit `instrument_type="counter"` parameter to `record_counter()` and `instrument_type="up_down_counter"` to `record_gauge()`
  - **Verification**: Counter instruments now correctly created as `_Counter` type with `add()` method instead of `_Histogram` with `record()` method
  - **Models Affected**: clinical-notes-summarizer-v2 and any other GenAI models using LiteLLM callback
  - **Action Required**: Redeploy models using SDK v0.8.8+ to populate counter metrics in aggregation tables

## [0.8.7] - 2026-03-27

### Fixed
- **CRITICAL: Counter Metric Instrumentation Bug** - Fixed `record_counter()` and `record_gauge()` methods creating incorrect instrument types
  - **Root Cause**: `record_counter()` was calling `record_metric()` without specifying `instrument_type="counter"`, causing counters to be created as Histograms
  - **Impact**: Counter metrics like `emms_genai_requests_total` were recorded as Histograms, making them invisible to aggregation queries that expect counter semantics (e.g., `counter-increase` CTEs in BigQuery)
  - **Affected Metrics**: All metrics recorded via `record_counter()` including:
    - `emms_genai_requests_total` (GenAI request counter)
    - `emms_model_predictions_total` (prediction counter)
    - Any custom counters recorded by applications
  - **Symptoms**: Aggregation ETL queries finding latency metrics but no request counters; traces present but counter data missing
  - **Fix**: Added explicit `instrument_type="counter"` parameter to `record_counter()` and `instrument_type="up_down_counter"` to `record_gauge()`
  - **Verification**: Counter instruments now correctly created as `_Counter` type with `add()` method instead of `_Histogram` with `record()` method
  - **Models Affected**: clinical-notes-summarizer-v2 and any other GenAI models using LiteLLM callback
  - **Action Required**: Redeploy models using SDK v0.8.7+ to populate counter metrics in aggregation tables

## [0.8.6] - 2026-03-24

### Changed
- Updated registry API endpoint from `/api/v1/configs/{model_id}` to `/models/{model_id}`
- Added support for version parameter in model configuration fetches
- Removed deprecation warning for version parameter in registry client
- Introduced `DEFAULT_MODEL_VERSION` constant to replace magic string "latest"
- Enhanced registry client telemetry with cache hit/miss logging and resolved version tracking

### Added - Backward Compatibility
- **`omit_default_version` parameter** for RegistryClient to support legacy registry APIs
  - Set to `True` to restore pre-0.8.6 behavior (omit version parameter)
  - Defaults to `False` (modern behavior: send `version=latest`)
  - Can be controlled via `MCA_REGISTRY_OMIT_DEFAULT_VERSION` environment variable
  - Logs warning when legacy mode is enabled
  - Allows migration path for systems with non-compliant registry APIs

### Fixed
- **Registry Client Version Resolution** (Story 8-8): Fixed model configuration fetching to explicitly request `version=latest` when no version is specified, resolving "Model config not found" errors for valid registered models
  - Affected models: patient-readmission-classifier-v1, medical-research-assistant, clinical-notes-summarizer-v2
  - Root cause: Mismatch between cache key semantics (using "latest" string) and API request (omitting version parameter)
  - Impact: Registry refresh now works correctly for all registered models
  - Solution: SDK now explicitly sends `version=latest` parameter when no version specified
  - Error messages improved with actionable guidance when model/version not found
- Registry client now properly sends version parameter to API when provided
- Fixed duplicate `capture_output_data` parameter in agentic example causing syntax error

### Added
- Comprehensive integration tests for registry version parameter handling (7 new tests)
- Thread-safety validation tests for concurrent registry operations
- Detailed error messages for 404 responses with troubleshooting guidance
- DEBUG-level logging for cache operations (hits/misses)
- INFO-level logging for successful registry fetches with resolved version information

### Documentation
- Added thread-safety guarantees to RegistryClient class and cache method docstrings
- Updated fetch_model_config docstring to clarify version parameter behavior
- Type hints validated with mypy (zero errors)
- **Async Environment Usage**: Added warnings to RegistryClient docstring and README about synchronous locking and event loop blocking
  - RegistryClient uses `threading.RLock` and is NOT safe to call from async functions
  - Recommended pattern: Initialize during startup, enable background refresh, use @predict() decorator
  - DO NOT call fetch_model_config() from FastAPI endpoints or async request handlers

## [0.8.5] - 2026-03-23

### Added
- **Mid-Lifecycle Telemetry Flush** (flush method)
  - Added `flush()` method to MCAClient for forcing telemetry export without shutdown
  - Allows batch processing scenarios to flush buffers between batches
  - Safe to call mid-lifecycle, does not affect client state
  - Returns boolean indicating success/failure across all providers
  - Configurable timeout (default: 30 seconds)

### Changed
- Removed `capture_input_data` parameter from agentic agent example for cleaner configuration

## [0.8.4] - 2026-03-21

### Added
- **Configurable Metric Filtering** (Story 8.7)
  - SDK now filters unnecessary auto-instrumentation and system metrics before export
  - Dropped metric patterns: `process.*`, `system.*`, `asyncio.*`, `cpython.*`, `http.client.*`, `otel.sdk.*`, `test.*`, `rpc.*`
  - Retained business metrics: `model.*`, `genai.*`, `agent.*`, `vendor.*`, `agentic.*`, `llm_eval/*`
  - Filtering configurable via `filter_system_metrics` parameter (default: True)
  - Reduces Cloud Monitoring costs by eliminating unnecessary custom metric descriptors
  - Implemented using OpenTelemetry Views and DropAggregation

### Fixed
- **Model Version Fallback Bug**
  - Fixed model_version not falling back to SDK version when not provided via config or registry
  - Ensures model.version resource attribute is always populated for telemetry filtering
- **Missing Trace Coverage for ML Errors**
  - Added trace span creation in record_error for predictive ML models
  - Ensures errors are visible in distributed traces alongside metrics and logs

## [0.8.3] - 2026-03-20

### Fixed
- **Metric Cardinality Explosion & Trace Correlation Issues**
  - Defensively pop `prediction_id`, `user_id`, and `session_id` from generic `attributes` in `record_prediction` and `record_error` to prevent metric cardinality explosions.
  - Introduce `span_attributes` parameter in `record_prediction` and `record_error` to allow high-cardinality data safely attached to OpenTelemetry spans instead of metrics.
  - Fix broken trace-to-error correlation in both manual error recording and batch tracking decorators by ensuring `prediction_id` flows through to spans and logging.

- **Trace Span Creation for Predictive & Generative Models**
  - Added `mca_client.trace()` context manager around prediction loops.
  - Generate `prediction_id` for trace correlation and add it as a span attribute.
  - Enables trace visibility in GCP Traces Explorer.

- **Data Generation Tests**
  - Updated data generation tests to match current model configuration.

## [0.8.2] - 2026-03-19

### Changed

- **Registry URL Optional in All Environments (Story 2.7 + Adversarial Review Fixes)**
  - Registry connection is now optional in all environments (dev, prod, staging)
  - Previously: dev/prod required registry_url and model_id, raised ConfigurationError if missing
  - Now: SDK operates normally without registry in any environment, logs informational messages
  - **CRITICAL FIXES** (from adversarial review):
    - `_validate_registry_requirements()` now returns bool to actually control hydration (was theatrical before)
    - Removed dangerous fallback to service_name in `_hydrate_from_registry()`
    - Staging now skips hydration when auth missing (prevents slow 401/timeout on startup)
    - Production defaults to `strict_validation=True` even without registry (prevents security downgrade)
    - CLI template now comments out `model_id` by default (prevents log spam)
  - When both registry_url and model_id are provided, authentication is still required (dev/prod)
  - Staging skips hydration when auth incomplete (prevents startup performance degradation)
  - **Migration**: Prod users without registry should explicitly set `strict_validation=True` if needed
  - **Impact**: Removes artificial barrier for SDK users who don't need registry integration
  - Test coverage: 8 tests (7 updated + 1 new AC2 telemetry test) with network isolation assertions

### Added

- **SDK Version in Resource Attributes (Story 1-21)**
  - SDK version automatically added to all resource attributes
  - Two attributes injected: `mca.sdk.version` (custom) and `telemetry.sdk.version` (OpenTelemetry standard)
  - **Immutable**: Cannot be overridden by `extra_resource`, kwargs, or registry config
  - **Universal**: Present in all telemetry signals (metrics, logs, traces)
  - **Tamper-Proof**: User attempts to override are blocked, real SDK version is always used
  - **Use Cases**: Track deployed SDK versions, correlate telemetry with version changes, backward compatibility analysis
  - **Architecture**: Dedicated `mca_sdk/version.py` module eliminates circular dependencies
  - **Testing**: Comprehensive unit and integration tests validate presence in actual exported payloads
  - **Documentation**: Updated `docs/data-models.md` and `docs/api-contracts.md`

## [0.8.1] - 2026-03-19

### Fixed

- **LLM Completion Tracking Performance**
  - Fixed major performance bug where OpenTelemetry instruments were dynamically created on every LLM call
  - Implemented pre-created instrument caching in `genai.py` with thread-safe global cache
  - Added `_get_instruments()` helper to retrieve or initialize instruments once per client
  - Reduced overhead for high-frequency LLM operations (GenAI workloads)
  - All metric instruments now created once and reused: requests_total, requests_failed_total, tokens_total, latency_seconds, cost_dollars, embedding_*
  - Thread-safe implementation using locks to prevent race conditions
  - Impact: Significant performance improvement for applications with high LLM request rates

### Changed

- **Registry URL Optional in All Environments (Story 2.7 + Adversarial Review Fixes)**
  - Registry connection is now optional in all environments (dev, prod, staging)
  - Previously: dev/prod required registry_url and model_id, raised ConfigurationError if missing
  - Now: SDK operates normally without registry in any environment, logs informational messages
  - **CRITICAL FIXES** (from adversarial review):
    - `_validate_registry_requirements()` now returns bool to actually control hydration (was theatrical before)
    - Removed dangerous fallback to service_name in `_hydrate_from_registry()`
    - Staging now skips hydration when auth missing (prevents slow 401/timeout on startup)
    - Production defaults to `strict_validation=True` even without registry (prevents security downgrade)
    - CLI template now comments out `model_id` by default (prevents log spam)
  - When both registry_url and model_id are provided, authentication is still required (dev/prod)
  - Staging skips hydration when auth incomplete (prevents startup performance degradation)
  - **Migration**: Prod users without registry should explicitly set `strict_validation=True` if needed
  - **Impact**: Removes artificial barrier for SDK users who don't need registry integration
  - Test coverage: 8 tests (7 updated + 1 new AC2 telemetry test) with network isolation assertions

## [0.8.0] - 2026-03-18

### Added

- **SDK Version in Resource Attributes (Story 1-21)**
  - SDK version automatically added to all resource attributes
  - Two attributes injected: `mca.sdk.version` (custom) and `telemetry.sdk.version` (OpenTelemetry standard)
  - **Immutable**: Cannot be overridden by `extra_resource`, kwargs, or registry config
  - **Universal**: Present in all telemetry signals (metrics, logs, traces)
  - **Tamper-Proof**: User attempts to override are blocked, real SDK version is always used
  - **Use Cases**: Track deployed SDK versions, correlate telemetry with version changes, backward compatibility analysis
  - **Architecture**: Dedicated `mca_sdk/version.py` module eliminates circular dependencies
  - **Testing**: Comprehensive unit and integration tests validate presence in actual exported payloads
  - **Documentation**: Updated `docs/data-models.md` and `docs/api-contracts.md`
### Changed (BREAKING)

- **PHI/PII Masking Removed**
  - PHI_PATTERNS regex list removed from client.py
  - _sanitize_phi() function removed from gcp_logging.py
  - All PHI masking tests removed (test_phi_masking.py, test_gcp_logging_phi.py)
  - "Dual-purpose masking" security model removed
  - **KEPT**: CREDENTIAL_PATTERNS and credential masking (unchanged)
  - **KEPT**: phi_utils.py field name extraction (helps apps identify PHI)
  - **Impact**: Applications now have 100% responsibility for PHI sanitization
  - **SDK only masks technical credentials** (AWS/GCP keys, tokens, JWTs)
  - **No breaking API changes** - removal is internal implementation only
  - **Migration**: Review your application code to ensure PHI is sanitized BEFORE calling SDK methods
  - Do not rely on SDK to catch PHI patterns
  - Use `phi_utils.extract_phi_field_names()` to identify PHI fields from registry
  - **Rationale**: SDK cannot know domain-specific PHI context. Regex-based masking is insufficient and creates false positives. Clear responsibility boundaries prevent security assumptions.

### Changed (BREAKING)

- **Default Data Capture Enabled**
  - `capture_input_data` default changed from `False` to `True`
  - `capture_output_data` default changed from `False` to `True`
  - `capture_input` default changed from `False` to `True` (Decorators)
  - `capture_output` default changed from `False` to `True` (Decorators)
  - `record_error_messages` default changed from `False` to `True` (LiteLLM Callback)
  - `persist_queue` default changed from `False` to `True` (Buffering)
  - **Impact**: Prompts, completions, errors, and input/output payload traces are NOW captured and buffered by default.
  - **Migration**: Explicitly set these parameters to `False` if you do not want input/output capture.
  - **Rationale**: PII and PHI handling relies on downstream access controls and sanitization policies.
  - Affects:
    - `MCAConfig` defaults
    - `create_genai_client()` helper function (now defaults to True)
    - `MCALiteLLMCallback` fallback defaults and constructor
    - `@predict()` and `@instrument_model` decorator behavior

### Added

- **Thresholds as Resource Attributes (Production-Ready)**
  - Registry API thresholds automatically added as OpenTelemetry resource attributes
  - Thresholds prefixed with `threshold.` (e.g., `threshold.latency_ms`, `threshold.accuracy_min`)
  - **Preserves native numeric types**: int stays int, float stays float (no forced conversion)
  - Enables proper numeric filtering, sorting, and aggregations in downstream systems
  - **IMPORTANT:** Resources are immutable - thresholds evaluated ONCE at SDK initialization
  - **Comprehensive Safeguards:**
    - Boolean rejection: Explicitly rejects `bool` types (subclass of `int` in Python)
    - Infinity rejection: `math.isinf()` check prevents OpenTelemetry exporter crashes
    - NaN rejection: `math.isnan()` check prevents invalid telemetry data
    - None rejection: Prevents string "None" pollution
    - Maximum 50 thresholds: First 50 kept (dict iteration order), rest dropped with clear warning
    - Key sanitization with collision detection: Invalid chars→`_`, collisions logged
    - Key length accounting: Max 53-char names (63 total with "threshold." prefix)
    - User precedence: User-provided `extra_resource` keys never silently overwritten
    - Module-level imports: `math` and `re` imported at module level (not function level)
    - SDK logger used throughout (not global `logging`)

## [0.7.7] - 2026-03-12

### Fixed

- **Registry Token Validation with GCP Auth**
  - Allow GCP auth without requiring registry token in production environment
  - Fixed validation logic to check for `use_gcp_auth` before requiring `MCA_REGISTRY_TOKEN`
  - When `use_gcp_auth=True`, token is no longer required

## [0.7.2] - 2026-03-10

### Fixed

- **mca-instrument Endpoint Mapping**
  - Map `MCA_COLLECTOR_ENDPOINT` to `OTEL_EXPORTER_OTLP_ENDPOINT` for opentelemetry-instrument
  - Map `MCA_SERVICE_NAME` to `OTEL_SERVICE_NAME` for proper service identification
  - Fixes issue where telemetry was sent to localhost instead of configured collector endpoint

## [0.7.1] - 2026-03-09

### Fixed

- **CLI Entry Points Not Installed**
  - Added `[project.scripts]` section to `pyproject.toml` for CLI tools
  - Fixes `mca-init`, `mca-instrument`, and `mca-run` commands not being available after pip install
  - CLI tools now properly installed to user's PATH

## [0.7.0] - 2026-03-09

### Changed (BREAKING)

- **Default Validation Mode Changed to Relaxed**
  - `strict_validation` default changed from `True` to `False`
  - **Impact**: Users who relied on default strict validation will now get relaxed validation
  - **Migration**: Explicitly set `strict_validation=True` if you need strict validation
  - **Rationale**: Lower barrier to entry for development and prototyping while maintaining production safety

### Added

- **Flexible Validation System**
  - Relaxed validation mode (new default): Only requires `service.name`
  - Strict validation mode: Requires all model-type-specific fields (auto-enabled for registry integration)
  - INFO-level logging for missing optional fields in relaxed mode
  - Auto-detection: Strict validation automatically enabled when `registry_url` is configured

### Fixed

- **Package Configuration Sync**
  - Synced `pyproject.toml` with `setup.py` to include all optional dependencies
  - Added missing extras: `prometheus`, `gcp-auth`, `gcp-secrets`, `gcp-trace`, `gcp-logging`, `autolog`, `instrument`
  - Fixed `[instrument]` extra not being available in published package
  - Users can now install: `pip install "mca-sdk[instrument]"` for zero-code instrumentation

### Migration Guide

**If you need strict validation** (all model-type fields required):
```python
# Option 1: Explicit configuration
client = MCAClient(
    service_name="my-model",
    strict_validation=True,  # Explicitly enable
    # ... other required fields
)

# Option 2: Use registry integration (auto-enables strict validation)
client = MCAClient(
    service_name="my-model",
    registry_url="https://registry.example.com",  # Auto-enables strict validation
    # ... other required fields
)
```

**If you want minimal validation** (service_name only):
```python
# Default behavior in v0.7.0 - no changes needed
client = MCAClient(service_name="my-model")  # Works!
```

## [0.6.7] - 2026-03-05

### Fixed
- Add missing resource attributes for agentic model type

## [0.6.5] - 2026-03-05

### Fixed
- Lower OpenTelemetry requirement to >=1.35.0 for google-adk compatibility

## [0.6.4] - 2026-03-05

### Fixed
- Export print_gcp_auth_status and print_collector_status from shared module
- Add fallback for OpenTelemetry logs import compatibility across versions

## [0.6.1] - 2026-02-26

### Fixed
- **CRITICAL: Context Token Handling in Agentic AI Goal Tracking**
  - Fixed TypeError when detaching context in `record_goal_completed()`
  - **Problem**: `record_goal_started()` was incorrectly using `trace.use_span()` which returns a context manager, not a Token
  - **Error**: `TypeError: expected an instance of Token, got _AgnosticContextManager`
  - **Solution**: Changed to use `context.attach(trace.set_span_in_context(span))` for proper Token handling
  - **Impact**: All users of `record_goal_started()` + `record_goal_completed()` were affected
  - **Severity**: High - causes runtime errors for agentic AI features

### Documentation
- Rebuilt end-to-end demo notebook with comprehensive v0.6.0 feature coverage
  - Added demonstrations of all 11 telemetry features
  - Added model input/output capture examples (Story 1-18)
  - Fixed `record_goal_started()` parameter usage in examples
  - Created notebooks/README.md with usage guide and troubleshooting

## [0.6.0] - 2026-02-26

### Added
- **GCP Authentication Support for Registry Client**
  - Added `use_gcp_auth` parameter to `RegistryClient` initialization
  - Enables automatic GCP credential-based authentication for Registry API
  - Properly passed from `MCAConfig` to `RegistryClient` in client initialization

- **Comprehensive End-to-End Tests** (Story 6-16)
  - Full OTLP JSON validation for metrics, logs, and traces
  - Multi-model telemetry validation across all model types
  - Error scenario testing (network failures, invalid data, collector downtime)
  - Context propagation validation for distributed tracing

- **Integration Tests with OTel Collector** (Story 6-7)
  - Real collector telemetry flow validation
  - High availability features testing (HPA, persistent volumes, DLQ)
  - Collector self-monitoring validation
  - Batch processing and attribute enrichment verification

- **Load Testing Framework** (Story 6-8)
  - Performance testing at 1000 req/s
  - Telemetry completeness validation
  - Collector monitoring utilities
  - Load test report generation

- **Integration Guides** (Story 5-5)
  - Complete guides for Predictive ML, GenAI, Agentic AI, and Vendor Bridge patterns
  - pytest-based testing examples for each model type
  - Context propagation explanations with code examples

- **Input/Output Capture** (Story 1-18)
  - Enhanced `@predict()` decorator with input/output span attachment
  - Configurable via `capture_input` and `capture_output` parameters
  - Sanitization warnings for PHI protection

### Changed
- **OTel Collector Configuration Updates**
  - Enhanced self-monitoring metrics collection
  - Improved batch processor settings for high throughput
  - Updated attributes processor for GCP metadata enrichment

- **Integration Decorator Improvements**
  - Fixed `@predict()` and `@batch_predict()` decorators
  - Better error handling and span lifecycle management
  - Improved attribute validation and length limits

### Fixed
- Registry client GCP authentication parameter propagation from config
- Path validation in `TelemetryQueue` for disk persistence
- `ModelConfig` backward compatibility for older API responses
- Pipeline test failures in CI/CD
- Instrumentation decorator attribute handling

### Documentation
- Updated downstream integration docs for dot notation metric naming
- Added collector metrics guide
- Enhanced Kubernetes deployment README with HA features
- Added Pub/Sub DLQ documentation and replay processor

## [0.5.1] - 2026-02-25

### Fixed
- **CRITICAL: GenAI Metric Double-Prefix Bug** (Production Issue)
  - Fixed incorrect metric names in Google Cloud Monitoring / Managed Prometheus
  - **Problem**: Metrics appeared as `emms_emms_genai_requests_total` instead of `emms_genai_requests_total`
  - **Root Cause**: GenAI integration was prepending `emms_` prefix, but OTel Collector's Prometheus exporter also adds namespace prefix
  - **Solution**: Switched to dot notation (`genai.requests_total`) matching predictive ML pattern
  - **Affected Components**:
    - `track_llm_completion()` in `genai.py`
    - `track_llm_embedding()` in `genai.py`
    - `batch_predict` decorator in `decorators.py`
    - Metric validation schema updated to expect dot notation
  - **Impact**: All GenAI metrics from deployed agents (AI Ops Agent dogfooding) now report correctly
  - **Migration**: If you query Prometheus/GMP directly, update queries from `emms_emms_genai_*` to `emms_genai_*`

### Changed
- Metric naming convention now uses dot notation for all metric types (e.g., `genai.requests_total`, `model.predictions_total`)
- Validation schema updated to reflect dot-notation pattern

## [0.5.0] - 2026-02-25

### Added
- **Registry API GCP Authentication Documentation**
  - New comprehensive guide: [`docs/registry-gcp-auth-guide.md`](docs/registry-gcp-auth-guide.md)
  - Covers all authentication methods: ADC, service account JSON, Workload Identity, gcloud CLI
  - Complete examples for Vertex AI Workbench, Cloud Run, GKE, and local development
  - Token management and automatic refresh documentation
  - Troubleshooting guide with common issues and solutions
  - Security best practices for production deployments
  - Verified with integration tests against real Registry API

- **GCP Cloud Trace Direct Export** (Story 6-3)
  - New `GCPCloudTraceExporter` for exporting traces directly to Google Cloud Trace
  - Configuration fields: `gcp_trace_enabled` (bool), `gcp_project_id` (str)
  - Environment variables: `MCA_GCP_TRACE_ENABLED`, `MCA_GCP_PROJECT_ID`
  - Full span fidelity: status codes, span kinds, links, events, resource attributes
  - Production features: retry logic, timeout protection, batch splitting, validation
  - Internal telemetry metrics for observability (spans exported, failures, retries)
  - Install with: `pip install mca-sdk[gcp-trace]` or `pip install mca-sdk[gcp]`
  - **Note**: Requires GCP credentials with scope `https://www.googleapis.com/auth/trace.append`

- **Registry API Response Structure Updates** (Story 2.2 - API Integration)
  - Updated [`ModelConfig`](mca-prototype/mca_sdk/registry/models.py) dataclass to match actual API response structure
  - Added `registry_id` field (optional, can be null for vendor models)
  - Added `deployment_id` field (optional, can be null for vendor models)
  - Added `association_id_column` field (optional, for time-series models)
  - Added `created_at` and `updated_at` timestamp fields (optional, for debugging)
  - Updated [`RegistryClient`](mca-prototype/mca_sdk/registry/client.py) to correctly extract fields from API response:
    - `model_version` now extracted from top-level (not nested in config)
    - `thresholds` extracted from top-level (not nested in config)
    - `extra_resource` extracted from nested `config.extra_resource` (not top-level)
    - Handles `config: null` case gracefully
    - Handles `phi_fields: {}` empty dict case
  - Added [`extract_phi_field_names()`](mca-prototype/mca_sdk/utils/phi_utils.py) utility function
    - Extracts field names from nested PHI structure: `{"patient_id": {"criticality": "yes"}}`
    - Handles empty dict case: `{}`
    - Used for PHI masking operations
  - **Note**: All `extra_resource` values are strings (JSONB storage), passed as-is to OpenTelemetry

### Changed
- **BREAKING CHANGE: predict() Decorator Defaults** (Story 1.13.6 - Adversarial Review Finding 1)
  - `capture_input` default changed from `True` to **`False`**
  - `capture_output` default changed from `True` to **`False`**
  - **Rationale**: Safer HIPAA compliance defaults - prevents accidental PHI capture
  - **Migration Guide**: If you rely on automatic input/output capture, explicitly set parameters:
    ```python
    # OLD (implicit True - will now be False)
    @client.predict()
    def predict(data):
        return model.predict(data)

    # NEW (explicit opt-in required)
    @client.predict(capture_input=True, capture_output=True)
    def predict(data):
        # IMPORTANT: Ensure data is sanitized before calling
        return model.predict(data)
    ```
  - **Impact**: Code using `@client.predict()` without parameters will no longer capture input/output data
  - **Security**: This change reduces risk of accidental PHI exposure in telemetry

### Added
- **Enhanced PHI Masking Patterns** (Story 1.13.6 - Adversarial Review Finding 2)
  - Added ZIP code pattern (5-digit and ZIP+4 formats)
  - Added common name patterns (First Last, Last First formats)
  - Added street address pattern (basic detection)
  - **Note**: Regex-based PHI masking has inherent limitations. Applications MUST sanitize data before instrumentation.

### Security
- Improved default security posture for data capture in predict() decorator
- Expanded PHI detection patterns for better protection in error messages and logs

### Changed (from Unreleased)
- **BREAKING CHANGE: HTTPS Enforcement** (Story 3.7)
  - Both `registry_url` and `collector_endpoint` now **require HTTPS** for non-localhost endpoints
  - HTTP connections to production endpoints are blocked with `ConfigurationError`
  - **Migration Guide**: Update all production endpoint URLs from `http://` to `https://`
  - Localhost exception: HTTP remains allowed for `localhost`, full `127.0.0.0/8` range, and `::1` for development
  - Enhanced security validation prevents credential exposure over unencrypted connections
  - **Previous Behavior**: HTTP to production endpoints raised a `SecurityWarning` but was allowed
  - **New Behavior**: HTTP to production endpoints raises `ConfigurationError` and blocks initialization

### Security
- Enhanced loopback detection with full IPv4 127.0.0.0/8 range support and IPv6 ::1
- Protection against hostname substring attacks (e.g., `localhost.attacker.com`)
- Case-insensitive localhost matching per RFC 4343

## [0.4.0] - 2026-01-26

### Added
- **Background Registry Refresh Thread** (Story 2.3)
  - Automatic refresh of registry config at configurable intervals
  - Daemon thread with graceful shutdown
  - Thread-safe config updates with locking
  - Comprehensive unit tests for thread safety and lifecycle

- **Attributes Processor Integration** (Story 4.3)
  - OpenTelemetry Collector attributes processor for metadata enrichment
  - Automatic addition of `gcp.region` and `environment` attributes
  - Non-overwriting insertion semantics to preserve existing attributes
  - Validated with functional testing across all telemetry types (metrics, logs, traces)

- **Enhanced Demo Scripts**
  - OTLP endpoint configuration for better local development
  - Improved logging and error handling

### Changed
- **OpenTelemetry Dependencies** - Updated from 1.20.0 to 1.39.0
  - `opentelemetry-api>=1.39.0`
  - `opentelemetry-sdk>=1.39.0`
  - `opentelemetry-exporter-otlp-proto-http>=1.39.0`
  - **Breaking Changes**: OTel 1.39.0 includes significant changes including `LogData` removal (replaced with `ReadableLogRecord`/`ReadWriteLogRecord`), Events API deprecation, and Python 3.8 support dropped. Review [OTel Python CHANGELOG](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md) for migration guidance.
- **Dependency Reorganization** - Moved `requests` from core dependencies to optional `[vendor]` extras. Install with `pip install mca-sdk[vendor]` if using vendor model integrations.

### Fixed
- CI pipeline refinements for better reliability
- Removed orphaned pyproject.toml scan after merge
- Updated pip-audit command to skip editable packages

### Security
- Added `protobuf>=5.0,<6.0` constraint to mitigate CVE-2026-0994 (DoS via nested Any messages) until patched version is available

## [0.3.0] - 2026-01-22

### Added
- **Custom Exception Hierarchy** (Story 1.14)
  - `MCASDKError` - Base exception class for all SDK errors
  - `ValidationError` - Raised when validation fails (metric names, attributes)
  - `BufferingError` - Raised when buffering operations fail (queue full, disk failure)
  - `ConfigurationError` - Raised when configuration is invalid or incomplete
  - `RegistryError` - Base exception for registry operations
  - `RegistryConnectionError` - Raised when unable to connect to registry
  - `RegistryConfigNotFoundError` - Raised when model config not found in registry
  - `RegistryAuthError` - Raised when registry authentication fails
  - All exceptions exported from main `mca_sdk` package for easy catching
  - Helpful error messages with context for debugging

### Changed
- **Enhanced Configuration Validation** (Story 1.15)
  - `MCAConfig.__post_init__` now raises `ConfigurationError` (was generic `Exception`)
  - `MCAConfig.from_env` raises `ConfigurationError` for invalid integer values
  - `MCAConfig.from_file` raises `ConfigurationError` for file/parsing errors
  - `MCAConfig.load` enforces security: `registry_token` via environment only
  - Better error messages with suggested fixes for missing `service_name`
  - URL validation for `collector_endpoint` and `registry_url`
  - Security warnings for HTTP endpoints (non-localhost) using `SecurityWarning` category

## [0.2.0] - 2026-01-12

### Added
- **Model Registry Integration**: Centralized configuration management system
  - `RegistryClient` for fetching model config from registry API
  - Support for `GET /models/{model_id}` and `GET /deployments/{deployment_id}` endpoints
  - Automatic retry with exponential backoff using existing `RetryPolicy`
  - In-memory cache with configurable TTL (default 10 minutes)
  - Bearer token authentication with HTTPS enforcement

- **Dynamic Configuration**
  - `MCAConfig` extended with 5 new fields: `registry_url`, `registry_token`, `refresh_interval_secs`, `prefer_registry`, `deployment_id`
  - Environment variable support: `MCA_REGISTRY_URL`, `MCA_REGISTRY_TOKEN`, etc.
  - Config precedence: kwargs > registry > env > YAML > defaults

- **Background Refresh**
  - Automatic refresh of thresholds and PHI fields every 10 minutes (configurable)
  - Identity fields (service_name, model_id) are immutable after startup
  - Graceful handling of registry failures with fallback to last-known config

- **PHI Fields Management**
  - Union of local and registry PHI fields for comprehensive masking
  - Dynamic updates via background refresh

- **Registry Telemetry**
  - Self-monitoring metrics: `mca.registry.refresh_success_total`, `mca.registry.refresh_latency_seconds`, `mca.registry.errors_total`

- **Security**
  - HTTPS required for non-localhost registry endpoints
  - Token never logged (even at DEBUG level)
  - Query parameters excluded from logs

- **Testing**
  - 22 unit tests for RegistryClient (fetch, cache, retry, auth)
  - 11 unit tests for hydration flow and config precedence
  - 8 integration tests with FastAPI mock registry

### Changed
- `MCAClient.__init__` now performs registry hydration before provider setup
- `MCAClient.shutdown` now stops background refresh thread and closes registry client
- `setup_all_providers` accepts `extra_resource` parameter for registry attributes
- Provider resource attributes now include registry-provided `extra_resource` fields

### API Additions
- `MCAClient.thresholds` property - Access current thresholds from registry
- `RegistryClient` class exported from main package
- `ModelConfig` and `DeploymentConfig` dataclasses exported from main package

## [0.1.0] - 2026-01-21

### Added
- Core MCA SDK with `MCAClient` for HIPAA-compliant OpenTelemetry instrumentation
- Multi-source configuration system with precedence: kwargs > registry > env > YAML > defaults
- Registry integration with automatic caching and retry mechanisms
- Validation and buffering systems for telemetry data
- Schema validation for metric naming and resource attributes
- Four working SDK examples:
  - Internal model instrumentation
  - GenAI model integration
  - Agentic AI workflows
  - Vendor model integration
- Docker Compose orchestration for local development
- Integration helpers for vendor models, GenAI, and Python decorators
- Comprehensive test suite with unit and integration tests
- Support for Python 3.10, 3.11, and 3.12

### Changed
- Buffering disabled by default for backward compatibility
- Improved demo scripts with better logging and metric handling
- Refactored metric names for consistency and clarity

### Removed
- PHI detection and masking features (architectural decision to simplify initial release)

### Fixed
- Buffering backward compatibility in `from_env()` configuration
- Critical `SecurityWarning` class placement in `MCAConfig`
- Multiple security and reliability issues identified in code review
- Test coverage expanded to meet 85% requirement

### Security
- Security hardening for multi-source configuration system
- Registry token blocked from YAML configuration files to prevent credential exposure
- Localhost validation fixed to prevent substring attacks
- Comprehensive security review addressing CRITICAL and HIGH priority issues

[unreleased]: https://gitlab.com/bhsf/ai_ml/mca-sdk/compare/v0.8.0...HEAD
[0.8.0]: https://gitlab.com/bhsf/ai_ml/mca-sdk/releases/tag/v0.8.0
[0.1.0]: https://gitlab.com/bhsf/ai_ml/mca-sdk/releases/tag/v0.1.0