polyrob 0.5.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.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
api/app.py
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI application factory for POLYROB platform.
|
|
3
|
+
Serves AutoV2 HTTP API endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI, Request, HTTPException, Header
|
|
14
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
15
|
+
from fastapi.responses import JSONResponse
|
|
16
|
+
from fastapi.exceptions import RequestValidationError
|
|
17
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
18
|
+
import time
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from dotenv import load_dotenv
|
|
21
|
+
|
|
22
|
+
# Set up project root
|
|
23
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
24
|
+
if project_root not in sys.path:
|
|
25
|
+
sys.path.insert(0, project_root)
|
|
26
|
+
|
|
27
|
+
# Try to import sqlite3 replacement if needed
|
|
28
|
+
try:
|
|
29
|
+
import pysqlite3
|
|
30
|
+
sys.modules['sqlite3'] = pysqlite3
|
|
31
|
+
except ImportError:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# Import core components
|
|
35
|
+
from core.config import BotConfig
|
|
36
|
+
from core.container import DependencyContainer
|
|
37
|
+
from core.exceptions import AuthError, InsufficientCreditsError
|
|
38
|
+
from core.logging import setup_logging, get_component_logger
|
|
39
|
+
from core.version import get_version
|
|
40
|
+
|
|
41
|
+
# core.bot.Bot pulls core.initialization (the entire agent/LLM/embedder stack). It is only
|
|
42
|
+
# needed inside the lifespan (which builds the bot via build_server_bot), so it must NOT be
|
|
43
|
+
# imported at module load — that dragged torch/SDKs/agents into every `import api.app`
|
|
44
|
+
# (every uvicorn worker boot). See docs/plans/2026-06-26-runtime-architecture-finalization-FUSION.md (P0-S).
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from core.bot import Bot as CoreBot
|
|
47
|
+
|
|
48
|
+
# Import Task HTTP router
|
|
49
|
+
from api.task_http_api import router as task_router
|
|
50
|
+
|
|
51
|
+
# Import Auth endpoints
|
|
52
|
+
try:
|
|
53
|
+
from api.auth_endpoints import router as auth_router
|
|
54
|
+
AUTH_ROUTER_AVAILABLE = True
|
|
55
|
+
except ImportError:
|
|
56
|
+
AUTH_ROUTER_AVAILABLE = False
|
|
57
|
+
|
|
58
|
+
# Import API models and router for message handling
|
|
59
|
+
try:
|
|
60
|
+
from api.models import MessageRequest, MessageResponse
|
|
61
|
+
from api.conversation_manager import APIConversationManager
|
|
62
|
+
from api.middleware import AuthenticationMiddleware, RateLimitMiddleware
|
|
63
|
+
API_MODELS_AVAILABLE = True
|
|
64
|
+
except ImportError:
|
|
65
|
+
API_MODELS_AVAILABLE = False
|
|
66
|
+
|
|
67
|
+
# Global references
|
|
68
|
+
app_state = {
|
|
69
|
+
"bot": None,
|
|
70
|
+
"container": None,
|
|
71
|
+
"config": None,
|
|
72
|
+
"logger": None,
|
|
73
|
+
"active_updates": set(), # Track active update IDs
|
|
74
|
+
"update_semaphore": None, # Limit concurrent updates
|
|
75
|
+
"conversation_manager": None # API conversation manager
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Background task for periodic cleanup
|
|
79
|
+
async def periodic_cleanup_task():
|
|
80
|
+
"""Run periodic cleanup tasks (nonces, expired sessions, etc.)."""
|
|
81
|
+
import asyncio
|
|
82
|
+
logger = app_state.get("logger")
|
|
83
|
+
|
|
84
|
+
while True:
|
|
85
|
+
try:
|
|
86
|
+
await asyncio.sleep(300) # Run every 5 minutes
|
|
87
|
+
|
|
88
|
+
container = app_state.get("container")
|
|
89
|
+
if not container:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
db = container.get_service("database")
|
|
93
|
+
if not db:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Clean up expired nonces
|
|
97
|
+
try:
|
|
98
|
+
result = await db.execute("""
|
|
99
|
+
DELETE FROM auth_nonces
|
|
100
|
+
WHERE expires_at < datetime('now')
|
|
101
|
+
""")
|
|
102
|
+
if result.rowcount > 0:
|
|
103
|
+
logger.debug(f"Cleaned up {result.rowcount} expired nonces")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.debug(f"Nonce cleanup error (non-critical): {e}")
|
|
106
|
+
|
|
107
|
+
# Clean up old audit log entries (keep 90 days)
|
|
108
|
+
try:
|
|
109
|
+
result = await db.execute("""
|
|
110
|
+
DELETE FROM user_mcp_audit_log
|
|
111
|
+
WHERE timestamp < datetime('now', '-90 days')
|
|
112
|
+
""")
|
|
113
|
+
if result.rowcount > 0:
|
|
114
|
+
logger.debug(f"Cleaned up {result.rowcount} old audit log entries")
|
|
115
|
+
except Exception:
|
|
116
|
+
pass # Table may not exist
|
|
117
|
+
|
|
118
|
+
except asyncio.CancelledError:
|
|
119
|
+
logger.info("Periodic cleanup task cancelled")
|
|
120
|
+
break
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning(f"Error in periodic cleanup: {e}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Lifespan manager for startup/shutdown
|
|
126
|
+
@asynccontextmanager
|
|
127
|
+
async def lifespan(app: FastAPI):
|
|
128
|
+
"""Manage application lifecycle."""
|
|
129
|
+
logger = app_state["logger"]
|
|
130
|
+
cleanup_task = None
|
|
131
|
+
reap_task = None
|
|
132
|
+
reap_stop_event = None
|
|
133
|
+
autonomy_handles = None
|
|
134
|
+
dispatcher = None
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Startup
|
|
138
|
+
logger.info("="*50)
|
|
139
|
+
logger.info("🚀 FastAPI Application Starting")
|
|
140
|
+
logger.info("="*50)
|
|
141
|
+
|
|
142
|
+
# Initialize bot and container via shared bootstrap.
|
|
143
|
+
# build_server_bot adds phase-6 server services (auth, billing,
|
|
144
|
+
# payments) on top of the core phases 1-5 that build_bot does.
|
|
145
|
+
from core.bootstrap import build_server_bot
|
|
146
|
+
bot = await build_server_bot()
|
|
147
|
+
|
|
148
|
+
app_state["config"] = bot.container.config
|
|
149
|
+
app_state["bot"] = bot
|
|
150
|
+
app_state["container"] = bot.container
|
|
151
|
+
|
|
152
|
+
# Initialize update semaphore to limit concurrent processing
|
|
153
|
+
# This prevents resource exhaustion from too many simultaneous updates
|
|
154
|
+
# Increased from 5 to 50 for better concurrency handling
|
|
155
|
+
max_concurrent = int(os.environ.get("MAX_CONCURRENT_UPDATES", "50"))
|
|
156
|
+
app_state["update_semaphore"] = asyncio.Semaphore(max_concurrent)
|
|
157
|
+
logger.info(f"Initialized update semaphore with {max_concurrent} concurrent updates limit")
|
|
158
|
+
|
|
159
|
+
# Initialize API conversation manager
|
|
160
|
+
if API_MODELS_AVAILABLE:
|
|
161
|
+
try:
|
|
162
|
+
conversation_manager = APIConversationManager()
|
|
163
|
+
app_state["conversation_manager"] = conversation_manager
|
|
164
|
+
|
|
165
|
+
logger.info("✅ Conversation manager initialized")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.warning(f"Could not initialize conversation manager: {e}")
|
|
168
|
+
|
|
169
|
+
# x402 is now handled via fastapi-x402 middleware (no custom handler needed)
|
|
170
|
+
|
|
171
|
+
logger.info("✅ Bot and container initialized")
|
|
172
|
+
|
|
173
|
+
# Apply any pending schema migrations to bot.db BEFORE serving (C3), snapshotting
|
|
174
|
+
# first if a real change is pending (C2). Idempotent + single-flight-locked +
|
|
175
|
+
# fail-open: a migration failure logs loudly and leaves the DB on the inline
|
|
176
|
+
# schema (prior behavior) rather than crashing startup. On the first engagement
|
|
177
|
+
# it just baselines the already-at-HEAD schema; future migrations auto-apply here.
|
|
178
|
+
try:
|
|
179
|
+
from migrations.boot import run_boot_migrations
|
|
180
|
+
mig = await run_boot_migrations(bot.container, local=False)
|
|
181
|
+
if mig.get("applied"):
|
|
182
|
+
logger.info("✅ Schema migrations applied at boot: %s", mig["applied"])
|
|
183
|
+
elif mig.get("error"):
|
|
184
|
+
logger.error("⚠️ Boot migration reported an error (serving anyway): %s", mig["error"])
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error("⚠️ Boot migration wiring failed (serving anyway): %s", e)
|
|
187
|
+
|
|
188
|
+
# Log voice-transcription readiness at startup so a 'voice silently dropped'
|
|
189
|
+
# deploy is immediately visible in the journal (Task 1.6 core-seam migration).
|
|
190
|
+
try:
|
|
191
|
+
from core.surfaces.transcription import log_transcription_readiness
|
|
192
|
+
log_transcription_readiness(bot.container)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.debug("log_transcription_readiness unavailable: %s", e)
|
|
195
|
+
|
|
196
|
+
# Start periodic cleanup task
|
|
197
|
+
cleanup_task = asyncio.create_task(periodic_cleanup_task())
|
|
198
|
+
logger.info("✅ Periodic cleanup task started")
|
|
199
|
+
|
|
200
|
+
# Start the autonomy background loops (cron / goals / curator) via the
|
|
201
|
+
# shared runtime — the SINGLE place both the FastAPI server and the CLI
|
|
202
|
+
# REPL start them. Each loop is independently gated + fail-open; one loop
|
|
203
|
+
# failing to build never blocks the others.
|
|
204
|
+
from core.autonomy_runtime import start_autonomy
|
|
205
|
+
autonomy_data_dir = getattr(bot.container.config, "data_dir", "data")
|
|
206
|
+
autonomy_handles = start_autonomy(
|
|
207
|
+
task_agent=bot.container.get_agent("task_agent"),
|
|
208
|
+
data_dir=autonomy_data_dir,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
dispatcher = bot.container.get_service("outbound_dispatcher")
|
|
212
|
+
if dispatcher is not None:
|
|
213
|
+
dispatcher.start()
|
|
214
|
+
|
|
215
|
+
# Start session-registry reaper (P6) — opt-in via SESSION_REGISTRY_BACKEND=sqlite.
|
|
216
|
+
# Inert for the default in-process registry (no reap_stale). Periodically prunes
|
|
217
|
+
# rows whose last_seen_at is older than the ttl, so a worker that died without
|
|
218
|
+
# remove() can't leave a dead-PID row that 409-loops routing forever. The ttl
|
|
219
|
+
# (300s) is comfortably larger than the per-step heartbeat cadence.
|
|
220
|
+
if os.getenv("SESSION_REGISTRY_BACKEND", "memory").strip().lower() == "sqlite":
|
|
221
|
+
try:
|
|
222
|
+
reap_task_agent = bot.container.get_agent("task_agent")
|
|
223
|
+
reap_registry = getattr(reap_task_agent, "_registry", None)
|
|
224
|
+
if reap_registry is not None and hasattr(reap_registry, "reap_stale"):
|
|
225
|
+
reap_stop_event = asyncio.Event()
|
|
226
|
+
|
|
227
|
+
async def _reap_loop(registry, stop_event):
|
|
228
|
+
REAP_INTERVAL = 60 # seconds between sweeps
|
|
229
|
+
REAP_TTL = 300 # seconds before a row is considered stale
|
|
230
|
+
while not stop_event.is_set():
|
|
231
|
+
try:
|
|
232
|
+
await asyncio.wait_for(stop_event.wait(), timeout=REAP_INTERVAL)
|
|
233
|
+
break # stop event set
|
|
234
|
+
except asyncio.TimeoutError:
|
|
235
|
+
pass # interval elapsed -> run a sweep
|
|
236
|
+
try:
|
|
237
|
+
reaped = registry.reap_stale(ttl_seconds=REAP_TTL)
|
|
238
|
+
if reaped:
|
|
239
|
+
logger.info(f"Session registry reaped {len(reaped)} stale session(s)")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.warning(f"Session registry reap failed: {e}")
|
|
242
|
+
|
|
243
|
+
reap_task = asyncio.create_task(_reap_loop(reap_registry, reap_stop_event))
|
|
244
|
+
logger.info("✅ Session registry reaper started (SESSION_REGISTRY_BACKEND=sqlite)")
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.warning(f"Could not start session registry reaper: {e}")
|
|
247
|
+
|
|
248
|
+
logger.info("="*50)
|
|
249
|
+
logger.info("✅ FastAPI Application Ready")
|
|
250
|
+
logger.info("="*50)
|
|
251
|
+
|
|
252
|
+
yield # Server is running
|
|
253
|
+
|
|
254
|
+
finally:
|
|
255
|
+
# Shutdown
|
|
256
|
+
logger.info("Shutting down FastAPI application...")
|
|
257
|
+
|
|
258
|
+
# Cancel cleanup task
|
|
259
|
+
if cleanup_task:
|
|
260
|
+
cleanup_task.cancel()
|
|
261
|
+
try:
|
|
262
|
+
await cleanup_task
|
|
263
|
+
except asyncio.CancelledError:
|
|
264
|
+
pass
|
|
265
|
+
logger.info("Cleanup task stopped")
|
|
266
|
+
|
|
267
|
+
# Stop the autonomy background loops (cron / goals / curator) via the
|
|
268
|
+
# shared runtime. Idempotent + fail-open.
|
|
269
|
+
if autonomy_handles is not None:
|
|
270
|
+
await autonomy_handles.stop()
|
|
271
|
+
|
|
272
|
+
# Stop the outbound dispatcher (fail-open).
|
|
273
|
+
if dispatcher is not None:
|
|
274
|
+
try:
|
|
275
|
+
await dispatcher.stop()
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
# Stop session-registry reaper (P6)
|
|
280
|
+
if reap_task:
|
|
281
|
+
if reap_stop_event:
|
|
282
|
+
reap_stop_event.set()
|
|
283
|
+
reap_task.cancel()
|
|
284
|
+
try:
|
|
285
|
+
await reap_task
|
|
286
|
+
except asyncio.CancelledError:
|
|
287
|
+
pass
|
|
288
|
+
logger.info("Session registry reaper stopped")
|
|
289
|
+
|
|
290
|
+
# Cleanup bot
|
|
291
|
+
if app_state["bot"]:
|
|
292
|
+
await app_state["bot"].cleanup()
|
|
293
|
+
logger.info("Bot cleanup completed")
|
|
294
|
+
|
|
295
|
+
logger.info("FastAPI application shutdown complete")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def fallback_auth_middleware(request: Request, call_next):
|
|
299
|
+
"""Fallback authentication for development when middleware not loaded.
|
|
300
|
+
|
|
301
|
+
Lifted out of create_app()'s closure (C4) so it's independently
|
|
302
|
+
unit-testable via `app.middleware("http")(fallback_auth_middleware)` —
|
|
303
|
+
it only reads `os.environ` and `request`, no closure-captured state.
|
|
304
|
+
"""
|
|
305
|
+
logger = app_state.get("logger") or logging.getLogger(__name__)
|
|
306
|
+
path = request.url.path
|
|
307
|
+
|
|
308
|
+
# Skip auth for health check, root, docs, test, and auth endpoints.
|
|
309
|
+
# NOTE: "/" must be matched EXACTLY — as a startswith() prefix it matches
|
|
310
|
+
# every path and would short-circuit the entire fallback (auth bypass).
|
|
311
|
+
# NOTE: "/api/x402/requests" (the payable-invoice challenge + pay routes) must be
|
|
312
|
+
# anonymous — a third-party payer has no POLYROB account. Payment authenticity is
|
|
313
|
+
# enforced cryptographically by the facilitator, not by this gate.
|
|
314
|
+
public_paths = ["/health", "/api/test-auth", "/api/auth", "/docs", "/redoc",
|
|
315
|
+
"/openapi.json", "/api/x402/requests", "/api/x402/pricing"]
|
|
316
|
+
if path == "/" or any(path.startswith(p) for p in public_paths):
|
|
317
|
+
return await call_next(request)
|
|
318
|
+
|
|
319
|
+
# Only apply fallback auth if proper middleware not loaded
|
|
320
|
+
if not getattr(request.state, "authenticated", False) and (
|
|
321
|
+
path.startswith("/api/") or path.startswith("/task/")
|
|
322
|
+
):
|
|
323
|
+
api_token = os.environ.get("API_AUTH_TOKEN")
|
|
324
|
+
|
|
325
|
+
# Log the request for debugging
|
|
326
|
+
logger.debug(f"🔍 Middleware processing: {request.method} {path}")
|
|
327
|
+
|
|
328
|
+
# SECURITY: Require API_AUTH_TOKEN to be configured
|
|
329
|
+
if not api_token or api_token.strip() == "":
|
|
330
|
+
logger.error(f"🚫 SECURITY: API_AUTH_TOKEN not configured. Rejecting request to {path}")
|
|
331
|
+
return JSONResponse(
|
|
332
|
+
status_code=503,
|
|
333
|
+
content={
|
|
334
|
+
"error": "Service misconfigured",
|
|
335
|
+
"detail": "Authentication not properly configured. Contact administrator."
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Check X-API-KEY, Authorization Bearer headers, and auth_token cookie
|
|
340
|
+
auth_header = request.headers.get("X-API-KEY")
|
|
341
|
+
bearer_header = request.headers.get("Authorization")
|
|
342
|
+
cookie_token = request.cookies.get("auth_token")
|
|
343
|
+
|
|
344
|
+
provided_token = None
|
|
345
|
+
is_jwt_token = False
|
|
346
|
+
|
|
347
|
+
logger.debug(f"🔑 Auth headers for {path}: X-API-KEY={bool(auth_header)}, Authorization={bool(bearer_header)}, Cookie={bool(cookie_token)}")
|
|
348
|
+
|
|
349
|
+
if auth_header:
|
|
350
|
+
provided_token = auth_header
|
|
351
|
+
# Also check if X-API-KEY contains a JWT
|
|
352
|
+
is_jwt_token = auth_header.count('.') == 2
|
|
353
|
+
logger.debug(f"🔑 Using X-API-KEY header, is_jwt={is_jwt_token}")
|
|
354
|
+
elif bearer_header and bearer_header.startswith("Bearer "):
|
|
355
|
+
provided_token = bearer_header[7:] # Remove "Bearer " prefix
|
|
356
|
+
# Check if this looks like a JWT (three base64 parts separated by dots)
|
|
357
|
+
is_jwt_token = provided_token.count('.') == 2
|
|
358
|
+
logger.debug(f"🔑 Using Bearer header, is_jwt={is_jwt_token}")
|
|
359
|
+
elif cookie_token:
|
|
360
|
+
# Check if cookie contains JWT token
|
|
361
|
+
provided_token = cookie_token
|
|
362
|
+
is_jwt_token = provided_token.count('.') == 2
|
|
363
|
+
logger.debug(f"🔑 Using cookie, is_jwt={is_jwt_token}")
|
|
364
|
+
|
|
365
|
+
if not provided_token:
|
|
366
|
+
logger.warning(f"🔑 No auth token provided for {path}")
|
|
367
|
+
return JSONResponse(
|
|
368
|
+
status_code=401,
|
|
369
|
+
content={
|
|
370
|
+
"error": "Missing authentication",
|
|
371
|
+
"detail": "Provide X-API-KEY header, Authorization: Bearer token, or auth_token cookie"
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Try to decode JWT token from wallet auth
|
|
376
|
+
if is_jwt_token:
|
|
377
|
+
try:
|
|
378
|
+
import jwt
|
|
379
|
+
jwt_secret = os.environ.get("JWT_SECRET_KEY")
|
|
380
|
+
if jwt_secret:
|
|
381
|
+
decoded = jwt.decode(provided_token, jwt_secret, algorithms=["HS256"])
|
|
382
|
+
from api.auth_state import set_auth_state
|
|
383
|
+
set_auth_state(
|
|
384
|
+
request.state,
|
|
385
|
+
user_id=decoded.get("sub", "api_user"),
|
|
386
|
+
tier=decoded.get("tier", "free"),
|
|
387
|
+
role=decoded.get("role", "user"),
|
|
388
|
+
payment_method=None,
|
|
389
|
+
authenticated=True,
|
|
390
|
+
)
|
|
391
|
+
request.state.wallet_address = decoded.get("wallet")
|
|
392
|
+
|
|
393
|
+
# SECURITY: Use admin_wallet flag from JWT (set at login time)
|
|
394
|
+
# This is more secure than checking wallet list every request
|
|
395
|
+
is_admin_by_wallet = decoded.get("admin_wallet", False)
|
|
396
|
+
|
|
397
|
+
# Set admin flag from any ADMIN_ROLES role (e.g. 'admin',
|
|
398
|
+
# 'owner') OR admin_wallet flag in JWT. H1 fix: this used
|
|
399
|
+
# to hardcode `role == 'admin'` — a THIRD ad-hoc
|
|
400
|
+
# admin-truth path independent of core.constants.ADMIN_ROLES
|
|
401
|
+
# that would deny an owner-login session (role="owner").
|
|
402
|
+
from api.auth_constants import is_admin_role
|
|
403
|
+
is_admin_by_role = is_admin_role(request.state.role)
|
|
404
|
+
|
|
405
|
+
# Debug logging
|
|
406
|
+
logger.debug(f"🔍 Admin check: role={request.state.role}, admin_wallet_flag={is_admin_by_wallet}, is_admin_by_role={is_admin_by_role}")
|
|
407
|
+
|
|
408
|
+
if is_admin_by_role or is_admin_by_wallet:
|
|
409
|
+
request.state.is_admin = True
|
|
410
|
+
logger.debug(f"🔐 Admin access granted for role={request.state.role}")
|
|
411
|
+
else:
|
|
412
|
+
request.state.is_admin = False
|
|
413
|
+
|
|
414
|
+
logger.debug(f"JWT auth successful for user {request.state.user_id} (tier: {request.state.tier}, admin: {request.state.is_admin})")
|
|
415
|
+
return await call_next(request)
|
|
416
|
+
except jwt.ExpiredSignatureError:
|
|
417
|
+
return JSONResponse(
|
|
418
|
+
status_code=401,
|
|
419
|
+
content={"error": "Token expired"}
|
|
420
|
+
)
|
|
421
|
+
except jwt.InvalidTokenError as e:
|
|
422
|
+
logger.warning(f"Invalid JWT token: {e}")
|
|
423
|
+
# Fall through to check if it's an API key
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.error(f"JWT decode error: {e}")
|
|
426
|
+
# Fall through to check if it's an API key
|
|
427
|
+
|
|
428
|
+
# Check against API_AUTH_TOKEN (for non-JWT tokens)
|
|
429
|
+
if provided_token != api_token:
|
|
430
|
+
logger.warning(f"Invalid API key attempt for {path}")
|
|
431
|
+
return JSONResponse(
|
|
432
|
+
status_code=401,
|
|
433
|
+
content={"error": "Invalid API key"}
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Set authenticated user context for API key auth.
|
|
437
|
+
# C4 fix: role="admin" must be set alongside tier="admin" — the admin
|
|
438
|
+
# bypass in verify_payment_for_request reads `role` (via
|
|
439
|
+
# extract_admin_info), not `tier`. Before this fix, a valid
|
|
440
|
+
# API_AUTH_TOKEN request had tier="admin" but role defaulted to
|
|
441
|
+
# 'user', so it fell through to an unconditional 402.
|
|
442
|
+
from api.auth_state import set_auth_state
|
|
443
|
+
set_auth_state(
|
|
444
|
+
request.state,
|
|
445
|
+
user_id="authenticated_api_user",
|
|
446
|
+
tier="admin",
|
|
447
|
+
role="admin",
|
|
448
|
+
payment_method=None,
|
|
449
|
+
authenticated=True,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return await call_next(request)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def create_app() -> FastAPI:
|
|
456
|
+
"""Create and configure FastAPI application."""
|
|
457
|
+
|
|
458
|
+
# Set up logging
|
|
459
|
+
log_level = os.environ.get('LOG_LEVEL', 'INFO')
|
|
460
|
+
setup_logging(log_level=log_level)
|
|
461
|
+
logger = get_component_logger('api')
|
|
462
|
+
app_state["logger"] = logger
|
|
463
|
+
|
|
464
|
+
# Load environment via shared bootstrap
|
|
465
|
+
from core.bootstrap import load_env
|
|
466
|
+
env = load_env()
|
|
467
|
+
logger.info(f"Using configuration environment: {env}")
|
|
468
|
+
|
|
469
|
+
# Create FastAPI app with lifespan manager
|
|
470
|
+
app = FastAPI(
|
|
471
|
+
title="POLYROB Platform API",
|
|
472
|
+
description="AutoV2 automation platform with HTTP API",
|
|
473
|
+
version=get_version(),
|
|
474
|
+
lifespan=lifespan
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# CORS configuration
|
|
478
|
+
# SECURITY FIX: Restrict CORS to specific origins and headers
|
|
479
|
+
cors_origins_str = os.environ.get("CORS_ALLOW_ORIGINS", "http://localhost:3000")
|
|
480
|
+
cors_origins = [origin.strip() for origin in cors_origins_str.split(',') if origin.strip()]
|
|
481
|
+
|
|
482
|
+
# Define allowed headers explicitly instead of "*"
|
|
483
|
+
# This prevents potential security issues with credential-based CORS
|
|
484
|
+
allowed_headers = [
|
|
485
|
+
"Accept",
|
|
486
|
+
"Accept-Language",
|
|
487
|
+
"Content-Language",
|
|
488
|
+
"Content-Type",
|
|
489
|
+
"Authorization",
|
|
490
|
+
"X-API-KEY",
|
|
491
|
+
"X-Requested-With",
|
|
492
|
+
"X-Admin-Token",
|
|
493
|
+
"Cache-Control",
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
# Define allowed methods explicitly
|
|
497
|
+
allowed_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]
|
|
498
|
+
|
|
499
|
+
# C3: register the fallback auth FIRST so Starlette (which inserts each
|
|
500
|
+
# add_middleware at index 0) leaves it as the INNERMOST layer — it then runs
|
|
501
|
+
# LAST, after AuthenticationMiddleware/JWTAuthMiddleware/X402 have authenticated
|
|
502
|
+
# the request. Registering it last made it outermost and shadowed the DB-backed
|
|
503
|
+
# rob_xxx API-key validator (self-service keys were rejected before it ran).
|
|
504
|
+
app.middleware("http")(fallback_auth_middleware)
|
|
505
|
+
|
|
506
|
+
app.add_middleware(
|
|
507
|
+
CORSMiddleware,
|
|
508
|
+
allow_origins=cors_origins,
|
|
509
|
+
allow_credentials=True,
|
|
510
|
+
allow_methods=allowed_methods,
|
|
511
|
+
allow_headers=allowed_headers,
|
|
512
|
+
)
|
|
513
|
+
logger.info(f"CORS configured for origins: {cors_origins}")
|
|
514
|
+
|
|
515
|
+
# Add rate limiting middleware
|
|
516
|
+
if API_MODELS_AVAILABLE:
|
|
517
|
+
try:
|
|
518
|
+
rpm = int(os.environ.get("API_RATE_LIMIT_RPM", "60"))
|
|
519
|
+
rph = int(os.environ.get("API_RATE_LIMIT_RPH", "1000"))
|
|
520
|
+
burst = int(os.environ.get("API_RATE_LIMIT_BURST", "10"))
|
|
521
|
+
app.add_middleware(
|
|
522
|
+
RateLimitMiddleware,
|
|
523
|
+
requests_per_minute=rpm,
|
|
524
|
+
requests_per_hour=rph,
|
|
525
|
+
burst_size=burst
|
|
526
|
+
)
|
|
527
|
+
logger.info(f"Rate limiting enabled: {rpm} RPM, {rph} RPH, {burst} burst")
|
|
528
|
+
except Exception as e:
|
|
529
|
+
logger.warning(f"Could not add rate limiting middleware: {e}")
|
|
530
|
+
|
|
531
|
+
# Add authentication middleware
|
|
532
|
+
if API_MODELS_AVAILABLE:
|
|
533
|
+
try:
|
|
534
|
+
secret_key = os.environ.get("API_SECRET", os.environ.get("ADMIN_TOKEN"))
|
|
535
|
+
if secret_key:
|
|
536
|
+
app.add_middleware(
|
|
537
|
+
AuthenticationMiddleware,
|
|
538
|
+
secret_key=secret_key
|
|
539
|
+
)
|
|
540
|
+
logger.info("Authentication middleware enabled")
|
|
541
|
+
except Exception as e:
|
|
542
|
+
logger.warning(f"Could not add authentication middleware: {e}")
|
|
543
|
+
|
|
544
|
+
# Add JWT authentication middleware
|
|
545
|
+
from api.jwt_middleware import JWTAuthMiddleware
|
|
546
|
+
|
|
547
|
+
jwt_secret = os.environ.get("JWT_SECRET_KEY")
|
|
548
|
+
if jwt_secret:
|
|
549
|
+
# Validate JWT secret has minimum length for security
|
|
550
|
+
if len(jwt_secret) < 32:
|
|
551
|
+
logger.warning("⚠️ JWT_SECRET_KEY is shorter than 32 characters - consider using a stronger secret")
|
|
552
|
+
app.add_middleware(JWTAuthMiddleware, jwt_secret=jwt_secret)
|
|
553
|
+
logger.info("✅ JWT authentication middleware enabled")
|
|
554
|
+
else:
|
|
555
|
+
# SECURITY FIX: Check if we're in production - fail fast if so
|
|
556
|
+
# In development, allow running without JWT for testing
|
|
557
|
+
is_production = env == 'production' or os.environ.get('PRODUCTION', '').lower() == 'true'
|
|
558
|
+
if is_production:
|
|
559
|
+
logger.critical("🚨 FATAL: JWT_SECRET_KEY not set in production!")
|
|
560
|
+
raise RuntimeError(
|
|
561
|
+
"JWT_SECRET_KEY environment variable is required in production. "
|
|
562
|
+
"Set it to a secure random string of at least 32 characters."
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
logger.warning("⚠️ JWT_SECRET_KEY not set - authentication disabled (development only)")
|
|
566
|
+
|
|
567
|
+
# Add x402 payment middleware (runs BEFORE JWT middleware due to LIFO order)
|
|
568
|
+
# This allows x402 payments to bypass JWT auth for pay-per-request access
|
|
569
|
+
# Uses fastapi-x402 library for proper on-chain verification via Coinbase facilitator
|
|
570
|
+
x402_enabled = os.environ.get("X402_ENABLED", "false").lower() == "true"
|
|
571
|
+
if x402_enabled:
|
|
572
|
+
try:
|
|
573
|
+
from modules.x402.middleware import X402PaymentMiddleware
|
|
574
|
+
|
|
575
|
+
app.add_middleware(X402PaymentMiddleware, enabled=True)
|
|
576
|
+
logger.info("✅ x402 payment middleware enabled (using fastapi-x402)")
|
|
577
|
+
except ImportError as e:
|
|
578
|
+
logger.warning(f"Could not add x402 middleware: {e}")
|
|
579
|
+
|
|
580
|
+
# Global exception handlers
|
|
581
|
+
@app.exception_handler(StarletteHTTPException)
|
|
582
|
+
async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException):
|
|
583
|
+
"""Handle HTTP exceptions globally."""
|
|
584
|
+
logger.error(f"HTTP exception: {exc.detail} | Path: {request.url.path}")
|
|
585
|
+
return JSONResponse(
|
|
586
|
+
status_code=exc.status_code,
|
|
587
|
+
content={"error": exc.detail}
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
@app.exception_handler(RequestValidationError)
|
|
591
|
+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
592
|
+
"""Handle validation errors globally."""
|
|
593
|
+
logger.error(f"Validation error: {exc.errors()} | Path: {request.url.path}")
|
|
594
|
+
return JSONResponse(
|
|
595
|
+
status_code=422,
|
|
596
|
+
content={"error": "Validation failed", "details": exc.errors()}
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
@app.exception_handler(AuthError)
|
|
600
|
+
async def auth_error_handler(request: Request, exc: AuthError):
|
|
601
|
+
"""Translate auth/tier domain exceptions raised by modules/auth/.
|
|
602
|
+
|
|
603
|
+
Status code comes from the exception class (e.g. TierError=403,
|
|
604
|
+
UserNotFoundError=404). Keeps fastapi out of modules/auth/.
|
|
605
|
+
"""
|
|
606
|
+
status = getattr(exc, 'status_code', 403)
|
|
607
|
+
logger.warning(f"Auth error ({status}): {exc} | Path: {request.url.path}")
|
|
608
|
+
return JSONResponse(status_code=status, content={"error": str(exc)})
|
|
609
|
+
|
|
610
|
+
@app.exception_handler(InsufficientCreditsError)
|
|
611
|
+
async def insufficient_credits_handler(request: Request, exc: InsufficientCreditsError):
|
|
612
|
+
"""Translate billing domain exception to HTTP 402."""
|
|
613
|
+
logger.info(f"Insufficient credits: user={exc.user_id} required={exc.required} available={exc.available}")
|
|
614
|
+
return JSONResponse(
|
|
615
|
+
status_code=402,
|
|
616
|
+
content={
|
|
617
|
+
"error": str(exc),
|
|
618
|
+
"user_id": exc.user_id,
|
|
619
|
+
"required": exc.required,
|
|
620
|
+
"available": exc.available,
|
|
621
|
+
},
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
@app.exception_handler(Exception)
|
|
625
|
+
async def global_exception_handler(request: Request, exc: Exception):
|
|
626
|
+
"""Global exception handler for unhandled errors."""
|
|
627
|
+
error_id = f"error_{int(time.time() * 1000)}"
|
|
628
|
+
|
|
629
|
+
# Log the full exception details
|
|
630
|
+
logger.error(
|
|
631
|
+
f"Unhandled exception {error_id} | "
|
|
632
|
+
f"Path: {request.url.path} | "
|
|
633
|
+
f"Method: {request.method} | "
|
|
634
|
+
f"Error: {str(exc)}",
|
|
635
|
+
exc_info=True
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# Clean up active updates if present
|
|
640
|
+
if hasattr(request.state, 'update_id'):
|
|
641
|
+
app_state["active_updates"].discard(request.state.update_id)
|
|
642
|
+
|
|
643
|
+
# Return a proper error response
|
|
644
|
+
return JSONResponse(
|
|
645
|
+
status_code=500,
|
|
646
|
+
content={
|
|
647
|
+
"error": "Internal server error",
|
|
648
|
+
"error_id": error_id,
|
|
649
|
+
"message": "An unexpected error occurred. Please try again later."
|
|
650
|
+
}
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# (fallback_auth_middleware is registered earlier — see C3 note above — so it
|
|
654
|
+
# runs innermost/last, after the real auth middlewares.)
|
|
655
|
+
|
|
656
|
+
# Health check endpoint with metrics
|
|
657
|
+
@app.get("/health")
|
|
658
|
+
async def health_check():
|
|
659
|
+
"""Health check endpoint with system metrics."""
|
|
660
|
+
active_count = len(app_state.get("active_updates", set()))
|
|
661
|
+
semaphore_available = app_state["update_semaphore"]._value if app_state.get("update_semaphore") else 0
|
|
662
|
+
|
|
663
|
+
health_status = {
|
|
664
|
+
"status": "healthy" if active_count < 40 else "degraded",
|
|
665
|
+
"service": "rob-platform",
|
|
666
|
+
"metrics": {
|
|
667
|
+
"active_updates": active_count,
|
|
668
|
+
"semaphore_available": semaphore_available,
|
|
669
|
+
"bot_initialized": app_state.get("bot") is not None
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# Set appropriate status code
|
|
674
|
+
status_code = 200 if health_status["status"] == "healthy" else 503
|
|
675
|
+
return JSONResponse(content=health_status, status_code=status_code)
|
|
676
|
+
|
|
677
|
+
# Mount Task API router on canonical path only
|
|
678
|
+
app.include_router(task_router, prefix="/api")
|
|
679
|
+
|
|
680
|
+
# Mount Auth router (wallet authentication)
|
|
681
|
+
if AUTH_ROUTER_AVAILABLE:
|
|
682
|
+
app.include_router(auth_router, prefix="/api/auth", tags=["authentication"])
|
|
683
|
+
logger.info("✅ Auth endpoints registered at /api/auth")
|
|
684
|
+
|
|
685
|
+
# Mount Payment router (deposits, credits, transactions)
|
|
686
|
+
try:
|
|
687
|
+
from api.payment_endpoints import router as payment_router
|
|
688
|
+
app.include_router(payment_router, prefix="/api", tags=["payments"])
|
|
689
|
+
logger.info("✅ Payment endpoints registered at /api/payments")
|
|
690
|
+
except ImportError as e:
|
|
691
|
+
logger.warning(f"Payment endpoints not available: {e}")
|
|
692
|
+
|
|
693
|
+
# Mount x402 router (always register - endpoints self-check if x402 is configured)
|
|
694
|
+
try:
|
|
695
|
+
from api.x402_endpoints import router as x402_router
|
|
696
|
+
app.include_router(x402_router, prefix="/api", tags=["x402"])
|
|
697
|
+
x402_enabled = os.environ.get("X402_ENABLED", "false").lower() == "true"
|
|
698
|
+
logger.info(f"✅ x402 endpoints registered at /api/x402 (payments {'enabled' if x402_enabled else 'info-only'})")
|
|
699
|
+
except ImportError as e:
|
|
700
|
+
logger.debug(f"x402 endpoints not available: {e}")
|
|
701
|
+
|
|
702
|
+
# Mount OpenAI-compatible /v1 router (gated; default OFF). Reuses POLYROB auth +
|
|
703
|
+
# per-request billing + the tool-light chat agent — multi-tenant-safe.
|
|
704
|
+
try:
|
|
705
|
+
from api.openai_compat.router import router as openai_compat_router, openai_compat_enabled
|
|
706
|
+
if openai_compat_enabled():
|
|
707
|
+
app.include_router(openai_compat_router)
|
|
708
|
+
logger.info("✅ OpenAI-compatible endpoints registered at /v1")
|
|
709
|
+
else:
|
|
710
|
+
logger.debug("OpenAI-compatible /v1 endpoints disabled (OPENAI_COMPAT_API_ENABLED off)")
|
|
711
|
+
except ImportError as e:
|
|
712
|
+
logger.warning(f"OpenAI-compatible endpoints not available: {e}")
|
|
713
|
+
|
|
714
|
+
# Mount KB (knowledge-base) /api/kb router (gated; default OFF). Reuses POLYROB
|
|
715
|
+
# auth (get_user_id dependency) — multi-tenant-safe; user_id never from body.
|
|
716
|
+
try:
|
|
717
|
+
from api.kb.endpoints import router as kb_router, kb_api_enabled
|
|
718
|
+
if kb_api_enabled():
|
|
719
|
+
app.include_router(kb_router)
|
|
720
|
+
logger.info("✅ KB endpoints registered at /api/kb")
|
|
721
|
+
else:
|
|
722
|
+
logger.debug("KB endpoints disabled (KB_API_ENABLED off)")
|
|
723
|
+
except ImportError as e:
|
|
724
|
+
logger.warning(f"KB endpoints not available: {e}")
|
|
725
|
+
|
|
726
|
+
# Mount Admin router
|
|
727
|
+
try:
|
|
728
|
+
from api.admin_endpoints import router as admin_router
|
|
729
|
+
app.include_router(admin_router, prefix="/api", tags=["admin"])
|
|
730
|
+
logger.info("✅ Admin endpoints registered at /api/admin")
|
|
731
|
+
except ImportError as e:
|
|
732
|
+
logger.warning(f"Admin endpoints not available: {e}")
|
|
733
|
+
|
|
734
|
+
# Mount MCP management router
|
|
735
|
+
try:
|
|
736
|
+
from api.mcp_routes import router as mcp_router
|
|
737
|
+
app.include_router(mcp_router, prefix="/api/mcp", tags=["mcp"])
|
|
738
|
+
logger.info("✅ MCP endpoints registered at /api/mcp")
|
|
739
|
+
except ImportError as e:
|
|
740
|
+
logger.warning(f"MCP endpoints not available: {e}")
|
|
741
|
+
|
|
742
|
+
# Mount Polymarket router
|
|
743
|
+
try:
|
|
744
|
+
from api.polymarket_routes import router as polymarket_router
|
|
745
|
+
app.include_router(polymarket_router, prefix="/api/polymarket", tags=["polymarket"])
|
|
746
|
+
logger.info("✅ Polymarket endpoints registered at /api/polymarket")
|
|
747
|
+
except ImportError as e:
|
|
748
|
+
logger.warning(f"Polymarket endpoints not available: {e}")
|
|
749
|
+
|
|
750
|
+
# Mount Hyperliquid router
|
|
751
|
+
try:
|
|
752
|
+
from api.hyperliquid_routes import router as hyperliquid_router
|
|
753
|
+
app.include_router(hyperliquid_router, tags=["hyperliquid"])
|
|
754
|
+
logger.info("✅ Hyperliquid endpoints registered at /api/hyperliquid")
|
|
755
|
+
except ImportError as e:
|
|
756
|
+
logger.warning(f"Hyperliquid endpoints not available: {e}")
|
|
757
|
+
|
|
758
|
+
# Mount Skills management router
|
|
759
|
+
try:
|
|
760
|
+
from api.skill_endpoints import router as skill_router
|
|
761
|
+
app.include_router(skill_router, tags=["skills"])
|
|
762
|
+
logger.info("✅ Skills endpoints registered at /api/skills")
|
|
763
|
+
except ImportError as e:
|
|
764
|
+
logger.warning(f"Skills endpoints not available: {e}")
|
|
765
|
+
|
|
766
|
+
# Mount Pricing router (transparent token-based pricing)
|
|
767
|
+
try:
|
|
768
|
+
from api.pricing_endpoints import router as pricing_router
|
|
769
|
+
app.include_router(pricing_router, prefix="/api")
|
|
770
|
+
logger.info("✅ Pricing endpoints registered at /api/pricing")
|
|
771
|
+
except ImportError as e:
|
|
772
|
+
logger.warning(f"Pricing endpoints not available: {e}")
|
|
773
|
+
|
|
774
|
+
# Mount A2A (Agent-to-Agent) Protocol routers
|
|
775
|
+
try:
|
|
776
|
+
from api.a2a.agent_card import router as a2a_card_router
|
|
777
|
+
from api.a2a.endpoints import router as a2a_endpoints_router
|
|
778
|
+
from api.a2a.streaming import router as a2a_streaming_router
|
|
779
|
+
|
|
780
|
+
# Agent Card at well-known path (RFC 8615) - no prefix
|
|
781
|
+
app.include_router(a2a_card_router, tags=["a2a-discovery"])
|
|
782
|
+
|
|
783
|
+
# A2A API endpoints
|
|
784
|
+
app.include_router(a2a_endpoints_router, tags=["a2a"])
|
|
785
|
+
app.include_router(a2a_streaming_router, tags=["a2a-streaming"])
|
|
786
|
+
|
|
787
|
+
logger.info("✅ A2A Protocol endpoints registered:")
|
|
788
|
+
logger.info(" - Agent Card: /.well-known/agent.json")
|
|
789
|
+
logger.info(" - JSON-RPC: /a2a/rpc")
|
|
790
|
+
logger.info(" - REST API: /a2a/tasks/*")
|
|
791
|
+
logger.info(" - Streaming: /a2a/message/stream, /a2a/tasks/*/stream")
|
|
792
|
+
except ImportError as e:
|
|
793
|
+
logger.warning(f"A2A endpoints not available: {e}")
|
|
794
|
+
|
|
795
|
+
# Mount ERC-8004 Trustless Agents router
|
|
796
|
+
try:
|
|
797
|
+
from api.eip8004_endpoints import router as eip8004_router
|
|
798
|
+
app.include_router(eip8004_router, tags=["eip8004-trustless-agents"])
|
|
799
|
+
|
|
800
|
+
eip8004_enabled = os.environ.get("EIP8004_ENABLED", "false").lower() == "true"
|
|
801
|
+
logger.info(f"✅ ERC-8004 Trustless Agents endpoints registered at /eip8004")
|
|
802
|
+
logger.info(f" - Registration: /eip8004/registration.json")
|
|
803
|
+
logger.info(f" - Reputation: /eip8004/reputation/*")
|
|
804
|
+
logger.info(f" - Validation: /eip8004/validation/*")
|
|
805
|
+
logger.info(f" - Status: {'enabled' if eip8004_enabled else 'discovery-only'}")
|
|
806
|
+
except ImportError as e:
|
|
807
|
+
logger.warning(f"ERC-8004 endpoints not available: {e}")
|
|
808
|
+
|
|
809
|
+
# Mount inbound webhook router (GET verify handshake + POST delegate to WebhookSurface).
|
|
810
|
+
# Empty registry → 404s; mounting is always safe (additive, fail-open).
|
|
811
|
+
try:
|
|
812
|
+
from api.webhooks import router as webhooks_router, set_container_provider
|
|
813
|
+
set_container_provider(lambda: app_state.get("container"))
|
|
814
|
+
app.include_router(webhooks_router, tags=["webhooks"])
|
|
815
|
+
logger.info("✅ Inbound webhooks registered at /webhooks/{surface_id}")
|
|
816
|
+
except Exception as e:
|
|
817
|
+
logger.warning("webhooks router not mounted: %s", e)
|
|
818
|
+
|
|
819
|
+
# Canonical public chat endpoint — served by the unified task agent (chat_once)
|
|
820
|
+
if API_MODELS_AVAILABLE:
|
|
821
|
+
@app.post("/api/chat/message", response_model=MessageResponse)
|
|
822
|
+
async def send_chat_message(request: MessageRequest, req: Request):
|
|
823
|
+
"""Public HTTP chat endpoint — served by the unified task agent.
|
|
824
|
+
|
|
825
|
+
The legacy ChatAgent was retired (HANDOFF-C); chat is now handled
|
|
826
|
+
solely by ``TaskAgent.chat_once`` via the canonical handler, which
|
|
827
|
+
returns a graceful ``MessageResponse(success=False)`` on failure.
|
|
828
|
+
"""
|
|
829
|
+
try:
|
|
830
|
+
container = app_state.get("container")
|
|
831
|
+
if not container:
|
|
832
|
+
raise HTTPException(status_code=503, detail="Container not initialized")
|
|
833
|
+
# SECURITY (C1): identity comes ONLY from the authenticated request
|
|
834
|
+
# state, never from the client-supplied body. Trusting request.user_id
|
|
835
|
+
# let any authenticated caller bill/recall memory as another tenant.
|
|
836
|
+
from utils.auth_utils import get_authenticated_user_id
|
|
837
|
+
user_id = get_authenticated_user_id(req)
|
|
838
|
+
|
|
839
|
+
from api.chat_via_task import handle_chat_via_task_agent
|
|
840
|
+
return await handle_chat_via_task_agent(
|
|
841
|
+
container, user_id, request.text, request.chat_id or user_id,
|
|
842
|
+
)
|
|
843
|
+
except HTTPException:
|
|
844
|
+
raise
|
|
845
|
+
except Exception as e:
|
|
846
|
+
logger.error(f"Error processing chat message: {e}", exc_info=True)
|
|
847
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
848
|
+
|
|
849
|
+
# Legacy endpoint redirect
|
|
850
|
+
@app.post("/api/message", response_model=MessageResponse)
|
|
851
|
+
async def legacy_send_message(request: MessageRequest, req: Request):
|
|
852
|
+
"""Legacy message endpoint - redirects to /api/chat/message."""
|
|
853
|
+
# For now, keep the same functionality but log the usage
|
|
854
|
+
logger.info("Legacy /api/message endpoint used, consider migrating to /api/chat/message")
|
|
855
|
+
return await send_chat_message(request, req)
|
|
856
|
+
|
|
857
|
+
# Removed legacy alias routes. Use canonical /api/task/* endpoints via included router.
|
|
858
|
+
|
|
859
|
+
# API Key Management Endpoints (admin only)
|
|
860
|
+
@app.post("/api/admin/generate-key")
|
|
861
|
+
async def generate_api_key(admin_token: str = Header(None, alias="X-Admin-Token")):
|
|
862
|
+
"""Generate a new API key (requires admin token)."""
|
|
863
|
+
import secrets
|
|
864
|
+
import hashlib
|
|
865
|
+
|
|
866
|
+
# Check admin authorization
|
|
867
|
+
expected_admin_token = os.environ.get("ADMIN_TOKEN")
|
|
868
|
+
if not expected_admin_token:
|
|
869
|
+
return JSONResponse(
|
|
870
|
+
status_code=503,
|
|
871
|
+
content={"error": "Admin authentication not configured"}
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
if not admin_token or admin_token != expected_admin_token:
|
|
875
|
+
return JSONResponse(
|
|
876
|
+
status_code=401,
|
|
877
|
+
content={"error": "Unauthorized admin access"}
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# Generate a secure API key
|
|
881
|
+
api_key = secrets.token_urlsafe(32)
|
|
882
|
+
key_hash = hashlib.sha256(api_key.encode()).hexdigest()[:8]
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
"api_key": api_key,
|
|
886
|
+
"key_id": f"key_{key_hash}",
|
|
887
|
+
"created_at": datetime.now().isoformat(),
|
|
888
|
+
"note": "Store this key securely. It won't be shown again."
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
@app.get("/api/test-auth")
|
|
892
|
+
async def test_auth(x_api_key: Optional[str] = Header(None)):
|
|
893
|
+
"""Test endpoint to verify API key authentication."""
|
|
894
|
+
api_token = os.environ.get("API_AUTH_TOKEN")
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
"authenticated": bool(x_api_key and api_token and x_api_key == api_token),
|
|
898
|
+
"has_key": bool(x_api_key),
|
|
899
|
+
"auth_configured": bool(api_token),
|
|
900
|
+
"message": "Authentication test endpoint"
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return app
|
|
904
|
+
|
|
905
|
+
# Factory function for uvicorn
|
|
906
|
+
def get_app() -> FastAPI:
|
|
907
|
+
"""Get FastAPI application instance for uvicorn."""
|
|
908
|
+
return create_app()
|
|
909
|
+
|
|
910
|
+
# For development/testing. Prefer `python main.py`, which already gates
|
|
911
|
+
# reload behind UVICORN_RELOAD (default false) -- this direct-run path now
|
|
912
|
+
# matches it instead of hardcoding a file-watcher that polls the whole repo
|
|
913
|
+
# tree even when idle.
|
|
914
|
+
if __name__ == "__main__":
|
|
915
|
+
import os
|
|
916
|
+
import uvicorn
|
|
917
|
+
uvicorn.run(
|
|
918
|
+
"api.app:get_app",
|
|
919
|
+
host="127.0.0.1",
|
|
920
|
+
port=9000,
|
|
921
|
+
reload=os.environ.get("UVICORN_RELOAD", "false").lower() == "true",
|
|
922
|
+
factory=True
|
|
923
|
+
)
|