mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
S5b (local E2E 2026-09-22, F2): analyze_goal's "a sensible default value per field" led the LLM to prefill render_api_url=https://api.yourcompany.com/... and Authorization: Bearer YOUR_API_KEY for a goal that named neither -- caught only because "yourcompany" happened to match an existing placeholder-URL regex; a plausible invented host would slip through and recreate ESQ1-302's launch failure one step upstream, in the requirements form itself. Adds services/dify_builder/agent/user_supplied.py (pure, no I/O): url_hosts, is_user_supplied_url, CREDENTIAL_PLACEHOLDER_RE, is_user_supplied_secret, trusted_text_for -- shared primitives for telling a literal the user actually wrote from one an LLM invented, used by this task and consumed by later tasks in the same plan. build.analyze_goal now tells the model never to invent such a field's default, and _scrub_invented_defaults deterministically blanks any top-level field whose value contains a URL host, or a credential-shaped or credential-keyed literal, not present in the goal text -- decided on hosts and literal text, never on LLM prose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
"""Telling a user-supplied URL or credential from one an LLM invented.
|
|
|
|
Pure, no I/O -- these helpers make no network or model calls and import
|
|
nothing from ``services`` (besides each other), ``flask``, or model-runtime,
|
|
so they are safe to call from anywhere in Dify Builder's cognition.
|
|
|
|
ESQ1-302 traces to the Builder inventing a plausible-looking endpoint when a
|
|
goal gave it none (``https://api.example.com/ppt/generate``); S5b/F2 shows
|
|
the same failure one step earlier, in requirements analysis, which also
|
|
invented an ``Authorization: Bearer YOUR_API_KEY`` header. These helpers let
|
|
callers decide "did the user actually write this?" by comparing literal text
|
|
against what the user actually typed -- never by asking an LLM to judge its
|
|
own output (no control flow here is keyed on LLM prose).
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
from urllib.parse import urlsplit
|
|
|
|
# A URL literal as it appears embedded in freeform prose -- stops at
|
|
# whitespace or a quoting/bracketing character so "https://a.com/x." at the
|
|
# end of a sentence doesn't pull in trailing punctuation.
|
|
_URL_RE = re.compile(r"https?://[^\s\"'<>]+", re.IGNORECASE)
|
|
|
|
# A Dify workflow template reference, e.g. ``{{#node1.url#}}``. It names a
|
|
# variable, not a literal value, so it can never be "invented" the way a
|
|
# hardcoded URL or secret can -- it's already bound to whatever that
|
|
# variable resolves to at runtime.
|
|
_TEMPLATE_MARKER = "{{#"
|
|
|
|
_AUTH_SCHEME_PREFIX_RE = re.compile(r"^(?:bearer|basic|token)\s+", re.IGNORECASE)
|
|
|
|
# Case-insensitive tokens an LLM writes in place of a secret it doesn't have.
|
|
CREDENTIAL_PLACEHOLDER_RE = re.compile(
|
|
r"YOUR[_-]?(API[_-]?)?(KEY|TOKEN|SECRET)"
|
|
r"|(API[_-]?KEY|TOKEN|SECRET)[_-]?HERE"
|
|
r"|<[^>]+>"
|
|
r"|REPLACE[_-]?ME"
|
|
r"|CHANGE[_-]?ME"
|
|
r"|x{6,}"
|
|
r"|\*{6,}",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def url_hosts(text: str) -> set[str]:
|
|
"""Lowercase hostnames of every ``http(s)://…`` URL literally present in
|
|
``text``. This is the trust set for a URL check: a host the user's own
|
|
text actually names."""
|
|
hosts: set[str] = set()
|
|
for raw_url in _URL_RE.findall(text or ""):
|
|
try:
|
|
host = urlsplit(raw_url).hostname
|
|
except ValueError:
|
|
continue
|
|
if host:
|
|
hosts.add(host.lower())
|
|
return hosts
|
|
|
|
|
|
def is_user_supplied_url(url: str, trusted_text: str) -> bool:
|
|
"""True iff ``url`` is something the user actually gave: its host
|
|
appears among the URLs literally present in ``trusted_text``.
|
|
|
|
A Dify template reference (contains ``{{#``) is a variable, not a
|
|
literal, so it is never a fabrication and always returns True.
|
|
"""
|
|
if not url:
|
|
return False
|
|
if _TEMPLATE_MARKER in url:
|
|
return True
|
|
try:
|
|
host = urlsplit(url).hostname
|
|
except ValueError:
|
|
return False
|
|
if not host:
|
|
return False
|
|
return host.lower() in url_hosts(trusted_text)
|
|
|
|
|
|
def is_user_supplied_secret(value: str, trusted_text: str) -> bool:
|
|
"""True iff ``value`` -- after stripping a leading ``Bearer ``/``Basic
|
|
``/``Token `` auth-scheme prefix -- is a literal the user actually
|
|
wrote, rather than an LLM's placeholder for a secret it does not have.
|
|
"""
|
|
remainder = _AUTH_SCHEME_PREFIX_RE.sub("", value or "", count=1)
|
|
if not remainder:
|
|
return False
|
|
if _TEMPLATE_MARKER in remainder:
|
|
return True
|
|
if CREDENTIAL_PLACEHOLDER_RE.search(remainder):
|
|
return False
|
|
return remainder in (trusted_text or "")
|
|
|
|
|
|
def trusted_text_for(goal_text: str, requirements: Mapping[str, Any]) -> str:
|
|
"""The text a candidate URL/credential is checked against: what the user
|
|
actually submitted -- the goal prose plus the requirements form they
|
|
filled in, as they submitted it (never anything an LLM wrote about
|
|
either)."""
|
|
return f"{goal_text}\n{json.dumps(dict(requirements), ensure_ascii=False)}"
|