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
tools/mcp/rate_limit.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Per-(user, server) MCP tool-execution rate limiter (WS-B3).
|
|
2
|
+
|
|
3
|
+
A sliding-window limiter keyed by an arbitrary hashable (typically
|
|
4
|
+
``(user_id, server_name)``) so a single agent/user cannot hammer expensive MCP
|
|
5
|
+
tools (crawl/scrape). Distinct from the per-tool throttle in ``BaseTool.rate_limit``
|
|
6
|
+
(global, service-dependent) and the add_server limiter in ``user_mcp_service``.
|
|
7
|
+
|
|
8
|
+
The clock is injectable (``time_fn``) so behavior is testable without real sleeps.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import Callable, Dict, Hashable, List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MCPExecRateLimiter:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
max_calls: int = 20,
|
|
20
|
+
window_seconds: int = 60,
|
|
21
|
+
time_fn: Callable[[], float] = time.time,
|
|
22
|
+
):
|
|
23
|
+
self.max_calls = max_calls
|
|
24
|
+
self.window_seconds = window_seconds
|
|
25
|
+
self._time_fn = time_fn
|
|
26
|
+
self._calls: Dict[Hashable, List[float]] = {}
|
|
27
|
+
|
|
28
|
+
def _prune(self, key: Hashable, now: float) -> List[float]:
|
|
29
|
+
cutoff = now - self.window_seconds
|
|
30
|
+
kept = [t for t in self._calls.get(key, []) if t > cutoff]
|
|
31
|
+
self._calls[key] = kept
|
|
32
|
+
return kept
|
|
33
|
+
|
|
34
|
+
def check(self, key: Hashable) -> bool:
|
|
35
|
+
"""Return True and record the call if under the limit, else False."""
|
|
36
|
+
now = self._time_fn()
|
|
37
|
+
kept = self._prune(key, now)
|
|
38
|
+
if len(kept) >= self.max_calls:
|
|
39
|
+
return False
|
|
40
|
+
kept.append(now)
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def retry_after(self, key: Hashable) -> float:
|
|
44
|
+
"""Seconds until the next slot frees (0 if a slot is available now)."""
|
|
45
|
+
now = self._time_fn()
|
|
46
|
+
kept = self._prune(key, now)
|
|
47
|
+
if len(kept) < self.max_calls:
|
|
48
|
+
return 0.0
|
|
49
|
+
oldest = min(kept)
|
|
50
|
+
return max(0.0, (oldest + self.window_seconds) - now)
|
tools/mcp/security.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Security utilities.
|
|
3
|
+
|
|
4
|
+
Provides encryption for API keys and URL validation for SSRF protection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import socket
|
|
10
|
+
import ipaddress
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Tuple, Dict, Any
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Path for persisted encryption key (development fallback).
|
|
19
|
+
# Anchored to the install/repo root (this file is tools/mcp/security.py ->
|
|
20
|
+
# parents[2]), NOT the process CWD: a relative path would regenerate the dev
|
|
21
|
+
# Fernet key whenever CWD changed, orphaning previously-encrypted MCP creds.
|
|
22
|
+
def _key_file_path() -> Path:
|
|
23
|
+
return Path(__file__).resolve().parents[2] / "data" / ".mcp_encryption_key"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_KEY_FILE_PATH = _key_file_path()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_production() -> bool:
|
|
30
|
+
"""Check if running in production mode."""
|
|
31
|
+
env = os.getenv('POLYROB_ENV', os.getenv('ENVIRONMENT', 'development')).lower()
|
|
32
|
+
return env in ('production', 'prod')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_or_generate_key() -> str:
|
|
36
|
+
"""
|
|
37
|
+
Load encryption key from environment, file, or generate new one.
|
|
38
|
+
|
|
39
|
+
Priority:
|
|
40
|
+
1. MCP_ENCRYPTION_KEY environment variable (always preferred)
|
|
41
|
+
2. Persisted key file (development fallback)
|
|
42
|
+
3. Generate new key (development only, persisted to file)
|
|
43
|
+
|
|
44
|
+
In production, FAILS HARD if no key is configured.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Fernet key as string
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RuntimeError: In production if MCP_ENCRYPTION_KEY is not set
|
|
51
|
+
"""
|
|
52
|
+
from cryptography.fernet import Fernet
|
|
53
|
+
|
|
54
|
+
# 1. Try environment variable first
|
|
55
|
+
key_str = os.getenv('MCP_ENCRYPTION_KEY')
|
|
56
|
+
if key_str:
|
|
57
|
+
logger.debug("Using MCP_ENCRYPTION_KEY from environment")
|
|
58
|
+
return key_str
|
|
59
|
+
|
|
60
|
+
# 2. In production, fail hard - no fallbacks allowed
|
|
61
|
+
if _is_production():
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
"CRITICAL: MCP_ENCRYPTION_KEY environment variable is required in production! "
|
|
64
|
+
"Generate a key with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 3. Development: Try to load from persisted file
|
|
68
|
+
if _KEY_FILE_PATH.exists():
|
|
69
|
+
try:
|
|
70
|
+
key_str = _KEY_FILE_PATH.read_text().strip()
|
|
71
|
+
if key_str:
|
|
72
|
+
logger.info(f"Loaded MCP encryption key from {_KEY_FILE_PATH}")
|
|
73
|
+
# Also set in environment so it's consistent for this session
|
|
74
|
+
os.environ['MCP_ENCRYPTION_KEY'] = key_str
|
|
75
|
+
return key_str
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning(f"Failed to read key file {_KEY_FILE_PATH}: {e}")
|
|
78
|
+
|
|
79
|
+
# 4. Development: Generate new key and persist it
|
|
80
|
+
logger.warning(
|
|
81
|
+
"MCP_ENCRYPTION_KEY not set - generating and persisting key for development. "
|
|
82
|
+
"This key will be saved to data/.mcp_encryption_key and reused across restarts. "
|
|
83
|
+
"In production, set MCP_ENCRYPTION_KEY environment variable!"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
key_str = Fernet.generate_key().decode()
|
|
87
|
+
|
|
88
|
+
# Persist to file for reuse
|
|
89
|
+
try:
|
|
90
|
+
_KEY_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
_KEY_FILE_PATH.write_text(key_str)
|
|
92
|
+
# Set restrictive permissions (owner read/write only)
|
|
93
|
+
_KEY_FILE_PATH.chmod(0o600)
|
|
94
|
+
logger.info(f"Persisted MCP encryption key to {_KEY_FILE_PATH}")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.warning(f"Failed to persist key to {_KEY_FILE_PATH}: {e}")
|
|
97
|
+
|
|
98
|
+
# Set in environment for this session
|
|
99
|
+
os.environ['MCP_ENCRYPTION_KEY'] = key_str
|
|
100
|
+
|
|
101
|
+
return key_str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class MCPEncryption:
|
|
105
|
+
"""Encrypt/decrypt sensitive MCP data using Fernet (AES-128-CBC)."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, key: Optional[bytes] = None):
|
|
108
|
+
"""
|
|
109
|
+
Initialize encryption with a key.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
key: Fernet key (32 url-safe base64-encoded bytes).
|
|
113
|
+
If not provided, loads from MCP_ENCRYPTION_KEY env var or persisted file.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
RuntimeError: In production if MCP_ENCRYPTION_KEY is not set
|
|
117
|
+
"""
|
|
118
|
+
# Lazy import to avoid dependency issues if cryptography not installed
|
|
119
|
+
from cryptography.fernet import Fernet
|
|
120
|
+
|
|
121
|
+
if key:
|
|
122
|
+
self.fernet = Fernet(key)
|
|
123
|
+
else:
|
|
124
|
+
# Load key using proper priority chain
|
|
125
|
+
key_str = _load_or_generate_key()
|
|
126
|
+
self.fernet = Fernet(key_str.encode() if isinstance(key_str, str) else key_str)
|
|
127
|
+
|
|
128
|
+
def encrypt(self, data: str) -> bytes:
|
|
129
|
+
"""
|
|
130
|
+
Encrypt a string.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
data: Plain text string to encrypt
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Encrypted bytes
|
|
137
|
+
"""
|
|
138
|
+
if not data:
|
|
139
|
+
return b''
|
|
140
|
+
return self.fernet.encrypt(data.encode())
|
|
141
|
+
|
|
142
|
+
def decrypt(self, encrypted: bytes) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Decrypt to string.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
encrypted: Encrypted bytes
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Decrypted string
|
|
151
|
+
"""
|
|
152
|
+
if not encrypted:
|
|
153
|
+
return ''
|
|
154
|
+
return self.fernet.decrypt(encrypted).decode()
|
|
155
|
+
|
|
156
|
+
def encrypt_dict(self, data: Dict[str, Any]) -> bytes:
|
|
157
|
+
"""
|
|
158
|
+
Encrypt a dictionary as JSON.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
data: Dictionary to encrypt
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Encrypted bytes
|
|
165
|
+
"""
|
|
166
|
+
if not data:
|
|
167
|
+
return b''
|
|
168
|
+
return self.encrypt(json.dumps(data))
|
|
169
|
+
|
|
170
|
+
def decrypt_dict(self, encrypted: bytes) -> Dict[str, Any]:
|
|
171
|
+
"""
|
|
172
|
+
Decrypt to dictionary.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
encrypted: Encrypted bytes
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Decrypted dictionary
|
|
179
|
+
"""
|
|
180
|
+
if not encrypted:
|
|
181
|
+
return {}
|
|
182
|
+
return json.loads(self.decrypt(encrypted))
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def generate_key() -> bytes:
|
|
186
|
+
"""
|
|
187
|
+
Generate a new encryption key.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
New Fernet key (url-safe base64-encoded)
|
|
191
|
+
"""
|
|
192
|
+
from cryptography.fernet import Fernet
|
|
193
|
+
return Fernet.generate_key()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class MCPURLValidator:
|
|
197
|
+
"""
|
|
198
|
+
Validate MCP server URLs for security (SSRF protection).
|
|
199
|
+
|
|
200
|
+
Blocks:
|
|
201
|
+
- Non-HTTPS URLs
|
|
202
|
+
- Localhost and loopback addresses
|
|
203
|
+
- Private IP ranges (10.x, 172.16-31.x, 192.168.x)
|
|
204
|
+
- Link-local addresses (169.254.x)
|
|
205
|
+
- Cloud metadata endpoints
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
# Blocked hostnames
|
|
209
|
+
BLOCKED_HOSTS = {
|
|
210
|
+
'localhost',
|
|
211
|
+
'127.0.0.1',
|
|
212
|
+
'0.0.0.0',
|
|
213
|
+
'::1',
|
|
214
|
+
'[::1]',
|
|
215
|
+
'metadata.google.internal',
|
|
216
|
+
'metadata.goog',
|
|
217
|
+
'169.254.169.254', # AWS/GCP/Azure metadata
|
|
218
|
+
'metadata.internal',
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Blocked IP networks (private/internal ranges). Belt-and-suspenders alongside the
|
|
222
|
+
# property-based classification in _is_blocked_ip (which is the authoritative check).
|
|
223
|
+
BLOCKED_NETWORKS = [
|
|
224
|
+
ipaddress.ip_network('0.0.0.0/8'), # "this network" (0.x routes to localhost)
|
|
225
|
+
ipaddress.ip_network('10.0.0.0/8'), # Private class A
|
|
226
|
+
ipaddress.ip_network('172.16.0.0/12'), # Private class B
|
|
227
|
+
ipaddress.ip_network('192.168.0.0/16'), # Private class C
|
|
228
|
+
ipaddress.ip_network('127.0.0.0/8'), # Loopback
|
|
229
|
+
ipaddress.ip_network('169.254.0.0/16'), # Link-local
|
|
230
|
+
ipaddress.ip_network('100.64.0.0/10'), # CGNAT / shared address space
|
|
231
|
+
ipaddress.ip_network('::1/128'), # IPv6 loopback
|
|
232
|
+
ipaddress.ip_network('fc00::/7'), # IPv6 private
|
|
233
|
+
ipaddress.ip_network('fe80::/10'), # IPv6 link-local
|
|
234
|
+
ipaddress.ip_network('::ffff:0:0/96'), # IPv4-mapped IPv6
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _is_blocked_ip(ip_obj: "ipaddress._BaseAddress") -> bool:
|
|
239
|
+
"""Authoritative SSRF check by IP *property*, not a hand-maintained range list.
|
|
240
|
+
|
|
241
|
+
Unmaps IPv4-mapped IPv6 (``::ffff:a.b.c.d``) first — otherwise a v4 network
|
|
242
|
+
check silently misses it — then blocks any non-global address class
|
|
243
|
+
(private/loopback/link-local/reserved/multicast/unspecified) plus CGNAT.
|
|
244
|
+
"""
|
|
245
|
+
if isinstance(ip_obj, ipaddress.IPv6Address):
|
|
246
|
+
if ip_obj.ipv4_mapped is not None:
|
|
247
|
+
ip_obj = ip_obj.ipv4_mapped
|
|
248
|
+
elif getattr(ip_obj, "sixtofour", None) is not None:
|
|
249
|
+
ip_obj = ip_obj.sixtofour
|
|
250
|
+
if (ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local
|
|
251
|
+
or ip_obj.is_reserved or ip_obj.is_multicast or ip_obj.is_unspecified):
|
|
252
|
+
return True
|
|
253
|
+
# CGNAT is not is_private on every Python version — block explicitly.
|
|
254
|
+
if ip_obj.version == 4 and ip_obj in ipaddress.ip_network('100.64.0.0/10'):
|
|
255
|
+
return True
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def __init__(self, allow_http: bool = False):
|
|
259
|
+
"""
|
|
260
|
+
Initialize validator.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
allow_http: If True, allow HTTP URLs (for local development only)
|
|
264
|
+
"""
|
|
265
|
+
self.allow_http = allow_http
|
|
266
|
+
|
|
267
|
+
def validate(self, url: str) -> Tuple[bool, Optional[str]]:
|
|
268
|
+
"""
|
|
269
|
+
Validate URL is safe for user MCP servers.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
url: URL to validate
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Tuple of (is_valid, error_message)
|
|
276
|
+
error_message is None if valid
|
|
277
|
+
|
|
278
|
+
Note:
|
|
279
|
+
This 2-tuple contract is depended upon by existing callers
|
|
280
|
+
(``user_mcp_service.add_server``/``update_server``). For connect-time
|
|
281
|
+
SSRF/DNS-rebinding protection, callers that need the resolved IP
|
|
282
|
+
should use :meth:`validate_and_resolve`, which returns the validated
|
|
283
|
+
pinned IP so the socket connects to the IP that was actually checked.
|
|
284
|
+
"""
|
|
285
|
+
is_valid, error, _pinned_ip = self.validate_and_resolve(url)
|
|
286
|
+
return is_valid, error
|
|
287
|
+
|
|
288
|
+
def validate_and_resolve(
|
|
289
|
+
self, url: str
|
|
290
|
+
) -> Tuple[bool, Optional[str], Optional[str]]:
|
|
291
|
+
"""
|
|
292
|
+
Validate URL AND return the validated, resolved IP to pin.
|
|
293
|
+
|
|
294
|
+
Resolves the hostname exactly once and checks every resolved address
|
|
295
|
+
against the blocked networks. On success it returns a single concrete
|
|
296
|
+
IP that the caller should connect to directly (pinning), so a DNS
|
|
297
|
+
rebind between validation and connection cannot redirect the socket to
|
|
298
|
+
an internal/metadata address.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
url: URL to validate
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Tuple of (is_valid, error_message, pinned_ip)
|
|
305
|
+
- On success: (True, None, "<resolved public ip>")
|
|
306
|
+
- On failure: (False, "<reason>", None)
|
|
307
|
+
|
|
308
|
+
Security:
|
|
309
|
+
The returned ``pinned_ip`` is the address that was validated. Callers
|
|
310
|
+
MUST connect to this IP (preserving the original Host header / TLS
|
|
311
|
+
SNI) rather than re-resolving DNS, otherwise the rebinding window
|
|
312
|
+
reopens.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
parsed = urlparse(url)
|
|
316
|
+
|
|
317
|
+
# Check scheme
|
|
318
|
+
if not self.allow_http and parsed.scheme != 'https':
|
|
319
|
+
return False, "URL must use HTTPS", None
|
|
320
|
+
|
|
321
|
+
if parsed.scheme not in ('http', 'https'):
|
|
322
|
+
return False, f"Invalid scheme: {parsed.scheme}", None
|
|
323
|
+
|
|
324
|
+
# Check hostname exists
|
|
325
|
+
hostname = parsed.hostname
|
|
326
|
+
if not hostname:
|
|
327
|
+
return False, "Invalid URL: no hostname", None
|
|
328
|
+
|
|
329
|
+
# Check for blocked hostnames
|
|
330
|
+
hostname_lower = hostname.lower()
|
|
331
|
+
if hostname_lower in self.BLOCKED_HOSTS:
|
|
332
|
+
return False, f"Blocked host: {hostname}", None
|
|
333
|
+
|
|
334
|
+
# Check for blocked hostname patterns
|
|
335
|
+
if hostname_lower.endswith('.internal') or hostname_lower.endswith('.local'):
|
|
336
|
+
return False, f"Internal hostname not allowed: {hostname}", None
|
|
337
|
+
|
|
338
|
+
# Check port (optional - some MCP servers use non-standard ports)
|
|
339
|
+
port = parsed.port
|
|
340
|
+
if port is not None:
|
|
341
|
+
# Block commonly abused ports
|
|
342
|
+
if port in (22, 23, 25, 110, 143, 445, 3389):
|
|
343
|
+
return False, f"Port {port} is not allowed", None
|
|
344
|
+
|
|
345
|
+
# Resolve hostname ONCE and check every IP. The first valid,
|
|
346
|
+
# non-blocked address is returned as the pin target.
|
|
347
|
+
pinned_ip: Optional[str] = None
|
|
348
|
+
try:
|
|
349
|
+
# Get all IPs for the hostname
|
|
350
|
+
addresses = socket.getaddrinfo(hostname, port)
|
|
351
|
+
for addr_info in addresses:
|
|
352
|
+
ip_str = addr_info[4][0]
|
|
353
|
+
try:
|
|
354
|
+
ip_obj = ipaddress.ip_address(ip_str)
|
|
355
|
+
except ValueError:
|
|
356
|
+
# Not a valid IP address, skip
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
# Property-based classification (authoritative) — closes the whole
|
|
360
|
+
# class of internal addresses incl. IPv4-mapped IPv6, 0.0.0.0/8, CGNAT.
|
|
361
|
+
if self._is_blocked_ip(ip_obj):
|
|
362
|
+
return False, f"IP address in blocked range: {ip_str}", None
|
|
363
|
+
# Belt-and-suspenders: explicit range list too.
|
|
364
|
+
for network in self.BLOCKED_NETWORKS:
|
|
365
|
+
if ip_obj in network:
|
|
366
|
+
return False, f"IP address in blocked range: {ip_str}", None
|
|
367
|
+
|
|
368
|
+
# First clean address becomes the pin target
|
|
369
|
+
if pinned_ip is None:
|
|
370
|
+
pinned_ip = ip_str
|
|
371
|
+
|
|
372
|
+
except socket.gaierror as e:
|
|
373
|
+
return False, f"Cannot resolve hostname: {hostname} ({e})", None
|
|
374
|
+
|
|
375
|
+
if pinned_ip is None:
|
|
376
|
+
return False, f"Cannot resolve hostname to an IP: {hostname}", None
|
|
377
|
+
|
|
378
|
+
return True, None, pinned_ip
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
return False, f"URL validation error: {e}", None
|
|
382
|
+
|
|
383
|
+
def validate_server_type(self, server_type: str) -> Tuple[bool, Optional[str]]:
|
|
384
|
+
"""
|
|
385
|
+
Validate server type is allowed for user MCP servers.
|
|
386
|
+
|
|
387
|
+
STDIO is NOT allowed for user servers (security risk - executes local processes).
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
server_type: Server type to validate ('sse', 'http', 'streamable_http', 'stdio')
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Tuple of (is_valid, error_message)
|
|
394
|
+
"""
|
|
395
|
+
allowed_types = ('sse', 'http', 'streamable_http')
|
|
396
|
+
|
|
397
|
+
if server_type == 'stdio':
|
|
398
|
+
return False, "STDIO server type is not allowed for user servers (security risk)"
|
|
399
|
+
|
|
400
|
+
if server_type not in allowed_types:
|
|
401
|
+
return False, f"Invalid server type: {server_type}. Must be one of: {', '.join(allowed_types)}"
|
|
402
|
+
|
|
403
|
+
return True, None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def get_encryption() -> MCPEncryption:
|
|
407
|
+
"""Get singleton encryption instance."""
|
|
408
|
+
global _encryption_instance
|
|
409
|
+
if '_encryption_instance' not in globals():
|
|
410
|
+
_encryption_instance = MCPEncryption()
|
|
411
|
+
return _encryption_instance
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def get_url_validator(allow_http: bool = False) -> MCPURLValidator:
|
|
415
|
+
"""
|
|
416
|
+
Get URL validator instance.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
allow_http: Allow HTTP URLs (development only)
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
MCPURLValidator instance
|
|
423
|
+
"""
|
|
424
|
+
return MCPURLValidator(allow_http=allow_http)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Agent-initiated MCP install (P0-C).
|
|
2
|
+
|
|
3
|
+
Ties together: allowlist check (MCPCatalog) → exfiltration/injection screen (reusing the
|
|
4
|
+
memory threat scanner) → approval gate → MCPServerManager.add_server + config persist.
|
|
5
|
+
Gated OFF by default (MCP_SELF_INSTALL_ENABLED). The server runs OUT of process, so this
|
|
6
|
+
is the lowest-risk "install its own tools" rung. ``screen_config`` is pure (no I/O). No
|
|
7
|
+
action closures — ``from __future__`` is safe.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def self_install_enabled() -> bool:
|
|
19
|
+
from core.env import bool_env
|
|
20
|
+
return bool_env("MCP_SELF_INSTALL_ENABLED", False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def screen_config(cfg: Dict[str, Any]) -> Optional[str]:
|
|
24
|
+
"""Return a rejection reason if ``cfg`` looks injected/exfiltrating, else None.
|
|
25
|
+
|
|
26
|
+
Serialises the whole config and runs the shared instruction-override scanner over it
|
|
27
|
+
(catch a prompt-injection smuggled through a description/arg), plus conservative
|
|
28
|
+
structural checks (no remote-script-piped-to-shell stdio command).
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
blob = json.dumps(cfg, default=str)
|
|
32
|
+
except Exception:
|
|
33
|
+
return "config is not JSON-serialisable"
|
|
34
|
+
from modules.memory.task.threat_scan import is_suspicious, has_invisible_unicode
|
|
35
|
+
if is_suspicious(blob):
|
|
36
|
+
return "config rejected: contains instruction-override / prompt-injection text"
|
|
37
|
+
if has_invisible_unicode(blob):
|
|
38
|
+
return "config rejected: contains invisible/bidi control characters"
|
|
39
|
+
cmd = cfg.get("command")
|
|
40
|
+
if isinstance(cmd, (list, tuple)):
|
|
41
|
+
joined = " ".join(str(c) for c in cmd).lower()
|
|
42
|
+
for bad in ("curl", "wget", "| sh", "|sh", "| bash", "|bash"):
|
|
43
|
+
if bad in joined:
|
|
44
|
+
return f"config rejected: stdio command looks like a remote-exec ({bad!r})"
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _entry_to_config_dict(entry) -> Dict[str, Any]:
|
|
49
|
+
d: Dict[str, Any] = {"transport": entry.transport, "description": entry.description, "trust": entry.trust}
|
|
50
|
+
if entry.url:
|
|
51
|
+
d["url"] = entry.url
|
|
52
|
+
if entry.command:
|
|
53
|
+
d["command"] = list(entry.command)
|
|
54
|
+
return d
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def perform_mcp_install(
|
|
58
|
+
server_id: str,
|
|
59
|
+
*,
|
|
60
|
+
catalog,
|
|
61
|
+
server_manager,
|
|
62
|
+
approve,
|
|
63
|
+
persist=None,
|
|
64
|
+
context: Any = None,
|
|
65
|
+
) -> Tuple[bool, str]:
|
|
66
|
+
"""Install a catalog MCP server: gate → allowlist → screen → approve → add_server → persist.
|
|
67
|
+
|
|
68
|
+
``approve`` is an async callable ``(action_name, params, context) -> bool`` (the
|
|
69
|
+
approval provider). Returns ``(ok, message)``.
|
|
70
|
+
"""
|
|
71
|
+
if not self_install_enabled():
|
|
72
|
+
return False, "MCP self-install is disabled (set MCP_SELF_INSTALL_ENABLED=true)."
|
|
73
|
+
if not catalog.is_allowed(server_id):
|
|
74
|
+
return False, f"'{server_id}' is not in the MCP install allowlist ({catalog.ids()})."
|
|
75
|
+
entry = catalog.get(server_id)
|
|
76
|
+
if entry is None:
|
|
77
|
+
return False, f"no catalog entry for '{server_id}'."
|
|
78
|
+
|
|
79
|
+
cfg_dict = _entry_to_config_dict(entry)
|
|
80
|
+
reason = screen_config(cfg_dict)
|
|
81
|
+
if reason:
|
|
82
|
+
return False, reason
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
allowed = await approve("mcp_install", {"server_id": server_id, **cfg_dict}, context)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return False, f"approval error for mcp_install: {e}"
|
|
88
|
+
if not allowed:
|
|
89
|
+
return False, f"mcp_install of '{server_id}' was not approved."
|
|
90
|
+
|
|
91
|
+
from tools.mcp.config import MCPServerConfig, MCPServerType
|
|
92
|
+
type_map = {
|
|
93
|
+
"sse": MCPServerType.SSE,
|
|
94
|
+
"http": getattr(MCPServerType, "HTTP", MCPServerType.SSE),
|
|
95
|
+
"stdio": MCPServerType.STDIO,
|
|
96
|
+
}
|
|
97
|
+
config = MCPServerConfig(
|
|
98
|
+
type=type_map.get(entry.transport, MCPServerType.SSE),
|
|
99
|
+
url=entry.url,
|
|
100
|
+
command=list(entry.command) if entry.command else None,
|
|
101
|
+
enabled=True,
|
|
102
|
+
)
|
|
103
|
+
ok = await server_manager.add_server(server_id, config)
|
|
104
|
+
if not ok:
|
|
105
|
+
return False, f"MCP server '{server_id}' failed to install/connect."
|
|
106
|
+
if persist is not None:
|
|
107
|
+
try:
|
|
108
|
+
persist(server_id, cfg_dict)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.warning("mcp_install persist failed for %s: %s", server_id, e)
|
|
111
|
+
return True, f"Installed MCP server '{server_id}' ({entry.description})."
|