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/user_directory.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Shared UserDirectory — surface-agnostic identity store (P2c, Singular Chat Interface).
|
|
2
|
+
|
|
3
|
+
ONE identity store behind POLYROB's many surfaces (CLI / WebView / Telegram). It maps a
|
|
4
|
+
raw platform id (a Telegram chat id, a CLI id, …) to a STABLE INTERNAL ``user_id``
|
|
5
|
+
and provides the reverse / email lookups that out-of-band delivery
|
|
6
|
+
(``cron/delivery.py``) needs — keyed on the internal id, sync.
|
|
7
|
+
|
|
8
|
+
Ported (reused, not forked) from the old Telegram-only bot
|
|
9
|
+
(../rob_dev_telegram_version):
|
|
10
|
+
- ``modules/database/user_profiles.py`` — the ``user_profiles`` table shape
|
|
11
|
+
(``user_id TEXT PK`` + ``tg_user_id TEXT UNIQUE``), ``get_or_create_by_tg_id``,
|
|
12
|
+
and the SHA256 ``generate_user_id`` derivation.
|
|
13
|
+
- ``utils/user_id_utils.py`` — ``UserIDResolver`` bidirectional tg<->internal mapping.
|
|
14
|
+
|
|
15
|
+
Key differences from the old bot (deliberate):
|
|
16
|
+
- The internal id is **deterministically derived** from ``(surface, raw_id)`` via
|
|
17
|
+
SHA256, so it is stable WITHOUT a prior row (the old code minted a random UUID per
|
|
18
|
+
row). A row is still upserted so the reverse lookup (internal -> chat id) works.
|
|
19
|
+
- Identity is **namespaced by surface** — the same raw "12345" on telegram vs cli
|
|
20
|
+
resolves to different internal ids, so cross-surface ids never collide.
|
|
21
|
+
- Sync API (no async DB layer) — ``cron/delivery.py`` calls ``get_telegram_chat_id``
|
|
22
|
+
/ ``get_email`` without ``await``; SQLite + ``core/sqlite_util`` is the right shape.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from core.sqlite_util import execute_retry, wal_connect
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Length of the hex slice used for the internal id (mirrors the old 24-char hash).
|
|
36
|
+
_ID_HEX_LEN = 24
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UserDirectory:
|
|
40
|
+
"""Container-registerable identity store over a SQLite ``user_profiles`` table."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, db_path: str):
|
|
43
|
+
self.db_path = db_path
|
|
44
|
+
self._ensure_table()
|
|
45
|
+
|
|
46
|
+
# --- schema --------------------------------------------------------------
|
|
47
|
+
def _ensure_table(self) -> None:
|
|
48
|
+
conn = wal_connect(self.db_path)
|
|
49
|
+
try:
|
|
50
|
+
conn.execute(
|
|
51
|
+
"""
|
|
52
|
+
CREATE TABLE IF NOT EXISTS user_profiles (
|
|
53
|
+
user_id TEXT PRIMARY KEY,
|
|
54
|
+
surface_id TEXT,
|
|
55
|
+
tg_user_id TEXT UNIQUE,
|
|
56
|
+
chat_id TEXT,
|
|
57
|
+
email TEXT,
|
|
58
|
+
profile TEXT,
|
|
59
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
60
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
61
|
+
)
|
|
62
|
+
"""
|
|
63
|
+
)
|
|
64
|
+
conn.execute(
|
|
65
|
+
"CREATE INDEX IF NOT EXISTS idx_user_profiles_tg "
|
|
66
|
+
"ON user_profiles(tg_user_id)"
|
|
67
|
+
)
|
|
68
|
+
conn.commit()
|
|
69
|
+
finally:
|
|
70
|
+
conn.close()
|
|
71
|
+
|
|
72
|
+
# --- id derivation -------------------------------------------------------
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _derive_internal_id(surface_id: str, raw_id: str) -> str:
|
|
75
|
+
"""Deterministic, stable internal id from (surface, raw_id).
|
|
76
|
+
|
|
77
|
+
SHA256 of ``surface_id:raw_id`` so the same raw id on two surfaces never
|
|
78
|
+
collides and the id is reproducible without a prior DB row.
|
|
79
|
+
"""
|
|
80
|
+
seed = f"{surface_id}:{str(raw_id).strip()}".encode("utf-8")
|
|
81
|
+
digest = hashlib.sha256(seed).hexdigest()[:_ID_HEX_LEN]
|
|
82
|
+
return f"u_{digest}"
|
|
83
|
+
|
|
84
|
+
# --- public API (contracts) ---------------------------------------------
|
|
85
|
+
def get_or_create_by_tg_id(self, tg_id: str, profile: Optional[dict] = None) -> str:
|
|
86
|
+
"""Return a STABLE internal ``user_id`` for a Telegram id (idempotent).
|
|
87
|
+
|
|
88
|
+
Upserts a row mapping ``tg_id`` <-> internal id, persisting ``chat_id`` (==
|
|
89
|
+
the tg id, for reverse lookup), an optional ``email`` and JSON profile blob.
|
|
90
|
+
"""
|
|
91
|
+
tg_id = str(tg_id).strip()
|
|
92
|
+
user_id = self._derive_internal_id("telegram", tg_id)
|
|
93
|
+
self._upsert(
|
|
94
|
+
user_id=user_id,
|
|
95
|
+
surface_id="telegram",
|
|
96
|
+
tg_user_id=tg_id,
|
|
97
|
+
chat_id=tg_id,
|
|
98
|
+
profile=profile,
|
|
99
|
+
)
|
|
100
|
+
return user_id
|
|
101
|
+
|
|
102
|
+
def resolve_internal(self, raw_user_id: str, surface_id: str) -> str:
|
|
103
|
+
"""Map a raw platform id to a stable internal ``user_id`` (idempotent).
|
|
104
|
+
|
|
105
|
+
``surface_id == "telegram"`` is exactly ``get_or_create_by_tg_id``. Other
|
|
106
|
+
surfaces derive/persist the same way, namespaced by ``surface_id`` so the
|
|
107
|
+
same raw id on different surfaces resolves to different internal ids.
|
|
108
|
+
"""
|
|
109
|
+
raw_user_id = str(raw_user_id).strip()
|
|
110
|
+
if surface_id == "telegram":
|
|
111
|
+
return self.get_or_create_by_tg_id(raw_user_id)
|
|
112
|
+
user_id = self._derive_internal_id(surface_id, raw_user_id)
|
|
113
|
+
self._upsert(
|
|
114
|
+
user_id=user_id,
|
|
115
|
+
surface_id=surface_id,
|
|
116
|
+
tg_user_id=None,
|
|
117
|
+
chat_id=raw_user_id,
|
|
118
|
+
profile=None,
|
|
119
|
+
)
|
|
120
|
+
return user_id
|
|
121
|
+
|
|
122
|
+
def get_telegram_chat_id(self, user_id: str) -> Optional[str]:
|
|
123
|
+
"""Reverse lookup: internal ``user_id`` -> telegram chat id (the tg id).
|
|
124
|
+
|
|
125
|
+
Exact name/shape consumed by ``cron/delivery.py::_owner_telegram``.
|
|
126
|
+
"""
|
|
127
|
+
row = execute_retry(
|
|
128
|
+
self.db_path,
|
|
129
|
+
"SELECT tg_user_id, chat_id, surface_id FROM user_profiles WHERE user_id = ?",
|
|
130
|
+
(user_id,),
|
|
131
|
+
fetch="one",
|
|
132
|
+
)
|
|
133
|
+
if not row:
|
|
134
|
+
return None
|
|
135
|
+
# Only telegram-origin rows have a meaningful tg chat id.
|
|
136
|
+
if row["surface_id"] != "telegram":
|
|
137
|
+
return None
|
|
138
|
+
return row["tg_user_id"] or row["chat_id"]
|
|
139
|
+
|
|
140
|
+
def get_email(self, user_id: str) -> Optional[str]:
|
|
141
|
+
"""Read the optional email for an internal ``user_id`` (None if unset)."""
|
|
142
|
+
row = execute_retry(
|
|
143
|
+
self.db_path,
|
|
144
|
+
"SELECT email FROM user_profiles WHERE user_id = ?",
|
|
145
|
+
(user_id,),
|
|
146
|
+
fetch="one",
|
|
147
|
+
)
|
|
148
|
+
if not row:
|
|
149
|
+
return None
|
|
150
|
+
return row["email"]
|
|
151
|
+
|
|
152
|
+
# --- internals -----------------------------------------------------------
|
|
153
|
+
def _upsert(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
user_id: str,
|
|
157
|
+
surface_id: str,
|
|
158
|
+
tg_user_id: Optional[str],
|
|
159
|
+
chat_id: Optional[str],
|
|
160
|
+
profile: Optional[dict],
|
|
161
|
+
) -> None:
|
|
162
|
+
email = profile.get("email") if profile else None
|
|
163
|
+
profile_json = json.dumps(profile) if profile else None
|
|
164
|
+
# COALESCE preserves previously-set fields when a later call passes None.
|
|
165
|
+
execute_retry(
|
|
166
|
+
self.db_path,
|
|
167
|
+
"""
|
|
168
|
+
INSERT INTO user_profiles (user_id, surface_id, tg_user_id, chat_id, email, profile)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
170
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
171
|
+
surface_id = excluded.surface_id,
|
|
172
|
+
tg_user_id = COALESCE(excluded.tg_user_id, user_profiles.tg_user_id),
|
|
173
|
+
chat_id = COALESCE(excluded.chat_id, user_profiles.chat_id),
|
|
174
|
+
email = COALESCE(excluded.email, user_profiles.email),
|
|
175
|
+
profile = COALESCE(excluded.profile, user_profiles.profile),
|
|
176
|
+
updated_at = CURRENT_TIMESTAMP
|
|
177
|
+
""",
|
|
178
|
+
(user_id, surface_id, tg_user_id, chat_id, email, profile_json),
|
|
179
|
+
)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Stateless, SSRF-safe single-page fetch core (no browser, no Chromium).
|
|
2
|
+
|
|
3
|
+
Security model (see docs/plans/2026-06-29-web-fetch-tier1-IMPLEMENTATION-PLAN.md):
|
|
4
|
+
- Auto-redirects are OFF; every hop is re-validated.
|
|
5
|
+
- Each hop is validated with MCPURLValidator.validate_and_resolve(), which returns a
|
|
6
|
+
pinned IP; the connection is pinned to that IP (Host/SNI preserved) so a DNS rebind
|
|
7
|
+
between validation and connect cannot redirect the socket internally.
|
|
8
|
+
- Hard caps on redirects, total time, and response bytes (read incrementally so a
|
|
9
|
+
decompression bomb is aborted before it is fully buffered).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import socket
|
|
14
|
+
import ssl
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
from urllib.parse import urljoin, urlparse
|
|
18
|
+
|
|
19
|
+
import aiohttp
|
|
20
|
+
import certifi
|
|
21
|
+
from aiohttp.abc import AbstractResolver
|
|
22
|
+
|
|
23
|
+
from tools.mcp.security import get_url_validator
|
|
24
|
+
|
|
25
|
+
_REDIRECT_STATUS = {301, 302, 303, 307, 308}
|
|
26
|
+
|
|
27
|
+
# An honest, disclosed bot UA. Many sites (e.g. Wikipedia) reject requests with no
|
|
28
|
+
# User-Agent; the "Mozilla/5.0 (compatible; ...)" form is the widely-accepted shape.
|
|
29
|
+
_DEFAULT_HEADERS = {
|
|
30
|
+
"User-Agent": "Mozilla/5.0 (compatible; polyrob-web-fetch/1.0; +https://github.com/theselfruleorg)",
|
|
31
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WebFetchError(Exception):
|
|
36
|
+
"""Raised when a fetch is blocked, oversized, or exceeds the redirect cap."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class FetchResult:
|
|
41
|
+
final_url: str
|
|
42
|
+
status: int
|
|
43
|
+
content_type: str
|
|
44
|
+
body: bytes
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _PinnedResolver(AbstractResolver):
|
|
48
|
+
"""Force a single host to resolve to a pre-validated IP (defeats DNS rebinding).
|
|
49
|
+
|
|
50
|
+
aiohttp connects to the ``host`` field (our pinned IP) but uses ``hostname`` for
|
|
51
|
+
TLS SNI and keeps the URL's host for the Host header, so pinning is transparent.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, hostname: str, pinned_ip: str) -> None:
|
|
55
|
+
self._hostname = hostname
|
|
56
|
+
self._ip = pinned_ip
|
|
57
|
+
|
|
58
|
+
async def resolve(self, host, port=0, family=socket.AF_INET):
|
|
59
|
+
ip = self._ip if host == self._hostname else host
|
|
60
|
+
return [{"hostname": host, "host": ip, "port": port,
|
|
61
|
+
"family": family, "proto": 0, "flags": 0}]
|
|
62
|
+
|
|
63
|
+
async def close(self) -> None: # pragma: no cover - trivial
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _default_session_factory(pinned_ip: Optional[str], hostname: Optional[str]):
|
|
68
|
+
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
|
69
|
+
if pinned_ip and hostname:
|
|
70
|
+
connector = aiohttp.TCPConnector(resolver=_PinnedResolver(hostname, pinned_ip), ssl=ssl_ctx)
|
|
71
|
+
else:
|
|
72
|
+
connector = aiohttp.TCPConnector(ssl=ssl_ctx)
|
|
73
|
+
return aiohttp.ClientSession(connector=connector)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def safe_fetch(
|
|
77
|
+
url: str,
|
|
78
|
+
*,
|
|
79
|
+
max_bytes: int = 10_485_760,
|
|
80
|
+
max_redirects: int = 5,
|
|
81
|
+
timeout_sec: float = 15.0,
|
|
82
|
+
validate: bool = True,
|
|
83
|
+
validator=None,
|
|
84
|
+
session_factory: Optional[Callable] = None,
|
|
85
|
+
) -> FetchResult:
|
|
86
|
+
"""Fetch ``url`` with per-hop SSRF validation, IP pinning, no auto-redirects, and caps.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
validate: when False, skip SSRF validation entirely (single-user/local only).
|
|
90
|
+
validator: an object exposing ``validate_and_resolve(url) -> (ok, err, pinned_ip)``.
|
|
91
|
+
Defaults to the shared MCP URL validator (allow_http=True). Injectable for tests.
|
|
92
|
+
session_factory: ``factory(pinned_ip) -> async-context-session`` (injectable for tests).
|
|
93
|
+
"""
|
|
94
|
+
if validate and validator is None:
|
|
95
|
+
validator = get_url_validator(allow_http=True)
|
|
96
|
+
|
|
97
|
+
current = url
|
|
98
|
+
for _hop in range(max_redirects + 1):
|
|
99
|
+
pinned_ip: Optional[str] = None
|
|
100
|
+
# WS-4 (compute posture): a NARROW exception for the agent's OWN sandbox server —
|
|
101
|
+
# a loopback URL on a host port the sandbox actually published skips SSRF
|
|
102
|
+
# validation and pins to 127.0.0.1. Empty unless the sandbox published a port
|
|
103
|
+
# (posture >= 1); never matches RFC1918 or cloud metadata (not loopback).
|
|
104
|
+
_loopback_ok = False
|
|
105
|
+
try:
|
|
106
|
+
from tools.shell.loopback_allow import is_loopback_allowed
|
|
107
|
+
_loopback_ok = is_loopback_allowed(current)
|
|
108
|
+
except Exception:
|
|
109
|
+
_loopback_ok = False
|
|
110
|
+
if validate and _loopback_ok:
|
|
111
|
+
pinned_ip = "127.0.0.1"
|
|
112
|
+
elif validate:
|
|
113
|
+
# validate_and_resolve does a BLOCKING socket.getaddrinfo; offload it to a
|
|
114
|
+
# thread so a slow/sinkhole DNS can't freeze the whole event loop (every
|
|
115
|
+
# other session/request on this worker) for the resolver timeout.
|
|
116
|
+
ok, err, pinned_ip = await asyncio.get_running_loop().run_in_executor(
|
|
117
|
+
None, validator.validate_and_resolve, current)
|
|
118
|
+
if not ok:
|
|
119
|
+
raise WebFetchError(f"blocked URL ({current}): {err}")
|
|
120
|
+
hostname = urlparse(current).hostname
|
|
121
|
+
|
|
122
|
+
if session_factory is not None:
|
|
123
|
+
session_cm = session_factory(pinned_ip)
|
|
124
|
+
else:
|
|
125
|
+
session_cm = _default_session_factory(pinned_ip, hostname)
|
|
126
|
+
|
|
127
|
+
async with session_cm as session:
|
|
128
|
+
timeout = aiohttp.ClientTimeout(total=timeout_sec)
|
|
129
|
+
async with session.get(current, allow_redirects=False, timeout=timeout, headers=_DEFAULT_HEADERS) as resp:
|
|
130
|
+
if resp.status in _REDIRECT_STATUS:
|
|
131
|
+
location = resp.headers.get("Location")
|
|
132
|
+
if not location:
|
|
133
|
+
raise WebFetchError(f"redirect with no Location ({current})")
|
|
134
|
+
current = urljoin(current, location)
|
|
135
|
+
continue
|
|
136
|
+
ctype = resp.headers.get("Content-Type", "application/octet-stream")
|
|
137
|
+
buf = bytearray()
|
|
138
|
+
async for chunk in resp.content.iter_chunked(8192):
|
|
139
|
+
buf.extend(chunk)
|
|
140
|
+
if len(buf) > max_bytes:
|
|
141
|
+
raise WebFetchError(f"response exceeds {max_bytes} bytes ({current})")
|
|
142
|
+
return FetchResult(final_url=current, status=resp.status,
|
|
143
|
+
content_type=ctype, body=bytes(buf))
|
|
144
|
+
raise WebFetchError(f"too many redirects (>{max_redirects})")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Pure HTML->markdown rendering + content-type classification for web_fetch."""
|
|
2
|
+
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
from markdownify import markdownify
|
|
5
|
+
|
|
6
|
+
SPA_SHELL_SIGNAL = (
|
|
7
|
+
"[web_fetch: this page returned almost no readable text — it is likely a "
|
|
8
|
+
"JavaScript-rendered single-page app. Use the 'browser' tool (tool_ids=['browser']) "
|
|
9
|
+
"if you need its rendered content or interaction.]"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
_BOILERPLATE_TAGS = ("script", "style", "nav", "header", "footer", "aside", "noscript")
|
|
13
|
+
_MIN_TEXT_CHARS = 200
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def classify_content(content_type: str, body: bytes) -> str:
|
|
17
|
+
"""Return ``"html"``, ``"pdf"``, or ``"binary"`` (content-type first, magic-byte fallback)."""
|
|
18
|
+
ct = (content_type or "").lower()
|
|
19
|
+
if "html" in ct or "xhtml" in ct:
|
|
20
|
+
return "html"
|
|
21
|
+
if "pdf" in ct or body[:5] == b"%PDF-":
|
|
22
|
+
return "pdf"
|
|
23
|
+
if "text/plain" in ct:
|
|
24
|
+
return "html" # treat plain text as renderable passthrough
|
|
25
|
+
return "binary"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def render_html_to_markdown(html: str, max_chars: int = 40_000) -> str:
|
|
29
|
+
"""Strip boilerplate, convert to markdown, head-truncate with a marker.
|
|
30
|
+
|
|
31
|
+
Returns ``SPA_SHELL_SIGNAL`` when the page has almost no readable text (likely a
|
|
32
|
+
client-rendered SPA), so the agent's escalation cue is explicit.
|
|
33
|
+
"""
|
|
34
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
35
|
+
for tag in soup(_BOILERPLATE_TAGS):
|
|
36
|
+
tag.decompose()
|
|
37
|
+
if len(soup.get_text(strip=True)) < _MIN_TEXT_CHARS:
|
|
38
|
+
return SPA_SHELL_SIGNAL
|
|
39
|
+
root = soup.body or soup
|
|
40
|
+
md = markdownify(str(root)).strip()
|
|
41
|
+
if len(md) > max_chars:
|
|
42
|
+
dropped = len(md) - max_chars
|
|
43
|
+
md = md[:max_chars] + f"\n\n[... {dropped} chars truncated ...]"
|
|
44
|
+
return md
|
tools/web_fetch/tool.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""WebFetchTool: stateless web page reader (URL -> markdown). No browser/Chromium."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from core.config import BotConfig
|
|
7
|
+
from tools.base_tool import BaseTool
|
|
8
|
+
from tools.controller.views import WebFetchAction
|
|
9
|
+
from tools.web_fetch.fetcher import safe_fetch, WebFetchError
|
|
10
|
+
from tools.web_fetch.render import render_html_to_markdown, classify_content
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _allow_private_urls() -> bool:
|
|
14
|
+
return os.getenv("WEB_FETCH_ALLOW_PRIVATE_URLS", "false").strip().lower() in ("1", "true", "yes", "on")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WebFetchTool(BaseTool):
|
|
18
|
+
"""Fetch a URL and return clean markdown. The lightweight default web reader."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, name: str, config: BotConfig, container: Optional[Any] = None):
|
|
21
|
+
super().__init__(name=name, config=config, container=container)
|
|
22
|
+
self._enabled = True
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def required_config(self) -> Dict[str, str]:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
@BaseTool.action(
|
|
29
|
+
description=(
|
|
30
|
+
"Fetch a single web page by URL and return its main content as markdown. "
|
|
31
|
+
"Use this to READ a page you have the URL for (articles, docs, API pages). "
|
|
32
|
+
"Lightweight and fast — no browser. For SEARCH use perplexity/anysite; for "
|
|
33
|
+
"pages needing login/clicks/forms use the browser tool."
|
|
34
|
+
),
|
|
35
|
+
param_model=WebFetchAction,
|
|
36
|
+
)
|
|
37
|
+
async def fetch_url(self, params: WebFetchAction) -> str:
|
|
38
|
+
# No services/state to set up — fetch is self-contained.
|
|
39
|
+
url = (params.url or "").strip()
|
|
40
|
+
if not url:
|
|
41
|
+
return "Please provide a url to fetch."
|
|
42
|
+
try:
|
|
43
|
+
result = await safe_fetch(url, validate=not _allow_private_urls())
|
|
44
|
+
except WebFetchError as e:
|
|
45
|
+
return f"Could not fetch the page (blocked or unreachable): {e}"
|
|
46
|
+
except Exception as e: # network/timeout — fail soft, agent can escalate to browser
|
|
47
|
+
return f"Could not fetch the page: {e}"
|
|
48
|
+
|
|
49
|
+
kind = classify_content(result.content_type, result.body)
|
|
50
|
+
if kind == "pdf":
|
|
51
|
+
return (f"[web_fetch: {result.final_url} is a PDF, not an HTML page. "
|
|
52
|
+
f"Save it and use the filesystem document-processing tools to extract text.]")
|
|
53
|
+
if kind == "binary":
|
|
54
|
+
return (f"[web_fetch: {result.final_url} returned non-HTML content "
|
|
55
|
+
f"({result.content_type}); cannot render as markdown.]")
|
|
56
|
+
try:
|
|
57
|
+
html = result.body.decode("utf-8", errors="replace")
|
|
58
|
+
except Exception:
|
|
59
|
+
html = result.body.decode("latin-1", errors="replace")
|
|
60
|
+
return render_html_to_markdown(html, max_chars=params.max_chars)
|
tools/x402/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Agent x402 paying tool package (gated; default OFF)."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def x402_client_enabled() -> bool:
|
|
6
|
+
return os.getenv("X402_CLIENT_ENABLED", "false").lower() == "true"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_x402_tool(force: bool = False) -> bool:
|
|
10
|
+
"""Register the 'x402_pay' descriptor + class IFF X402_CLIENT_ENABLED (or forced).
|
|
11
|
+
|
|
12
|
+
Mirrors tools/code_exec.register_code_exec_tool. Descriptor inserted first
|
|
13
|
+
(register_tool_class is a silent no-op for unknown names). Never in default tool_ids.
|
|
14
|
+
"""
|
|
15
|
+
if not (force or x402_client_enabled()):
|
|
16
|
+
return False
|
|
17
|
+
from tools.descriptors import (
|
|
18
|
+
TOOL_DESCRIPTORS, ToolDescriptor, ToolCategory, register_tool_class,
|
|
19
|
+
)
|
|
20
|
+
from tools.x402.service import X402PayTool
|
|
21
|
+
|
|
22
|
+
if "x402_pay" not in TOOL_DESCRIPTORS:
|
|
23
|
+
TOOL_DESCRIPTORS["x402_pay"] = ToolDescriptor(
|
|
24
|
+
name="x402_pay",
|
|
25
|
+
description="Pay for paid resources via the x402 protocol (agent personal wallet)",
|
|
26
|
+
category=ToolCategory.INTEGRATION,
|
|
27
|
+
is_optional=True,
|
|
28
|
+
init_priority=80,
|
|
29
|
+
)
|
|
30
|
+
register_tool_class("x402_pay", X402PayTool)
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def x402_invoicing_enabled() -> bool:
|
|
35
|
+
from core.env import bool_env
|
|
36
|
+
return bool_env("X402_INVOICE_ENABLED", False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register_x402_invoice_tool(force: bool = False) -> bool:
|
|
40
|
+
"""Register 'x402_invoice' (x402_request/x402_invoices/accounting) IFF
|
|
41
|
+
X402_INVOICE_ENABLED (agent money loop). Same shape as register_x402_tool;
|
|
42
|
+
never in default tool_ids. Distinct flag from X402_CLIENT_ENABLED — invoicing
|
|
43
|
+
(receivables) needs a treasury address, not an agent wallet."""
|
|
44
|
+
if not (force or x402_invoicing_enabled()):
|
|
45
|
+
return False
|
|
46
|
+
from tools.descriptors import (
|
|
47
|
+
TOOL_DESCRIPTORS, ToolDescriptor, ToolCategory, register_tool_class,
|
|
48
|
+
)
|
|
49
|
+
from tools.x402.invoice_tool import X402InvoiceTool
|
|
50
|
+
|
|
51
|
+
if "x402_invoice" not in TOOL_DESCRIPTORS:
|
|
52
|
+
TOOL_DESCRIPTORS["x402_invoice"] = ToolDescriptor(
|
|
53
|
+
name="x402_invoice",
|
|
54
|
+
description="Create/track x402 payment requests (invoices) + the unified accounting ledger",
|
|
55
|
+
category=ToolCategory.INTEGRATION,
|
|
56
|
+
is_optional=True,
|
|
57
|
+
init_priority=80,
|
|
58
|
+
)
|
|
59
|
+
register_tool_class("x402_invoice", X402InvoiceTool)
|
|
60
|
+
return True
|
tools/x402/client.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""x402 client boundary. The tool depends on X402PaymentClient (an interface),
|
|
2
|
+
so the official x402 SDK (RealX402Client, Task 8) is swappable and tests use a
|
|
3
|
+
deterministic FakeX402Client. The signer is the agent wallet's x402 signer —
|
|
4
|
+
the raw key never leaves it.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional, Protocol
|
|
10
|
+
|
|
11
|
+
from core.wallet.signer import Signer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class X402Result:
|
|
16
|
+
body: str
|
|
17
|
+
paid: bool
|
|
18
|
+
amount_usd: float
|
|
19
|
+
tx_hash: Optional[str]
|
|
20
|
+
pay_to: Optional[str]
|
|
21
|
+
status_code: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class X402PaymentClient(Protocol):
|
|
25
|
+
async def quote(self, url: str) -> Optional[float]: ...
|
|
26
|
+
async def fetch_with_payment(
|
|
27
|
+
self, *, url: str, method: str, body: Optional[str], signer: Signer,
|
|
28
|
+
network: str, facilitator_url: str, max_amount_usd: float,
|
|
29
|
+
) -> X402Result: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FakeX402Client:
|
|
33
|
+
"""Deterministic test double."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, price_usd: Optional[float], pay_to: Optional[str], paid_body: str):
|
|
36
|
+
self._price = price_usd
|
|
37
|
+
self._pay_to = pay_to
|
|
38
|
+
self._body = paid_body
|
|
39
|
+
|
|
40
|
+
async def quote(self, url: str) -> Optional[float]:
|
|
41
|
+
return self._price
|
|
42
|
+
|
|
43
|
+
async def fetch_with_payment(self, *, url, method, body, signer, network, facilitator_url, max_amount_usd) -> X402Result:
|
|
44
|
+
if self._price is None:
|
|
45
|
+
return X402Result(body=self._body, paid=False, amount_usd=0.0, tx_hash=None, pay_to=None, status_code=200)
|
|
46
|
+
return X402Result(body=self._body, paid=True, amount_usd=self._price,
|
|
47
|
+
tx_hash="0xfake", pay_to=self._pay_to, status_code=200)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""X402InvoiceTool — the agent invoices and accounts for itself.
|
|
2
|
+
|
|
3
|
+
Three actions, all riding existing seams:
|
|
4
|
+
|
|
5
|
+
- ``x402_request`` — create a payment request (invoice) from inside a session:
|
|
6
|
+
amount (ceiling-bounded), purpose, optional payer hint, expiry. Outward-facing
|
|
7
|
+
money: default-OFF flag, per-day cap, first-class ``payment_requested`` event,
|
|
8
|
+
and listed in the recommended approval set (operators add ``x402_request`` to
|
|
9
|
+
``APPROVAL_REQUIRED_TOOLS``; at compute posture ≥2 it is auto-gated).
|
|
10
|
+
- ``x402_invoices`` — list this tenant's invoices (read-only).
|
|
11
|
+
- ``accounting`` — the unified ledger view (earned / spent / pending, evidence
|
|
12
|
+
from usage_records + wallet_spend events + x402 receipts). Read-only.
|
|
13
|
+
|
|
14
|
+
Server-tier stores (``modules.x402``/``modules.credits``) are imported lazily at
|
|
15
|
+
action time so the core agent import graph stays server-free (C3 boundary).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations # safe: @BaseTool.action uses explicit param_model
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from tools.base_tool import BaseTool
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InvoiceParams(BaseModel):
|
|
30
|
+
amount_usd: float = Field(..., gt=0, description="Invoice amount in USD (bounded by X402_INVOICE_MAX_USD)")
|
|
31
|
+
purpose: str = Field(..., min_length=3, description="What the payer is paying for (shown to them and audited)")
|
|
32
|
+
payer_hint: Optional[str] = Field(None, description="Optional payer identity/contact hint for your own records")
|
|
33
|
+
expiry_hours: float = Field(72.0, gt=0, le=24 * 30, description="Hours until the request expires (default 72)")
|
|
34
|
+
payer_surface: Optional[str] = Field(
|
|
35
|
+
None, description="If invoicing a correspondent you already contacted, their surface "
|
|
36
|
+
"(e.g. 'email'/'telegram'); settlement is delivered to you as DATA, not a command")
|
|
37
|
+
payer_address: Optional[str] = Field(
|
|
38
|
+
None, description="The correspondent payer's address/handle on that surface (pairs with payer_surface)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InvoiceListParams(BaseModel):
|
|
42
|
+
status: Optional[str] = Field(None, description="Filter: pending|completed|expired (default all)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AccountingParams(BaseModel):
|
|
46
|
+
days: int = Field(7, ge=1, le=365, description="Trailing window in days (default 7)")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ctx_ids(execution_context) -> tuple:
|
|
50
|
+
user_id = getattr(execution_context, "user_id", "") or ""
|
|
51
|
+
session_id = getattr(execution_context, "session_id", "") or ""
|
|
52
|
+
return user_id, session_id
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_ANON_REFUSED = ("this action requires an authenticated tenant — the execution "
|
|
56
|
+
"context has no user_id (anonymous financial state is refused)")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class X402InvoiceTool(BaseTool):
|
|
60
|
+
def __init__(self, name: str = "x402_invoice", config=None, container=None):
|
|
61
|
+
import types
|
|
62
|
+
super().__init__(name=name,
|
|
63
|
+
config=config if config is not None else types.SimpleNamespace(),
|
|
64
|
+
container=container)
|
|
65
|
+
|
|
66
|
+
def _ar(self, *, content: str = None, error: str = None):
|
|
67
|
+
from tools.controller.types import ActionResult
|
|
68
|
+
if error is not None:
|
|
69
|
+
return ActionResult(error=error)
|
|
70
|
+
return ActionResult(extracted_content=content)
|
|
71
|
+
|
|
72
|
+
@BaseTool.action(
|
|
73
|
+
"Create an x402 payment request (invoice): amount, purpose, expiry. "
|
|
74
|
+
"Returns payment instructions the payer needs (recipient/chain/amount/id).",
|
|
75
|
+
param_model=InvoiceParams)
|
|
76
|
+
async def x402_request(self, params: InvoiceParams, execution_context=None):
|
|
77
|
+
user_id, session_id = _ctx_ids(execution_context)
|
|
78
|
+
if not user_id:
|
|
79
|
+
return self._ar(error=f"x402_request refused: {_ANON_REFUSED}")
|
|
80
|
+
try:
|
|
81
|
+
from modules.x402.invoicing import create_payment_request
|
|
82
|
+
correspondent_ref = None
|
|
83
|
+
if params.payer_surface and params.payer_address:
|
|
84
|
+
correspondent_ref = {"surface": params.payer_surface,
|
|
85
|
+
"address": params.payer_address, "thread_id": ""}
|
|
86
|
+
inv = await create_payment_request(
|
|
87
|
+
user_id=user_id, session_id=session_id,
|
|
88
|
+
amount_usd=params.amount_usd, purpose=params.purpose,
|
|
89
|
+
payer_hint=params.payer_hint, expiry_hours=params.expiry_hours,
|
|
90
|
+
correspondent_ref=correspondent_ref,
|
|
91
|
+
)
|
|
92
|
+
except ValueError as e:
|
|
93
|
+
return self._ar(error=f"x402_request refused: {e}")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error("x402_request failed: %s", e, exc_info=True)
|
|
96
|
+
return self._ar(error=f"x402_request failed: {e}")
|
|
97
|
+
return self._ar(content=(
|
|
98
|
+
f"Payment request created.\n"
|
|
99
|
+
f" request_id: {inv['request_id']}\n"
|
|
100
|
+
f" amount: ${inv['amount_usd']:.2f} {inv['asset'].upper()} on {inv['chain']}\n"
|
|
101
|
+
f" pay to: {inv['recipient']}\n"
|
|
102
|
+
f" purpose: {inv['purpose']}\n"
|
|
103
|
+
f" expires: epoch {inv['expires_at_epoch']}\n"
|
|
104
|
+
"Share these instructions with the payer (e.g. via the message tool). "
|
|
105
|
+
"You will be woken in this session when it settles."
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
@BaseTool.action("List your x402 payment requests (invoices) and their status",
|
|
109
|
+
param_model=InvoiceListParams)
|
|
110
|
+
async def x402_invoices(self, params: InvoiceListParams, execution_context=None):
|
|
111
|
+
user_id, _ = _ctx_ids(execution_context)
|
|
112
|
+
if not user_id:
|
|
113
|
+
return self._ar(error=f"x402_invoices refused: {_ANON_REFUSED}")
|
|
114
|
+
try:
|
|
115
|
+
from modules.x402.invoicing import list_payment_requests
|
|
116
|
+
rows = await list_payment_requests(user_id=user_id, status=params.status)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return self._ar(error=f"x402_invoices failed: {e}")
|
|
119
|
+
if not rows:
|
|
120
|
+
return self._ar(content="No payment requests found.")
|
|
121
|
+
lines = ["Payment requests (newest first):"]
|
|
122
|
+
for r in rows:
|
|
123
|
+
lines.append(
|
|
124
|
+
f" {r['request_id']}: ${float(r['amount_usd'] or 0):.2f} "
|
|
125
|
+
f"[{r['status']}] — {r.get('purpose') or '(no purpose)'} "
|
|
126
|
+
f"(created {r.get('created_at')})")
|
|
127
|
+
return self._ar(content="\n".join(lines))
|
|
128
|
+
|
|
129
|
+
@BaseTool.action(
|
|
130
|
+
"Your unified financial ledger: earned / pending / spent (LLM + wallet) "
|
|
131
|
+
"over a trailing window. Read-only, evidence-backed.",
|
|
132
|
+
param_model=AccountingParams)
|
|
133
|
+
async def accounting(self, params: AccountingParams, execution_context=None):
|
|
134
|
+
user_id, _ = _ctx_ids(execution_context)
|
|
135
|
+
if not user_id:
|
|
136
|
+
return self._ar(error=f"accounting refused: {_ANON_REFUSED}")
|
|
137
|
+
try:
|
|
138
|
+
from modules.credits.unified_ledger import build_ledger, format_ledger
|
|
139
|
+
ledger = await build_ledger(user_id, days=params.days)
|
|
140
|
+
return self._ar(content=format_ledger(ledger))
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error("accounting failed: %s", e, exc_info=True)
|
|
143
|
+
return self._ar(error=f"accounting failed: {e}")
|