Files
replypier/backend/app/utils/agno_eval_guard.py
T
WangShen007 54b949b392 chore(brand): rename the product to ReplyPier
The upstream project name is gone from code, assets, docs and build
output: brand strings, domains (replypier.site and its app/api/docs/help
subdomains), env prefixes, image and network names, the widget's public
JS API (window.ReplyPier, replypierId/BaseUrl/Config/Token), CSS tokens
(--rp-*), file and directory names, and the WordPress plugin package.

- widget loader is now webclient/replypier.min.js; both bundles rebuilt
  from source (backend/assets/widget.{js,css}, frontend/public/webclient)
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and config.dev.js
  rewritten rather than string-swapped; the demo config no longer ships
  the previous vendor's Firebase/Razorpay/Google keys
- new migration replypier_ai_model_001 adds REPLYPIER to the managed
  AIModelType and rewrites pre-rename rows
- local compose project, database, role and volume renamed; LOCAL_DEPLOY
  documents carrying the old local data over
2026-10-26 00:51:03 +08:00

76 lines
3.1 KiB
Python

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# Runtime guard for the pinned agno==1.7.6 (GHSA-77rh-m34w-rv36, fixed
# upstream in 2.3.24). Remove this module with the agno 2.x upgrade.
#
# agno turns a user-input field's type name back into a Python type by calling
# eval() on a string it did not produce: in agno.models.base the string is the
# `field_type` of a `get_user_input` tool call, i.e. text the model wrote, and
# in agno.tools.function it is read back from a stored run response. A tool
# named get_user_input is all it takes to reach that eval, and MCP connectors
# register tools under whatever name the server advertises, so a hostile or
# prompt-injected connector could run arbitrary Python inside the backend.
#
# Both modules look `eval` up as a global name, so binding a module-level `eval`
# shadows the builtin for those two modules only. The replacement never
# evaluates anything: it maps a bare builtin type name to the type and refuses
# everything else.
import agno.models.base
import agno.tools.function
from app.core.logger import get_logger
logger = get_logger(__name__)
# Every type UserInputField.to_dict can emit (it writes type.__name__). Nothing
# else was ever a valid field type, so refusing it changes no legitimate path.
_FIELD_TYPES = {t.__name__: t for t in (str, int, float, bool, list, dict, tuple, set, bytes)}
_GUARDED_MODULES = (agno.models.base, agno.tools.function)
class UnsafeFieldTypeError(NameError):
"""Raised for any field_type that is not a plain builtin type name.
A NameError on purpose: agno's model loop catches (NameError, SyntaxError)
around its eval and falls back to str, so a rejected value degrades exactly
the way an unknown name always did — the run continues, nothing executes.
"""
def resolve_field_type(expr, *_args, **_kwargs):
"""Drop-in for eval() that only ever maps a type name to the type.
Accepts eval's optional globals/locals so any call shape agno uses works.
"""
if isinstance(expr, str):
field_type = _FIELD_TYPES.get(expr.strip())
if field_type is not None:
return field_type
# agno swallows the error and carries on with str, so without this line a
# blocked payload would leave no trace at all. Nothing legitimate reaches
# here, which makes every one of these worth seeing.
logger.warning(f"Blocked an unsafe user-input field type: {expr!r}")
raise UnsafeFieldTypeError(f"Unsupported user-input field type: {expr!r}")
def install_eval_guard() -> None:
"""Shadow eval in the agno modules that hand model-supplied text to it."""
for module in _GUARDED_MODULES:
module.eval = resolve_field_type