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