Files
dify/api/services/dify_builder/agent/user_supplied.py
T
Charles YaoandClaude Opus 5 be24b00a2a fix(dify-builder): requirements analysis stops inventing endpoints and credentials
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>
2026-09-22 08:40:33 -07:00

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)}"