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.
- aria_code/__init__.py +42 -0
- aria_code/_version.py +20 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +34 -0
- aria_code/agents/base.py +318 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +303 -0
- aria_code/agents/financial/fundamental.py +191 -0
- aria_code/agents/financial/macro.py +99 -0
- aria_code/agents/financial/news.py +207 -0
- aria_code/agents/financial/risk.py +165 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +258 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +363 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +158 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +492 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +15 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +95 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/bootstrap.py +145 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +16 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +369 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +239 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
- aria_code/apps/cli/commands/broker_cmds.py +1234 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +87 -0
- aria_code/apps/cli/commands/core_cmds.py +2520 -0
- aria_code/apps/cli/commands/data_cmds.py +457 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +434 -0
- aria_code/apps/cli/commands/market_cmds.py +1381 -0
- aria_code/apps/cli/commands/market_context.py +425 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2153 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
- aria_code/apps/cli/commands/report.py +437 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +669 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
- aria_code/apps/cli/commands/workflow_cmds.py +480 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +78 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/deterministic.py +191 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +196 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
- aria_code/apps/cli/handlers/market_handlers.py +2513 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/helpers.py +302 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +297 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +276 -0
- aria_code/apps/cli/intent_signals.py +344 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +536 -0
- aria_code/apps/cli/main.py +29 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/model_catalog.py +620 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +59 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +53 -0
- aria_code/apps/cli/prompts/select.py +142 -0
- aria_code/apps/cli/prompts/system_prompts.py +552 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +521 -0
- aria_code/apps/cli/providers/chat_routing.py +128 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
- aria_code/apps/cli/providers/runtime_bridge.py +353 -0
- aria_code/apps/cli/providers/vertexai_stream.py +316 -0
- aria_code/apps/cli/runtime_consumer.py +657 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_store.py +112 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +823 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +106 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +180 -0
- aria_code/apps/cli/tools/market_tools.py +620 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +726 -0
- aria_code/apps/cli/tools/write_tools.py +845 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +153 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1597 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +6743 -0
- aria_code/aria_daemon.py +1398 -0
- aria_code/aria_feishu_bot.py +1423 -0
- aria_code/aria_mcp_server.py +7 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_relay_server.py +490 -0
- aria_code/aria_telegram_bot.py +202 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +182 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1976 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +181 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/demo_player.py +207 -0
- aria_code/doctor.py +813 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/backtest_engine.py +354 -0
- aria_code/domain/backtest_report.py +472 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/harness.py +598 -0
- aria_code/evals/runner.py +238 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +386 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +639 -0
- aria_code/memory_manager.py +246 -0
- aria_code/model_capability.py +486 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +23 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +52 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +201 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +111 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1550 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +396 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +79 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +268 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +57 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +157 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +8 -0
- aria_code/packages/aria_tools/financial/_paths.py +130 -0
- aria_code/packages/aria_tools/financial/compliance.py +31 -0
- aria_code/packages/aria_tools/financial/factors.py +33 -0
- aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
- aria_code/packages/aria_tools/financial/strategy.py +31 -0
- aria_code/packages/aria_tools/registry.py +128 -0
- aria_code/packages/quant_engine/__init__.py +34 -0
- aria_code/packages/quant_engine/_namespace.py +62 -0
- aria_code/packages/quant_engine/backtest/__init__.py +14 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +96 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +80 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/ml_model.py +377 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/logistics/__init__.py +169 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +129 -0
- aria_code/providers/llm/openai_compat.py +313 -0
- aria_code/providers/llm/registry.py +444 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +589 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/runtime/__init__.py +169 -0
- aria_code/runtime/acceptance.py +481 -0
- aria_code/runtime/agent_loop.py +1520 -0
- aria_code/runtime/approval.py +57 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +432 -0
- aria_code/runtime/crash_report.py +174 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +150 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/repo_map.py +700 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +450 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/subagent.py +547 -0
- aria_code/runtime/task_ledger.py +86 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/tool_executor.py +217 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +23 -0
- aria_code/safety/permissions.py +306 -0
- aria_code/safety/service.py +102 -0
- aria_code/setup_wizard.py +673 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +504 -0
- aria_code/tools/data_analysis_tools.py +809 -0
- aria_code/tools/enterprise_finance_tools.py +156 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +885 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3417 -0
- aria_code/tools/logistics_tools.py +135 -0
- aria_code/tools/macro_tools.py +369 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +659 -0
- aria_code/tools/spreadsheet_tools.py +388 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +508 -0
- aria_code/ui/completer.py +501 -0
- aria_code/ui/console.py +281 -0
- aria_code/ui/image_render.py +243 -0
- aria_code/ui/input_box.py +488 -0
- aria_code/ui/picker.py +201 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/finance.py +1705 -0
- aria_code/ui/render/market.py +225 -0
- aria_code/ui/render/output.py +775 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +354 -0
- aria_code/ui/robot.py +235 -0
- aria_code/ui/startup_dashboard.py +109 -0
- aria_code/utils/vscode_uri.py +37 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +271 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +193 -0
- aria_code/workspace/verify.py +113 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_context.py +233 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.52.0.dist-info/METADATA +237 -0
- aria_code-0.52.0.dist-info/RECORD +478 -0
- aria_code-0.52.0.dist-info/WHEEL +5 -0
- aria_code-0.52.0.dist-info/entry_points.txt +3 -0
- aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
- 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
|