programgarden-core 2.6.3__tar.gz → 3.0.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.
Files changed (90) hide show
  1. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/PKG-INFO +2 -52
  2. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/README.md +1 -51
  3. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/listener.py +1 -1
  4. programgarden_core-3.0.0/programgarden_core/code_node.py +29 -0
  5. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/i18n/locales/en.json +4 -2
  6. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/i18n/locales/ko.json +4 -2
  7. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest_stock.py +56 -8
  8. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/pyproject.toml +1 -1
  9. programgarden_core-2.6.3/programgarden_core/code_node.py +0 -419
  10. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/__init__.py +0 -0
  11. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/__init__.py +0 -0
  12. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/client.py +0 -0
  13. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/components.py +0 -0
  14. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/mixins.py +0 -0
  15. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/products.py +0 -0
  16. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/sql.py +0 -0
  17. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/bases/storage.py +0 -0
  18. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/exceptions.py +0 -0
  19. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/expression/__init__.py +0 -0
  20. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/expression/evaluator.py +0 -0
  21. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/expression/reference.py +0 -0
  22. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/i18n/__init__.py +0 -0
  23. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/i18n/translator.py +0 -0
  24. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/korea_alias.py +0 -0
  25. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/market_calendar.py +0 -0
  26. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/__init__.py +0 -0
  27. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/connection_rule.py +0 -0
  28. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/credential.py +0 -0
  29. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/edge.py +0 -0
  30. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/event.py +0 -0
  31. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/exchange.py +0 -0
  32. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/field_binding.py +0 -0
  33. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/job.py +0 -0
  34. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/order_diagnostics.py +0 -0
  35. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/plugin_resource.py +0 -0
  36. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/resilience.py +0 -0
  37. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/resource.py +0 -0
  38. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/validation.py +0 -0
  39. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/models/workflow.py +0 -0
  40. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/__init__.py +0 -0
  41. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/account_futures.py +0 -0
  42. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/account_korea_stock.py +0 -0
  43. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/account_stock.py +0 -0
  44. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/ai.py +0 -0
  45. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest.py +0 -0
  46. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest_futures.py +0 -0
  47. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest_korea_stock.py +0 -0
  48. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/base.py +0 -0
  49. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/broker.py +0 -0
  50. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/calculation.py +0 -0
  51. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/code.py +0 -0
  52. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/condition.py +0 -0
  53. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/contract_info_futures.py +0 -0
  54. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/data.py +0 -0
  55. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/data_futures.py +0 -0
  56. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/data_korea_stock.py +0 -0
  57. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/data_stock.py +0 -0
  58. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/display.py +0 -0
  59. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/event.py +0 -0
  60. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
  61. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
  62. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/infra.py +0 -0
  63. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/open_orders_futures.py +0 -0
  64. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
  65. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
  66. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/order.py +0 -0
  67. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/orderable_quantity_futures.py +0 -0
  68. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/portfolio.py +0 -0
  69. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/ranking_korea_stock.py +0 -0
  70. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/ranking_stock.py +0 -0
  71. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/realtime_futures.py +0 -0
  72. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/realtime_korea_stock.py +0 -0
  73. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/realtime_stock.py +0 -0
  74. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/risk.py +0 -0
  75. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/session_gate.py +0 -0
  76. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol.py +0 -0
  77. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol_futures.py +0 -0
  78. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
  79. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol_stock.py +0 -0
  80. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/nodes/trigger.py +0 -0
  81. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/presets/__init__.py +0 -0
  82. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/presets/news_analyst.json +0 -0
  83. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/presets/risk_manager.json +0 -0
  84. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/presets/strategist.json +0 -0
  85. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/presets/technical_analyst.json +0 -0
  86. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/registry/__init__.py +0 -0
  87. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/registry/credential_registry.py +0 -0
  88. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/registry/node_registry.py +0 -0
  89. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/registry/plugin_registry.py +0 -0
  90. {programgarden_core-2.6.3 → programgarden_core-3.0.0}/programgarden_core/retry_executor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden-core
3
- Version: 2.6.3
3
+ Version: 3.0.0
4
4
  Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -15,24 +15,8 @@ Requires-Dist: pydantic (>=2.0.1,<3.0.0)
15
15
  Requires-Dist: tzdata (>=2025.2,<2026.0)
16
16
  Description-Content-Type: text/markdown
17
17
 
18
- The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
19
-
20
- US regular-session requests can opt into SessionGateNode.exchange_calendar (XNYS/XNAS) for scheduled holidays and early closes. See [session gates](../../docs/session-gates-and-guarded-split.md).
21
-
22
18
  # ProgramGarden Core
23
19
 
24
- An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
25
- limits the overseas-stock catalog to its implemented g3204 periods and rejects
26
- unsupported intraday requests before execution. Package adoption is pending.
27
-
28
- Unreleased schema additions: OverseasFuturesContractInfoNode, SQLiteNode's opt-in
29
- storage_scope=execution, and SessionGateNode's XHKG calendar. See the
30
- [runtime contract](../../docs/futures-contract-details-and-rollover-state.md).
31
-
32
- The REST overseas-stock position catalog declares `sellable_qty`, matching the
33
- broker-reported capacity preserved by the engine. Missing evidence remains null;
34
- see [the contract](../../docs/overseas-stock-sellable-quantity.md).
35
-
36
20
  ProgramGarden은 AI 시대에 맞춰 파이썬을 모르는 투자자도 개인화된 시스템 트레이딩을 자동으로 수행할 수 있게 돕는 오픈소스입니다. 본 저장소는 노드 기반 DSL의 핵심 타입, 베이스 클래스, 레지스트리, i18n을 정의하는 "코어" 모듈입니다.
37
21
 
38
22
  - 문서(비개발자 빠른 시작): https://programgarden.gitbook.io/docs/invest/non_dev_quick_guide
@@ -119,44 +103,10 @@ programgarden_core/
119
103
 
120
104
  자세한 변경 사항은 `CHANGELOG.md`를 참고하세요.
121
105
 
122
- ## Workflow PnL event contract
123
-
124
- Futures workflow PnL events preserve native gross estimates separately from
125
- accounting results. `workflow_*`, `other_*`, `total_*`, account and competition
126
- monetary/rate scalars remain null when accounting evidence is unavailable.
127
- `pnl_by_currency` contains gross price-change subtotals by contract currency;
128
- `monetary_positions` retains their basis/status and unmodified, unconfirmed
129
- `broker_pnl_amount`. No fee, FX, margin/equity return or verified contest score
130
- is inferred from these estimates. Actual zero and negative amounts are retained.
131
- `currency` is null for mixed or unavailable currencies. Consumers must handle
132
- nullable monetary fields and must not coerce them to zero.
133
-
134
- `WorkflowPnLEvent.personal_metrics` is an optional versioned envelope for retained
135
- local workflow executions. Version 1 includes `scope` (kind, product, provider,
136
- trading mode), timezone-aware `as_of`, `basis`, executed-order count/status,
137
- per-symbol/exchange/currency realized amounts/status, and unavailable MDD.
138
- It is unverified personal evidence, independent of whole-account contest results.
139
- Stock realized amounts are stored long-only FIFO gross values excluding fees;
140
- unknown currency stays null. Futures monetary values and portfolio MDD require
141
- accounting evidence absent from this ledger and remain null. Consumers must
142
- preserve null/status and use the latest cumulative observation, not sum dates.
143
-
144
106
  ## Workflow account boundary
145
107
 
146
108
  Each workflow allows one account and one product through at most one broker
147
109
  connection. Additional connections produce `DUPLICATE_BROKER_NODE`, including
148
110
  unbound or cross-product nodes. Reuse one broker for all consumers. Account-free
149
- workflows and unrelated credentials remain supported. See
150
- [the shared contract](../../docs/workflow_account_policy.md).
151
-
152
-
153
- ## Cancellation result contract (unreleased P4)
154
-
155
- CancelOrder metadata describes request acknowledgement (`accepted`,
156
- `confirmation_pending=true`), not final cancellation. Replacement orders require
157
- separate matching completion evidence. See
158
- [the cancellation contract](../../docs/cancellation-acknowledgements.md).
159
-
160
- Session windows and IfNode routing inside Split are described in
161
- [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md).
111
+ workflows and unrelated credentials remain supported.
162
112
 
@@ -1,21 +1,5 @@
1
- The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
2
-
3
- US regular-session requests can opt into SessionGateNode.exchange_calendar (XNYS/XNAS) for scheduled holidays and early closes. See [session gates](../../docs/session-gates-and-guarded-split.md).
4
-
5
1
  # ProgramGarden Core
6
2
 
7
- An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
8
- limits the overseas-stock catalog to its implemented g3204 periods and rejects
9
- unsupported intraday requests before execution. Package adoption is pending.
10
-
11
- Unreleased schema additions: OverseasFuturesContractInfoNode, SQLiteNode's opt-in
12
- storage_scope=execution, and SessionGateNode's XHKG calendar. See the
13
- [runtime contract](../../docs/futures-contract-details-and-rollover-state.md).
14
-
15
- The REST overseas-stock position catalog declares `sellable_qty`, matching the
16
- broker-reported capacity preserved by the engine. Missing evidence remains null;
17
- see [the contract](../../docs/overseas-stock-sellable-quantity.md).
18
-
19
3
  ProgramGarden은 AI 시대에 맞춰 파이썬을 모르는 투자자도 개인화된 시스템 트레이딩을 자동으로 수행할 수 있게 돕는 오픈소스입니다. 본 저장소는 노드 기반 DSL의 핵심 타입, 베이스 클래스, 레지스트리, i18n을 정의하는 "코어" 모듈입니다.
20
4
 
21
5
  - 문서(비개발자 빠른 시작): https://programgarden.gitbook.io/docs/invest/non_dev_quick_guide
@@ -102,43 +86,9 @@ programgarden_core/
102
86
 
103
87
  자세한 변경 사항은 `CHANGELOG.md`를 참고하세요.
104
88
 
105
- ## Workflow PnL event contract
106
-
107
- Futures workflow PnL events preserve native gross estimates separately from
108
- accounting results. `workflow_*`, `other_*`, `total_*`, account and competition
109
- monetary/rate scalars remain null when accounting evidence is unavailable.
110
- `pnl_by_currency` contains gross price-change subtotals by contract currency;
111
- `monetary_positions` retains their basis/status and unmodified, unconfirmed
112
- `broker_pnl_amount`. No fee, FX, margin/equity return or verified contest score
113
- is inferred from these estimates. Actual zero and negative amounts are retained.
114
- `currency` is null for mixed or unavailable currencies. Consumers must handle
115
- nullable monetary fields and must not coerce them to zero.
116
-
117
- `WorkflowPnLEvent.personal_metrics` is an optional versioned envelope for retained
118
- local workflow executions. Version 1 includes `scope` (kind, product, provider,
119
- trading mode), timezone-aware `as_of`, `basis`, executed-order count/status,
120
- per-symbol/exchange/currency realized amounts/status, and unavailable MDD.
121
- It is unverified personal evidence, independent of whole-account contest results.
122
- Stock realized amounts are stored long-only FIFO gross values excluding fees;
123
- unknown currency stays null. Futures monetary values and portfolio MDD require
124
- accounting evidence absent from this ledger and remain null. Consumers must
125
- preserve null/status and use the latest cumulative observation, not sum dates.
126
-
127
89
  ## Workflow account boundary
128
90
 
129
91
  Each workflow allows one account and one product through at most one broker
130
92
  connection. Additional connections produce `DUPLICATE_BROKER_NODE`, including
131
93
  unbound or cross-product nodes. Reuse one broker for all consumers. Account-free
132
- workflows and unrelated credentials remain supported. See
133
- [the shared contract](../../docs/workflow_account_policy.md).
134
-
135
-
136
- ## Cancellation result contract (unreleased P4)
137
-
138
- CancelOrder metadata describes request acknowledgement (`accepted`,
139
- `confirmation_pending=true`), not final cancellation. Replacement orders require
140
- separate matching completion evidence. See
141
- [the cancellation contract](../../docs/cancellation-acknowledgements.md).
142
-
143
- Session windows and IfNode routing inside Split are described in
144
- [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md).
94
+ workflows and unrelated credentials remain supported.
@@ -346,7 +346,7 @@ class WorkflowPnLEvent:
346
346
  # 워크플로우 **비율**을 못 낸 사유. 값을 비우는 것만으로는 "안 샀다" 와 "증권사가
347
347
  # 평단·현재가를 안 보냈다" 가 구분되지 않는다 — 서버가 이 사유를 그대로 적재한다.
348
348
  # 어휘: no_workflow_positions | basis_unreported | price_unreported | no_tracker |
349
- # computation_failed (dsl-api RATE_UNAVAILABLE_REASONS 와 공용).
349
+ # computation_failed (플랫폼의 수익률 미산출 사유 어휘와 공용).
350
350
  # 🔴 monetary_* 를 재사용하지 않는다 — 그건 선물 전용 계약이고, 주식 이벤트에서
351
351
  # monetary_status is None 임을 고정하는 테스트가 있다.
352
352
  workflow_rate_unavailable_reason: Optional[str] = None
@@ -0,0 +1,29 @@
1
+ """Public stub of the CodeNode support module. The user-code screener and sandbox are
2
+ not part of the open-source build; CodeNode is declared so workflows parse and pass
3
+ structural validation, but execution refuses (see WorkflowExecutor)."""
4
+
5
+
6
+ class CodeScreenResult:
7
+ """Structural-validation result. The open-source build performs no code screening."""
8
+
9
+ def __init__(self):
10
+ self.ok = True
11
+ self.error_code = None
12
+ self.message = None
13
+ self.suggestion = None
14
+ self.details = {}
15
+
16
+
17
+ def compile_code_node(code="", node_id="code", *args, **kwargs):
18
+ return CodeScreenResult()
19
+
20
+
21
+ DEFAULT_ALLOWED_IMPORTS: frozenset = frozenset({
22
+ "math", "cmath", "statistics", "decimal", "fractions", "numbers",
23
+ "random", "json", "re", "datetime", "time", "calendar",
24
+ "collections", "itertools", "functools",
25
+ "string", "textwrap", "unicodedata",
26
+ "heapq", "bisect", "array", "copy",
27
+ "dataclasses", "enum", "typing",
28
+ "hashlib", "hmac", "base64", "binascii", "zlib",
29
+ })
@@ -144,7 +144,7 @@
144
144
  "nodes.OverseasStockBrokerNode.name": "Stock Broker",
145
145
  "nodes.OverseasStockCancelOrderNode.description": "Cancels a pending US stock order by its original order ID. Requires original_order_id from OverseasStockOpenOrdersNode or OverseasStockRealOrderEventNode. Outputs cancel result and cancelled order ID. Rate-limited to 5-second minimum interval. Typical connection: StockOpenOrdersNode → StockCancelOrderNode, or StockRealOrderEventNode → StockCancelOrderNode.",
146
146
  "nodes.OverseasStockCancelOrderNode.name": "Stock Cancel Order",
147
- "nodes.OverseasStockHistoricalDataNode.description": "Fetches historical OHLCV (Open, High, Low, Close, Volume) chart data for overseas stocks over a specified date range. Uses LS g3204 for daily, weekly, monthly and yearly bars. Intraday g3203 is unsupported. Enable 'adjust' for split/dividend-adjusted prices (recommended for backtesting). Auto-iterates when previous node outputs multiple symbols. Requires an upstream OverseasStockBrokerNode. Typical connection: WatchlistNode → StockHistoricalDataNode → ConditionNode or → BacktestEngineNode. Supported exchanges: NYSE, NASDAQ, AMEX. Can be used as an AI Agent tool.",
147
+ "nodes.OverseasStockHistoricalDataNode.description": "Fetches historical OHLCV (Open, High, Low, Close, Volume) chart data for overseas stocks over a specified date range. Uses LS g3204 for daily, weekly, monthly and yearly bars and LS g3203 for 1/5/10/15/30/60-minute bars (one page of the newest up to 500 bars). Enable 'adjust' for split/dividend-adjusted prices (recommended for backtesting). Auto-iterates when previous node outputs multiple symbols. Requires an upstream OverseasStockBrokerNode. Typical connection: WatchlistNode → StockHistoricalDataNode → ConditionNode or → BacktestEngineNode. Supported exchanges: NYSE, NASDAQ, AMEX. Can be used as an AI Agent tool.",
148
148
  "nodes.OverseasStockHistoricalDataNode.name": "Stock Historical Data",
149
149
  "nodes.OverseasStockFundamentalNode.description": "Fetches fundamental financial data for overseas stocks via LS Securities API. Provides 16 metrics including PER, EPS, PBR, market capitalization, dividend yield, 52-week high/low, and more. Auto-iterates when previous node outputs multiple symbols. Requires an upstream OverseasStockBrokerNode. Typical connection: WatchlistNode → StockFundamentalNode → ConditionNode (for value-based screening). Supported exchanges: NYSE, NASDAQ, AMEX. Can be used as an AI Agent tool.",
150
150
  "nodes.OverseasStockFundamentalNode.name": "Stock Fundamentals",
@@ -430,7 +430,7 @@
430
430
  "fields.OverseasStockHistoricalDataNode.adjust.short": "Apply adjusted price",
431
431
  "fields.OverseasStockHistoricalDataNode.end_date": "End date (YYYYMMDD)",
432
432
  "fields.OverseasStockHistoricalDataNode.end_date.help_text": "e.g., 20241231. With binding: {{ date.today(format='yyyymmdd') }}",
433
- "fields.OverseasStockHistoricalDataNode.interval": "LS g3204 interval (1d, 1w, 1M, 1Y); intraday is unsupported",
433
+ "fields.OverseasStockHistoricalDataNode.interval": "Bar size: 1d, 1w, 1M, 1Y (LS g3204) or 1m, 5m, 10m, 15m, 30m, 60m minute bars (LS g3203, newest up to 500 bars)",
434
434
  "fields.OverseasStockHistoricalDataNode.start_date": "Start date (YYYYMMDD)",
435
435
  "fields.OverseasStockHistoricalDataNode.start_date.help_text": "e.g., 20240101. With binding: {{ date.ago(30, format='yyyymmdd') }}",
436
436
  "fields.OverseasStockHistoricalDataNode.symbol": "Enter the symbol code to query",
@@ -1059,6 +1059,7 @@
1059
1059
  "enums.kr_price_type.conditional_limit": "Conditional Limit",
1060
1060
  "enums.kr_price_type.limit": "Limit",
1061
1061
  "enums.kr_price_type.market": "Market",
1062
+ "enums.interval.10m": "10 Minutes",
1062
1063
  "enums.interval.15m": "15 Minutes",
1063
1064
  "enums.interval.1M": "1 Month",
1064
1065
  "enums.interval.1Y": "1 year",
@@ -1069,6 +1070,7 @@
1069
1070
  "enums.interval.1m": "1 Minute",
1070
1071
  "enums.interval.1w": "1 Week",
1071
1072
  "enums.interval.5m": "5 Minutes",
1073
+ "enums.interval.60m": "60 Minutes",
1072
1074
  "enums.operator.all": "All Match (AND) - Every condition must pass",
1073
1075
  "enums.operator.any": "Any Match (OR) - At least one must pass",
1074
1076
  "enums.operator.at_least": "At Least N - threshold or more must pass",
@@ -144,7 +144,7 @@
144
144
  "nodes.OverseasStockBrokerNode.name": "해외주식 브로커",
145
145
  "nodes.OverseasStockCancelOrderNode.description": "미국 주식 미체결 주문을 원래 주문 ID로 취소합니다. OverseasStockOpenOrdersNode 또는 OverseasStockRealOrderEventNode에서 original_order_id를 받아야 합니다. 출력: 취소 결과, 취소된 주문 ID. 최소 5초 간격 제한 적용. 일반적인 연결: StockOpenOrdersNode → StockCancelOrderNode 또는 StockRealOrderEventNode → StockCancelOrderNode.",
146
146
  "nodes.OverseasStockCancelOrderNode.name": "해외주식 취소주문",
147
- "nodes.OverseasStockHistoricalDataNode.description": "지정 기간의 해외주식 과거 OHLCV(시가, 고가, 저가, 종가, 거래량) 차트 데이터를 조회합니다. LS g3204로 일봉·주봉·월봉·연봉을 조회합니다. 분봉·시간봉(g3203)은 지원하지 않습니다. 수정주가(주식 분할/배당 반영) 활성화 권장(백테스트용). 이전 노드가 여러 종목을 출력하면 자동 반복 실행. 상위 OverseasStockBrokerNode 연결 필수. 일반적인 연결: WatchlistNode → StockHistoricalDataNode → ConditionNode 또는 → BacktestEngineNode. 지원 거래소: NYSE, NASDAQ, AMEX. AI Agent 도구로 사용 가능.",
147
+ "nodes.OverseasStockHistoricalDataNode.description": "지정 기간의 해외주식 과거 OHLCV(시가, 고가, 저가, 종가, 거래량) 차트 데이터를 조회합니다. LS g3204로 일봉·주봉·월봉·연봉을, LS g3203으로 1·5·10·15·30·60분봉(최근 최대 500개 한 페이지)을 조회합니다. 수정주가(주식 분할/배당 반영) 활성화 권장(백테스트용). 이전 노드가 여러 종목을 출력하면 자동 반복 실행. 상위 OverseasStockBrokerNode 연결 필수. 일반적인 연결: WatchlistNode → StockHistoricalDataNode → ConditionNode 또는 → BacktestEngineNode. 지원 거래소: NYSE, NASDAQ, AMEX. AI Agent 도구로 사용 가능.",
148
148
  "nodes.OverseasStockHistoricalDataNode.name": "해외주식 과거데이터",
149
149
  "nodes.OverseasStockFundamentalNode.description": "LS증권 API로 해외주식 펀더멘털 재무 데이터를 조회합니다. PER, EPS, PBR, 시가총액, 배당수익률, 52주 고/저가 등 16개 지표를 제공합니다. 이전 노드가 여러 종목을 출력하면 자동 반복 실행. 상위 OverseasStockBrokerNode 연결 필수. 일반적인 연결: WatchlistNode → StockFundamentalNode → ConditionNode (가치 기반 스크리닝). 지원 거래소: NYSE, NASDAQ, AMEX. AI Agent 도구로 사용 가능.",
150
150
  "nodes.OverseasStockFundamentalNode.name": "해외주식 종목정보",
@@ -430,7 +430,7 @@
430
430
  "fields.OverseasStockHistoricalDataNode.adjust.short": "수정주가 적용 여부",
431
431
  "fields.OverseasStockHistoricalDataNode.end_date": "조회 종료일 (YYYYMMDD)",
432
432
  "fields.OverseasStockHistoricalDataNode.end_date.help_text": "예: 20241231. 바인딩 시 {{ date.today(format='yyyymmdd') }} 사용 가능",
433
- "fields.OverseasStockHistoricalDataNode.interval": "LS g3204 봉 종류 (1d, 1w, 1M, 1Y). 분봉·시간봉 미지원",
433
+ "fields.OverseasStockHistoricalDataNode.interval": "봉 종류: 1d, 1w, 1M, 1Y (LS g3204) 또는 1m, 5m, 10m, 15m, 30m, 60m 분봉 (LS g3203, 최근 최대 500개)",
434
434
  "fields.OverseasStockHistoricalDataNode.start_date": "조회 시작일 (YYYYMMDD)",
435
435
  "fields.OverseasStockHistoricalDataNode.start_date.help_text": "예: 20240101. 바인딩 시 {{ date.ago(30, format='yyyymmdd') }} 사용 가능",
436
436
  "fields.OverseasStockHistoricalDataNode.symbol": "조회할 종목 코드를 입력하세요",
@@ -1059,6 +1059,7 @@
1059
1059
  "enums.kr_price_type.conditional_limit": "조건부지정가 (Conditional Limit)",
1060
1060
  "enums.kr_price_type.limit": "지정가 (Limit)",
1061
1061
  "enums.kr_price_type.market": "시장가 (Market)",
1062
+ "enums.interval.10m": "10분",
1062
1063
  "enums.interval.15m": "15분",
1063
1064
  "enums.interval.1M": "1개월",
1064
1065
  "enums.interval.1Y": "1년",
@@ -1069,6 +1070,7 @@
1069
1070
  "enums.interval.1m": "1분",
1070
1071
  "enums.interval.1w": "1주",
1071
1072
  "enums.interval.5m": "5분",
1073
+ "enums.interval.60m": "60분",
1072
1074
  "enums.operator.all": "전체 만족 (AND) - 모든 조건 통과 필요",
1073
1075
  "enums.operator.any": "하나 이상 (OR) - 1개만 통과해도 OK",
1074
1076
  "enums.operator.at_least": "최소 N개 이상 - threshold 개수 이상 통과",
@@ -65,9 +65,10 @@ class OverseasStockHistoricalDataNode(BaseNode):
65
65
  default="{{ date.today(format='yyyymmdd') }}",
66
66
  description="End date (YYYY-MM-DD or {{ date.today(format='yyyymmdd') }})",
67
67
  )
68
- interval: Literal["1d", "1w", "1M", "1Y"] = Field(
68
+ interval: Literal["1d", "1w", "1M", "1Y", "1m", "5m", "10m", "15m", "30m", "60m"] = Field(
69
69
  default="1d",
70
- description="LS g3204 interval: daily, weekly, monthly or yearly. Intraday is unsupported.",
70
+ description=("Bar size: 1d, 1w, 1M, 1Y read LS g3204; 1m, 5m, 10m, 15m, 30m, 60m read LS g3203 "
71
+ "minute bars (one page, newest up to 500 bars)."),
71
72
  )
72
73
  adjust: bool = Field(
73
74
  default=False,
@@ -81,6 +82,7 @@ class OverseasStockHistoricalDataNode(BaseNode):
81
82
  _usage: ClassVar[Dict[str, Any]] = {
82
83
  "when_to_use": [
83
84
  "Retrieve daily, weekly, monthly or yearly OHLCV history for a US-listed stock to compute technical indicators or run backtests",
85
+ "Retrieve 1/5/10/15/30/60-minute bars of a US-listed stock for an intraday indicator (one page of the newest bars)",
84
86
  "Feed time-series data into ConditionNode plugins (RSI, MACD, Bollinger, etc.) that require a `data` list of candles",
85
87
  "Use `{{ date.ago(N, format='yyyymmdd') }}` for start_date to get a rolling lookback window without hardcoding dates",
86
88
  ],
@@ -97,7 +99,9 @@ class OverseasStockHistoricalDataNode(BaseNode):
97
99
  }
98
100
  _features: ClassVar[List[str]] = [
99
101
  "Returns a list of OHLCV candles in `value.time_series`: [{date, open, high, low, close, volume}, ...]",
100
- "Supports intervals: 1d, 1w, 1M, 1Y through LS g3204; intraday history is unsupported",
102
+ "Supports intervals: 1d, 1w, 1M, 1Y through LS g3204 and 1m, 5m, 10m, 15m, 30m, 60m through LS g3203",
103
+ "A minute interval reads ONE g3203 page: the newest up to 500 bars at or before end_date within start_date..end_date; a longer range is not completed by paging",
104
+ "A minute bar's date is YYYYMMDDHHMMSS in the exchange's local time as LS labels it; pre-market, after-hours and overnight-session bars are included as LS returns them",
101
105
  "start_date / end_date accept YYYYMMDD strings or expression namespace calls: `{{ date.ago(30, format='yyyymmdd') }}`",
102
106
  "adjust=True applies split/dividend-adjusted prices for accurate long-term backtesting",
103
107
  "is_tool_enabled=True — AI Agent can fetch historical data autonomously for technical analysis",
@@ -156,12 +160,48 @@ class OverseasStockHistoricalDataNode(BaseNode):
156
160
  },
157
161
  "expected_output": "value port: {symbol, exchange, time_series: [{date, open, high, low, close, volume}, ...]}. RSI is computed from time_series.",
158
162
  },
163
+ {
164
+ "title": "15-minute bars for an intraday RSI",
165
+ "description": "Fetch the newest 15-minute bars of NVDA (LS g3203, one page of up to 500 bars) and compute RSI on them. Bar dates are YYYYMMDDHHMMSS in exchange local time and include extended-hours bars.",
166
+ "workflow_snippet": {
167
+ "id": "overseas_stock_minute_rsi",
168
+ "name": "Minute-bar RSI Signal",
169
+ "nodes": [
170
+ {"id": "start", "type": "StartNode"},
171
+ {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
172
+ {"id": "split", "type": "SplitNode", "array": [{"symbol": "NVDA", "exchange": "NASDAQ"}]},
173
+ {"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(5, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "15m"},
174
+ {"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"}},
175
+ {"id": "split_results", "type": "AggregateNode", "mode": "collect"},
176
+ ],
177
+ "edges": [
178
+ {"from": "start", "to": "broker"},
179
+ {"from": "broker", "to": "split"},
180
+ {"from": "split", "to": "historical"},
181
+ {"from": "broker", "to": "historical"},
182
+ {"from": "historical", "to": "condition"},
183
+ {"from": "condition", "to": "split_results"},
184
+ ],
185
+ "credentials": [
186
+ {
187
+ "credential_id": "broker_cred",
188
+ "type": "broker_ls_overseas_stock",
189
+ "data": [
190
+ {"key": "appkey", "value": "", "type": "password", "label": "App Key"},
191
+ {"key": "appsecret", "value": "", "type": "password", "label": "App Secret"},
192
+ ],
193
+ }
194
+ ],
195
+ },
196
+ "expected_output": "value port: {symbol, exchange, time_series: [{date: 'YYYYMMDDHHMMSS', open, high, low, close, volume}, ...]} oldest first. RSI is computed from the 15-minute closes.",
197
+ },
159
198
  ]
160
199
  _node_guide: ClassVar[Dict[str, Any]] = {
161
200
  "input_handling": (
162
201
  "The `symbol` field takes a single {exchange, symbol} dict — bind to `{{ nodes.split.item }}` in a per-symbol loop. "
163
202
  "start_date / end_date accept 'YYYYMMDD' strings or `{{ date.ago(N, format='yyyymmdd') }}` expressions. "
164
- "Supported intervals: 1d, 1w, 1M, 1Y through LS g3204. Intraday g3203 is not connected to this node."
203
+ "Supported intervals: 1d, 1w, 1M, 1Y through LS g3204; 1m, 5m, 10m, 15m, 30m, 60m through LS g3203 "
204
+ "(one page of the newest up to 500 bars, date = YYYYMMDDHHMMSS exchange local time)."
165
205
  ),
166
206
  "output_consumption": (
167
207
  "The `value` port emits a dict: {symbol, exchange, time_series: [{date, open, high, low, close, volume}, ...]}. "
@@ -177,9 +217,11 @@ class OverseasStockHistoricalDataNode(BaseNode):
177
217
  "pitfalls": [
178
218
  "`symbol: {{ item }}` / `{{ nodes.<split>.item }}` resolves only inside a per-symbol loop (auto-iterate over an upstream array, or a SplitNode branch). If the upstream array is empty for this run the node logs `No symbols to fetch` and returns an empty series — normal no-signal, not a defect. A `No symbols provided` WARNING means the symbol source is missing or the binding did not resolve",
179
219
  "Access time_series via `{{ nodes.historical.value.time_series }}` — the value port is a dict, not a plain list",
180
- "Intraday intervals (1m/5m/15m/1h) are unsupported; never substitute daily bars or invent a g3203 node connection",
220
+ "Minute intervals are 1m, 5m, 10m, 15m, 30m and 60m (not 1h or 1min); daily bars are never a substitute for an intraday strategy",
221
+ "Minute bars include every session LS returns: pre-market, after-hours and the overnight session from 20:00 local time of the previous day (a Sunday date can appear); the regular session is 09:30-16:00 exchange local time, so filter by the time part of `date` when a strategy needs the regular session only",
222
+ "`adjust` has no effect on minute intervals: LS g3203 has no adjusted-price option, minute bars are the prices as traded",
181
223
  "adjust=False (default) returns unadjusted prices; set True when comparing long-term price levels across splits",
182
- "The current executor requests one g3204 page; it does not expose response-time, continuation-completion or candle-finality evidence",
224
+ "The executor requests one g3204 or g3203 page; it does not expose response-time, continuation-completion or candle-finality evidence (the newest minute bar may still be forming)",
183
225
  ],
184
226
  }
185
227
 
@@ -300,12 +342,18 @@ class OverseasStockHistoricalDataNode(BaseNode):
300
342
  description="i18n:fields.OverseasStockHistoricalDataNode.interval",
301
343
  default="1d",
302
344
  required=True,
303
- enum_values=["1d", "1w", "1M", "1Y"],
345
+ enum_values=["1d", "1w", "1M", "1Y", "1m", "5m", "10m", "15m", "30m", "60m"],
304
346
  enum_labels={
305
347
  "1d": "i18n:enums.interval.1d",
306
348
  "1w": "i18n:enums.interval.1w",
307
349
  "1M": "i18n:enums.interval.1M",
308
- "1Y": "i18n:enums.interval.1Y"
350
+ "1Y": "i18n:enums.interval.1Y",
351
+ "1m": "i18n:enums.interval.1m",
352
+ "5m": "i18n:enums.interval.5m",
353
+ "10m": "i18n:enums.interval.10m",
354
+ "15m": "i18n:enums.interval.15m",
355
+ "30m": "i18n:enums.interval.30m",
356
+ "60m": "i18n:enums.interval.60m",
309
357
  },
310
358
  category=FieldCategory.PARAMETERS,
311
359
  expression_mode=ExpressionMode.FIXED_ONLY,
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden-core"
8
- version = "2.6.3"
8
+ version = "3.0.0"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의"
11
11
  readme = "README.md"
@@ -1,419 +0,0 @@
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 underscore rule in _scan_ast — any ATTRIBUTE beginning
108
- # with '_' is rejected (subsumes every dunder and private module re-export like
109
- # random._os); STRING literals are rejected when they contain a dunder or name a
110
- # blocked attribute re-export (bare single-underscore data-key strings such as
111
- # "_partial_failure" are allowed). This explicit set is retained for message clarity.
112
- FORBIDDEN_DUNDERS: frozenset = frozenset({
113
- "__class__", "__bases__", "__base__", "__subclasses__", "__mro__",
114
- "__globals__", "__code__", "__closure__", "__func__", "__self__",
115
- "__dict__", "__getattribute__", "__setattr__", "__delattr__",
116
- "__reduce__", "__reduce_ex__", "__builtins__", "__import__",
117
- "__loader__", "__spec__", "__init_subclass__", "__subclasshook__",
118
- "__getattr__", "__wrapped__", "__objclass__",
119
- "gi_frame", "gi_code", "cr_frame", "cr_code", "ag_frame",
120
- "f_back", "f_globals", "f_locals", "f_builtins", "tb_frame",
121
- })
122
-
123
- # Format replacement-field that performs attribute access, e.g. "{0.__class__}"
124
- # or "{x.sys}" — used to walk attributes via str.format without an AST Attribute
125
- # node. Rejected wholesale in string constants.
126
- import re as _re
127
- _FORMAT_ATTR_RE = _re.compile(r"\{[^{}]*\.[^{}]*\}")
128
-
129
-
130
- @dataclass
131
- class CodeScreenResult:
132
- """Structured outcome of `compile_code_node`. `ok=False` carries an
133
- AI-chatbot-consumable error (code + message + suggestion + location)."""
134
-
135
- ok: bool
136
- code_object: Optional[CodeType] = None
137
- error_code: Optional[str] = None # one of CODE_NODE_SYNTAX_ERROR / _FORBIDDEN / _NO_EXECUTE
138
- message: Optional[str] = None
139
- suggestion: Optional[str] = None
140
- line: Optional[int] = None
141
- offset: Optional[int] = None
142
- details: Dict[str, Any] = field(default_factory=dict)
143
-
144
-
145
- def _module_root(name: Optional[str]) -> str:
146
- return (name or "").split(".")[0]
147
-
148
-
149
- def _scan_ast(tree: ast.AST, allowed_imports: Set[str]) -> List[Dict[str, Any]]:
150
- """Return a list of violation dicts (empty when clean).
151
-
152
- Each violation: {kind, name, line, col, reason}. `kind` ∈
153
- {import, builtin, dunder}.
154
- """
155
- violations: List[Dict[str, Any]] = []
156
-
157
- for node in ast.walk(tree):
158
- # ── Imports (whitelist-enforced) ──
159
- if isinstance(node, ast.Import):
160
- for alias in node.names:
161
- root = _module_root(alias.name)
162
- if root not in allowed_imports:
163
- reason = (
164
- f"module '{alias.name}' is blocked (filesystem/process/network access)"
165
- if root in FORBIDDEN_MODULES
166
- else f"module '{alias.name}' is not in the allowed import list"
167
- )
168
- violations.append({
169
- "kind": "import", "name": alias.name,
170
- "line": node.lineno, "col": node.col_offset, "reason": reason,
171
- })
172
- elif isinstance(node, ast.ImportFrom):
173
- root = _module_root(node.module)
174
- # `from . import x` (level>0) is always blocked — no package context.
175
- if node.level and node.level > 0:
176
- violations.append({
177
- "kind": "import", "name": node.module or ".",
178
- "line": node.lineno, "col": node.col_offset,
179
- "reason": "relative imports are not allowed in CodeNode",
180
- })
181
- elif root not in allowed_imports:
182
- reason = (
183
- f"module '{node.module}' is blocked (filesystem/process/network access)"
184
- if root in FORBIDDEN_MODULES
185
- else f"module '{node.module}' is not in the allowed import list"
186
- )
187
- violations.append({
188
- "kind": "import", "name": node.module or "",
189
- "line": node.lineno, "col": node.col_offset, "reason": reason,
190
- })
191
-
192
- # ── Forbidden builtin names (call or alias) ──
193
- elif isinstance(node, ast.Name):
194
- if node.id in FORBIDDEN_BUILTINS:
195
- violations.append({
196
- "kind": "builtin", "name": node.id,
197
- "line": node.lineno, "col": node.col_offset,
198
- "reason": f"use of '{node.id}' is not allowed",
199
- })
200
-
201
- # ── Attribute access screen ──
202
- # (1) BLANKET: any attribute beginning with '_' is rejected — this
203
- # subsumes every dunder (__class__/__globals__/__subclasses__/...)
204
- # AND private module re-exports (random._os, random._urandom, ...).
205
- # (2) A denylist of dangerous PUBLIC names (module re-exports & shell
206
- # hops): statistics.sys, uuid.os, x.system, x.modules, type().mro().
207
- elif isinstance(node, ast.Attribute):
208
- if node.attr.startswith("_"):
209
- violations.append({
210
- "kind": "dunder", "name": node.attr,
211
- "line": node.lineno, "col": node.col_offset,
212
- "reason": f"access to underscore attribute '{node.attr}' is not allowed (introspection escape)",
213
- })
214
- elif node.attr in FORBIDDEN_ATTR_NAMES:
215
- violations.append({
216
- "kind": "attr", "name": node.attr,
217
- "line": node.lineno, "col": node.col_offset,
218
- "reason": f"attribute '{node.attr}' access is not allowed (module/escape re-export)",
219
- })
220
-
221
- # ── String-constant screen ──
222
- # Defeats attribute-walk via strings: getattr/attrgetter args, str.format
223
- # attribute fields, subscript-string dunder access. Reject strings that
224
- # start with '_', contain a dunder, name a dangerous attribute, or embed
225
- # a format field with attribute access.
226
- elif isinstance(node, ast.Constant) and isinstance(node.value, str):
227
- v = node.value
228
- bad = None
229
- # A single leading underscore names a documented native data marker
230
- # read as a dict key (e.g. balance["_partial_failure"], "_source"),
231
- # so it is allowed. Still rejected: dunder-style names, and blocked
232
- # public/underscored attribute re-exports ("os"/"sys"/"system"/"_os"/
233
- # "_sys"... -> in FORBIDDEN_ATTR_NAMES via lstrip("_")), and attribute-
234
- # walking format fields. getattr/attrgetter/operator/format are
235
- # independently blocked, so a bare underscore data-key string cannot
236
- # be turned into attribute access.
237
- if "__" in v:
238
- bad = "string literal contains a dunder (introspection escape)"
239
- elif v.lstrip("_") in FORBIDDEN_ATTR_NAMES:
240
- bad = f"string literal '{v}' names a blocked attribute (introspection escape)"
241
- elif _FORMAT_ATTR_RE.search(v):
242
- bad = "format string performs attribute access (introspection escape)"
243
- if bad:
244
- violations.append({
245
- "kind": "string", "name": v[:40],
246
- "line": node.lineno, "col": node.col_offset,
247
- "reason": bad,
248
- })
249
-
250
- return violations
251
-
252
-
253
- def _has_execute(tree: ast.Module) -> bool:
254
- for node in tree.body:
255
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == EXECUTE_FN_NAME:
256
- return True
257
- return False
258
-
259
-
260
- def compile_code_node(
261
- code: str,
262
- node_id: str,
263
- *,
264
- screen: bool = True,
265
- allowed_imports: Optional[Set[str]] = None,
266
- ) -> CodeScreenResult:
267
- """Parse, security-screen, and compile CodeNode source WITHOUT executing it.
268
-
269
- Pipeline:
270
- 1. `ast.parse` — SyntaxError → CODE_NODE_SYNTAX_ERROR (line/offset).
271
- 2. AST denylist scan (when `screen=True`) — CODE_NODE_FORBIDDEN.
272
- 3. `execute` function must be defined — CODE_NODE_NO_EXECUTE.
273
- 4. `compile(filename=f'<code:{node_id}>')` — preserves traceback lines.
274
-
275
- Args:
276
- code: Python source text (must define `execute`).
277
- node_id: node id, used only for the compiled filename (traceback anchor).
278
- screen: run the AST denylist scan (always True in production).
279
- allowed_imports: override the import whitelist (defaults to
280
- DEFAULT_ALLOWED_IMPORTS). Enforced both here and by the runtime
281
- safe `__import__`.
282
-
283
- Returns:
284
- CodeScreenResult — `ok=True` with `code_object`, or `ok=False` with a
285
- structured, English, chatbot-consumable error. Never raises for
286
- screening/syntax failures (they are returned as data).
287
- """
288
- allowed = set(allowed_imports) if allowed_imports is not None else set(DEFAULT_ALLOWED_IMPORTS)
289
-
290
- if not isinstance(code, str) or not code.strip():
291
- return CodeScreenResult(
292
- ok=False,
293
- error_code="CODE_NODE_SYNTAX_ERROR",
294
- message="CodeNode 'code' is empty.",
295
- suggestion="Provide Python source that defines `async def execute(data, params, context)`.",
296
- line=1, offset=0,
297
- )
298
-
299
- # 1. Parse
300
- try:
301
- tree = ast.parse(code, filename=f"<code:{node_id}>", mode="exec")
302
- except SyntaxError as e:
303
- return CodeScreenResult(
304
- ok=False,
305
- error_code="CODE_NODE_SYNTAX_ERROR",
306
- message=f"CodeNode syntax error: {e.msg}",
307
- suggestion="Fix the Python syntax at the reported line/offset.",
308
- line=e.lineno,
309
- offset=e.offset,
310
- details={"raw": str(e)},
311
- )
312
-
313
- # 2. Screen
314
- if screen:
315
- violations = _scan_ast(tree, allowed)
316
- if violations:
317
- first = violations[0]
318
- names = sorted({v["name"] for v in violations})
319
- return CodeScreenResult(
320
- ok=False,
321
- error_code="CODE_NODE_FORBIDDEN",
322
- message=(
323
- f"CodeNode uses forbidden construct(s): {', '.join(names)} "
324
- f"({first['reason']})."
325
- ),
326
- suggestion=(
327
- "Remove the blocked import/builtin/introspection. CodeNode allows "
328
- "pure-computation stdlib only (math, statistics, json, datetime, ...); "
329
- "credentials, filesystem, network, subprocess, and object-graph "
330
- "introspection are not available."
331
- ),
332
- line=first["line"],
333
- offset=first["col"],
334
- details={"violations": violations, "forbidden_names": names},
335
- )
336
-
337
- # 3. execute() must exist
338
- if not _has_execute(tree):
339
- return CodeScreenResult(
340
- ok=False,
341
- error_code="CODE_NODE_NO_EXECUTE",
342
- message=f"CodeNode code must define a function named '{EXECUTE_FN_NAME}'.",
343
- suggestion=(
344
- "Define `async def execute(data, params, context):` (a plain "
345
- "`def execute(...)` is also accepted and auto-wrapped)."
346
- ),
347
- line=1, offset=0,
348
- )
349
-
350
- # 4. Compile (never exec here)
351
- try:
352
- code_object = compile(tree, filename=f"<code:{node_id}>", mode="exec")
353
- except (SyntaxError, ValueError) as e: # e.g. too-deeply-nested
354
- return CodeScreenResult(
355
- ok=False,
356
- error_code="CODE_NODE_SYNTAX_ERROR",
357
- message=f"CodeNode failed to compile: {e}",
358
- suggestion="Simplify the code so it compiles as a module.",
359
- line=getattr(e, "lineno", 1) or 1,
360
- offset=getattr(e, "offset", 0) or 0,
361
- details={"raw": str(e)},
362
- )
363
-
364
- return CodeScreenResult(ok=True, code_object=code_object)
365
-
366
-
367
- # The real import machinery, captured once so the sandbox can delegate to it
368
- # after the whitelist check.
369
- _REAL_IMPORT = _builtins.__import__
370
-
371
- # Curated safe builtins — everything a pure-compute function needs, and nothing
372
- # that grants escape (no eval/exec/compile/open/getattr/setattr/globals/...).
373
- _SAFE_BUILTIN_NAMES: Tuple[str, ...] = (
374
- "abs", "all", "any", "ascii", "bin", "bool", "bytearray", "bytes",
375
- "callable", "chr", "complex", "dict", "divmod", "enumerate", "filter",
376
- "float", "format", "frozenset", "hash", "hex", "int", "isinstance",
377
- "issubclass", "iter", "len", "list", "map", "max", "min", "next", "oct",
378
- "ord", "pow", "print", "range", "repr", "reversed", "round", "set",
379
- "slice", "sorted", "str", "sum", "tuple", "type", "zip", "hasattr",
380
- # exceptions & sentinels
381
- "Exception", "BaseException", "ValueError", "TypeError", "KeyError",
382
- "IndexError", "AttributeError", "ZeroDivisionError", "ArithmeticError",
383
- "OverflowError", "RuntimeError", "StopIteration", "StopAsyncIteration",
384
- "NotImplementedError", "AssertionError", "LookupError", "NameError",
385
- "RecursionError", "FloatingPointError", "GeneratorExit", "KeyboardInterrupt",
386
- "True", "False", "None", "NotImplemented", "Ellipsis",
387
- "object", "property", "staticmethod", "classmethod", "super",
388
- # __build_class__ is required for `class` statements in user code; it is not
389
- # itself an escape vector (dunder screen blocks the dangerous follow-ups).
390
- "__build_class__",
391
- )
392
-
393
-
394
- def build_restricted_builtins(allowed_imports: Optional[Set[str]] = None) -> Dict[str, Any]:
395
- """Build the `__builtins__` dict used when exec-ing CodeNode source.
396
-
397
- Contains only the curated safe builtins plus a whitelist-gated
398
- `__import__`. Pure function (no exec, stdlib only) so it lives in core and
399
- is reused by the runtime worker.
400
- """
401
- allowed = set(allowed_imports) if allowed_imports is not None else set(DEFAULT_ALLOWED_IMPORTS)
402
-
403
- def _safe_import(name, globals=None, locals=None, fromlist=(), level=0):
404
- if level and level != 0:
405
- raise ImportError("relative imports are not allowed in CodeNode")
406
- root = _module_root(name)
407
- if root not in allowed:
408
- raise ImportError(
409
- f"import of '{name}' is not allowed in CodeNode "
410
- f"(allowed: pure-computation stdlib only)"
411
- )
412
- return _REAL_IMPORT(name, globals, locals, fromlist, level)
413
-
414
- safe: Dict[str, Any] = {}
415
- for bn in _SAFE_BUILTIN_NAMES:
416
- if hasattr(_builtins, bn):
417
- safe[bn] = getattr(_builtins, bn)
418
- safe["__import__"] = _safe_import
419
- return safe