aria-code 0.52.0__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 (478) hide show
  1. aria_code/__init__.py +42 -0
  2. aria_code/_version.py +20 -0
  3. aria_code/adk_apps/__init__.py +1 -0
  4. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  5. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  6. aria_code/adk_apps/aria_research/__init__.py +1 -0
  7. aria_code/adk_apps/aria_research/agent.py +31 -0
  8. aria_code/agents/__init__.py +34 -0
  9. aria_code/agents/base.py +318 -0
  10. aria_code/agents/code_review.py +200 -0
  11. aria_code/agents/deep/__init__.py +37 -0
  12. aria_code/agents/deep/calibration_loop.py +144 -0
  13. aria_code/agents/deep/critic.py +125 -0
  14. aria_code/agents/deep/deepen.py +193 -0
  15. aria_code/agents/deep/models.py +149 -0
  16. aria_code/agents/deep/pipeline.py +164 -0
  17. aria_code/agents/deep/quant_fusion.py +192 -0
  18. aria_code/agents/deep/themes.py +95 -0
  19. aria_code/agents/deep/tiers.py +106 -0
  20. aria_code/agents/engineering/__init__.py +14 -0
  21. aria_code/agents/engineering/coder.py +222 -0
  22. aria_code/agents/engineering/tester.py +145 -0
  23. aria_code/agents/financial/__init__.py +21 -0
  24. aria_code/agents/financial/cashflow_burn.py +119 -0
  25. aria_code/agents/financial/catalyst.py +279 -0
  26. aria_code/agents/financial/corporate_finance.py +175 -0
  27. aria_code/agents/financial/debate.py +224 -0
  28. aria_code/agents/financial/earnings.py +303 -0
  29. aria_code/agents/financial/fundamental.py +191 -0
  30. aria_code/agents/financial/macro.py +99 -0
  31. aria_code/agents/financial/news.py +207 -0
  32. aria_code/agents/financial/risk.py +165 -0
  33. aria_code/agents/financial/sector.py +279 -0
  34. aria_code/agents/financial/strategist.py +121 -0
  35. aria_code/agents/financial/stripe_revenue.py +127 -0
  36. aria_code/agents/financial/synthesis.py +274 -0
  37. aria_code/agents/financial/technical.py +258 -0
  38. aria_code/agents/orchestrator.py +44 -0
  39. aria_code/agents/portfolio_agent.py +363 -0
  40. aria_code/agents/realty/__init__.py +62 -0
  41. aria_code/agents/realty/asset_diagnosis.py +152 -0
  42. aria_code/agents/realty/business_match.py +166 -0
  43. aria_code/agents/realty/cashflow_verify.py +209 -0
  44. aria_code/agents/realty/contract_rules.py +210 -0
  45. aria_code/agents/realty/energy_anomaly.py +189 -0
  46. aria_code/agents/realty/exit_settlement.py +208 -0
  47. aria_code/agents/realty/fulfillment_risk.py +206 -0
  48. aria_code/agents/realty/ops_optimize.py +160 -0
  49. aria_code/agents/realty/revenue_share.py +215 -0
  50. aria_code/agents/registry.py +158 -0
  51. aria_code/agents/signal_scheme.py +111 -0
  52. aria_code/agents/sports/__init__.py +0 -0
  53. aria_code/agents/sports/football_agent.py +197 -0
  54. aria_code/agents/supervisor.py +113 -0
  55. aria_code/agents/team.py +492 -0
  56. aria_code/agents/warehouse/__init__.py +25 -0
  57. aria_code/agents/warehouse/contracts.py +26 -0
  58. aria_code/agents/warehouse/dashboard.py +155 -0
  59. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  60. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  61. aria_code/agents/warehouse/inventory_health.py +24 -0
  62. aria_code/agents/warehouse/local_run.py +71 -0
  63. aria_code/agents/warehouse/local_snapshot.py +132 -0
  64. aria_code/agents/warehouse/logistics_cost.py +84 -0
  65. aria_code/agents/warehouse/logistics_sync.py +21 -0
  66. aria_code/agents/warehouse/workflow.py +31 -0
  67. aria_code/aliyun_data_client.py +4 -0
  68. aria_code/apps/README.md +12 -0
  69. aria_code/apps/__init__.py +2 -0
  70. aria_code/apps/channels/README.md +15 -0
  71. aria_code/apps/channels/__init__.py +17 -0
  72. aria_code/apps/channels/intake.py +92 -0
  73. aria_code/apps/channels/registry.py +95 -0
  74. aria_code/apps/channels/tradingview.py +94 -0
  75. aria_code/apps/cli/README.md +13 -0
  76. aria_code/apps/cli/__init__.py +2 -0
  77. aria_code/apps/cli/_optional.py +120 -0
  78. aria_code/apps/cli/bootstrap.py +145 -0
  79. aria_code/apps/cli/broker_render.py +206 -0
  80. aria_code/apps/cli/codegen_paths.py +29 -0
  81. aria_code/apps/cli/commands/__init__.py +16 -0
  82. aria_code/apps/cli/commands/_business_render.py +78 -0
  83. aria_code/apps/cli/commands/_ui.py +71 -0
  84. aria_code/apps/cli/commands/analysis_cmds.py +369 -0
  85. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  86. aria_code/apps/cli/commands/auth_cmds.py +239 -0
  87. aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
  88. aria_code/apps/cli/commands/broker_cmds.py +1234 -0
  89. aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
  90. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  91. aria_code/apps/cli/commands/catalog.py +87 -0
  92. aria_code/apps/cli/commands/core_cmds.py +2520 -0
  93. aria_code/apps/cli/commands/data_cmds.py +457 -0
  94. aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
  95. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  96. aria_code/apps/cli/commands/file_cmds.py +312 -0
  97. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  98. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  99. aria_code/apps/cli/commands/market.py +434 -0
  100. aria_code/apps/cli/commands/market_cmds.py +1381 -0
  101. aria_code/apps/cli/commands/market_context.py +425 -0
  102. aria_code/apps/cli/commands/market_render.py +7 -0
  103. aria_code/apps/cli/commands/model_cmds.py +2153 -0
  104. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  105. aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
  106. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  107. aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
  108. aria_code/apps/cli/commands/report.py +437 -0
  109. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  110. aria_code/apps/cli/commands/session_cmds.py +236 -0
  111. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  112. aria_code/apps/cli/commands/team.py +669 -0
  113. aria_code/apps/cli/commands/team_render.py +8 -0
  114. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  115. aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
  116. aria_code/apps/cli/commands/workflow_cmds.py +480 -0
  117. aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
  118. aria_code/apps/cli/config_paths.py +73 -0
  119. aria_code/apps/cli/config_store.py +78 -0
  120. aria_code/apps/cli/constants.py +88 -0
  121. aria_code/apps/cli/context.py +34 -0
  122. aria_code/apps/cli/deterministic.py +191 -0
  123. aria_code/apps/cli/direct.py +48 -0
  124. aria_code/apps/cli/finance_service_catalog.py +111 -0
  125. aria_code/apps/cli/football_reports.py +231 -0
  126. aria_code/apps/cli/github_app_auth.py +135 -0
  127. aria_code/apps/cli/google_login.py +196 -0
  128. aria_code/apps/cli/handlers/__init__.py +11 -0
  129. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  130. aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
  131. aria_code/apps/cli/handlers/market_handlers.py +2513 -0
  132. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  133. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  134. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  135. aria_code/apps/cli/helpers.py +302 -0
  136. aria_code/apps/cli/hooks.py +180 -0
  137. aria_code/apps/cli/i18n.py +297 -0
  138. aria_code/apps/cli/intent.py +136 -0
  139. aria_code/apps/cli/intent_router.py +276 -0
  140. aria_code/apps/cli/intent_signals.py +344 -0
  141. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  142. aria_code/apps/cli/local_tool_schemas.py +536 -0
  143. aria_code/apps/cli/main.py +29 -0
  144. aria_code/apps/cli/market_metadata.py +135 -0
  145. aria_code/apps/cli/market_universe.py +487 -0
  146. aria_code/apps/cli/message_processing.py +310 -0
  147. aria_code/apps/cli/model_catalog.py +620 -0
  148. aria_code/apps/cli/pdf_report.py +977 -0
  149. aria_code/apps/cli/plan_mode.py +139 -0
  150. aria_code/apps/cli/plotly_html.py +15 -0
  151. aria_code/apps/cli/prediction_feedback.py +202 -0
  152. aria_code/apps/cli/preflight.py +502 -0
  153. aria_code/apps/cli/project_aria.py +60 -0
  154. aria_code/apps/cli/prompt_assembly.py +59 -0
  155. aria_code/apps/cli/prompts/__init__.py +0 -0
  156. aria_code/apps/cli/prompts/coding.py +53 -0
  157. aria_code/apps/cli/prompts/select.py +142 -0
  158. aria_code/apps/cli/prompts/system_prompts.py +552 -0
  159. aria_code/apps/cli/prompts/ui.py +436 -0
  160. aria_code/apps/cli/provider_endpoints.py +237 -0
  161. aria_code/apps/cli/provider_keys.py +73 -0
  162. aria_code/apps/cli/providers/__init__.py +1 -0
  163. aria_code/apps/cli/providers/base.py +521 -0
  164. aria_code/apps/cli/providers/chat_routing.py +128 -0
  165. aria_code/apps/cli/providers/collaboration.py +98 -0
  166. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  167. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  168. aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
  169. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  170. aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
  171. aria_code/apps/cli/providers/runtime_bridge.py +353 -0
  172. aria_code/apps/cli/providers/vertexai_stream.py +316 -0
  173. aria_code/apps/cli/runtime_consumer.py +657 -0
  174. aria_code/apps/cli/sandbox.py +32 -0
  175. aria_code/apps/cli/session_export.py +120 -0
  176. aria_code/apps/cli/session_jsonl.py +208 -0
  177. aria_code/apps/cli/session_store.py +112 -0
  178. aria_code/apps/cli/skills_catalog.py +273 -0
  179. aria_code/apps/cli/todo_tracker.py +190 -0
  180. aria_code/apps/cli/tool_executor.py +823 -0
  181. aria_code/apps/cli/tool_registry.py +34 -0
  182. aria_code/apps/cli/tool_scope.py +106 -0
  183. aria_code/apps/cli/tools/__init__.py +40 -0
  184. aria_code/apps/cli/tools/context.py +46 -0
  185. aria_code/apps/cli/tools/file_tools.py +180 -0
  186. aria_code/apps/cli/tools/market_tools.py +620 -0
  187. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  188. aria_code/apps/cli/tools/system_tools.py +726 -0
  189. aria_code/apps/cli/tools/write_tools.py +845 -0
  190. aria_code/apps/cli/tradingview_bridge.py +529 -0
  191. aria_code/apps/cli/turn_planning.py +137 -0
  192. aria_code/apps/cli/update_check.py +153 -0
  193. aria_code/apps/cli/utils/__init__.py +0 -0
  194. aria_code/apps/cli/utils/market_detect.py +1597 -0
  195. aria_code/apps/daemon/README.md +14 -0
  196. aria_code/apps/vscode/README.md +115 -0
  197. aria_code/apps/vscode/package.json +70 -0
  198. aria_code/aria_cli.py +6743 -0
  199. aria_code/aria_daemon.py +1398 -0
  200. aria_code/aria_feishu_bot.py +1423 -0
  201. aria_code/aria_mcp_server.py +7 -0
  202. aria_code/aria_relay_client.py +4 -0
  203. aria_code/aria_relay_server.py +490 -0
  204. aria_code/aria_telegram_bot.py +202 -0
  205. aria_code/ariarc.py +385 -0
  206. aria_code/artifacts.py +581 -0
  207. aria_code/backtest_engine.py +4 -0
  208. aria_code/backtest_report.py +4 -0
  209. aria_code/brokers/__init__.py +78 -0
  210. aria_code/brokers/automation.py +193 -0
  211. aria_code/brokers/base.py +231 -0
  212. aria_code/brokers/capabilities.py +264 -0
  213. aria_code/brokers/cn/__init__.py +10 -0
  214. aria_code/brokers/cn/easytrader_broker.py +193 -0
  215. aria_code/brokers/cn/futu_broker.py +194 -0
  216. aria_code/brokers/cn/longbridge_broker.py +190 -0
  217. aria_code/brokers/cn/tiger_broker.py +196 -0
  218. aria_code/brokers/cn/xtquant_broker.py +225 -0
  219. aria_code/brokers/config.py +414 -0
  220. aria_code/brokers/intl/__init__.py +5 -0
  221. aria_code/brokers/intl/alpaca_broker.py +183 -0
  222. aria_code/brokers/intl/ibkr_broker.py +215 -0
  223. aria_code/brokers/intl/trading212_broker.py +183 -0
  224. aria_code/brokers/intl/webull_broker.py +156 -0
  225. aria_code/brokers/paper_broker.py +266 -0
  226. aria_code/brokers/planning.py +296 -0
  227. aria_code/brokers/registry.py +213 -0
  228. aria_code/brokers/trading.py +267 -0
  229. aria_code/canva_client.py +4 -0
  230. aria_code/change_store.py +127 -0
  231. aria_code/clients/__init__.py +0 -0
  232. aria_code/clients/aliyun_data_client.py +660 -0
  233. aria_code/clients/aria_relay_client.py +182 -0
  234. aria_code/clients/canva_client.py +284 -0
  235. aria_code/clients/figma_client.py +100 -0
  236. aria_code/clients/football_data_client.py +1671 -0
  237. aria_code/clients/kling_video_client.py +182 -0
  238. aria_code/clients/market_data_client.py +1976 -0
  239. aria_code/clients/openai_image_client.py +242 -0
  240. aria_code/clients/runway_video_client.py +157 -0
  241. aria_code/clients/warehouse_erp_client.py +105 -0
  242. aria_code/cloud_memory.py +181 -0
  243. aria_code/command_safety.py +19 -0
  244. aria_code/computer_use_tools.py +4 -0
  245. aria_code/dashboard_generator.py +579 -0
  246. aria_code/data_analysis_tools.py +4 -0
  247. aria_code/data_cleaner.py +483 -0
  248. aria_code/data_service.py +513 -0
  249. aria_code/datasources/__init__.py +23 -0
  250. aria_code/datasources/base.py +166 -0
  251. aria_code/datasources/router.py +253 -0
  252. aria_code/datasources/sources/__init__.py +15 -0
  253. aria_code/datasources/sources/akshare_source.py +269 -0
  254. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  255. aria_code/datasources/sources/edgar_source.py +219 -0
  256. aria_code/datasources/sources/finnhub_source.py +198 -0
  257. aria_code/datasources/sources/fred_source.py +223 -0
  258. aria_code/datasources/sources/tushare_source.py +143 -0
  259. aria_code/datasources/sources/web_scraper_source.py +278 -0
  260. aria_code/datasources/sources/world_bank_source.py +206 -0
  261. aria_code/datasources/sources/yfinance_source.py +152 -0
  262. aria_code/demo_player.py +207 -0
  263. aria_code/doctor.py +813 -0
  264. aria_code/domain/__init__.py +0 -0
  265. aria_code/domain/backtest_engine.py +354 -0
  266. aria_code/domain/backtest_report.py +472 -0
  267. aria_code/domain/finance_formulas.py +389 -0
  268. aria_code/domain/portfolio_ledger.py +307 -0
  269. aria_code/domain/strategy_vault.py +421 -0
  270. aria_code/evals/__init__.py +37 -0
  271. aria_code/evals/harness.py +598 -0
  272. aria_code/evals/runner.py +238 -0
  273. aria_code/external_agent_runner.py +115 -0
  274. aria_code/figma_client.py +4 -0
  275. aria_code/file_analysis_tools.py +4 -0
  276. aria_code/finance_formulas.py +4 -0
  277. aria_code/football_data_client.py +4 -0
  278. aria_code/image_gen_tools.py +4 -0
  279. aria_code/intent_classifier.py +386 -0
  280. aria_code/kling_video_client.py +4 -0
  281. aria_code/licensing.py +151 -0
  282. aria_code/local_finance_tools.py +4 -0
  283. aria_code/local_image_provider.py +4 -0
  284. aria_code/local_llm_provider.py +4 -0
  285. aria_code/macro_tools.py +4 -0
  286. aria_code/markdown_pdf.py +289 -0
  287. aria_code/market_data_client.py +4 -0
  288. aria_code/mcp_client.py +639 -0
  289. aria_code/memory_manager.py +246 -0
  290. aria_code/model_capability.py +486 -0
  291. aria_code/notification_tools.py +4 -0
  292. aria_code/openai_image_client.py +4 -0
  293. aria_code/packages/__init__.py +23 -0
  294. aria_code/packages/adk_bridge/__init__.py +11 -0
  295. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  296. aria_code/packages/adk_bridge/market_tools.py +125 -0
  297. aria_code/packages/aria_agents/__init__.py +5 -0
  298. aria_code/packages/aria_agents/manifest.py +78 -0
  299. aria_code/packages/aria_core/__init__.py +34 -0
  300. aria_code/packages/aria_core/architecture.py +269 -0
  301. aria_code/packages/aria_core/export.py +147 -0
  302. aria_code/packages/aria_core/manifest.py +65 -0
  303. aria_code/packages/aria_core/paths.py +57 -0
  304. aria_code/packages/aria_core/secure_file.py +68 -0
  305. aria_code/packages/aria_infra/__init__.py +15 -0
  306. aria_code/packages/aria_infra/arthera.py +52 -0
  307. aria_code/packages/aria_infra/doctor.py +246 -0
  308. aria_code/packages/aria_infra/product.py +40 -0
  309. aria_code/packages/aria_mcp/__init__.py +25 -0
  310. aria_code/packages/aria_mcp/bridge.py +201 -0
  311. aria_code/packages/aria_mcp/circuit.py +82 -0
  312. aria_code/packages/aria_mcp/config.py +111 -0
  313. aria_code/packages/aria_mcp/installer.py +95 -0
  314. aria_code/packages/aria_mcp/server.py +1550 -0
  315. aria_code/packages/aria_mcp/tools.py +81 -0
  316. aria_code/packages/aria_sdk/__init__.py +19 -0
  317. aria_code/packages/aria_sdk/client.py +396 -0
  318. aria_code/packages/aria_sdk/providers.py +70 -0
  319. aria_code/packages/aria_sdk/streaming.py +79 -0
  320. aria_code/packages/aria_sdk/types.py +86 -0
  321. aria_code/packages/aria_services/__init__.py +92 -0
  322. aria_code/packages/aria_services/cache.py +60 -0
  323. aria_code/packages/aria_services/context.py +356 -0
  324. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  325. aria_code/packages/aria_services/data.py +11 -0
  326. aria_code/packages/aria_services/provider_health.py +268 -0
  327. aria_code/packages/aria_services/references.py +247 -0
  328. aria_code/packages/aria_services/registry.py +249 -0
  329. aria_code/packages/aria_services/research_protocol.py +57 -0
  330. aria_code/packages/aria_services/research_quality.py +186 -0
  331. aria_code/packages/aria_services/settings.py +157 -0
  332. aria_code/packages/aria_services/usage.py +151 -0
  333. aria_code/packages/aria_skills/__init__.py +46 -0
  334. aria_code/packages/aria_skills/catalog.py +149 -0
  335. aria_code/packages/aria_skills/loader.py +455 -0
  336. aria_code/packages/aria_skills/registry.py +84 -0
  337. aria_code/packages/aria_tools/__init__.py +5 -0
  338. aria_code/packages/aria_tools/financial/__init__.py +8 -0
  339. aria_code/packages/aria_tools/financial/_paths.py +130 -0
  340. aria_code/packages/aria_tools/financial/compliance.py +31 -0
  341. aria_code/packages/aria_tools/financial/factors.py +33 -0
  342. aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
  343. aria_code/packages/aria_tools/financial/strategy.py +31 -0
  344. aria_code/packages/aria_tools/registry.py +128 -0
  345. aria_code/packages/quant_engine/__init__.py +34 -0
  346. aria_code/packages/quant_engine/_namespace.py +62 -0
  347. aria_code/packages/quant_engine/backtest/__init__.py +14 -0
  348. aria_code/packages/quant_engine/backtest/core.py +53 -0
  349. aria_code/packages/quant_engine/backtest/engine.py +96 -0
  350. aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
  351. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  352. aria_code/packages/quant_engine/sports/__init__.py +80 -0
  353. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  354. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  355. aria_code/packages/quant_engine/sports/elo.py +300 -0
  356. aria_code/packages/quant_engine/sports/form.py +188 -0
  357. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  358. aria_code/packages/quant_engine/sports/ml_model.py +377 -0
  359. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  360. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  361. aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
  362. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  363. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  364. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  365. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  366. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  367. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  368. aria_code/packs/__init__.py +85 -0
  369. aria_code/packs/base.py +168 -0
  370. aria_code/packs/finance/__init__.py +250 -0
  371. aria_code/packs/logistics/__init__.py +169 -0
  372. aria_code/packs/payments/__init__.py +139 -0
  373. aria_code/packs/realty/__init__.py +190 -0
  374. aria_code/packs/registry.py +202 -0
  375. aria_code/packs/rules.py +90 -0
  376. aria_code/plan_utils.py +194 -0
  377. aria_code/plugin_loader.py +328 -0
  378. aria_code/portfolio_ledger.py +4 -0
  379. aria_code/preview_server.py +449 -0
  380. aria_code/privacy/__init__.py +5 -0
  381. aria_code/privacy/feedback.py +123 -0
  382. aria_code/project_review.py +278 -0
  383. aria_code/project_review_api.py +235 -0
  384. aria_code/project_review_server.py +18 -0
  385. aria_code/project_tools.py +4 -0
  386. aria_code/providers/__init__.py +30 -0
  387. aria_code/providers/llm/__init__.py +19 -0
  388. aria_code/providers/llm/anthropic.py +184 -0
  389. aria_code/providers/llm/autoconfig.py +213 -0
  390. aria_code/providers/llm/base.py +139 -0
  391. aria_code/providers/llm/custom.py +158 -0
  392. aria_code/providers/llm/ollama.py +129 -0
  393. aria_code/providers/llm/openai_compat.py +313 -0
  394. aria_code/providers/llm/registry.py +444 -0
  395. aria_code/providers/local_image_provider.py +202 -0
  396. aria_code/providers/local_llm_provider.py +589 -0
  397. aria_code/realty_data_tools.py +4 -0
  398. aria_code/report_exporters.py +150 -0
  399. aria_code/report_generator.py +1764 -0
  400. aria_code/runtime/__init__.py +169 -0
  401. aria_code/runtime/acceptance.py +481 -0
  402. aria_code/runtime/agent_loop.py +1520 -0
  403. aria_code/runtime/approval.py +57 -0
  404. aria_code/runtime/budget.py +232 -0
  405. aria_code/runtime/checkpoints.py +432 -0
  406. aria_code/runtime/crash_report.py +174 -0
  407. aria_code/runtime/events.py +113 -0
  408. aria_code/runtime/gateway.py +150 -0
  409. aria_code/runtime/lsp.py +484 -0
  410. aria_code/runtime/orchestrator.py +55 -0
  411. aria_code/runtime/repo_map.py +700 -0
  412. aria_code/runtime/run_state.py +127 -0
  413. aria_code/runtime/run_store.py +450 -0
  414. aria_code/runtime/self_healing.py +422 -0
  415. aria_code/runtime/subagent.py +547 -0
  416. aria_code/runtime/task_ledger.py +86 -0
  417. aria_code/runtime/task_orchestrator.py +90 -0
  418. aria_code/runtime/tool_executor.py +217 -0
  419. aria_code/runtime/tool_policy.py +107 -0
  420. aria_code/runtime/worktrees.py +215 -0
  421. aria_code/runway_video_client.py +4 -0
  422. aria_code/safety/__init__.py +23 -0
  423. aria_code/safety/permissions.py +306 -0
  424. aria_code/safety/service.py +102 -0
  425. aria_code/setup_wizard.py +673 -0
  426. aria_code/spreadsheet_tools.py +4 -0
  427. aria_code/strategy_vault.py +4 -0
  428. aria_code/tools/__init__.py +0 -0
  429. aria_code/tools/artifact_tools.py +252 -0
  430. aria_code/tools/broker_tools.py +92 -0
  431. aria_code/tools/build_quant_engine.py +97 -0
  432. aria_code/tools/code_audit_tools.py +192 -0
  433. aria_code/tools/computer_use_tools.py +504 -0
  434. aria_code/tools/data_analysis_tools.py +809 -0
  435. aria_code/tools/enterprise_finance_tools.py +156 -0
  436. aria_code/tools/extended_tools.py +240 -0
  437. aria_code/tools/file_analysis_tools.py +885 -0
  438. aria_code/tools/image_gen_tools.py +114 -0
  439. aria_code/tools/local_finance_tools.py +3417 -0
  440. aria_code/tools/logistics_tools.py +135 -0
  441. aria_code/tools/macro_tools.py +369 -0
  442. aria_code/tools/notification_tools.py +249 -0
  443. aria_code/tools/project_tools.py +525 -0
  444. aria_code/tools/realty_data_tools.py +659 -0
  445. aria_code/tools/spreadsheet_tools.py +388 -0
  446. aria_code/tools/stripe_tools.py +115 -0
  447. aria_code/ui/__init__.py +105 -0
  448. aria_code/ui/banner.py +508 -0
  449. aria_code/ui/completer.py +501 -0
  450. aria_code/ui/console.py +281 -0
  451. aria_code/ui/image_render.py +243 -0
  452. aria_code/ui/input_box.py +488 -0
  453. aria_code/ui/picker.py +201 -0
  454. aria_code/ui/render/__init__.py +11 -0
  455. aria_code/ui/render/finance.py +1705 -0
  456. aria_code/ui/render/market.py +225 -0
  457. aria_code/ui/render/output.py +775 -0
  458. aria_code/ui/render/responsive.py +75 -0
  459. aria_code/ui/render/team.py +354 -0
  460. aria_code/ui/robot.py +235 -0
  461. aria_code/ui/startup_dashboard.py +109 -0
  462. aria_code/utils/vscode_uri.py +37 -0
  463. aria_code/video_analysis.py +147 -0
  464. aria_code/video_editor.py +271 -0
  465. aria_code/workspace/__init__.py +6 -0
  466. aria_code/workspace/files.py +193 -0
  467. aria_code/workspace/verify.py +113 -0
  468. aria_code/workspace_changes.py +199 -0
  469. aria_code/workspace_context.py +233 -0
  470. aria_code/workspace_service.py +196 -0
  471. aria_code/workspace_tools.py +150 -0
  472. aria_code-0.52.0.dist-info/METADATA +237 -0
  473. aria_code-0.52.0.dist-info/RECORD +478 -0
  474. aria_code-0.52.0.dist-info/WHEEL +5 -0
  475. aria_code-0.52.0.dist-info/entry_points.txt +3 -0
  476. aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
  477. aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
  478. aria_code-0.52.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,193 @@
1
+ """Safe workspace file operations for Aria Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+ import re
7
+ import tempfile
8
+ import os
9
+ from dataclasses import dataclass
10
+ from typing import Any, Dict, List
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ReadResult:
15
+ path: str
16
+ lines: int
17
+ content: str
18
+
19
+
20
+ class WorkspaceSecurity:
21
+ """Path security policy shared by CLI tools and future runtimes."""
22
+
23
+ BLOCKED_ROOTS = (
24
+ "/etc",
25
+ "/sys",
26
+ "/proc",
27
+ "/dev",
28
+ "/boot",
29
+ "/root",
30
+ )
31
+
32
+ def __init__(
33
+ self,
34
+ cwd: str | pathlib.Path | None = None,
35
+ *,
36
+ allowed_roots: List[str | pathlib.Path] | None = None,
37
+ allow_home: bool | None = None,
38
+ ) -> None:
39
+ self.cwd = pathlib.Path(cwd or pathlib.Path.cwd()).expanduser().resolve()
40
+ self._configured_roots = tuple(
41
+ pathlib.Path(root).expanduser().resolve() for root in (allowed_roots or ())
42
+ )
43
+ if allow_home is None:
44
+ # Local CLI keeps its historical convenience. Remote/API workers must
45
+ # set ARIA_RUNTIME_SCOPE=remote (or pass allow_home=False) so one user
46
+ # can never inspect the worker account's home directory.
47
+ allow_home = os.getenv("ARIA_RUNTIME_SCOPE", "local").strip().lower() != "remote"
48
+ self.allow_home = bool(allow_home)
49
+
50
+ def allowed_roots(self) -> List[pathlib.Path]:
51
+ roots = [self.cwd, *self._configured_roots]
52
+ if self.allow_home:
53
+ roots.append(pathlib.Path.home().resolve())
54
+ # Temporary roots are required by the local CLI, but a remote worker is
55
+ # confined to its explicitly assigned workspace root.
56
+ if not self.allow_home:
57
+ return list(dict.fromkeys(roots))
58
+ roots.append(pathlib.Path("/tmp").resolve())
59
+ for tmp_candidate in (
60
+ "/var/folders",
61
+ "/private/tmp",
62
+ "/private/var/folders",
63
+ "/var/tmp",
64
+ "/private/var/tmp",
65
+ tempfile.gettempdir(),
66
+ ):
67
+ try:
68
+ root = pathlib.Path(tmp_candidate).resolve()
69
+ if root not in roots:
70
+ roots.append(root)
71
+ except Exception:
72
+ pass
73
+ return roots
74
+
75
+ def resolve(self, path: str | pathlib.Path) -> pathlib.Path:
76
+ return pathlib.Path(path).expanduser().resolve()
77
+
78
+ def is_safe_path(self, path: str | pathlib.Path) -> bool:
79
+ resolved = self.resolve(path)
80
+ for blocked in self.BLOCKED_ROOTS:
81
+ try:
82
+ resolved.relative_to(pathlib.Path(blocked).resolve())
83
+ return False
84
+ except ValueError:
85
+ pass
86
+ for root in self.allowed_roots():
87
+ try:
88
+ resolved.relative_to(root)
89
+ return True
90
+ except ValueError:
91
+ pass
92
+ return False
93
+
94
+ def require_safe(self, path: str | pathlib.Path) -> pathlib.Path:
95
+ resolved = self.resolve(path)
96
+ if not self.is_safe_path(resolved):
97
+ raise PermissionError(f"Access denied: path '{resolved}' is outside allowed directories")
98
+ return resolved
99
+
100
+
101
+ class WorkspaceFiles:
102
+ """Read, list, and search files under Aria Code's safety policy."""
103
+
104
+ MAX_READ_BYTES = 2_000_000
105
+ LARGE_FILE_BYTES = 500_000
106
+ LARGE_FILE_DEFAULT_LINES = 500
107
+ MAX_SEARCH_BYTES = 5_000_000
108
+
109
+ def __init__(self, security: WorkspaceSecurity | None = None) -> None:
110
+ self.security = security or WorkspaceSecurity()
111
+
112
+ def read_file(self, path: str, offset: int = 0, limit: int = 0) -> ReadResult:
113
+ target = self.security.require_safe(path)
114
+ if not target.exists():
115
+ raise FileNotFoundError(f"File not found: {target}")
116
+ if not target.is_file():
117
+ raise ValueError(f"Not a file: {target}")
118
+ size = target.stat().st_size
119
+ if size > self.MAX_READ_BYTES:
120
+ raise ValueError(
121
+ f"File too large: {size:,} bytes (max 2 MB). "
122
+ "Use offset/limit parameters to read sections."
123
+ )
124
+ text = target.read_text(errors="replace")
125
+ lines = text.splitlines()
126
+ total_lines = len(lines)
127
+ if not offset and not limit and size > self.LARGE_FILE_BYTES:
128
+ limit = self.LARGE_FILE_DEFAULT_LINES
129
+ if offset or limit:
130
+ end = offset + limit if limit else total_lines
131
+ shown = lines[offset:end]
132
+ content = "\n".join(f"{i + offset + 1:4d}│ {line}" for i, line in enumerate(shown))
133
+ if limit and end < total_lines:
134
+ content += f"\n... [{len(shown)} of {total_lines} lines shown — use offset/limit to read more]"
135
+ return ReadResult(str(target), len(shown), content[:30000])
136
+ content = "\n".join(f"{i + 1:4d}│ {line}" for i, line in enumerate(lines))
137
+ return ReadResult(str(target), len(lines), content[:30000])
138
+
139
+ def list_files(self, path: str = ".", pattern: str = "*") -> Dict[str, Any]:
140
+ target = self.security.require_safe(path)
141
+ if not target.exists():
142
+ raise FileNotFoundError(f"Path not found: {target}")
143
+ if target.is_file():
144
+ read = self.read_file(str(target))
145
+ return {
146
+ "path": read.path,
147
+ "pattern": pattern,
148
+ "count": 1,
149
+ "items": [{"name": target.name, "type": "file", "size": target.stat().st_size}],
150
+ "content": read.content,
151
+ }
152
+ matches = sorted(target.glob(pattern))[:100]
153
+ items = []
154
+ for match in matches:
155
+ try:
156
+ self.security.require_safe(match)
157
+ except PermissionError:
158
+ continue
159
+ rel = match.relative_to(target) if match.is_relative_to(target) else match
160
+ kind = "dir" if match.is_dir() else "file"
161
+ size = match.stat().st_size if match.is_file() else 0
162
+ items.append({"name": str(rel), "type": kind, "size": size})
163
+ return {"path": str(target), "pattern": pattern, "count": len(items), "items": items}
164
+
165
+ def search_code(self, pattern: str, path: str = ".", file_glob: str = "**/*.py") -> Dict[str, Any]:
166
+ if not pattern:
167
+ raise ValueError("Missing 'pattern' parameter")
168
+ target = self.security.require_safe(path)
169
+ regex = re.compile(pattern, re.IGNORECASE)
170
+ matches = []
171
+ for file_path in sorted(target.glob(file_glob))[:200]:
172
+ try:
173
+ safe_file = self.security.require_safe(file_path)
174
+ except PermissionError:
175
+ continue
176
+ if not safe_file.is_file() or safe_file.stat().st_size > self.MAX_SEARCH_BYTES:
177
+ continue
178
+ try:
179
+ lines = safe_file.read_text(errors="replace").splitlines()
180
+ except Exception:
181
+ continue
182
+ for line_number, line in enumerate(lines, 1):
183
+ if regex.search(line):
184
+ matches.append({
185
+ "file": str(safe_file.relative_to(target) if safe_file.is_relative_to(target) else safe_file),
186
+ "line": line_number,
187
+ "content": line.strip()[:200],
188
+ })
189
+ if len(matches) >= 50:
190
+ break
191
+ if len(matches) >= 50:
192
+ break
193
+ return {"pattern": pattern, "path": str(target), "count": len(matches), "matches": matches}
@@ -0,0 +1,113 @@
1
+ """Verification planning for Aria Code workspaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import pathlib
7
+ import shlex
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+ from typing import Iterable, List
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class VerificationPlan:
15
+ commands: List[str]
16
+ reason: str
17
+
18
+
19
+ class VerificationPlanner:
20
+ """Infer focused verification commands from changed files and project files."""
21
+
22
+ def __init__(self, root: str | pathlib.Path = ".") -> None:
23
+ self.root = pathlib.Path(root).expanduser().resolve()
24
+
25
+ def infer(self, paths: Iterable[str] | None = None) -> VerificationPlan:
26
+ changed = [p for p in (paths or []) if p]
27
+ if not changed:
28
+ changed = self._git_changed_files()
29
+ commands: List[str] = []
30
+ reasons: List[str] = []
31
+
32
+ py_files = [p for p in changed if p.endswith(".py")]
33
+ if py_files:
34
+ quoted = " ".join(shlex.quote(p) for p in py_files[:20])
35
+ commands.append(f"python3 -m py_compile {quoted}")
36
+ reasons.append("Python files changed")
37
+ if self._has_any("pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini") or (self.root / "tests").exists():
38
+ commands.append("python3 -m pytest -q")
39
+
40
+ frontend_files = [
41
+ p for p in changed
42
+ if p.endswith((".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".css"))
43
+ ]
44
+ if frontend_files and (self.root / "package.json").exists():
45
+ scripts = self._package_scripts()
46
+ reasons.append("Node/TypeScript files changed")
47
+ if "test" in scripts:
48
+ commands.append("npm test")
49
+ if "build" in scripts:
50
+ commands.append("npm run build")
51
+ elif (self.root / "tsconfig.json").exists():
52
+ commands.append("npx tsc --noEmit")
53
+
54
+ if not commands:
55
+ if (self.root / "pyproject.toml").exists() or (self.root / "tests").exists():
56
+ commands.append("python3 -m pytest -q")
57
+ reasons.append("Python project detected")
58
+ elif (self.root / "package.json").exists():
59
+ scripts = self._package_scripts()
60
+ if "test" in scripts:
61
+ commands.append("npm test")
62
+ reasons.append("Node project detected")
63
+ elif "build" in scripts:
64
+ commands.append("npm run build")
65
+ reasons.append("Node project detected")
66
+
67
+ deduped = list(dict.fromkeys(commands))
68
+ reason = "; ".join(dict.fromkeys(reasons)) if reasons else "No focused check inferred"
69
+ return VerificationPlan(deduped, reason)
70
+
71
+ def _has_any(self, *names: str) -> bool:
72
+ return any((self.root / name).exists() for name in names)
73
+
74
+ def _package_scripts(self) -> dict:
75
+ try:
76
+ data = json.loads((self.root / "package.json").read_text(encoding="utf-8"))
77
+ return data.get("scripts", {}) if isinstance(data.get("scripts", {}), dict) else {}
78
+ except Exception:
79
+ return {}
80
+
81
+ def _git_changed_files(self) -> List[str]:
82
+ try:
83
+ proc = subprocess.run(
84
+ ["git", "diff", "--name-only", "HEAD"],
85
+ cwd=self.root,
86
+ capture_output=True,
87
+ text=True,
88
+ timeout=5,
89
+ check=False,
90
+ )
91
+ if proc.returncode == 0 and proc.stdout.strip():
92
+ return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
93
+ except Exception:
94
+ pass
95
+ try:
96
+ proc = subprocess.run(
97
+ ["git", "status", "--short"],
98
+ cwd=self.root,
99
+ capture_output=True,
100
+ text=True,
101
+ timeout=5,
102
+ check=False,
103
+ )
104
+ if proc.returncode != 0:
105
+ return []
106
+ files = []
107
+ for line in proc.stdout.splitlines():
108
+ if not line.strip():
109
+ continue
110
+ files.append(line[3:].strip())
111
+ return files
112
+ except Exception:
113
+ return []
@@ -0,0 +1,199 @@
1
+ """Approval-ready text changes for isolated Aria Code workspaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import secrets
10
+ import tempfile
11
+ from dataclasses import asdict, dataclass
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path, PurePosixPath
14
+ from typing import Any
15
+
16
+ from aria_code.project_review import _is_sensitive
17
+ from aria_code.workspace_service import WorkspaceService, WorkspaceServiceError
18
+
19
+
20
+ MAX_CHANGE_BYTES = 1 * 1024 * 1024
21
+ MAX_DIFF_CHARS = 120_000
22
+
23
+
24
+ class WorkspaceChangeError(ValueError):
25
+ pass
26
+
27
+
28
+ def _now() -> str:
29
+ return datetime.now(timezone.utc).isoformat()
30
+
31
+
32
+ def _digest(data: bytes) -> str:
33
+ return hashlib.sha256(data).hexdigest()
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class WorkspaceChange:
38
+ schema_version: str
39
+ change_id: str
40
+ workspace_id: str
41
+ path: str
42
+ operation: str
43
+ status: str
44
+ base_sha256: str
45
+ proposed_sha256: str
46
+ created_at: str
47
+ updated_at: str
48
+ diff: str
49
+
50
+ def public_dict(self) -> dict[str, Any]:
51
+ return asdict(self)
52
+
53
+
54
+ class WorkspaceChangeService:
55
+ """Stages text changes outside the source tree until approval is verified."""
56
+
57
+ def __init__(self, workspaces: WorkspaceService) -> None:
58
+ self.workspaces = workspaces
59
+
60
+ def _change_root(self, workspace_id: str) -> Path:
61
+ root = self.workspaces._workspace_dir(workspace_id) / ".aria-changes"
62
+ root.mkdir(mode=0o700, exist_ok=True)
63
+ return root
64
+
65
+ def _directory(self, workspace_id: str, change_id: str) -> Path:
66
+ if not change_id.startswith("chg_") or len(change_id) != 28:
67
+ raise WorkspaceChangeError("Invalid change id.")
68
+ root = self._change_root(workspace_id).resolve()
69
+ directory = (root / change_id).resolve()
70
+ if directory.parent != root:
71
+ raise WorkspaceChangeError("Invalid change path.")
72
+ return directory
73
+
74
+ @staticmethod
75
+ def _metadata(directory: Path) -> Path:
76
+ return directory / "change.json"
77
+
78
+ def propose(
79
+ self,
80
+ workspace_id: str,
81
+ *,
82
+ owner_id: str,
83
+ path: str,
84
+ content: str,
85
+ expected_sha256: str | None = None,
86
+ ) -> WorkspaceChange:
87
+ relative = PurePosixPath(str(path or "").replace("\\", "/"))
88
+ if not relative.parts or relative.is_absolute() or ".." in relative.parts:
89
+ raise WorkspaceChangeError("Change path must be relative to the workspace.")
90
+ if _is_sensitive(relative):
91
+ raise WorkspaceChangeError("Sensitive configuration and key files cannot be written.")
92
+ proposed = content.encode("utf-8")
93
+ if len(proposed) > MAX_CHANGE_BYTES:
94
+ raise WorkspaceChangeError("Proposed file exceeds the 1 MB text-change limit.")
95
+ try:
96
+ target = self.workspaces.source_path(workspace_id, str(relative), owner_id=owner_id)
97
+ except WorkspaceServiceError as exc:
98
+ raise WorkspaceChangeError(str(exc)) from exc
99
+ if target.exists() and not target.is_file():
100
+ raise WorkspaceChangeError("Change target is not a regular file.")
101
+ try:
102
+ before = target.read_bytes() if target.exists() else b""
103
+ before_text = before.decode("utf-8")
104
+ except (OSError, UnicodeDecodeError) as exc:
105
+ raise WorkspaceChangeError("Only UTF-8 text files can be changed.") from exc
106
+ base_hash = _digest(before)
107
+ if expected_sha256 and not secrets.compare_digest(expected_sha256, base_hash):
108
+ raise WorkspaceChangeError("File changed since it was reviewed; refresh before proposing a patch.")
109
+ if before == proposed:
110
+ raise WorkspaceChangeError("Proposed content is identical to the current file.")
111
+ diff = "".join(difflib.unified_diff(
112
+ before_text.splitlines(keepends=True),
113
+ content.splitlines(keepends=True),
114
+ fromfile=f"a/{relative}",
115
+ tofile=f"b/{relative}",
116
+ ))
117
+ if len(diff) > MAX_DIFF_CHARS:
118
+ diff = diff[:MAX_DIFF_CHARS] + "\n... diff truncated ...\n"
119
+ change_id = f"chg_{secrets.token_hex(12)}"
120
+ directory = self._directory(workspace_id, change_id)
121
+ directory.mkdir(mode=0o700)
122
+ (directory / "before.bin").write_bytes(before)
123
+ (directory / "proposed.txt").write_bytes(proposed)
124
+ created = _now()
125
+ change = WorkspaceChange(
126
+ schema_version="aria.workspace-change.v1",
127
+ change_id=change_id,
128
+ workspace_id=workspace_id,
129
+ path=str(relative),
130
+ operation="update" if target.exists() else "create",
131
+ status="proposed",
132
+ base_sha256=base_hash,
133
+ proposed_sha256=_digest(proposed),
134
+ created_at=created,
135
+ updated_at=created,
136
+ diff=diff,
137
+ )
138
+ self._metadata(directory).write_text(json.dumps(asdict(change), indent=2), encoding="utf-8")
139
+ return change
140
+
141
+ def get(self, workspace_id: str, change_id: str, *, owner_id: str) -> WorkspaceChange:
142
+ self.workspaces.get(workspace_id, owner_id=owner_id)
143
+ try:
144
+ raw = json.loads(self._metadata(self._directory(workspace_id, change_id)).read_text(encoding="utf-8"))
145
+ return WorkspaceChange(**raw)
146
+ except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
147
+ raise WorkspaceChangeError("Change proposal not found.") from exc
148
+
149
+ def _store(self, change: WorkspaceChange) -> None:
150
+ directory = self._directory(change.workspace_id, change.change_id)
151
+ self._metadata(directory).write_text(json.dumps(asdict(change), indent=2), encoding="utf-8")
152
+
153
+ def apply(self, workspace_id: str, change_id: str, *, owner_id: str) -> WorkspaceChange:
154
+ change = self.get(workspace_id, change_id, owner_id=owner_id)
155
+ if change.status != "proposed":
156
+ raise WorkspaceChangeError("Only a proposed change can be applied.")
157
+ target = self.workspaces.source_path(workspace_id, change.path, owner_id=owner_id)
158
+ current = target.read_bytes() if target.exists() else b""
159
+ if not secrets.compare_digest(_digest(current), change.base_sha256):
160
+ raise WorkspaceChangeError("File changed after the proposal; approval is stale.")
161
+ proposed = (self._directory(workspace_id, change_id) / "proposed.txt").read_bytes()
162
+ if not secrets.compare_digest(_digest(proposed), change.proposed_sha256):
163
+ raise WorkspaceChangeError("Stored proposal failed its integrity check.")
164
+ target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
165
+ fd, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
166
+ try:
167
+ with os.fdopen(fd, "wb") as stream:
168
+ stream.write(proposed)
169
+ stream.flush()
170
+ os.fsync(stream.fileno())
171
+ os.chmod(temporary, 0o600)
172
+ os.replace(temporary, target)
173
+ finally:
174
+ if os.path.exists(temporary):
175
+ os.unlink(temporary)
176
+ updated = WorkspaceChange(**{**asdict(change), "status": "applied", "updated_at": _now()})
177
+ self._store(updated)
178
+ return updated
179
+
180
+ def rollback(self, workspace_id: str, change_id: str, *, owner_id: str) -> WorkspaceChange:
181
+ change = self.get(workspace_id, change_id, owner_id=owner_id)
182
+ if change.status != "applied":
183
+ raise WorkspaceChangeError("Only an applied change can be rolled back.")
184
+ target = self.workspaces.source_path(workspace_id, change.path, owner_id=owner_id)
185
+ current = target.read_bytes() if target.exists() else b""
186
+ if not secrets.compare_digest(_digest(current), change.proposed_sha256):
187
+ raise WorkspaceChangeError("File changed after apply; automatic rollback would overwrite newer work.")
188
+ before = (self._directory(workspace_id, change_id) / "before.bin").read_bytes()
189
+ if change.operation == "create":
190
+ target.unlink(missing_ok=True)
191
+ else:
192
+ target.write_bytes(before)
193
+ try:
194
+ target.chmod(0o600)
195
+ except OSError:
196
+ pass
197
+ updated = WorkspaceChange(**{**asdict(change), "status": "rolled_back", "updated_at": _now()})
198
+ self._store(updated)
199
+ return updated