programgarden-core 1.15.2__tar.gz → 1.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/PKG-INFO +1 -1
- programgarden_core-1.16.0/programgarden_core/code_node.py +411 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/locales/en.json +7 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/locales/ko.json +7 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/validation.py +7 -3
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/__init__.py +2 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_futures.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_stock.py +1 -1
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest.py +5 -5
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest_futures.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest_korea_stock.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest_stock.py +2 -2
- programgarden_core-1.16.0/programgarden_core/nodes/code.py +338 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data.py +5 -5
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/display.py +40 -24
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/order.py +15 -14
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/realtime_futures.py +22 -3
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/realtime_korea_stock.py +22 -3
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/realtime_stock.py +22 -3
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/risk.py +64 -30
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol.py +6 -4
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/__init__.py +0 -12
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/node_registry.py +2 -2
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/pyproject.toml +1 -1
- programgarden_core-1.15.2/programgarden_core/registry/dynamic_node_registry.py +0 -409
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/README.md +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/client.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/components.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/listener.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/mixins.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/products.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/sql.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/bases/storage.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/exceptions.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/expression/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/expression/evaluator.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/translator.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/korea_alias.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/connection_rule.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/credential.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/edge.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/event.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/exchange.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/field_binding.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/job.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/order_diagnostics.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/plugin_resource.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/resilience.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/resource.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/workflow.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/ai.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/base.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/broker.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/calculation.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/condition.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data_futures.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/data_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/event.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/infra.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/market_external.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/market_status.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/open_orders_futures.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/portfolio.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol_futures.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/symbol_stock.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/trigger.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/__init__.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/news_analyst.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/risk_manager.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/strategist.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/presets/technical_analyst.json +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/credential_registry.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/registry/plugin_registry.py +0 -0
- {programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/retry_executor.py +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""ProgramGarden Core — CodeNode static compilation & AST security screening.
|
|
2
|
+
|
|
3
|
+
This module is the single source of truth for the CodeNode security lists and
|
|
4
|
+
the compile/screen pipeline. It is intentionally dependency-free (stdlib only)
|
|
5
|
+
so it can be reused verbatim by:
|
|
6
|
+
|
|
7
|
+
1. `WorkflowResolver.validate()` — pre-flight structured errors before run.
|
|
8
|
+
2. `CodeNodeExecutor` / subprocess worker — screen again right before `exec`.
|
|
9
|
+
3. An external web-platform "share gate" server — re-run the same hard AST scan.
|
|
10
|
+
|
|
11
|
+
Security model (see plan §2): CodeNode runs untrusted Python. Defense is layered:
|
|
12
|
+
|
|
13
|
+
* Layer 1 — scrubbed context (runtime, programgarden): the code never receives
|
|
14
|
+
credential accessors.
|
|
15
|
+
* Layer 2 — restricted builtins + AST denylist (THIS module): a curated
|
|
16
|
+
`__builtins__` (no eval/exec/open/getattr/...), a safe `__import__` gated by a
|
|
17
|
+
whitelist, and a static AST scan that rejects dangerous imports, builtin
|
|
18
|
+
calls, and introspection-escape dunders BEFORE any code runs.
|
|
19
|
+
* Layer 3 — binding seal (runtime, resolver): credentials cannot flow into
|
|
20
|
+
`data`/`params`.
|
|
21
|
+
* Layer 4 — subprocess isolation (runtime, programgarden): the code always runs
|
|
22
|
+
in a credential-free child process.
|
|
23
|
+
|
|
24
|
+
`compile_code_node` NEVER executes the code — it only parses, screens, and
|
|
25
|
+
compiles. Execution (exec + calling `execute`) is the runtime's responsibility.
|
|
26
|
+
|
|
27
|
+
All user-facing strings are English (AI-chatbot consumer contract).
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import ast
|
|
32
|
+
import builtins as _builtins
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from types import CodeType
|
|
35
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
36
|
+
|
|
37
|
+
# Name of the entry-point coroutine the code text must define.
|
|
38
|
+
EXECUTE_FN_NAME = "execute"
|
|
39
|
+
|
|
40
|
+
# ⚠️ SECURITY POSTURE — READ THIS.
|
|
41
|
+
# AST screening + restricted builtins CANNOT be a sound sandbox for CPython.
|
|
42
|
+
# A determined attacker who can run Python can, in the general case, escape any
|
|
43
|
+
# in-process denylist (this is a well-known result; see the pysandbox author's
|
|
44
|
+
# own withdrawal). An adversarial audit of an earlier version of this file
|
|
45
|
+
# reproduced RCE via `operator.attrgetter`, `random._os`, and string-obfuscated
|
|
46
|
+
# dunders. The rules below are HARDENED defense-in-depth that close every
|
|
47
|
+
# demonstrated vector and raise the bar substantially — but they are NOT a
|
|
48
|
+
# containment guarantee. TRUE containment of untrusted code is the runtime's
|
|
49
|
+
# job (subprocess with no in-memory keys) + the operator's job (per-user pod
|
|
50
|
+
# isolation, no-egress NetworkPolicy, restricted file perms, scrubbed env).
|
|
51
|
+
# Treat CodeNode as "run trusted code, defense-in-depth against mistakes and
|
|
52
|
+
# opportunistic abuse", not "safe to run arbitrary hostile code".
|
|
53
|
+
|
|
54
|
+
# ── Whitelist: modules importable from CodeNode code ────────────────────────
|
|
55
|
+
# Pure-computation stdlib only. `operator` is deliberately EXCLUDED — its
|
|
56
|
+
# attrgetter/methodcaller are getattr/dispatch substitutes. Modules that
|
|
57
|
+
# re-export os/sys as public attributes (e.g. random._os, uuid.os) are still
|
|
58
|
+
# covered by the attribute screen below, not by this set. The embedding app may
|
|
59
|
+
# EXTEND this via `allowed_imports=` (e.g. numpy) but the default is minimal.
|
|
60
|
+
DEFAULT_ALLOWED_IMPORTS: frozenset = frozenset({
|
|
61
|
+
"math", "cmath", "statistics", "decimal", "fractions", "numbers",
|
|
62
|
+
"random", "json", "re", "datetime", "time", "calendar",
|
|
63
|
+
"collections", "itertools", "functools",
|
|
64
|
+
"string", "textwrap", "unicodedata",
|
|
65
|
+
"heapq", "bisect", "array", "copy",
|
|
66
|
+
"dataclasses", "enum", "typing",
|
|
67
|
+
"hashlib", "hmac", "base64", "binascii", "zlib",
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
# ── Denylist: modules called out for a clearer error message ────────────────
|
|
71
|
+
# Enforcement is whitelist-based (not in DEFAULT_ALLOWED_IMPORTS → blocked);
|
|
72
|
+
# this set only makes the message explicit for well-known dangerous modules.
|
|
73
|
+
FORBIDDEN_MODULES: frozenset = frozenset({
|
|
74
|
+
"os", "sys", "subprocess", "socket", "shutil", "importlib", "imp",
|
|
75
|
+
"ctypes", "cffi", "gc", "inspect", "pathlib", "glob", "tempfile", "io",
|
|
76
|
+
"urllib", "http", "requests", "ftplib", "smtplib", "asyncio",
|
|
77
|
+
"multiprocessing", "threading", "concurrent", "_thread", "operator",
|
|
78
|
+
"builtins", "marshal", "pickle", "shelve", "dbm", "sqlite3",
|
|
79
|
+
"code", "codeop", "pty", "resource", "signal", "mmap", "fcntl",
|
|
80
|
+
"platform", "webbrowser", "ssl", "select", "selectors", "uuid",
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
# ── Denylist: builtin names that must never be called or aliased ────────────
|
|
84
|
+
FORBIDDEN_BUILTINS: frozenset = frozenset({
|
|
85
|
+
"eval", "exec", "compile", "open", "__import__",
|
|
86
|
+
"getattr", "setattr", "delattr",
|
|
87
|
+
"globals", "locals", "vars",
|
|
88
|
+
"input", "breakpoint", "help", "memoryview",
|
|
89
|
+
"exit", "quit", "__build_class__",
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# ── Denylist: dangerous PUBLIC attribute names (module re-exports & escapes) ──
|
|
93
|
+
# Many stdlib modules re-export `os`/`sys` as public attributes (statistics.sys,
|
|
94
|
+
# uuid.os, ...) — walking those reaches os.system. `mro`/`modules`/`system`/etc.
|
|
95
|
+
# are the follow-on hops. Blocking these attribute NAMES (in addition to the
|
|
96
|
+
# blanket underscore rule below) severs the module-attribute escape path.
|
|
97
|
+
FORBIDDEN_ATTR_NAMES: frozenset = frozenset({
|
|
98
|
+
"os", "sys", "subprocess", "socket", "builtins", "importlib",
|
|
99
|
+
"modules", "system", "popen", "spawn", "spawnl", "spawnv", "spawnve",
|
|
100
|
+
"fork", "forkpty", "execv", "execve", "execl", "execlp", "execvp",
|
|
101
|
+
"mro", "import_module", "load_module", "reload", "getattr", "setattr",
|
|
102
|
+
"delattr", "globals", "environ", "getenv", "putenv", "connect",
|
|
103
|
+
"urlopen", "call", "run", "check_output", "Popen", "loader", "spec",
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
# ── Denylist: introspection-escape / exfiltration dunders & frame attrs ──────
|
|
107
|
+
# NOTE: enforcement is the BLANKET underscore rule in _scan_ast (any attribute
|
|
108
|
+
# or string beginning with '_' is rejected), which subsumes every dunder. This
|
|
109
|
+
# explicit set is retained for documentation / message clarity.
|
|
110
|
+
FORBIDDEN_DUNDERS: frozenset = frozenset({
|
|
111
|
+
"__class__", "__bases__", "__base__", "__subclasses__", "__mro__",
|
|
112
|
+
"__globals__", "__code__", "__closure__", "__func__", "__self__",
|
|
113
|
+
"__dict__", "__getattribute__", "__setattr__", "__delattr__",
|
|
114
|
+
"__reduce__", "__reduce_ex__", "__builtins__", "__import__",
|
|
115
|
+
"__loader__", "__spec__", "__init_subclass__", "__subclasshook__",
|
|
116
|
+
"__getattr__", "__wrapped__", "__objclass__",
|
|
117
|
+
"gi_frame", "gi_code", "cr_frame", "cr_code", "ag_frame",
|
|
118
|
+
"f_back", "f_globals", "f_locals", "f_builtins", "tb_frame",
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
# Format replacement-field that performs attribute access, e.g. "{0.__class__}"
|
|
122
|
+
# or "{x.sys}" — used to walk attributes via str.format without an AST Attribute
|
|
123
|
+
# node. Rejected wholesale in string constants.
|
|
124
|
+
import re as _re
|
|
125
|
+
_FORMAT_ATTR_RE = _re.compile(r"\{[^{}]*\.[^{}]*\}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class CodeScreenResult:
|
|
130
|
+
"""Structured outcome of `compile_code_node`. `ok=False` carries an
|
|
131
|
+
AI-chatbot-consumable error (code + message + suggestion + location)."""
|
|
132
|
+
|
|
133
|
+
ok: bool
|
|
134
|
+
code_object: Optional[CodeType] = None
|
|
135
|
+
error_code: Optional[str] = None # one of CODE_NODE_SYNTAX_ERROR / _FORBIDDEN / _NO_EXECUTE
|
|
136
|
+
message: Optional[str] = None
|
|
137
|
+
suggestion: Optional[str] = None
|
|
138
|
+
line: Optional[int] = None
|
|
139
|
+
offset: Optional[int] = None
|
|
140
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _module_root(name: Optional[str]) -> str:
|
|
144
|
+
return (name or "").split(".")[0]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _scan_ast(tree: ast.AST, allowed_imports: Set[str]) -> List[Dict[str, Any]]:
|
|
148
|
+
"""Return a list of violation dicts (empty when clean).
|
|
149
|
+
|
|
150
|
+
Each violation: {kind, name, line, col, reason}. `kind` ∈
|
|
151
|
+
{import, builtin, dunder}.
|
|
152
|
+
"""
|
|
153
|
+
violations: List[Dict[str, Any]] = []
|
|
154
|
+
|
|
155
|
+
for node in ast.walk(tree):
|
|
156
|
+
# ── Imports (whitelist-enforced) ──
|
|
157
|
+
if isinstance(node, ast.Import):
|
|
158
|
+
for alias in node.names:
|
|
159
|
+
root = _module_root(alias.name)
|
|
160
|
+
if root not in allowed_imports:
|
|
161
|
+
reason = (
|
|
162
|
+
f"module '{alias.name}' is blocked (filesystem/process/network access)"
|
|
163
|
+
if root in FORBIDDEN_MODULES
|
|
164
|
+
else f"module '{alias.name}' is not in the allowed import list"
|
|
165
|
+
)
|
|
166
|
+
violations.append({
|
|
167
|
+
"kind": "import", "name": alias.name,
|
|
168
|
+
"line": node.lineno, "col": node.col_offset, "reason": reason,
|
|
169
|
+
})
|
|
170
|
+
elif isinstance(node, ast.ImportFrom):
|
|
171
|
+
root = _module_root(node.module)
|
|
172
|
+
# `from . import x` (level>0) is always blocked — no package context.
|
|
173
|
+
if node.level and node.level > 0:
|
|
174
|
+
violations.append({
|
|
175
|
+
"kind": "import", "name": node.module or ".",
|
|
176
|
+
"line": node.lineno, "col": node.col_offset,
|
|
177
|
+
"reason": "relative imports are not allowed in CodeNode",
|
|
178
|
+
})
|
|
179
|
+
elif root not in allowed_imports:
|
|
180
|
+
reason = (
|
|
181
|
+
f"module '{node.module}' is blocked (filesystem/process/network access)"
|
|
182
|
+
if root in FORBIDDEN_MODULES
|
|
183
|
+
else f"module '{node.module}' is not in the allowed import list"
|
|
184
|
+
)
|
|
185
|
+
violations.append({
|
|
186
|
+
"kind": "import", "name": node.module or "",
|
|
187
|
+
"line": node.lineno, "col": node.col_offset, "reason": reason,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
# ── Forbidden builtin names (call or alias) ──
|
|
191
|
+
elif isinstance(node, ast.Name):
|
|
192
|
+
if node.id in FORBIDDEN_BUILTINS:
|
|
193
|
+
violations.append({
|
|
194
|
+
"kind": "builtin", "name": node.id,
|
|
195
|
+
"line": node.lineno, "col": node.col_offset,
|
|
196
|
+
"reason": f"use of '{node.id}' is not allowed",
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
# ── Attribute access screen ──
|
|
200
|
+
# (1) BLANKET: any attribute beginning with '_' is rejected — this
|
|
201
|
+
# subsumes every dunder (__class__/__globals__/__subclasses__/...)
|
|
202
|
+
# AND private module re-exports (random._os, random._urandom, ...).
|
|
203
|
+
# (2) A denylist of dangerous PUBLIC names (module re-exports & shell
|
|
204
|
+
# hops): statistics.sys, uuid.os, x.system, x.modules, type().mro().
|
|
205
|
+
elif isinstance(node, ast.Attribute):
|
|
206
|
+
if node.attr.startswith("_"):
|
|
207
|
+
violations.append({
|
|
208
|
+
"kind": "dunder", "name": node.attr,
|
|
209
|
+
"line": node.lineno, "col": node.col_offset,
|
|
210
|
+
"reason": f"access to underscore attribute '{node.attr}' is not allowed (introspection escape)",
|
|
211
|
+
})
|
|
212
|
+
elif node.attr in FORBIDDEN_ATTR_NAMES:
|
|
213
|
+
violations.append({
|
|
214
|
+
"kind": "attr", "name": node.attr,
|
|
215
|
+
"line": node.lineno, "col": node.col_offset,
|
|
216
|
+
"reason": f"attribute '{node.attr}' access is not allowed (module/escape re-export)",
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
# ── String-constant screen ──
|
|
220
|
+
# Defeats attribute-walk via strings: getattr/attrgetter args, str.format
|
|
221
|
+
# attribute fields, subscript-string dunder access. Reject strings that
|
|
222
|
+
# start with '_', contain a dunder, name a dangerous attribute, or embed
|
|
223
|
+
# a format field with attribute access.
|
|
224
|
+
elif isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
225
|
+
v = node.value
|
|
226
|
+
bad = None
|
|
227
|
+
if v.startswith("_"):
|
|
228
|
+
bad = f"string literal '{v}' begins with '_' (introspection escape)"
|
|
229
|
+
elif "__" in v:
|
|
230
|
+
bad = f"string literal contains a dunder (introspection escape)"
|
|
231
|
+
elif v in FORBIDDEN_ATTR_NAMES:
|
|
232
|
+
bad = f"string literal '{v}' names a blocked attribute"
|
|
233
|
+
elif _FORMAT_ATTR_RE.search(v):
|
|
234
|
+
bad = "format string performs attribute access (introspection escape)"
|
|
235
|
+
if bad:
|
|
236
|
+
violations.append({
|
|
237
|
+
"kind": "string", "name": v[:40],
|
|
238
|
+
"line": node.lineno, "col": node.col_offset,
|
|
239
|
+
"reason": bad,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return violations
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _has_execute(tree: ast.Module) -> bool:
|
|
246
|
+
for node in tree.body:
|
|
247
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == EXECUTE_FN_NAME:
|
|
248
|
+
return True
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def compile_code_node(
|
|
253
|
+
code: str,
|
|
254
|
+
node_id: str,
|
|
255
|
+
*,
|
|
256
|
+
screen: bool = True,
|
|
257
|
+
allowed_imports: Optional[Set[str]] = None,
|
|
258
|
+
) -> CodeScreenResult:
|
|
259
|
+
"""Parse, security-screen, and compile CodeNode source WITHOUT executing it.
|
|
260
|
+
|
|
261
|
+
Pipeline:
|
|
262
|
+
1. `ast.parse` — SyntaxError → CODE_NODE_SYNTAX_ERROR (line/offset).
|
|
263
|
+
2. AST denylist scan (when `screen=True`) — CODE_NODE_FORBIDDEN.
|
|
264
|
+
3. `execute` function must be defined — CODE_NODE_NO_EXECUTE.
|
|
265
|
+
4. `compile(filename=f'<code:{node_id}>')` — preserves traceback lines.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
code: Python source text (must define `execute`).
|
|
269
|
+
node_id: node id, used only for the compiled filename (traceback anchor).
|
|
270
|
+
screen: run the AST denylist scan (always True in production).
|
|
271
|
+
allowed_imports: override the import whitelist (defaults to
|
|
272
|
+
DEFAULT_ALLOWED_IMPORTS). Enforced both here and by the runtime
|
|
273
|
+
safe `__import__`.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
CodeScreenResult — `ok=True` with `code_object`, or `ok=False` with a
|
|
277
|
+
structured, English, chatbot-consumable error. Never raises for
|
|
278
|
+
screening/syntax failures (they are returned as data).
|
|
279
|
+
"""
|
|
280
|
+
allowed = set(allowed_imports) if allowed_imports is not None else set(DEFAULT_ALLOWED_IMPORTS)
|
|
281
|
+
|
|
282
|
+
if not isinstance(code, str) or not code.strip():
|
|
283
|
+
return CodeScreenResult(
|
|
284
|
+
ok=False,
|
|
285
|
+
error_code="CODE_NODE_SYNTAX_ERROR",
|
|
286
|
+
message="CodeNode 'code' is empty.",
|
|
287
|
+
suggestion="Provide Python source that defines `async def execute(data, params, context)`.",
|
|
288
|
+
line=1, offset=0,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# 1. Parse
|
|
292
|
+
try:
|
|
293
|
+
tree = ast.parse(code, filename=f"<code:{node_id}>", mode="exec")
|
|
294
|
+
except SyntaxError as e:
|
|
295
|
+
return CodeScreenResult(
|
|
296
|
+
ok=False,
|
|
297
|
+
error_code="CODE_NODE_SYNTAX_ERROR",
|
|
298
|
+
message=f"CodeNode syntax error: {e.msg}",
|
|
299
|
+
suggestion="Fix the Python syntax at the reported line/offset.",
|
|
300
|
+
line=e.lineno,
|
|
301
|
+
offset=e.offset,
|
|
302
|
+
details={"raw": str(e)},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# 2. Screen
|
|
306
|
+
if screen:
|
|
307
|
+
violations = _scan_ast(tree, allowed)
|
|
308
|
+
if violations:
|
|
309
|
+
first = violations[0]
|
|
310
|
+
names = sorted({v["name"] for v in violations})
|
|
311
|
+
return CodeScreenResult(
|
|
312
|
+
ok=False,
|
|
313
|
+
error_code="CODE_NODE_FORBIDDEN",
|
|
314
|
+
message=(
|
|
315
|
+
f"CodeNode uses forbidden construct(s): {', '.join(names)} "
|
|
316
|
+
f"({first['reason']})."
|
|
317
|
+
),
|
|
318
|
+
suggestion=(
|
|
319
|
+
"Remove the blocked import/builtin/introspection. CodeNode allows "
|
|
320
|
+
"pure-computation stdlib only (math, statistics, json, datetime, ...); "
|
|
321
|
+
"credentials, filesystem, network, subprocess, and object-graph "
|
|
322
|
+
"introspection are not available."
|
|
323
|
+
),
|
|
324
|
+
line=first["line"],
|
|
325
|
+
offset=first["col"],
|
|
326
|
+
details={"violations": violations, "forbidden_names": names},
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# 3. execute() must exist
|
|
330
|
+
if not _has_execute(tree):
|
|
331
|
+
return CodeScreenResult(
|
|
332
|
+
ok=False,
|
|
333
|
+
error_code="CODE_NODE_NO_EXECUTE",
|
|
334
|
+
message=f"CodeNode code must define a function named '{EXECUTE_FN_NAME}'.",
|
|
335
|
+
suggestion=(
|
|
336
|
+
"Define `async def execute(data, params, context):` (a plain "
|
|
337
|
+
"`def execute(...)` is also accepted and auto-wrapped)."
|
|
338
|
+
),
|
|
339
|
+
line=1, offset=0,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# 4. Compile (never exec here)
|
|
343
|
+
try:
|
|
344
|
+
code_object = compile(tree, filename=f"<code:{node_id}>", mode="exec")
|
|
345
|
+
except (SyntaxError, ValueError) as e: # e.g. too-deeply-nested
|
|
346
|
+
return CodeScreenResult(
|
|
347
|
+
ok=False,
|
|
348
|
+
error_code="CODE_NODE_SYNTAX_ERROR",
|
|
349
|
+
message=f"CodeNode failed to compile: {e}",
|
|
350
|
+
suggestion="Simplify the code so it compiles as a module.",
|
|
351
|
+
line=getattr(e, "lineno", 1) or 1,
|
|
352
|
+
offset=getattr(e, "offset", 0) or 0,
|
|
353
|
+
details={"raw": str(e)},
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return CodeScreenResult(ok=True, code_object=code_object)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# The real import machinery, captured once so the sandbox can delegate to it
|
|
360
|
+
# after the whitelist check.
|
|
361
|
+
_REAL_IMPORT = _builtins.__import__
|
|
362
|
+
|
|
363
|
+
# Curated safe builtins — everything a pure-compute function needs, and nothing
|
|
364
|
+
# that grants escape (no eval/exec/compile/open/getattr/setattr/globals/...).
|
|
365
|
+
_SAFE_BUILTIN_NAMES: Tuple[str, ...] = (
|
|
366
|
+
"abs", "all", "any", "ascii", "bin", "bool", "bytearray", "bytes",
|
|
367
|
+
"callable", "chr", "complex", "dict", "divmod", "enumerate", "filter",
|
|
368
|
+
"float", "format", "frozenset", "hash", "hex", "int", "isinstance",
|
|
369
|
+
"issubclass", "iter", "len", "list", "map", "max", "min", "next", "oct",
|
|
370
|
+
"ord", "pow", "print", "range", "repr", "reversed", "round", "set",
|
|
371
|
+
"slice", "sorted", "str", "sum", "tuple", "type", "zip", "hasattr",
|
|
372
|
+
# exceptions & sentinels
|
|
373
|
+
"Exception", "BaseException", "ValueError", "TypeError", "KeyError",
|
|
374
|
+
"IndexError", "AttributeError", "ZeroDivisionError", "ArithmeticError",
|
|
375
|
+
"OverflowError", "RuntimeError", "StopIteration", "StopAsyncIteration",
|
|
376
|
+
"NotImplementedError", "AssertionError", "LookupError", "NameError",
|
|
377
|
+
"RecursionError", "FloatingPointError", "GeneratorExit", "KeyboardInterrupt",
|
|
378
|
+
"True", "False", "None", "NotImplemented", "Ellipsis",
|
|
379
|
+
"object", "property", "staticmethod", "classmethod", "super",
|
|
380
|
+
# __build_class__ is required for `class` statements in user code; it is not
|
|
381
|
+
# itself an escape vector (dunder screen blocks the dangerous follow-ups).
|
|
382
|
+
"__build_class__",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def build_restricted_builtins(allowed_imports: Optional[Set[str]] = None) -> Dict[str, Any]:
|
|
387
|
+
"""Build the `__builtins__` dict used when exec-ing CodeNode source.
|
|
388
|
+
|
|
389
|
+
Contains only the curated safe builtins plus a whitelist-gated
|
|
390
|
+
`__import__`. Pure function (no exec, stdlib only) so it lives in core and
|
|
391
|
+
is reused by the runtime worker.
|
|
392
|
+
"""
|
|
393
|
+
allowed = set(allowed_imports) if allowed_imports is not None else set(DEFAULT_ALLOWED_IMPORTS)
|
|
394
|
+
|
|
395
|
+
def _safe_import(name, globals=None, locals=None, fromlist=(), level=0):
|
|
396
|
+
if level and level != 0:
|
|
397
|
+
raise ImportError("relative imports are not allowed in CodeNode")
|
|
398
|
+
root = _module_root(name)
|
|
399
|
+
if root not in allowed:
|
|
400
|
+
raise ImportError(
|
|
401
|
+
f"import of '{name}' is not allowed in CodeNode "
|
|
402
|
+
f"(allowed: pure-computation stdlib only)"
|
|
403
|
+
)
|
|
404
|
+
return _REAL_IMPORT(name, globals, locals, fromlist, level)
|
|
405
|
+
|
|
406
|
+
safe: Dict[str, Any] = {}
|
|
407
|
+
for bn in _SAFE_BUILTIN_NAMES:
|
|
408
|
+
if hasattr(_builtins, bn):
|
|
409
|
+
safe[bn] = getattr(_builtins, bn)
|
|
410
|
+
safe["__import__"] = _safe_import
|
|
411
|
+
return safe
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/locales/en.json
RENAMED
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
"nodes.CandlestickChartNode.name": "Candlestick Chart",
|
|
55
55
|
"nodes.ConditionNode.description": "Evaluates trading conditions using one of 67 community strategy plugins (RSI, MACD, Bollinger Bands, Ichimoku, ZScore, TurtleBreakout, MagicFormula, SupportResistanceLevels, etc.). Feed OHLCV or price data through the items field with from/extract mapping, and select a plugin with optional parameters. Outputs is_condition_met (boolean), passed_symbols, failed_symbols, and calculated indicator values. Auto-iterates over input symbol arrays. Bind positions field for sell-side conditions (profit target, stop loss). Typical connection: HistoricalDataNode → ConditionNode → LogicNode or → NewOrderNode. Can be used as an AI Agent tool.",
|
|
56
56
|
"nodes.ConditionNode.name": "Condition",
|
|
57
|
+
"nodes.CodeNode.name": "Code",
|
|
58
|
+
"nodes.CodeNode.description": "Runs a custom Python function for logic no existing typed node covers — a bespoke indicator, a custom scoring formula, ad-hoc reshaping. The code defines async def execute(data, params, context) and returns a dict mapped to declared output ports (or a single 'result' port). Always sandboxed: it runs in a credential-free subprocess with restricted builtins, an AST denylist (no os/socket/urllib/subprocess/open or introspection), and a scrubbed read-only context — no credential, broker, or network access. Bind the whole upstream value to 'data' and loop in-code. Downstream nodes consume its outputs by writing {{ nodes.<id>.<port> }} into a generic input field (TableDisplayNode.data, IfNode.left/right, ...), not by typed-port matching — so when feeding a typed order/condition node, return the standard [{symbol, exchange, ...}] shape; for a display/sink or If scalar, any shape works. Typical connection: HistoricalDataNode → CodeNode → IfNode. Prefer typed nodes (ConditionNode/FieldMappingNode/IfNode) whenever they already cover the logic.",
|
|
57
59
|
"nodes.CurrencyRateNode.description": "Fetches real-time currency exchange rates based on European Central Bank (ECB) data. No broker connection or API key required. Configure a base currency and target currencies to get conversion rates. Includes a convenience KRW rate output port for Korean investors tracking USD/KRW. Built-in resilience with retry on timeout and rate-limit errors, plus fallback to open.er-api.com. Typical connection: CurrencyRateNode → ConditionNode (for FX-based signals) or → FieldMappingNode. Can be used as an AI Agent tool.",
|
|
58
60
|
"nodes.CurrencyRateNode.name": "Currency Rate",
|
|
59
61
|
"nodes.ErrorHandlerNode.description": "Handles errors from upstream nodes and executes recovery logic. Routes error data for logging, alerting, or fallback processing. Use to build resilient workflows that gracefully handle API failures, timeout errors, or invalid data.",
|
|
@@ -288,6 +290,10 @@
|
|
|
288
290
|
"fields.CurrencyRateNode.resilience": "Retry and fallback settings for API call failures",
|
|
289
291
|
"fields.CurrencyRateNode.target_currencies": "Target currencies. Select currencies to get exchange rates against the base currency.",
|
|
290
292
|
"fields.CurrencyRateNode.timeout_seconds": "API request timeout (seconds). Default 30s, adjust based on your network conditions.",
|
|
293
|
+
"fields.CodeNode.code": "Python source that defines async def execute(data, params, context). Sandboxed: only pure-computation stdlib imports (math, statistics, json, datetime, ...) are allowed; no credentials, network, filesystem, or introspection.",
|
|
294
|
+
"fields.CodeNode.outputs": "Output port declarations [{name, type}]. Leave empty for a single 'result' port carrying the whole return value. Declared ports are consumed downstream by {{ nodes.<id>.<port> }} expressions written into a consumer's generic input field (TableDisplayNode.data, IfNode.left/right, ...), not by typed-port matching; declaring them enables static typo-guarding.",
|
|
295
|
+
"fields.CodeNode.params": "Parameters passed to execute() as `params`. Expression bindings are allowed (e.g. a period or threshold).",
|
|
296
|
+
"fields.CodeNode.data": "Input data passed to execute() as `data`. Bind the whole upstream value (e.g. {{ nodes.hist.values }}) and loop over it inside the function.",
|
|
291
297
|
"fields.FieldMappingNode.data": "Input data to transform. Bind from HTTPRequestNode response or other node outputs.",
|
|
292
298
|
"fields.FieldMappingNode.mappings": "Field name mapping table. Converts source field names (from) to standard field names (to).",
|
|
293
299
|
"fields.FieldMappingNode.mappings.description": "Field description (for AI agent auto-mapping)",
|
|
@@ -847,6 +853,7 @@
|
|
|
847
853
|
"fieldNames.TradingHoursFilterNode.timezone": "Timezone",
|
|
848
854
|
"fieldNames.WatchlistNode.symbols": "Symbols",
|
|
849
855
|
"outputs.AIAgentNode.response": "AI agent response (text or JSON object depending on output format)",
|
|
856
|
+
"outputs.CodeNode.result": "The whole value returned by execute() when no output ports are declared. When outputs are declared, read the named ports instead.",
|
|
850
857
|
"outputs.ExclusionListNode.count": "Number of excluded symbols",
|
|
851
858
|
"outputs.ExclusionListNode.excluded": "Final exclusion list (manual + dynamic, deduplicated)",
|
|
852
859
|
"outputs.ExclusionListNode.filtered": "Source symbols minus excluded symbols (when input_symbols connected)",
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/i18n/locales/ko.json
RENAMED
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
"nodes.CandlestickChartNode.name": "캔들스틱차트",
|
|
55
55
|
"nodes.ConditionNode.description": "67개 커뮤니티 전략 플러그인(RSI, MACD, Bollinger Bands, Ichimoku, ZScore, TurtleBreakout, MagicFormula, SupportResistanceLevels 등)으로 매매 조건을 평가합니다. items 필드의 from/extract 매핑으로 OHLCV 또는 가격 데이터를 입력하고, 플러그인과 매개변수를 선택합니다. 출력: is_condition_met (boolean), passed_symbols, failed_symbols, 계산된 지표 값. 입력 종목 배열에 대해 자동 반복(auto-iterate) 실행합니다. 매도 조건에는 positions 필드를 바인딩하세요(익절, 손절). 일반적인 연결: HistoricalDataNode → ConditionNode → LogicNode 또는 → NewOrderNode. AI Agent 도구로 사용 가능.",
|
|
56
56
|
"nodes.ConditionNode.name": "조건",
|
|
57
|
+
"nodes.CodeNode.name": "코드",
|
|
58
|
+
"nodes.CodeNode.description": "기존 타입 노드로 표현할 수 없는 커스텀 로직(고유 지표, 커스텀 점수 산식, 임시 데이터 가공)을 파이썬 함수로 실행합니다. 코드는 async def execute(data, params, context) 를 정의하고, 반환한 dict 가 선언한 출력 포트(또는 단일 'result' 포트)로 매핑됩니다. 항상 샌드박스 실행: 앱키 없는 자식 프로세스, 제한된 builtins, AST 차단목록(os/socket/urllib/subprocess/open·introspection 불가), 스크럽된 읽기전용 context — credential·broker·네트워크 접근 없음. 상류 값 전체를 'data' 에 바인딩해 코드 안에서 반복하세요. 다운스트림은 소비자 노드의 범용 입력 필드(TableDisplayNode.data, IfNode.left/right 등)에 {{ nodes.<id>.<port> }} 를 써서 출력을 소비합니다 — 타입드 포트 매칭이 아님. 따라서 타입드 주문/조건 노드로 흘릴 땐 표준 [{symbol, exchange, ...}] 모양을 반환하고, 디스플레이/싱크나 If 스칼라엔 아무 모양이나 됩니다. 일반적 연결: HistoricalDataNode → CodeNode → IfNode. 타입 노드(ConditionNode/FieldMappingNode/IfNode)로 가능한 로직은 그쪽을 우선하세요.",
|
|
57
59
|
"nodes.CurrencyRateNode.description": "유럽중앙은행(ECB) 기반 실시간 환율을 조회합니다. 브로커 연결이나 API 키가 필요 없습니다. 기준 통화와 대상 통화를 설정하여 환율을 가져옵니다. 한국 투자자를 위한 USD/KRW 환율 전용 출력 포트를 제공합니다. 타임아웃 및 속도 제한 오류 시 자동 재시도, open.er-api.com 폴백 기능 내장. 일반적인 연결: CurrencyRateNode → ConditionNode (환율 기반 시그널) 또는 → FieldMappingNode. AI Agent 도구로 사용 가능.",
|
|
58
60
|
"nodes.CurrencyRateNode.name": "환율 조회",
|
|
59
61
|
"nodes.ErrorHandlerNode.description": "상위 노드의 에러를 처리하고 복구 로직을 실행합니다. 에러 데이터를 로깅, 알림, 또는 폴백 처리로 라우팅합니다. API 장애, 타임아웃 에러, 유효하지 않은 데이터를 우아하게 처리하는 견고한 워크플로우를 구축하세요.",
|
|
@@ -288,6 +290,10 @@
|
|
|
288
290
|
"fields.CurrencyRateNode.resilience": "API 호출 실패 시 재시도 및 폴백 설정",
|
|
289
291
|
"fields.CurrencyRateNode.target_currencies": "대상 통화 목록. 기준 통화 대비 환율을 조회할 통화를 선택하세요.",
|
|
290
292
|
"fields.CurrencyRateNode.timeout_seconds": "API 요청 타임아웃 (초). 기본 30초, 네트워크 환경에 따라 조정하세요.",
|
|
293
|
+
"fields.CodeNode.code": "async def execute(data, params, context) 를 정의하는 파이썬 소스. 샌드박스: 순수 계산용 표준 라이브러리(math, statistics, json, datetime, ...) import 만 허용, credential·네트워크·파일시스템·introspection 불가.",
|
|
294
|
+
"fields.CodeNode.outputs": "출력 포트 선언 [{name, type}]. 비워두면 반환값 전체를 담는 단일 'result' 포트가 됩니다. 선언한 포트는 소비자 노드의 범용 입력 필드(TableDisplayNode.data, IfNode.left/right 등)에 {{ nodes.<id>.<port> }} 표현식으로 소비됩니다 — 타입드 포트 매칭이 아니라 이름으로 dict 키를 꺼내는 방식. 선언하면 정적 오타 검증이 적용됩니다.",
|
|
295
|
+
"fields.CodeNode.params": "execute() 에 `params` 로 전달되는 파라미터. 표현식 바인딩 허용(예: 기간·임계값).",
|
|
296
|
+
"fields.CodeNode.data": "execute() 에 `data` 로 전달되는 입력 데이터. 상류 값 전체를 바인딩(예: {{ nodes.hist.values }})하고 함수 안에서 반복하세요.",
|
|
291
297
|
"fields.FieldMappingNode.data": "변환할 입력 데이터. HTTPRequestNode 응답이나 다른 노드 출력을 바인딩하세요.",
|
|
292
298
|
"fields.FieldMappingNode.mappings": "필드명 매핑 테이블. 원본 필드명(from)을 표준 필드명(to)으로 변환합니다.",
|
|
293
299
|
"fields.FieldMappingNode.mappings.description": "필드 설명 (AI 에이전트 자동 매핑용)",
|
|
@@ -847,6 +853,7 @@
|
|
|
847
853
|
"fieldNames.TradingHoursFilterNode.timezone": "시간대",
|
|
848
854
|
"fieldNames.WatchlistNode.symbols": "종목",
|
|
849
855
|
"outputs.AIAgentNode.response": "AI 에이전트 응답 (출력 형식에 따라 텍스트 또는 JSON 객체)",
|
|
856
|
+
"outputs.CodeNode.result": "출력 포트를 선언하지 않았을 때 execute() 가 반환한 값 전체. 출력 포트를 선언하면 해당 포트를 대신 읽으세요.",
|
|
850
857
|
"outputs.ExclusionListNode.count": "제외 종목 수",
|
|
851
858
|
"outputs.ExclusionListNode.excluded": "최종 제외 종목 목록 (수동 + 동적 합산, 중복 제거)",
|
|
852
859
|
"outputs.ExclusionListNode.filtered": "원본 종목에서 제외 종목을 뺀 결과 (input_symbols 연결 시)",
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/models/validation.py
RENAMED
|
@@ -31,12 +31,16 @@ class ErrorCode(str, Enum):
|
|
|
31
31
|
|
|
32
32
|
# Node / plugin registry
|
|
33
33
|
UNKNOWN_NODE_TYPE = "UNKNOWN_NODE_TYPE"
|
|
34
|
-
UNKNOWN_DYNAMIC_NODE_SCHEMA = "UNKNOWN_DYNAMIC_NODE_SCHEMA"
|
|
35
|
-
DYNAMIC_NODE_CREDENTIAL_FORBIDDEN = "DYNAMIC_NODE_CREDENTIAL_FORBIDDEN"
|
|
36
|
-
DYNAMIC_NODE_CLASS_NOT_INJECTED = "DYNAMIC_NODE_CLASS_NOT_INJECTED"
|
|
37
34
|
MISSING_PLUGIN = "MISSING_PLUGIN"
|
|
38
35
|
UNKNOWN_PLUGIN = "UNKNOWN_PLUGIN"
|
|
39
36
|
|
|
37
|
+
# CodeNode (custom Python code node)
|
|
38
|
+
CODE_NODE_SYNTAX_ERROR = "CODE_NODE_SYNTAX_ERROR" # ast.parse / compile failed (line/offset)
|
|
39
|
+
CODE_NODE_FORBIDDEN = "CODE_NODE_FORBIDDEN" # AST denylist violation (blocked import/builtin/dunder)
|
|
40
|
+
CODE_NODE_NO_EXECUTE = "CODE_NODE_NO_EXECUTE" # code does not define execute()
|
|
41
|
+
CODE_NODE_EXEC_ERROR = "CODE_NODE_EXEC_ERROR" # runtime exception / worker crash / timeout / bad serialization
|
|
42
|
+
CODE_NODE_DISABLED = "CODE_NODE_DISABLED" # allow_code_node=False environment
|
|
43
|
+
|
|
40
44
|
# Edges
|
|
41
45
|
INVALID_EDGE_REF = "INVALID_EDGE_REF"
|
|
42
46
|
INVALID_EDGE_PORT = "INVALID_EDGE_PORT"
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/__init__.py
RENAMED
|
@@ -48,6 +48,7 @@ from programgarden_core.nodes.symbol_stock import OverseasStockSymbolQueryNode
|
|
|
48
48
|
from programgarden_core.nodes.symbol_futures import OverseasFuturesSymbolQueryNode
|
|
49
49
|
# Data (상품 무관)
|
|
50
50
|
from programgarden_core.nodes.data import SQLiteNode, HTTPRequestNode, FieldMappingNode
|
|
51
|
+
from programgarden_core.nodes.code import CodeNode
|
|
51
52
|
# Market External (credential 불필요, 무료 외부 API)
|
|
52
53
|
from programgarden_core.nodes.market_external import CurrencyRateNode
|
|
53
54
|
# Market Status (JIF 장운영정보 — broker credential agnostic)
|
|
@@ -146,6 +147,7 @@ __all__ = [
|
|
|
146
147
|
"SQLiteNode",
|
|
147
148
|
"HTTPRequestNode",
|
|
148
149
|
"FieldMappingNode",
|
|
150
|
+
"CodeNode",
|
|
149
151
|
# Market External (credential 불필요)
|
|
150
152
|
"CurrencyRateNode",
|
|
151
153
|
# Market Status (JIF 장운영정보)
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_futures.py
RENAMED
|
@@ -124,7 +124,7 @@ class OverseasFuturesAccountNode(BaseNode):
|
|
|
124
124
|
{"id": "start", "type": "StartNode"},
|
|
125
125
|
{"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "futures_cred", "paper_trading": True},
|
|
126
126
|
{"id": "account", "type": "OverseasFuturesAccountNode"},
|
|
127
|
-
{"id": "if_margin", "type": "IfNode", "left": "{{ nodes.account.balance.
|
|
127
|
+
{"id": "if_margin", "type": "IfNode", "left": "{{ nodes.account.balance.orderable_amount }}", "operator": ">=", "right": 10000},
|
|
128
128
|
{"id": "order", "type": "OverseasFuturesNewOrderNode", "symbol": "ESH26", "exchange": "CME", "side": "buy", "order_type": "limit", "quantity": 1, "price": 5200.0},
|
|
129
129
|
],
|
|
130
130
|
"edges": [
|
|
@@ -144,7 +144,7 @@ class OverseasFuturesAccountNode(BaseNode):
|
|
|
144
144
|
}
|
|
145
145
|
],
|
|
146
146
|
},
|
|
147
|
-
"expected_output": "When
|
|
147
|
+
"expected_output": "When orderable_amount >= 10000: NewOrderNode places a buy limit order. Otherwise the order branch is skipped.",
|
|
148
148
|
},
|
|
149
149
|
]
|
|
150
150
|
_node_guide: ClassVar[Dict[str, Any]] = {
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/account_stock.py
RENAMED
|
@@ -125,7 +125,7 @@ class OverseasStockAccountNode(BaseNode):
|
|
|
125
125
|
{"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
126
126
|
{"id": "account", "type": "OverseasStockAccountNode"},
|
|
127
127
|
{"id": "market", "type": "OverseasStockMarketDataNode", "symbol": "{{ item }}"},
|
|
128
|
-
{"id": "display", "type": "TableDisplayNode", "title": "Live Prices", "data": "{{ nodes.market.
|
|
128
|
+
{"id": "display", "type": "TableDisplayNode", "title": "Live Prices", "data": "{{ nodes.market.values }}"},
|
|
129
129
|
],
|
|
130
130
|
"edges": [
|
|
131
131
|
{"from": "start", "to": "broker"},
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest.py
RENAMED
|
@@ -203,7 +203,7 @@ class BacktestEngineNode(BaseNode):
|
|
|
203
203
|
{"id": "start", "type": "StartNode"},
|
|
204
204
|
{"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
205
205
|
{"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "AAPL", "exchange": "NASDAQ"}], "period": "1d", "count": 252},
|
|
206
|
-
{"id": "condition", "type": "ConditionNode", "plugin": "RSI", "
|
|
206
|
+
{"id": "condition", "type": "ConditionNode", "plugin": "RSI", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 14, "threshold": 30, "direction": "below"}},
|
|
207
207
|
{
|
|
208
208
|
"id": "backtest",
|
|
209
209
|
"type": "BacktestEngineNode",
|
|
@@ -261,7 +261,7 @@ class BacktestEngineNode(BaseNode):
|
|
|
261
261
|
{"id": "start", "type": "StartNode"},
|
|
262
262
|
{"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
263
263
|
{"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "SPY", "exchange": "NYSE"}], "period": "1d", "count": 504},
|
|
264
|
-
{"id": "condition", "type": "ConditionNode", "plugin": "MACD", "
|
|
264
|
+
{"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
|
|
265
265
|
{
|
|
266
266
|
"id": "backtest",
|
|
267
267
|
"type": "BacktestEngineNode",
|
|
@@ -748,8 +748,8 @@ class BenchmarkCompareNode(BaseNode):
|
|
|
748
748
|
{"id": "start", "type": "StartNode"},
|
|
749
749
|
{"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
750
750
|
{"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "SPY", "exchange": "NYSE"}], "period": "1d", "count": 252},
|
|
751
|
-
{"id": "rsi_cond", "type": "ConditionNode", "plugin": "RSI", "
|
|
752
|
-
{"id": "macd_cond", "type": "ConditionNode", "plugin": "MACD", "
|
|
751
|
+
{"id": "rsi_cond", "type": "ConditionNode", "plugin": "RSI", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 14, "threshold": 30, "direction": "below"}},
|
|
752
|
+
{"id": "macd_cond", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
|
|
753
753
|
{
|
|
754
754
|
"id": "backtest_rsi",
|
|
755
755
|
"type": "BacktestEngineNode",
|
|
@@ -807,7 +807,7 @@ class BenchmarkCompareNode(BaseNode):
|
|
|
807
807
|
{"id": "start", "type": "StartNode"},
|
|
808
808
|
{"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
809
809
|
{"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbols": [{"symbol": "QQQ", "exchange": "NASDAQ"}], "period": "1d", "count": 252},
|
|
810
|
-
{"id": "condition", "type": "ConditionNode", "plugin": "MACD", "
|
|
810
|
+
{"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
|
|
811
811
|
{
|
|
812
812
|
"id": "backtest",
|
|
813
813
|
"type": "BacktestEngineNode",
|
{programgarden_core-1.15.2 → programgarden_core-1.16.0}/programgarden_core/nodes/backtest_futures.py
RENAMED
|
@@ -121,7 +121,7 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
|
|
|
121
121
|
{"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
122
122
|
{"id": "split", "type": "SplitNode", "items": [{"symbol": "ESH26", "exchange": "CME"}]},
|
|
123
123
|
{"id": "historical", "type": "OverseasFuturesHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(60, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1d", "adjust": False},
|
|
124
|
-
{"id": "condition", "type": "ConditionNode", "plugin": "BollingerBands", "
|
|
124
|
+
{"id": "condition", "type": "ConditionNode", "plugin": "BollingerBands", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 20, "std": 2.0, "direction": "below_lower"}},
|
|
125
125
|
],
|
|
126
126
|
"edges": [
|
|
127
127
|
{"from": "start", "to": "broker"},
|
|
@@ -154,7 +154,7 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
|
|
|
154
154
|
{"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
155
155
|
{"id": "split", "type": "SplitNode", "items": [{"symbol": "MHIH26", "exchange": "HKEX"}]},
|
|
156
156
|
{"id": "historical", "type": "OverseasFuturesHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(10, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1h", "adjust": False},
|
|
157
|
-
{"id": "condition", "type": "ConditionNode", "plugin": "MACD", "
|
|
157
|
+
{"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
|
|
158
158
|
],
|
|
159
159
|
"edges": [
|
|
160
160
|
{"from": "start", "to": "broker"},
|