Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions plugins/observability/langfuse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@ class TraceState:
"HERMES_LANGFUSE_SECRET_KEY": "sk-lf-",
}

# Template values that carry the RIGHT prefix but are still placeholders.
# The docs (plugin README, website env-var reference) show the keys as
# literally ``pk-lf-...`` / ``sk-lf-...`` — pasting that passes the prefix
# check above and reproduces the exact silent failure the guard exists to
# catch (#51399 and its six duplicates, all filed AFTER the prefix guard
# merged).
#
# Matched against the lowercased remainder AFTER the prefix:
# ``_PLACEHOLDER_REMAINDERS`` by whole-value equality (ellipsis/asterisks
# and the empty remainder of a bare prefix), ``_PLACEHOLDER_FRAGMENTS`` by
# substring. Fragments cannot false-positive on real keys: Langfuse issues
# UUID-shaped remainders (hex digits + dashes), and every fragment below
# contains at least one non-hex letter.
_PLACEHOLDER_REMAINDERS = frozenset({"", "...", "***"})
_PLACEHOLDER_FRAGMENTS = (
"placeholder",
"your-",
"your_",
"changeme",
"change-me",
"test-key",
"example",
"unset",
"dummy",
"xxx",
)


def _env(name: str, default: str = "") -> str:
return os.environ.get(name, default).strip()
Expand Down Expand Up @@ -138,12 +165,25 @@ def _validate_langfuse_key(env_name: str, value: str) -> Optional[str]:
expected = _LANGFUSE_KEY_PREFIXES.get(env_name, "")
if not expected:
return None
if value.startswith(expected):
return None
return (
f"{env_name}={_redact_key_preview(value)} "
f"(expected {expected!r} prefix)"
)
if not value.startswith(expected):
return (
f"{env_name}={_redact_key_preview(value)} "
f"(expected {expected!r} prefix)"
)
# Right prefix, but is the rest of it a leftover template? ``pk-lf-...``
# pasted straight from the docs sails through the prefix check and then
# fails exactly like #23823: the SDK accepts it at construction time and
# drops every trace at flush time, silently.
remainder = value[len(expected):].lower()
if remainder in _PLACEHOLDER_REMAINDERS or any(
fragment in remainder for fragment in _PLACEHOLDER_FRAGMENTS
):
return (
f"{env_name}={_redact_key_preview(value)} "
f"(has the {expected!r} prefix but looks like a template value, "
f"not an issued key)"
)
return None


def _get_langfuse() -> Optional[Langfuse]:
Expand Down
70 changes: 70 additions & 0 deletions tests/plugins/test_langfuse_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,58 @@ def test_validate_langfuse_key_unknown_name_passes(self, monkeypatch):
plugin = self._fresh_plugin()
assert plugin._validate_langfuse_key("HERMES_LANGFUSE_BASE_URL", "anything") is None

@pytest.mark.parametrize(
("env_name", "value"),
[
# The docs' own copy-paste templates (README, website env-var
# reference show the keys as literally ``pk-lf-...``) — the
# exact values behind #51399 and its duplicates.
("HERMES_LANGFUSE_PUBLIC_KEY", "pk-lf-..."),
("HERMES_LANGFUSE_SECRET_KEY", "sk-lf-..."),
("HERMES_LANGFUSE_PUBLIC_KEY", "pk-lf-***"),
# Bare prefix with nothing after it.
("HERMES_LANGFUSE_SECRET_KEY", "sk-lf-"),
# Common template fragments, case-insensitive.
("HERMES_LANGFUSE_PUBLIC_KEY", "pk-lf-placeholder"),
("HERMES_LANGFUSE_PUBLIC_KEY", "pk-lf-PLACEHOLDER"),
("HERMES_LANGFUSE_SECRET_KEY", "sk-lf-your-secret-key"),
("HERMES_LANGFUSE_SECRET_KEY", "sk-lf-test-key"),
("HERMES_LANGFUSE_PUBLIC_KEY", "pk-lf-xxxxxxxxxxxx"),
("HERMES_LANGFUSE_SECRET_KEY", "sk-lf-changeme"),
],
)
def test_validate_langfuse_key_rejects_right_prefix_templates(
self, monkeypatch, env_name, value
):
"""A leftover template that happens to carry the documented prefix
must be rejected — the prefix-only check let these through, which is
why the placeholder reports kept coming after the original guard
merged (#51399 and its six duplicates)."""
self._clear_env(monkeypatch)
plugin = self._fresh_plugin()
msg = plugin._validate_langfuse_key(env_name, value)
assert msg is not None, f"{value!r} must be rejected as a template"
assert env_name in msg
assert "template" in msg

@pytest.mark.parametrize(
("env_name", "value"),
[
# Langfuse issues UUID-shaped remainders — hex digits + dashes.
("HERMES_LANGFUSE_PUBLIC_KEY", "pk-lf-1234abcd-12ab-4cd9-9f00-aabbccddeeff"),
("HERMES_LANGFUSE_SECRET_KEY", "sk-lf-9f8e7d6c-5b4a-4321-8765-0123456789ab"),
],
)
def test_validate_langfuse_key_accepts_real_shaped_keys(
self, monkeypatch, env_name, value
):
"""Real issued keys (UUID remainder) must keep passing: every
placeholder fragment contains a non-hex letter, so a hex-and-dashes
remainder can never be flagged."""
self._clear_env(monkeypatch)
plugin = self._fresh_plugin()
assert plugin._validate_langfuse_key(env_name, value) is None

# -- end-to-end _get_langfuse() behaviour --------------------------------
# These tests pass `monkeypatch` to _fresh_plugin() so the helper can
# stub out `Langfuse` (the optional SDK). Without that, every call
Expand Down Expand Up @@ -560,6 +612,24 @@ def test_placeholder_secret_key_warns_and_skips(self, monkeypatch, caplog):
assert "'pk-lf-" not in text
assert _FakeLangfuse.instances == []

def test_docs_template_keys_warn_and_skip(self, monkeypatch, caplog):
"""The literal copy-paste from the plugin README / website docs
(``pk-lf-...`` / ``sk-lf-...``) must warn and short-circuit instead
of constructing a client that silently drops every trace — the
failure mode reported over and over in #51399's duplicate chain."""
self._clear_env(monkeypatch)
monkeypatch.setenv("HERMES_LANGFUSE_PUBLIC_KEY", "pk-lf-...")
monkeypatch.setenv("HERMES_LANGFUSE_SECRET_KEY", "sk-lf-...")
plugin = self._fresh_plugin(monkeypatch)
with caplog.at_level(logging.WARNING, logger=self.LOGGER_NAME):
assert plugin._get_langfuse() is None
text = caplog.text
assert "HERMES_LANGFUSE_PUBLIC_KEY" in text
assert "HERMES_LANGFUSE_SECRET_KEY" in text
assert "template" in text
# Never constructed the SDK client — short-circuited before that.
assert _FakeLangfuse.instances == []

def test_both_placeholders_one_warning_with_both_keys(self, monkeypatch, caplog):
self._clear_env(monkeypatch)
monkeypatch.setenv("HERMES_LANGFUSE_PUBLIC_KEY", "placeholder")
Expand Down