mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
feat(builder): safely interpret bounded native Template text
This commit is contained in:
@@ -518,14 +518,26 @@ def admitted_node_bindings(snapshot: RestrictedAdmissionSnapshot) -> tuple[Admit
|
||||
from graphon.nodes.end.end_node import EndNode
|
||||
from graphon.nodes.http_request.node import HttpRequestNode
|
||||
from graphon.nodes.start.start_node import StartNode
|
||||
from graphon.nodes.template_transform.template_transform_node import TemplateTransformNode
|
||||
from services.dify_builder.template_execution import validate_template_configuration, validate_template_source
|
||||
|
||||
admit_raw_execution_metadata(snapshot)
|
||||
graph = snapshot.graph
|
||||
canonical_digest(graph)
|
||||
nodes, edges = graph.get("nodes"), graph.get("edges")
|
||||
if not isinstance(nodes, list) or not 2 <= len(nodes) <= 128 or not isinstance(edges, list):
|
||||
raise BuilderExecutionPolicyError("unsupported_graph")
|
||||
audited = {"start": StartNode, "end": EndNode, "http-request": HttpRequestNode}
|
||||
# Source limits precede the existing graph digest's UTF-8 allocation.
|
||||
for node in nodes:
|
||||
if isinstance(node, dict) and isinstance(node.get("data"), dict):
|
||||
if node["data"].get("type") == "template-transform":
|
||||
validate_template_source(node["data"].get("template"))
|
||||
canonical_digest(graph)
|
||||
audited = {
|
||||
"start": StartNode,
|
||||
"end": EndNode,
|
||||
"http-request": HttpRequestNode,
|
||||
"template-transform": TemplateTransformNode,
|
||||
}
|
||||
bindings = []
|
||||
types: dict[str, str] = {}
|
||||
raw_data: dict[str, dict[str, Any]] = {}
|
||||
@@ -548,6 +560,8 @@ def admitted_node_bindings(snapshot: RestrictedAdmissionSnapshot) -> tuple[Admit
|
||||
raise BuilderExecutionPolicyError("unsupported_node_implementation")
|
||||
if node_type == "http-request":
|
||||
validate_restricted_http_defaults(data)
|
||||
elif node_type == "template-transform":
|
||||
validate_template_configuration(data)
|
||||
try:
|
||||
cls.validate_node_data(data)
|
||||
except (ValidationError, ValueError, TypeError):
|
||||
@@ -604,17 +618,29 @@ def admitted_node_bindings(snapshot: RestrictedAdmissionSnapshot) -> tuple[Admit
|
||||
raise BuilderExecutionPolicyError("unsupported_start_input")
|
||||
start_outputs = {v.get("variable") for v in variables}
|
||||
|
||||
def selector_allowed(node_id: str, selector: Any) -> bool:
|
||||
def selector_allowed(node_id: str, selector: Any, *, template_input: bool = False) -> bool:
|
||||
if (
|
||||
not isinstance(selector, (list, tuple))
|
||||
or len(selector) != 2
|
||||
or not isinstance(selector[0], str)
|
||||
or not isinstance(selector[1], str)
|
||||
or selector[0] not in upstream[node_id]
|
||||
):
|
||||
return False
|
||||
return selector[1] in (start_outputs if types[selector[0]] == "start" else {"body", "status_code", "headers"})
|
||||
outputs = {
|
||||
"start": start_outputs,
|
||||
"http-request": {"body"} if template_input else {"body", "status_code", "headers"},
|
||||
"template-transform": {"output"},
|
||||
}
|
||||
return selector[1] in outputs[types[selector[0]]]
|
||||
|
||||
for node_id, data in raw_data.items():
|
||||
if types[node_id] == "template-transform":
|
||||
if any(
|
||||
not selector_allowed(node_id, variable["value_selector"], template_input=True)
|
||||
for variable in data["variables"]
|
||||
):
|
||||
raise BuilderExecutionPolicyError("unsupported_template_selector")
|
||||
if types[node_id] == "http-request":
|
||||
auth = data.get("authorization")
|
||||
if (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from core.dify_builder.contract import TestResultCard
|
||||
from core.dify_builder.models import PublicationDecision, Run
|
||||
from services.dify_builder.execution_messages import FALLBACK, execution_refusal_message
|
||||
|
||||
REVIEW_NOTE = "Other paths and goal acceptance were not verified."
|
||||
SUCCESS_REPLY = "Execution succeeded; output needs review."
|
||||
@@ -148,8 +149,12 @@ def result_card(run: Run) -> TestResultCard:
|
||||
if is_execution_policy_blocker(run):
|
||||
card.outcome = "execution_unknown"
|
||||
card.failure_reason = (
|
||||
"Execution safety requires review or revised test inputs; automatic repair and publication are blocked."
|
||||
execution_refusal_message(run.execution_refusal.reason_code) if run.execution_refusal else FALLBACK
|
||||
)
|
||||
if run.execution_refusal is not None:
|
||||
card.terminal_outputs = None
|
||||
if not run.dify_run_id:
|
||||
card.executed_node_ids = []
|
||||
return card
|
||||
if run.status == "succeeded" and has_output_blocker(run):
|
||||
card.status = "failed"
|
||||
|
||||
@@ -98,6 +98,7 @@ from graphon.nodes.start.start_node import StartNode
|
||||
from graphon.nodes.tool.tool_node import ToolNode
|
||||
from graphon.variables.segments import ArrayObjectSegment, ObjectSegment
|
||||
from models.model import Conversation
|
||||
from services.dify_builder.template_execution import MAX_OUTPUT_BYTES, BoundedJinja2TemplateRenderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphon.entities import GraphInitParams
|
||||
@@ -600,6 +601,11 @@ class DifyNodeFactory(NodeFactory):
|
||||
"file_manager": files,
|
||||
"file_reference_factory": files,
|
||||
}
|
||||
elif node_data.type == BuiltinNodeTypes.TEMPLATE_TRANSFORM:
|
||||
kwargs = {
|
||||
"jinja2_template_renderer": BoundedJinja2TemplateRenderer(),
|
||||
"max_output_length": MAX_OUTPUT_BYTES,
|
||||
}
|
||||
return node_class(
|
||||
node_id=node_id,
|
||||
data=resolved_node_data.model_dump(mode="python", by_alias=True),
|
||||
@@ -746,7 +752,11 @@ class DifyNodeFactory(NodeFactory):
|
||||
if not isinstance(data, Mapping) or not isinstance(node_id, str):
|
||||
raise BuilderExecutionPolicyError("invalid_node_configuration")
|
||||
node_type, version = data.get("type"), data.get("version", "1")
|
||||
if not isinstance(node_type, str) or node_type not in {"start", "end", "http-request"} or version != "1":
|
||||
if (
|
||||
not isinstance(node_type, str)
|
||||
or node_type not in {"start", "end", "http-request", "template-transform"}
|
||||
or version != "1"
|
||||
):
|
||||
raise BuilderExecutionPolicyError("unsupported_node_implementation")
|
||||
if node_class is None:
|
||||
node_class = self._resolve_node_class(node_type=node_type, node_version=version, node_data=data)
|
||||
|
||||
@@ -27,8 +27,16 @@ from graphon.nodes.end.end_node import EndNode
|
||||
from graphon.nodes.http_request.entities import HttpRequestNodeConfig, HttpRequestNodeData
|
||||
from graphon.nodes.http_request.node import HttpRequestNode
|
||||
from graphon.nodes.start.start_node import StartNode
|
||||
from graphon.nodes.template_transform.entities import TemplateTransformNodeData
|
||||
from graphon.nodes.template_transform.template_transform_node import TemplateTransformNode
|
||||
from services.dify_builder.template_execution import validate_template_configuration, validate_template_source
|
||||
|
||||
_AUDITED_CLASSES = {"start": StartNode, "end": EndNode, "http-request": HttpRequestNode}
|
||||
_AUDITED_CLASSES = {
|
||||
"start": StartNode,
|
||||
"end": EndNode,
|
||||
"http-request": HttpRequestNode,
|
||||
"template-transform": TemplateTransformNode,
|
||||
}
|
||||
_CONTENT_TYPES = frozenset(
|
||||
("application/json", "text/plain", "application/json; charset=utf-8", "text/plain; charset=utf-8")
|
||||
)
|
||||
@@ -58,6 +66,9 @@ def validate_restricted_raw_node(
|
||||
or node_class.version() != "1"
|
||||
):
|
||||
raise BuilderExecutionPolicyError("unsupported_node_implementation")
|
||||
if node_class is TemplateTransformNode:
|
||||
# Refuse oversized drift before canonicalizing the raw binding.
|
||||
validate_template_source(raw_node_data.get("template"))
|
||||
binding = next((binding for binding in context.admitted_nodes if binding.node_id == node_id), None)
|
||||
if (
|
||||
binding is None
|
||||
@@ -66,6 +77,8 @@ def validate_restricted_raw_node(
|
||||
or binding.normalized_config_digest != canonical_digest({**raw_node_data, "version": "1"})
|
||||
):
|
||||
raise BuilderExecutionPolicyError("execution_binding_mismatch")
|
||||
if node_class is TemplateTransformNode:
|
||||
validate_template_configuration(raw_node_data)
|
||||
|
||||
|
||||
def validate_restricted_node(
|
||||
@@ -82,6 +95,10 @@ def validate_restricted_node(
|
||||
or resolved_node_data.version != "1"
|
||||
):
|
||||
raise BuilderExecutionPolicyError("unsupported_node_implementation")
|
||||
if node_class is TemplateTransformNode:
|
||||
if not isinstance(resolved_node_data, TemplateTransformNodeData):
|
||||
raise BuilderExecutionPolicyError("invalid_node_configuration")
|
||||
validate_template_configuration(resolved_node_data.model_dump(mode="python"))
|
||||
if node_class is HttpRequestNode:
|
||||
if not isinstance(resolved_node_data, HttpRequestNodeData):
|
||||
raise BuilderExecutionPolicyError("invalid_node_configuration")
|
||||
|
||||
@@ -16,6 +16,7 @@ from collections.abc import Callable
|
||||
from core.dify_builder import strings
|
||||
from core.dify_builder.models import ConversationItem
|
||||
from services.dify_builder.agent import llm
|
||||
from services.dify_builder.execution_messages import STATIC_COPY as EXECUTION_COPY
|
||||
from services.dify_builder.repair_gate import STATIC_COPY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -41,7 +42,7 @@ _LANG_CODE_RE = re.compile(r"^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$")
|
||||
# concurrent duplicate translation of the same key is harmless and idempotent
|
||||
# -- no lock needed.
|
||||
_TRANSLATION_CACHE: dict[tuple[str, str], str] = {}
|
||||
_PLAIN = strings.PLAIN | STATIC_COPY
|
||||
_PLAIN = strings.PLAIN | STATIC_COPY | EXECUTION_COPY
|
||||
|
||||
|
||||
class Localizer:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Finite sanitized execution-refusal copy; no graph/input/error interpolation."""
|
||||
|
||||
FALLBACK = "Execution safety requires review or revised test inputs; automatic repair and publication are blocked."
|
||||
_BLOCKED = " Automatic repair and publication are blocked."
|
||||
_MESSAGES = {
|
||||
"unsupported_node_implementation": (
|
||||
"This node is not supported by safe testing. Edit the workflow to use supported nodes; the test did not run."
|
||||
+ _BLOCKED
|
||||
),
|
||||
"unsupported_template_syntax": (
|
||||
"Template supports scalar text, if/elif/else, and trim. Edit unsupported syntax; the test did not run."
|
||||
+ _BLOCKED
|
||||
),
|
||||
"unsupported_template_configuration": (
|
||||
"Edit Template variables to use unique legal names and complete selectors; the test did not run." + _BLOCKED
|
||||
),
|
||||
"unsupported_template_selector": (
|
||||
"Use Template inputs from Start scalars, earlier Template output or HTTP body; the test did not run." + _BLOCKED
|
||||
),
|
||||
"template_execution_limit": (
|
||||
"Reduce Template size or selected text to fit safe testing limits; the test did not run." + _BLOCKED
|
||||
),
|
||||
}
|
||||
STATIC_COPY = frozenset({FALLBACK, *_MESSAGES.values()})
|
||||
|
||||
|
||||
def execution_refusal_message(reason_code: str) -> str:
|
||||
return _MESSAGES.get(reason_code, FALLBACK)
|
||||
@@ -0,0 +1,293 @@
|
||||
"""Pure, finite text interpretation for Builder's existing native Template node.
|
||||
|
||||
Jinja supplies only its lexer and parser. No compilation, rendering, globals,
|
||||
extensions or effectful collaborator is used. Admission inspects every branch;
|
||||
the native adapter repeats inspection and bounds all selected values and output.
|
||||
"""
|
||||
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Iterator, Mapping
|
||||
from typing import Any, NoReturn, override
|
||||
|
||||
from jinja2 import Environment, nodes
|
||||
|
||||
from core.dify_builder.execution_policy import BuilderExecutionPolicyError
|
||||
from graphon.template_rendering import Jinja2TemplateRenderer, TemplateRenderError
|
||||
|
||||
MAX_SOURCE_BYTES = 16384
|
||||
MAX_TOKENS = 512
|
||||
MAX_AST_NODES = 256
|
||||
MAX_AST_DEPTH = 16
|
||||
MAX_VARIABLES = 32
|
||||
MAX_TEXT_BYTES = 65536
|
||||
MAX_SELECTED_BYTES = 131072
|
||||
MAX_OUTPUT_BYTES = 65536
|
||||
MAX_VISITS = 512
|
||||
MAX_INTEGER = 2**53 - 1
|
||||
|
||||
Scalar = str | bool | int | float | None
|
||||
_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,63}\Z", re.ASCII)
|
||||
_FIELDS = {
|
||||
nodes.Template: ("body",),
|
||||
nodes.Output: ("nodes",),
|
||||
nodes.TemplateData: ("data",),
|
||||
nodes.If: ("test", "body", "elif_", "else_"),
|
||||
nodes.Name: ("name", "ctx"),
|
||||
nodes.Const: ("value",),
|
||||
nodes.Filter: ("node", "name", "args", "kwargs", "dyn_args", "dyn_kwargs"),
|
||||
nodes.And: ("left", "right"),
|
||||
nodes.Or: ("left", "right"),
|
||||
nodes.Not: ("node",),
|
||||
}
|
||||
|
||||
|
||||
def _refuse(reason: str = "unsupported_template_syntax") -> NoReturn:
|
||||
raise BuilderExecutionPolicyError(reason)
|
||||
|
||||
|
||||
def _text_bytes(value: object, limit: int) -> int:
|
||||
"""Bound character count before allocating any UTF-8 representation."""
|
||||
if type(value) is not str:
|
||||
_refuse("unsupported_template_configuration")
|
||||
if len(value) > limit:
|
||||
_refuse("template_execution_limit")
|
||||
try:
|
||||
size = len(value.encode("utf-8"))
|
||||
except UnicodeError:
|
||||
_refuse("unsupported_template_configuration")
|
||||
if size > limit:
|
||||
_refuse("template_execution_limit")
|
||||
return size
|
||||
|
||||
|
||||
def validate_template_source(template: object) -> None:
|
||||
"""Cheap preflight, including before an existing caller's graph digest."""
|
||||
_text_bytes(template, MAX_SOURCE_BYTES)
|
||||
|
||||
|
||||
def _names(values: object) -> frozenset[str]:
|
||||
if not isinstance(values, (list, tuple)) or len(values) > MAX_VARIABLES:
|
||||
_refuse("unsupported_template_configuration")
|
||||
names: set[str] = set()
|
||||
for name in values:
|
||||
if type(name) is not str or not _NAME.fullmatch(name) or name in names:
|
||||
_refuse("unsupported_template_configuration")
|
||||
names.add(name)
|
||||
return frozenset(names)
|
||||
|
||||
|
||||
def _scalar(value: object) -> Scalar:
|
||||
"""Exact built-ins only: no custom conversion/truthiness can execute."""
|
||||
if value is None or type(value) is bool:
|
||||
return value
|
||||
if type(value) is str:
|
||||
_text_bytes(value, MAX_TEXT_BYTES)
|
||||
return value
|
||||
if type(value) is int:
|
||||
if not -MAX_INTEGER <= value <= MAX_INTEGER:
|
||||
_refuse("template_execution_limit")
|
||||
return value
|
||||
if type(value) is float and math.isfinite(value):
|
||||
return value
|
||||
_refuse("unsupported_template_configuration")
|
||||
|
||||
|
||||
def _scalar_text(value: Scalar) -> str:
|
||||
# Values are checked before this conversion, including scalar constants.
|
||||
return value if type(value) is str else str(value)
|
||||
|
||||
|
||||
def _children(node: nodes.Node, declared: frozenset[str]) -> Iterator[nodes.Node]:
|
||||
"""Validate every field, yielding only the reviewed structural children."""
|
||||
kind = type(node)
|
||||
if kind not in _FIELDS or node.fields != _FIELDS[kind]:
|
||||
_refuse()
|
||||
if kind is nodes.Name:
|
||||
if node.ctx != "load" or node.name not in declared:
|
||||
_refuse()
|
||||
return
|
||||
if kind is nodes.Const:
|
||||
try:
|
||||
_scalar(node.value)
|
||||
except BuilderExecutionPolicyError:
|
||||
_refuse()
|
||||
return
|
||||
if kind is nodes.TemplateData:
|
||||
_text_bytes(node.data, MAX_SOURCE_BYTES)
|
||||
return
|
||||
if kind is nodes.Filter:
|
||||
if node.name != "trim" or node.args or node.kwargs or node.dyn_args is not None or node.dyn_kwargs is not None:
|
||||
_refuse()
|
||||
if node.node is None:
|
||||
_refuse()
|
||||
yield node.node
|
||||
return
|
||||
for field in _FIELDS[kind]:
|
||||
value = getattr(node, field)
|
||||
if isinstance(value, nodes.Node):
|
||||
yield value
|
||||
elif type(value) is list:
|
||||
for child in value:
|
||||
if not isinstance(child, nodes.Node):
|
||||
_refuse()
|
||||
yield child
|
||||
else:
|
||||
_refuse()
|
||||
|
||||
|
||||
def inspect_template(template: str, declared: frozenset[str]) -> nodes.Template:
|
||||
"""Refuse before parsing on source/token bounds, then inspect iteratively."""
|
||||
validate_template_source(template)
|
||||
try:
|
||||
environment = Environment()
|
||||
for count, _token in enumerate(environment.lex(template), 1):
|
||||
if count > MAX_TOKENS:
|
||||
_refuse("template_execution_limit")
|
||||
parsed = environment.parse(template)
|
||||
pending: list[tuple[nodes.Node, int]] = [(parsed, 0)]
|
||||
visited = 0
|
||||
while pending:
|
||||
node, depth = pending.pop()
|
||||
visited += 1
|
||||
if visited > MAX_AST_NODES or depth > MAX_AST_DEPTH:
|
||||
_refuse("template_execution_limit")
|
||||
for child in _children(node, declared):
|
||||
# Bound the work stack as well as total visited nodes.
|
||||
if visited + len(pending) >= MAX_AST_NODES:
|
||||
_refuse("template_execution_limit")
|
||||
pending.append((child, depth + 1))
|
||||
return parsed
|
||||
except BuilderExecutionPolicyError:
|
||||
raise
|
||||
except Exception:
|
||||
# Parser/lexer/AST failures, including recursion/overflow, never echo source.
|
||||
_refuse()
|
||||
|
||||
|
||||
def validate_template_configuration(data: Mapping[str, Any]) -> None:
|
||||
"""Raw names/selectors must be safe before permissive native DTO parsing."""
|
||||
template, variables = data.get("template"), data.get("variables")
|
||||
validate_template_source(template)
|
||||
# A render failure must remain a native failure, not a default-value branch.
|
||||
if data.get("error_strategy") not in (None, "") or data.get("default_value") not in (None, []):
|
||||
_refuse("unsupported_template_configuration")
|
||||
if data.get("default_values") not in (None, []):
|
||||
_refuse("unsupported_template_configuration")
|
||||
retry = data.get("retry_config")
|
||||
if retry is not None and (
|
||||
type(retry) is not dict
|
||||
or set(retry) - {"retry_enabled", "max_retries", "retry_interval"}
|
||||
or retry.get("retry_enabled", False) is not False
|
||||
or any(
|
||||
type(retry.get(key, 0)) is not int or not 0 <= retry.get(key, 0) <= MAX_INTEGER
|
||||
for key in ("max_retries", "retry_interval")
|
||||
)
|
||||
):
|
||||
_refuse("unsupported_template_configuration")
|
||||
if type(variables) is not list or len(variables) > MAX_VARIABLES:
|
||||
_refuse("unsupported_template_configuration")
|
||||
names = []
|
||||
for variable in variables:
|
||||
if type(variable) is not dict or set(variable) != {"variable", "value_selector"}:
|
||||
_refuse("unsupported_template_configuration")
|
||||
selector = variable["value_selector"]
|
||||
if (
|
||||
not isinstance(selector, (list, tuple))
|
||||
or len(selector) != 2
|
||||
or any(type(part) is not str or not part for part in selector)
|
||||
):
|
||||
_refuse("unsupported_template_selector")
|
||||
names.append(variable["variable"])
|
||||
inspect_template(template, _names(names))
|
||||
|
||||
|
||||
class _Evaluation:
|
||||
"""One bounded invocation, with cumulative append and visit accounting."""
|
||||
|
||||
def __init__(self, values: dict[str, Scalar]) -> None:
|
||||
self.values = values
|
||||
self.visits = 0
|
||||
self.output_bytes = 0
|
||||
self.fragments: list[str] = []
|
||||
|
||||
def _visit(self) -> None:
|
||||
if self.visits >= MAX_VISITS:
|
||||
_refuse("template_execution_limit")
|
||||
self.visits += 1
|
||||
|
||||
def expression(self, node: nodes.Node) -> Scalar:
|
||||
self._visit()
|
||||
kind = type(node)
|
||||
if kind is nodes.Name:
|
||||
return self.values[node.name]
|
||||
if kind is nodes.Const:
|
||||
return node.value
|
||||
if kind is nodes.TemplateData:
|
||||
return node.data
|
||||
if kind is nodes.Filter:
|
||||
return _scalar_text(self.expression(node.node)).strip()
|
||||
if kind is nodes.Not:
|
||||
return not self.expression(node.node)
|
||||
if kind is nodes.And:
|
||||
left = self.expression(node.left)
|
||||
return self.expression(node.right) if left else left
|
||||
if kind is nodes.Or:
|
||||
left = self.expression(node.left)
|
||||
return left or self.expression(node.right)
|
||||
_refuse()
|
||||
|
||||
def statement(self, node: nodes.Node) -> None:
|
||||
self._visit()
|
||||
kind = type(node)
|
||||
if kind is nodes.Template:
|
||||
for child in node.body:
|
||||
self.statement(child)
|
||||
elif kind is nodes.Output:
|
||||
for expression in node.nodes:
|
||||
text = _scalar_text(self.expression(expression))
|
||||
size = _text_bytes(text, MAX_OUTPUT_BYTES - self.output_bytes)
|
||||
self.output_bytes += size
|
||||
self.fragments.append(text)
|
||||
elif kind is nodes.If:
|
||||
branch = node.else_
|
||||
if self.expression(node.test):
|
||||
branch = node.body
|
||||
else:
|
||||
for alternative in node.elif_:
|
||||
self._visit()
|
||||
if self.expression(alternative.test):
|
||||
branch = alternative.body
|
||||
break
|
||||
for child in branch:
|
||||
self.statement(child)
|
||||
else:
|
||||
_refuse()
|
||||
|
||||
|
||||
class BoundedJinja2TemplateRenderer(Jinja2TemplateRenderer):
|
||||
"""Native nominal adapter; never delegates to ordinary Jinja rendering."""
|
||||
|
||||
@override
|
||||
def render_template(self, template: str, variables: Mapping[str, Any]) -> str:
|
||||
try:
|
||||
if type(variables) is not dict or len(variables) > MAX_VARIABLES:
|
||||
_refuse("unsupported_template_configuration")
|
||||
declared = _names(tuple(variables))
|
||||
values: dict[str, Scalar] = {}
|
||||
selected_bytes = 0
|
||||
for name, raw in variables.items():
|
||||
value = _scalar(raw)
|
||||
text = _scalar_text(value)
|
||||
selected_bytes += _text_bytes(text, MAX_SELECTED_BYTES - selected_bytes)
|
||||
values[name] = value
|
||||
parsed = inspect_template(template, declared)
|
||||
evaluation = _Evaluation(values)
|
||||
evaluation.statement(parsed)
|
||||
return "".join(evaluation.fragments)
|
||||
except BuilderExecutionPolicyError as error:
|
||||
if error.reason_code == "template_execution_limit":
|
||||
raise TemplateRenderError("Template rendering exceeded safe testing size limits.") from None
|
||||
raise TemplateRenderError("Template rendering refused unsupported syntax or scalar inputs.") from None
|
||||
except Exception:
|
||||
raise TemplateRenderError("Template rendering failed safely; review the template and inputs.") from None
|
||||
Reference in New Issue
Block a user