aria-code 4.1.3__py3-none-any.whl

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 (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
runtime/subagent.py ADDED
@@ -0,0 +1,258 @@
1
+ """Background subagent task system.
2
+
3
+ Allows the main agent to spawn independent sub-tasks that run concurrently.
4
+ Tasks are tracked in memory and optionally persisted to ~/.arthera/tasks/.
5
+
6
+ Tool functions exposed to the LLM:
7
+ spawn_task(prompt, context?) → {"task_id": "abc123", "status": "pending"}
8
+ task_status(task_id) → {"status": "running|done|failed", ...}
9
+ task_result(task_id) → {"result": "...", "success": bool}
10
+ task_list() → [{"task_id": ..., "status": ...}, ...]
11
+ task_cancel(task_id) → {"cancelled": bool}
12
+
13
+ Background execution is wired up in aria_cli.py via _subagent_runner().
14
+ If no runner is registered, spawn_task stores the task for manual execution.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import time
21
+ import uuid
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Callable, Dict, Optional
25
+
26
+ _TASKS: Dict[str, "SubagentTask"] = {}
27
+ _RUNNER: Optional[Callable] = None # set by aria_cli.py
28
+
29
+
30
+ @dataclass
31
+ class SubagentTask:
32
+ task_id: str
33
+ prompt: str
34
+ context: str = ""
35
+ status: str = "pending" # pending | running | done | failed | cancelled
36
+ result: str = ""
37
+ error: str = ""
38
+ created_at: float = field(default_factory=time.time)
39
+ completed_at: float = 0.0
40
+
41
+ def age_str(self) -> str:
42
+ elapsed = time.time() - self.created_at
43
+ if elapsed < 60:
44
+ return f"{int(elapsed)}s"
45
+ if elapsed < 3600:
46
+ return f"{elapsed/60:.1f}m"
47
+ return f"{elapsed/3600:.1f}h"
48
+
49
+ def to_dict(self) -> dict:
50
+ return {
51
+ "task_id": self.task_id,
52
+ "status": self.status,
53
+ "prompt": self.prompt[:200] + ("…" if len(self.prompt) > 200 else ""),
54
+ "age": self.age_str(),
55
+ }
56
+
57
+
58
+ def register_runner(runner: Callable) -> None:
59
+ """Register the async runner function from aria_cli.py."""
60
+ global _RUNNER
61
+ _RUNNER = runner
62
+
63
+
64
+ # ── Tool functions ─────────────────────────────────────────────────────────────
65
+
66
+ def tool_spawn_task(params: dict) -> dict:
67
+ """Spawn an independent background agent task."""
68
+ prompt = params.get("prompt", "")
69
+ context = params.get("context", "")
70
+ if not prompt:
71
+ return {"success": False, "error": "Missing 'prompt'"}
72
+
73
+ task_id = uuid.uuid4().hex[:8]
74
+ task = SubagentTask(task_id=task_id, prompt=prompt, context=context)
75
+ _TASKS[task_id] = task
76
+
77
+ if _RUNNER is not None:
78
+ try:
79
+ loop = asyncio.get_event_loop()
80
+ if loop.is_running():
81
+ asyncio.ensure_future(_run_background(task))
82
+ else:
83
+ loop.run_until_complete(_run_background(task))
84
+ except Exception as exc:
85
+ task.status = "failed"
86
+ task.error = str(exc)
87
+ else:
88
+ # No runner — task stays in "pending" for manual execution
89
+ task.status = "pending"
90
+
91
+ return {
92
+ "success": True,
93
+ "task_id": task_id,
94
+ "status": task.status,
95
+ "message": (
96
+ f"Task {task_id} spawned. Use task_status('{task_id}') to check progress."
97
+ if task.status == "running" else
98
+ f"Task {task_id} queued (no runner registered). Check /tasks."
99
+ ),
100
+ }
101
+
102
+
103
+ async def _run_background(task: SubagentTask) -> None:
104
+ task.status = "running"
105
+ try:
106
+ full_prompt = task.prompt
107
+ if task.context:
108
+ full_prompt = f"{task.context}\n\n{task.prompt}"
109
+ result_text = await _RUNNER(full_prompt)
110
+ task.result = result_text or ""
111
+ task.status = "done"
112
+ except asyncio.CancelledError:
113
+ task.status = "cancelled"
114
+ except Exception as exc:
115
+ task.error = str(exc)
116
+ task.status = "failed"
117
+ finally:
118
+ task.completed_at = time.time()
119
+
120
+
121
+ def tool_task_status(params: dict) -> dict:
122
+ """Check the status of a background task."""
123
+ task_id = params.get("task_id", "")
124
+ if not task_id:
125
+ return {"success": False, "error": "Missing 'task_id'"}
126
+ task = _TASKS.get(task_id)
127
+ if not task:
128
+ return {"success": False, "error": f"Task '{task_id}' not found"}
129
+ return {
130
+ "success": True,
131
+ "task_id": task_id,
132
+ "status": task.status,
133
+ "age": task.age_str(),
134
+ "prompt_preview": task.prompt[:100],
135
+ "error": task.error or None,
136
+ }
137
+
138
+
139
+ def tool_task_result(params: dict) -> dict:
140
+ """Retrieve the result of a completed background task."""
141
+ task_id = params.get("task_id", "")
142
+ if not task_id:
143
+ return {"success": False, "error": "Missing 'task_id'"}
144
+ task = _TASKS.get(task_id)
145
+ if not task:
146
+ return {"success": False, "error": f"Task '{task_id}' not found"}
147
+ if task.status == "running":
148
+ return {"success": False, "error": "Task is still running", "status": "running"}
149
+ if task.status == "failed":
150
+ return {"success": False, "error": task.error, "status": "failed"}
151
+ if task.status in ("pending", "cancelled"):
152
+ return {"success": False, "error": f"Task status is '{task.status}'", "status": task.status}
153
+ return {
154
+ "success": True,
155
+ "task_id": task_id,
156
+ "status": task.status,
157
+ "result": task.result,
158
+ "age": task.age_str(),
159
+ }
160
+
161
+
162
+ def tool_task_list(params: dict) -> dict:
163
+ """List all tracked background tasks."""
164
+ tasks = [t.to_dict() for t in _TASKS.values()]
165
+ if not tasks:
166
+ return {"success": True, "tasks": [], "message": "No active tasks."}
167
+ by_status: dict = {}
168
+ for t in tasks:
169
+ s = t["status"]
170
+ by_status.setdefault(s, []).append(t)
171
+ return {
172
+ "success": True,
173
+ "total": len(tasks),
174
+ "tasks": tasks,
175
+ "summary": {s: len(v) for s, v in by_status.items()},
176
+ }
177
+
178
+
179
+ def tool_task_cancel(params: dict) -> dict:
180
+ """Cancel a pending or running background task."""
181
+ task_id = params.get("task_id", "")
182
+ if not task_id:
183
+ return {"success": False, "error": "Missing 'task_id'"}
184
+ task = _TASKS.get(task_id)
185
+ if not task:
186
+ return {"success": False, "error": f"Task '{task_id}' not found"}
187
+ if task.status in ("done", "failed", "cancelled"):
188
+ return {"success": False, "error": f"Task already in terminal state: {task.status}"}
189
+ task.status = "cancelled"
190
+ task.completed_at = time.time()
191
+ return {"success": True, "task_id": task_id, "cancelled": True}
192
+
193
+
194
+ # ── Tool registry (added to LOCAL_TOOLS in aria_cli.py) ───────────────────────
195
+
196
+ SUBAGENT_TOOLS = {
197
+ "spawn_task": (tool_spawn_task, "Spawn a background sub-task; returns task_id"),
198
+ "task_status": (tool_task_status, "Check status of a background task by task_id"),
199
+ "task_result": (tool_task_result, "Retrieve result of a completed background task"),
200
+ "task_list": (tool_task_list, "List all active background tasks"),
201
+ "task_cancel": (tool_task_cancel, "Cancel a pending or running background task"),
202
+ }
203
+
204
+ SUBAGENT_SCHEMAS = [
205
+ {
206
+ "name": "spawn_task",
207
+ "description": "Spawn an independent background agent task. Useful for parallelising slow operations: research, data fetching, long analysis. Returns a task_id you can poll with task_status.",
208
+ "parameters": {
209
+ "type": "object",
210
+ "properties": {
211
+ "prompt": {"type": "string", "description": "The task for the sub-agent to perform"},
212
+ "context": {"type": "string", "description": "Optional background context to inject into the sub-agent's system prompt"},
213
+ },
214
+ "required": ["prompt"],
215
+ },
216
+ },
217
+ {
218
+ "name": "task_status",
219
+ "description": "Check the status of a background task spawned with spawn_task.",
220
+ "parameters": {
221
+ "type": "object",
222
+ "properties": {
223
+ "task_id": {"type": "string", "description": "The task_id returned by spawn_task"},
224
+ },
225
+ "required": ["task_id"],
226
+ },
227
+ },
228
+ {
229
+ "name": "task_result",
230
+ "description": "Retrieve the full result text of a completed background task.",
231
+ "parameters": {
232
+ "type": "object",
233
+ "properties": {
234
+ "task_id": {"type": "string", "description": "The task_id returned by spawn_task"},
235
+ },
236
+ "required": ["task_id"],
237
+ },
238
+ },
239
+ {
240
+ "name": "task_list",
241
+ "description": "List all active and completed background tasks with their statuses.",
242
+ "parameters": {
243
+ "type": "object",
244
+ "properties": {},
245
+ },
246
+ },
247
+ {
248
+ "name": "task_cancel",
249
+ "description": "Cancel a pending or running background task.",
250
+ "parameters": {
251
+ "type": "object",
252
+ "properties": {
253
+ "task_id": {"type": "string", "description": "The task_id to cancel"},
254
+ },
255
+ "required": ["task_id"],
256
+ },
257
+ },
258
+ ]
@@ -0,0 +1,104 @@
1
+ """Tool execution layer for Aria Code runtimes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import Any, Awaitable, Callable, Dict, Mapping, Optional
8
+
9
+ from .events import RuntimeTrace, ToolCallRecord
10
+
11
+ ToolHandler = Callable[[dict], dict]
12
+ RemoteExecutor = Callable[[str, dict], Awaitable[dict]]
13
+ Hook = Callable[[str, str, dict, Optional[dict]], None]
14
+
15
+
16
+ class ToolExecutor:
17
+ """Execute local/remote tools with hooks, policy injection, and trace records."""
18
+
19
+ def __init__(
20
+ self,
21
+ local_tools: Mapping[str, tuple],
22
+ *,
23
+ remote_executor: RemoteExecutor | None = None,
24
+ hook: Hook | None = None,
25
+ trace: RuntimeTrace | None = None,
26
+ config: Dict[str, Any] | None = None,
27
+ ) -> None:
28
+ self.local_tools = local_tools
29
+ self.remote_executor = remote_executor
30
+ self.hook = hook
31
+ self.trace = trace or RuntimeTrace()
32
+ self.config = config or {}
33
+
34
+ def execute_local(self, tool_name: str, params: dict) -> dict:
35
+ """Execute a local tool synchronously."""
36
+ if tool_name not in self.local_tools:
37
+ return {"success": False, "error": f"Unknown local tool: {tool_name}"}
38
+ handler = self.local_tools[tool_name][0]
39
+ params = self._prepare_params(tool_name, params)
40
+ return self._call_with_trace(tool_name, params, lambda: handler(params))
41
+
42
+ async def execute(self, tool_name: str, params: dict) -> dict:
43
+ """Execute a tool asynchronously, using remote executor when needed."""
44
+ if tool_name in self.local_tools:
45
+ params = self._prepare_params(tool_name, params)
46
+ loop = asyncio.get_event_loop()
47
+ return await loop.run_in_executor(None, self.execute_local, tool_name, params)
48
+ if self.remote_executor is None:
49
+ return {"success": False, "error": f"Unknown tool: {tool_name}"}
50
+ params = self._prepare_params(tool_name, params)
51
+ start = time.time()
52
+ self._run_hook("pre_tool", tool_name, params)
53
+ self.trace.emit("tool_call", {"tool": tool_name, "params": params})
54
+ try:
55
+ result = await self.remote_executor(tool_name, params)
56
+ except Exception as exc:
57
+ result = {"success": False, "error": str(exc)}
58
+ self._run_hook("post_tool", tool_name, params, result)
59
+ end = time.time()
60
+ self.trace.add_tool_call(ToolCallRecord(
61
+ tool=tool_name,
62
+ params=params,
63
+ result=result,
64
+ elapsed_ms=(end - start) * 1000,
65
+ started_at=start,
66
+ ended_at=end,
67
+ ))
68
+ return result
69
+
70
+ def _call_with_trace(self, tool_name: str, params: dict, fn: Callable[[], dict]) -> dict:
71
+ start = time.time()
72
+ self._run_hook("pre_tool", tool_name, params)
73
+ self.trace.emit("tool_call", {"tool": tool_name, "params": params})
74
+ try:
75
+ result = fn()
76
+ except Exception as exc:
77
+ result = {"success": False, "error": str(exc)}
78
+ self._run_hook("post_tool", tool_name, params, result)
79
+ end = time.time()
80
+ self.trace.add_tool_call(ToolCallRecord(
81
+ tool=tool_name,
82
+ params=params,
83
+ result=result,
84
+ elapsed_ms=(end - start) * 1000,
85
+ started_at=start,
86
+ ended_at=end,
87
+ ))
88
+ return result
89
+
90
+ def _prepare_params(self, tool_name: str, params: dict) -> dict:
91
+ prepared = dict(params or {})
92
+ if tool_name == "run_command":
93
+ prepared.setdefault("policy", self.config.get("command_policy", "safe"))
94
+ prepared.setdefault("permission_mode", self.config.get("permission_mode", "workspace-write"))
95
+ prepared.setdefault("network_enabled", bool(self.config.get("network_enabled", True)))
96
+ return prepared
97
+
98
+ def _run_hook(self, hook_type: str, tool_name: str, params: dict, result: dict | None = None) -> None:
99
+ if self.hook is None:
100
+ return
101
+ try:
102
+ self.hook(hook_type, tool_name, params, result)
103
+ except Exception:
104
+ pass
runtime/tool_policy.py ADDED
@@ -0,0 +1,106 @@
1
+ """Persistent per-tool execution policy: allowlist, denylist, ask-always.
2
+
3
+ Stores policy in ~/.arthera/tool_policy.json.
4
+ Checked in _confirm_tool_execution_decision() before any user prompt.
5
+
6
+ Usage:
7
+ from runtime.tool_policy import check_tool_policy, add_to_policy
8
+
9
+ verdict = check_tool_policy("write_file") # "allow" | "deny" | "ask" | "default"
10
+ add_to_policy("read_file", "allow") # permanently auto-approve
11
+ add_to_policy("run_command", "deny") # permanently block
12
+ add_to_policy("edit_file", "ask") # always prompt
13
+ remove_from_policy("read_file") # remove from all lists
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from pathlib import Path
20
+ from typing import Literal
21
+
22
+ PolicyVerdict = Literal["allow", "deny", "ask", "default"]
23
+
24
+ _DEFAULT_POLICY: dict = {
25
+ "allowed": [], # auto-approve without prompt
26
+ "denied": [], # always block, never execute
27
+ "ask_always": [], # always prompt even for non-CONFIRM_TOOLS
28
+ }
29
+
30
+
31
+ def _policy_file() -> Path:
32
+ return Path.home() / ".arthera" / "tool_policy.json"
33
+
34
+
35
+ def load_tool_policy() -> dict:
36
+ """Load policy from disk; returns defaults if missing or corrupt."""
37
+ f = _policy_file()
38
+ if f.exists():
39
+ try:
40
+ raw = json.loads(f.read_text(encoding="utf-8"))
41
+ return {
42
+ "allowed": list(raw.get("allowed", [])),
43
+ "denied": list(raw.get("denied", [])),
44
+ "ask_always": list(raw.get("ask_always", [])),
45
+ }
46
+ except Exception:
47
+ pass
48
+ return {k: list(v) for k, v in _DEFAULT_POLICY.items()}
49
+
50
+
51
+ def save_tool_policy(policy: dict) -> None:
52
+ f = _policy_file()
53
+ f.parent.mkdir(parents=True, exist_ok=True)
54
+ f.write_text(json.dumps(policy, indent=2, ensure_ascii=False), encoding="utf-8")
55
+
56
+
57
+ def check_tool_policy(tool_name: str) -> PolicyVerdict:
58
+ """Return the persistent verdict for *tool_name*.
59
+
60
+ "allow" → auto-approve, skip confirmation prompt
61
+ "deny" → block, never execute
62
+ "ask" → always prompt (overrides session auto-allow)
63
+ "default" → no override, fall through to normal flow
64
+ """
65
+ policy = load_tool_policy()
66
+ if tool_name in policy.get("denied", []):
67
+ return "deny"
68
+ if tool_name in policy.get("allowed", []):
69
+ return "allow"
70
+ if tool_name in policy.get("ask_always", []):
71
+ return "ask"
72
+ return "default"
73
+
74
+
75
+ def add_to_policy(tool_name: str, verdict: Literal["allow", "deny", "ask"]) -> None:
76
+ """Add *tool_name* to the given policy list, removing it from any other list first."""
77
+ policy = load_tool_policy()
78
+ for key in ("allowed", "denied", "ask_always"):
79
+ policy.setdefault(key, [])
80
+ if tool_name in policy[key]:
81
+ policy[key].remove(tool_name)
82
+ if verdict == "allow":
83
+ policy["allowed"].append(tool_name)
84
+ elif verdict == "deny":
85
+ policy["denied"].append(tool_name)
86
+ elif verdict == "ask":
87
+ policy["ask_always"].append(tool_name)
88
+ save_tool_policy(policy)
89
+
90
+
91
+ def remove_from_policy(tool_name: str) -> bool:
92
+ """Remove *tool_name* from all lists. Returns True if it was present."""
93
+ policy = load_tool_policy()
94
+ changed = False
95
+ for key in ("allowed", "denied", "ask_always"):
96
+ if tool_name in policy.get(key, []):
97
+ policy[key].remove(tool_name)
98
+ changed = True
99
+ if changed:
100
+ save_tool_policy(policy)
101
+ return changed
102
+
103
+
104
+ def policy_summary() -> dict:
105
+ """Return current policy as a human-readable dict."""
106
+ return load_tool_policy()
safety/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """Safety and permission primitives for Aria Code."""
2
+
3
+ from .permissions import (
4
+ PermissionDecision,
5
+ PermissionMode,
6
+ PermissionService,
7
+ PolicyDecision,
8
+ classify_command_risk,
9
+ evaluate_command_policy,
10
+ normalize_command,
11
+ )
12
+
13
+ __all__ = [
14
+ "PermissionDecision",
15
+ "PermissionMode",
16
+ "PermissionService",
17
+ "PolicyDecision",
18
+ "classify_command_risk",
19
+ "evaluate_command_policy",
20
+ "normalize_command",
21
+ ]