programgarden-core 2.7.0__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.7.0 → programgarden_core-3.0.0}/PKG-INFO +2 -52
  2. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/README.md +1 -51
  3. {programgarden_core-2.7.0 → 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.7.0 → programgarden_core-3.0.0}/pyproject.toml +1 -1
  6. programgarden_core-2.7.0/programgarden_core/code_node.py +0 -419
  7. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/__init__.py +0 -0
  8. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/bases/__init__.py +0 -0
  9. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/bases/client.py +0 -0
  10. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/bases/components.py +0 -0
  11. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/bases/mixins.py +0 -0
  12. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/bases/products.py +0 -0
  13. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/bases/sql.py +0 -0
  14. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/bases/storage.py +0 -0
  15. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/exceptions.py +0 -0
  16. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/expression/__init__.py +0 -0
  17. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/expression/evaluator.py +0 -0
  18. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/expression/reference.py +0 -0
  19. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/i18n/__init__.py +0 -0
  20. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/i18n/locales/en.json +0 -0
  21. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/i18n/locales/ko.json +0 -0
  22. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/i18n/translator.py +0 -0
  23. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/korea_alias.py +0 -0
  24. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/market_calendar.py +0 -0
  25. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/__init__.py +0 -0
  26. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/connection_rule.py +0 -0
  27. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/credential.py +0 -0
  28. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/edge.py +0 -0
  29. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/event.py +0 -0
  30. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/exchange.py +0 -0
  31. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/field_binding.py +0 -0
  32. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/job.py +0 -0
  33. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/order_diagnostics.py +0 -0
  34. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/plugin_resource.py +0 -0
  35. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/resilience.py +0 -0
  36. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/resource.py +0 -0
  37. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/validation.py +0 -0
  38. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/models/workflow.py +0 -0
  39. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/__init__.py +0 -0
  40. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/account_futures.py +0 -0
  41. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/account_korea_stock.py +0 -0
  42. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/account_stock.py +0 -0
  43. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/ai.py +0 -0
  44. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest.py +0 -0
  45. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest_futures.py +0 -0
  46. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest_korea_stock.py +0 -0
  47. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/backtest_stock.py +0 -0
  48. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/base.py +0 -0
  49. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/broker.py +0 -0
  50. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/calculation.py +0 -0
  51. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/code.py +0 -0
  52. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/condition.py +0 -0
  53. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/contract_info_futures.py +0 -0
  54. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/data.py +0 -0
  55. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/data_futures.py +0 -0
  56. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/data_korea_stock.py +0 -0
  57. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/data_stock.py +0 -0
  58. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/display.py +0 -0
  59. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/event.py +0 -0
  60. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
  61. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
  62. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/infra.py +0 -0
  63. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/open_orders_futures.py +0 -0
  64. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
  65. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
  66. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/order.py +0 -0
  67. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/orderable_quantity_futures.py +0 -0
  68. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/portfolio.py +0 -0
  69. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/ranking_korea_stock.py +0 -0
  70. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/ranking_stock.py +0 -0
  71. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/realtime_futures.py +0 -0
  72. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/realtime_korea_stock.py +0 -0
  73. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/realtime_stock.py +0 -0
  74. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/risk.py +0 -0
  75. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/session_gate.py +0 -0
  76. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol.py +0 -0
  77. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol_futures.py +0 -0
  78. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
  79. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/symbol_stock.py +0 -0
  80. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/nodes/trigger.py +0 -0
  81. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/presets/__init__.py +0 -0
  82. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/presets/news_analyst.json +0 -0
  83. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/presets/risk_manager.json +0 -0
  84. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/presets/strategist.json +0 -0
  85. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/presets/technical_analyst.json +0 -0
  86. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/registry/__init__.py +0 -0
  87. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/registry/credential_registry.py +0 -0
  88. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/registry/node_registry.py +0 -0
  89. {programgarden_core-2.7.0 → programgarden_core-3.0.0}/programgarden_core/registry/plugin_registry.py +0 -0
  90. {programgarden_core-2.7.0 → 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.7.0
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
+ })
@@ -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.7.0"
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