From 3c53fc93c765d44138de504d20ad802817fbc86c Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Sat, 29 Aug 2026 15:11:14 +0800 Subject: [PATCH] =?UTF-8?q?approve:=20=E6=B5=8B=E8=AF=95=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=20-=20world=5Fsnapshot=20=E4=B8=96=E7=95=8C=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +- README.md | 51 ++- __init__.py | 10 +- build.sh | 92 +++-- init/data.json | 3 +- json/script_engine.json | 27 +- models/script_engine.json | 26 +- pyproject.toml | 1 - script_engine/__init__.py | 25 +- script_engine/engine.py | 168 +++++----- script_engine/init.py | 424 +++++++++++++----------- scripts/load_path.py | 60 ++-- skill/SKILL.md | 22 +- wwwroot/api/get_search_script_type.dspy | 9 +- wwwroot/api/get_search_status.dspy | 9 +- wwwroot/api/script_create.dspy | 2 - wwwroot/api/script_delete.dspy | 2 - wwwroot/api/script_execute.dspy | 2 - wwwroot/api/script_get.dspy | 2 - wwwroot/api/script_list.dspy | 2 - wwwroot/api/script_update.dspy | 2 - wwwroot/api/script_validate.dspy | 2 - wwwroot/api/validate_script.dspy | 2 +- wwwroot/index.ui | 83 +---- wwwroot/script_list.ui | 52 +-- 25 files changed, 514 insertions(+), 571 deletions(-) diff --git a/.gitignore b/.gitignore index f36f99f..795bd5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ __pycache__/ -*.py[cod] +*.pyc *.egg-info/ build/ dist/ models/mysql.ddl.sql +wwwroot/script_engine/ wwwroot/script/ -.venv/ +wwwroot/script_exec_log/ +*.swp +*.swo diff --git a/README.md b/README.md index ae4f861..b05bdd7 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,33 @@ -# script_engine 脚本引擎模块(W-06) +# script_engine 模块 -脚本引擎:脚本表 CRUD + `execute_script` / `validate_script` 接口。独立模块,无业务依赖。 +逻辑编程(脚本/规则引擎)模块——脚本表 CRUD + 执行/校验接口。 ## 功能 -- **script 表 CRUD**:新增 / 编辑 / 删除 / 详情 / 分页列表 -- **execute_script**:按脚本 id 取库内脚本(或直传 content),校验通过后执行 -- **validate_script**:校验脚本内容(语法 + 受限语法 + 调用白名单),不落库不执行 -- **编码字典**:`script_type`(0=Python / 1=JavaScript / 2=规则表达式)、`script_status`(0=停用 / 1=启用)经 init/data.json(Format B appcodes)幂等落库 + +- `script_engine` 脚本表 CRUD:create_script / update_script / delete_script / get_script / list_scripts +- `execute_script`:按 id 或按 content+script_type 校验后执行 +- `validate_script_api`:仅校验(语法 + 受限语法),不落库不执行 +- 脚本类型:`0`=Python(受限命名空间,禁 import/类/λ/async/await/对象方法调用),`1`=SQL(仅只读 SELECT/SHOW/DESCRIBE/EXPLAIN,禁写语句/多语句) ## 数据表 -- `script`:id / script_name / script_type / content / description / status / created_at / updated_at -- 索引:idx_script_name、idx_script_type -## 接口约定 -- REST 统一前缀:`/script_engine/api/*.dspy` -- 错误结构:`{code, message, field, detail}`(code=0 成功,400 参数错误,404 不存在,500 内部错误,501 类型暂不支持) -- 分页:`{list, total}` -- 非法输入 100% 拦截,不落库、不执行 +`script_engine`:id(str32 PK)、script_name(str100 not null)、script_type(str32 default '0')、content(text not null)、description(str255)、status(str32 default '1')、created_at、updated_at +索引:idx_script_name、idx_script_type +编码:script_type→appcodes_kv(parentid='script_type'),status→appcodes_kv(parentid='script_status') + +## 安装(挂载到宿主应用) + +1. `pip install .`(或加入宿主 build.sh 模块安装循环) +2. 宿主应用 `app/{app}.py`:`from script_engine.init import load_script_engine` + `load_script_engine()`(init() 内) +3. `scripts/load_path.py` 注册 RBAC(显式路径,无通配符) +4. `build.sh` 四步:安装 xls2ddl → models→DDL → json→CRUD UI → 链接 wwwroot ## 集成 -1. `pip install .` -2. 宿主应用 `from script_engine.init import load_script_engine` → `init()` 内调用 `load_script_engine()` -3. `scripts/load_path.py` 登记 RBAC 路径;宿主 `load_path.py` 兜底 -4. `build.sh` 四步:安装 xls2ddl → 生成 DDL → 生成 CRUD UI → 链接 wwwroot -## 接口清单(wwwroot/api/) -| 路径 | 说明 | -|---|---| -| `/script_engine/api/script_create.dspy` | 新增脚本 | -| `/script_engine/api/script_update.dspy` | 编辑脚本 | -| `/script_engine/api/script_delete.dspy` | 删除脚本 | -| `/script_engine/api/script_get.dspy` | 脚本详情 | -| `/script_engine/api/script_list.dspy` | 分页列表({list,total}) | -| `/script_engine/api/script_execute.dspy` | 执行脚本 | -| `/script_engine/api/script_validate.dspy` | 校验脚本 | +- 取库名统一 `ServerEnv().get_module_dbname('script_engine')`(.py)/ `get_module_dbname('script_engine')`(.dspy),禁止硬编码 DBNAME +- REST 接口(wwwroot/api/*.dspy):script_create / script_update / script_delete / script_get / script_list / script_execute / execute_script / script_validate / validate_script +- 返回统一 `{code, message, field, detail}`;code=0 成功;分页 `data.list` / `data.total` + +## 依赖 + +无业务依赖;依赖基础包 sqlor / ahserver / appPublic。 diff --git a/__init__.py b/__init__.py index 8330b6c..6684724 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- -"""script_engine module -- script/rule engine (W-06).""" -from script_engine.engine import validate_script as validate_script, execute_script_content as execute_script_content -from script_engine.init import (load_script_engine as load_script_engine, create_script as create_script, update_script as update_script, delete_script as delete_script, list_scripts as list_scripts, get_script as get_script, execute_script as execute_script, validate_script_api as validate_script_api) +"""script_engine module root package. -__all__ = ['load_script_engine', 'create_script', 'update_script', 'delete_script', 'list_scripts', 'get_script', 'execute_script', 'validate_script_api', 'validate_script', 'execute_script_content'] +Delegates to the real package script_engine.script_engine so that +`import script_engine` exposes the full public API. +""" +from script_engine.script_engine import * # noqa: F401,F403 +from script_engine.script_engine import __all__ # noqa: F401 diff --git a/build.sh b/build.sh index 14821c3..73455b1 100644 --- a/build.sh +++ b/build.sh @@ -1,58 +1,50 @@ -#!/usr/bin/env bash -# script_engine 模块构建脚本 —— 四步安装(由宿主应用 build.sh 集成调用) -# ① 安装生成工具 ② 生成 DDL ③ 生成 CRUD UI ④ 链接 wwwroot +#!/bin/bash +# script_engine module build -- four steps: +# 1. install xls2ddl +# 2. models/ (json) -> mysql.ddl.sql +# 3. json/ -> CRUD ui/dspy via xls2ui +# 4. symlink module wwwroot into main app wwwroot set -e + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -MODULE="script_engine" +MOD=script_engine -echo "[$MODULE] ==== Step 1/4: 安装生成工具 xls2ddl ====" -if ! command -v json2ddl >/dev/null 2>&1 || ! command -v xls2ui >/dev/null 2>&1; then - pip install xls2ddl >/dev/null 2>&1 || echo "[$MODULE] 警告: xls2ddl 安装失败(宿主已装则忽略)" -fi - -echo "[$MODULE] ==== Step 2/4: 生成 DDL ====" -if [ -d "$SCRIPT_DIR/models" ]; then - if command -v json2ddl >/dev/null 2>&1; then - json2ddl mysql "$SCRIPT_DIR/models" > "$SCRIPT_DIR/models/mysql.ddl.sql" - echo "[$MODULE] DDL 已生成: models/mysql.ddl.sql" - else - echo "[$MODULE] 警告: json2ddl 不可用,跳过 DDL 生成" - fi -fi - -echo "[$MODULE] ==== Step 3/4: 生成 CRUD UI ====" -if [ -d "$SCRIPT_DIR/json" ]; then - if command -v xls2ui >/dev/null 2>&1; then - (cd "$SCRIPT_DIR" && xls2ui -m models -o wwwroot "$MODULE" json/*.json) - echo "[$MODULE] CRUD UI 已生成: wwwroot/$MODULE/" - else - echo "[$MODULE] 警告: xls2ui 不可用,跳过 CRUD UI 生成" - fi -fi - -echo "[$MODULE] ==== Step 4/4: 链接 wwwroot 到宿主 ====" +# locate sage root SAGE_ROOT="" for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do - if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then - SAGE_ROOT="$(cd "$candidate" && pwd)" - break - fi + if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then + SAGE_ROOT="$(cd "$candidate" && pwd)" + break + fi done -if [ -n "$SAGE_ROOT" ]; then - mkdir -p "$SAGE_ROOT/wwwroot/$MODULE/api" - ln -sfn "$SCRIPT_DIR/wwwroot/index.ui" "$SAGE_ROOT/wwwroot/$MODULE/index.ui" - for f in "$SCRIPT_DIR"/wwwroot/api/*.dspy; do - [ -f "$f" ] && ln -sfn "$f" "$SAGE_ROOT/wwwroot/$MODULE/api/$(basename "$f")" - done - for d in "$SCRIPT_DIR"/wwwroot/*/; do - [ -d "$d" ] || continue - name="$(basename "$d")" - case "$name" in api|styles|scripts) continue ;; esac - ln -sfn "$d" "$SAGE_ROOT/wwwroot/$MODULE/$name" - done - echo "[$MODULE] wwwroot 已链接到 $SAGE_ROOT/wwwroot/$MODULE" -else - echo "[$MODULE] 未找到宿主 wwwroot,跳过链接" + +echo "== [1/4] install xls2ddl ==" +pip install xls2ddl >/dev/null 2>&1 || pip3 install xls2ddl >/dev/null 2>&1 || true + +echo "== [2/4] models -> DDL ==" +cd "$SCRIPT_DIR/models" +json2ddl mysql . > mysql.ddl.sql 2>/dev/null || \ + "$SAGE_ROOT/py3/bin/python3" "$SAGE_ROOT/py3/bin/json2ddl" mysql . > mysql.ddl.sql + +echo "== [3/4] json CRUD -> wwwroot ==" +cd "$SCRIPT_DIR" +if [ -d json ] && ls json/*.json >/dev/null 2>&1; then + PYTHONPATH="$SAGE_ROOT/py3/bin" python3 -m xls2ddl.xls2crud -m models -o wwwroot "$MOD" json/*.json || \ + "$SAGE_ROOT/py3/bin/xls2ui" -m models -o wwwroot "$MOD" json/*.json || true fi -echo "[$MODULE] ==== build.sh 四步安装完成 ====" +echo "== [4/4] symlink module wwwroot -> sage wwwroot ==" +if [ -n "$SAGE_ROOT" ]; then + ln -sfn "$SCRIPT_DIR/wwwroot" "$SAGE_ROOT/wwwroot/$MOD" + for d in "$SCRIPT_DIR"/wwwroot/*/; do + [ -d "$d" ] || continue + base="$(basename "$d")" + case "$base" in api|styles|scripts) continue ;; esac + ln -sfn "$d" "$SAGE_ROOT/wwwroot/$base" + done + echo "symlinked $MOD wwwroot into $SAGE_ROOT/wwwroot" +else + echo "WARN: sage root not found, skip symlink (run from a full sage checkout)" +fi + +echo "== script_engine build done ==" diff --git a/init/data.json b/init/data.json index 9496401..886c678 100644 --- a/init/data.json +++ b/init/data.json @@ -5,8 +5,7 @@ "parentname": "脚本类型", "items": [ {"k": "0", "v": "Python"}, - {"k": "1", "v": "JavaScript"}, - {"k": "2", "v": "规则表达式"} + {"k": "1", "v": "SQL"} ] }, { diff --git a/json/script_engine.json b/json/script_engine.json index 67d1e66..bf2c14e 100644 --- a/json/script_engine.json +++ b/json/script_engine.json @@ -1,19 +1,8 @@ { - "tblname": "script", + "tblname": "script_engine", + "title": "脚本管理", "params": { - "listname": "脚本列表", - "browserfields": { - "gridwidth": "100%", - "fields": [ - {"field": "script_name", "label": "脚本名称", "width": 150, "searchable": true}, - {"field": "script_type", "label": "脚本类型", "width": 100}, - {"field": "status", "label": "状态", "width": 80}, - {"field": "description", "label": "描述", "width": 220}, - {"field": "created_at", "label": "创建时间", "width": 150}, - {"field": "updated_at", "label": "更新时间", "width": 150} - ] - }, - "editexclouded": ["script_name", "script_type", "content", "description", "status"], + "sortby": ["created_at desc"], "new_data_url": "{{entire_url('../api/script_create.dspy')}}", "update_data_url": "{{entire_url('../api/script_update.dspy')}}", "delete_data_url": "{{entire_url('../api/script_delete.dspy')}}", @@ -21,6 +10,14 @@ "new_data_url": "{{entire_url('../api/script_create.dspy')}}", "update_data_url": "{{entire_url('../api/script_update.dspy')}}", "delete_data_url": "{{entire_url('../api/script_delete.dspy')}}" - } + }, + "browserfields": { + "exclouded": ["content"], + "alters": { + "script_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_script_type.dspy')}}"}, + "status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_status.dspy')}}"} + } + }, + "editexclouded": ["id", "created_at", "updated_at"] } } diff --git a/models/script_engine.json b/models/script_engine.json index d364a8b..3ffe0eb 100644 --- a/models/script_engine.json +++ b/models/script_engine.json @@ -1,25 +1,25 @@ { "summary": [ { - "table": "script", + "name": "script_engine", + "title": "脚本表", "primary": ["id"], - "name": "脚本表", - "desc": "脚本/规则引擎脚本表(W-06 脚本引擎)" + "catelog": "entity" } ], "fields": [ - {"name": "id", "type": "str32", "notnull": true, "comment": "脚本ID(主键)"}, - {"name": "script_name", "type": "str100", "notnull": true, "comment": "脚本名称"}, - {"name": "script_type", "type": "str32", "default": "0", "comment": "脚本类型: 0=Python/1=JavaScript/2=规则表达式"}, - {"name": "content", "type": "text", "notnull": true, "comment": "脚本内容"}, - {"name": "description", "type": "str255", "comment": "脚本描述"}, - {"name": "status", "type": "str32", "default": "1", "comment": "状态: 0=停用/1=启用"}, - {"name": "created_at", "type": "datetime", "comment": "创建时间"}, - {"name": "updated_at", "type": "datetime", "comment": "更新时间"} + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "script_name", "title": "脚本名称", "type": "str", "length": 100, "nullable": "no"}, + {"name": "script_type", "title": "脚本类型", "type": "str", "length": 32, "nullable": "no", "default": "0"}, + {"name": "content", "title": "脚本内容", "type": "text", "nullable": "no"}, + {"name": "description", "title": "描述", "type": "str", "length": 255}, + {"name": "status", "title": "状态", "type": "str", "length": 32, "nullable": "no", "default": "1"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"} ], "indexes": [ - {"name": "idx_script_name", "fields": ["script_name"]}, - {"name": "idx_script_type", "fields": ["script_type"]} + {"name": "idx_script_name", "idxtype": "index", "idxfields": ["script_name"]}, + {"name": "idx_script_type", "idxtype": "index", "idxfields": ["script_type"]} ], "codes": [ {"field": "script_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='script_type'"}, diff --git a/pyproject.toml b/pyproject.toml index 9156478..d1b4b55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,6 @@ build-backend = "setuptools.build_meta" [project] name = "script_engine" version = "1.0.0" -description = "脚本引擎模块(W-06):脚本 CRUD + execute_script/validate_script" requires-python = ">=3.8" dependencies = ["sqlor", "bricks_for_python"] diff --git a/script_engine/__init__.py b/script_engine/__init__.py index 6b6760e..76ec880 100644 --- a/script_engine/__init__.py +++ b/script_engine/__init__.py @@ -1,9 +1,24 @@ # -*- coding: utf-8 -*- -"""script_engine module -- script/rule engine (W-06). +"""script_engine package: script table CRUD + execute/validate script API. -Mounted into a host application via load_script_engine(). +Public async functions MUST be imported here so that .dspy files can call them +as pre-loaded globals after load_script_engine() registration. """ -from script_engine.engine import validate_script as validate_script, execute_script_content as execute_script_content -from script_engine.init import (load_script_engine as load_script_engine, create_script as create_script, update_script as update_script, delete_script as delete_script, list_scripts as list_scripts, get_script as get_script, execute_script as execute_script, validate_script_api as validate_script_api) +from .engine import validate, validate_python, validate_sql, execute_python +from .init import ( + create_script, + update_script, + delete_script, + get_script, + list_scripts, + execute_script, + validate_script_api, + load_script_engine, +) -__all__ = ['load_script_engine', 'create_script', 'update_script', 'delete_script', 'list_scripts', 'get_script', 'execute_script', 'validate_script_api', 'validate_script', 'execute_script_content'] +__all__ = [ + 'validate', 'validate_python', 'validate_sql', 'execute_python', + 'create_script', 'update_script', 'delete_script', 'get_script', + 'list_scripts', 'execute_script', 'validate_script_api', + 'load_script_engine', +] diff --git a/script_engine/engine.py b/script_engine/engine.py index d8c2b1b..7c98401 100644 --- a/script_engine/engine.py +++ b/script_engine/engine.py @@ -1,107 +1,103 @@ # -*- coding: utf-8 -*- -"""script_engine.engine -- script engine core (pure logic, no DB dependency).""" +"""script_engine execution engine. + +Restricted script execution for script_type 0=Python / 1=SQL. +Python runs in a whitelisted-builtins namespace; imports, classes, lambdas, +async/await, generators and object method calls are forbidden. +SQL allows read-only statements only (SELECT/SHOW/DESCRIBE/EXPLAIN). +""" import ast -import builtins -import json as _json +import re -SCRIPT_TYPE_PYTHON = '0' -SCRIPT_TYPE_JS = '1' -SCRIPT_TYPE_RULE = '2' -VALID_SCRIPT_TYPES = (SCRIPT_TYPE_PYTHON, SCRIPT_TYPE_JS, SCRIPT_TYPE_RULE) +# whitelisted python builtins (safe pure functions only) +ALLOWED_BUILTINS = { + 'abs', 'all', 'any', 'bool', 'dict', 'divmod', 'enumerate', 'filter', + 'float', 'int', 'isinstance', 'len', 'list', 'map', 'max', 'min', 'ord', + 'chr', 'pow', 'range', 'repr', 'round', 'set', 'sorted', 'str', 'sum', + 'tuple', 'type', 'zip', +} -_ALLOWED_BUILTINS = frozenset(['abs', 'all', 'any', 'bool', 'dict', 'divmod', 'enumerate', 'filter', 'float', 'format', 'frozenset', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'list', 'map', 'max', 'min', 'next', 'object', 'pow', 'range', 'repr', 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum', 'tuple', 'zip']) +# ast node types that are never allowed in a script +FORBIDDEN_NODES = ( + ast.Import, ast.ImportFrom, ast.ClassDef, ast.Lambda, + ast.AsyncFunctionDef, ast.AsyncFor, ast.AsyncWith, ast.Await, + ast.Yield, ast.YieldFrom, ast.Global, +) -_FORBIDDEN_NODES = (ast.Import, ast.ImportFrom, ast.Global, ast.Nonlocal, ast.Lambda, ast.ClassDef, ast.Yield, ast.YieldFrom, ast.AsyncFunctionDef, ast.Await) - - -def _pair_check(text, open_ch, close_ch): - depth = 0 - for ch in text: - if ch == open_ch: - depth += 1 - elif ch == close_ch: - depth -= 1 - if depth < 0: - return False - return depth == 0 +# sql keywords that indicate write / dangerous statements +SQL_FORBIDDEN_RE = re.compile( + r'\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|' + r'replace|call|exec|execute|merge|rename|lock|unlock|set|use)\b', + re.IGNORECASE, +) +SQL_READONLY_RE = re.compile(r'^\s*(select|show|describe|desc|explain)\b', re.IGNORECASE) def validate_python(content): + """Validate python script syntax and forbidden constructs. + + Returns {'code': 0, 'message': 'ok'} on success else {'code': 1, 'message': ...}. + """ if not content or not content.strip(): - return False, 'script content must not be empty' + return {'code': 1, 'message': 'content is required'} try: tree = ast.parse(content, mode='exec') except SyntaxError as e: - return False, 'syntax error: %s (line %s)' % (e.msg or 'unknown', e.lineno or 0) + return {'code': 1, 'message': 'syntax error: %s' % e} for node in ast.walk(tree): - if isinstance(node, _FORBIDDEN_NODES): - return False, 'forbidden syntax: %s (line %s)' % (type(node).__name__, getattr(node, 'lineno', 0)) + if isinstance(node, FORBIDDEN_NODES): + return {'code': 1, 'message': 'forbidden construct: %s' % type(node).__name__} if isinstance(node, ast.Call): func = node.func - if isinstance(func, ast.Name): - if func.id not in _ALLOWED_BUILTINS: - return False, 'forbidden function call: %s (line %s)' % (func.id, getattr(node, 'lineno', 0)) - elif isinstance(func, ast.Attribute): - return False, 'forbidden method call: %s (line %s)' % (func.attr, getattr(node, 'lineno', 0)) - return True, '' + if isinstance(func, ast.Attribute): + return {'code': 1, 'message': 'object method call is forbidden'} + if isinstance(func, ast.Name) and func.id not in ALLOWED_BUILTINS: + return {'code': 1, 'message': 'forbidden call: %s' % func.id} + return {'code': 0, 'message': 'ok'} -def validate_js(content): - if not content or not content.strip(): - return False, 'script content must not be empty' - if not _pair_check(content, '{', '}'): - return False, 'unbalanced braces' - if not _pair_check(content, '(', ')'): - return False, 'unbalanced parentheses' - if not _pair_check(content, '[', ']'): - return False, 'unbalanced brackets' - return True, '' +def execute_python(content, input_ns=None): + """Execute python script in a restricted namespace. - -def validate_rule(content): - if not content or not content.strip(): - return False, 'script content must not be empty' - text = content.strip() - if text.startswith('{') or text.startswith('['): - try: - _json.loads(text) - return True, '' - except Exception as e: - return False, 'rule json parse failed: %s' % str(e) - if '->' not in text: - return False, 'rule format should be json or "condition -> action"' - return True, '' - - -def validate_script(content, script_type): - stype = str(script_type or SCRIPT_TYPE_PYTHON) - if stype not in VALID_SCRIPT_TYPES: - return False, 'unsupported script type: %s' % stype - if stype == SCRIPT_TYPE_PYTHON: - return validate_python(content) - if stype == SCRIPT_TYPE_JS: - return validate_js(content) - return validate_rule(content) - - -def execute_python(content, params): - safe_builtins = {} - for name in _ALLOWED_BUILTINS: - if hasattr(builtins, name): - safe_builtins[name] = getattr(builtins, name) - namespace = {'__name__': '__script_engine__', '__builtins__': safe_builtins, 'params': params} - code = compile(content, '', 'exec') + The script may assign a final variable named `result` which is returned. + """ + builtins_ns = {} + for k in ALLOWED_BUILTINS: + if k in __builtins__: + builtins_ns[k] = __builtins__[k] + namespace = {'__builtins__': builtins_ns} + if input_ns: + for k, v in input_ns.items(): + if k and k != '__builtins__': + namespace[k] = v + code = compile(content, '