hyperagents 1.2.3rc5.post3__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 (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
hyperclaw/server.py ADDED
@@ -0,0 +1,847 @@
1
+ """
2
+ HyperClaw Production Server
3
+ Fully integrated FastAPI server with all components connected.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import time
9
+ import asyncio
10
+ import logging
11
+ from contextlib import asynccontextmanager
12
+ from pathlib import Path
13
+ from typing import Optional
14
+ from datetime import datetime
15
+
16
+ from dotenv import load_dotenv
17
+ from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
18
+ from fastapi.staticfiles import StaticFiles
19
+ from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from pydantic import BaseModel
22
+
23
+ # Configure logging
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
27
+ )
28
+ logger = logging.getLogger("hyperclaw.server")
29
+
30
+ # Load environment
31
+ HYPERCLAW_ROOT = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw"))
32
+ SECRETS_PATH = HYPERCLAW_ROOT / "workspace" / "secrets"
33
+
34
+ # Try to load from secrets directory first
35
+ env_path = SECRETS_PATH / ".env"
36
+ if env_path.exists():
37
+ load_dotenv(env_path)
38
+ else:
39
+ load_dotenv()
40
+ from hyperclaw.teamsetup.secrets import export_env as _export_secrets # noqa: E402
41
+ _export_secrets() # team installs keep keys in the OS secret store
42
+
43
+ # Add project to path
44
+ sys.path.insert(0, str(Path(__file__).parent.parent))
45
+
46
+ from hyperclaw.orchestrator import get_orchestrator, Orchestrator
47
+ from hyperclaw.agent_coordinator import get_coordinator, Task, TaskStatus
48
+ from hyperclaw.model_router import get_model_router, ModelTier
49
+ from hyperclaw.setup import run_setup, HYPERCLAW_ROOT
50
+
51
+ # ============================================================================
52
+ # GLOBALS
53
+ # ============================================================================
54
+
55
+ _orchestrator: Optional[Orchestrator] = None
56
+ _db_pool = None
57
+ START_TIME = time.time()
58
+
59
+
60
+ # ============================================================================
61
+ # DATABASE CONNECTION
62
+ # ============================================================================
63
+
64
+ async def create_db_pool():
65
+ """Create database connection pool."""
66
+ db_url = os.environ.get("DATABASE_URL")
67
+ if not db_url:
68
+ logger.warning("DATABASE_URL not set - running without database")
69
+ return None
70
+
71
+ try:
72
+ import asyncpg
73
+ pool = await asyncpg.create_pool(
74
+ db_url,
75
+ min_size=1,
76
+ max_size=10,
77
+ command_timeout=30
78
+ )
79
+ logger.info("Database connection pool created")
80
+ return pool
81
+ except ImportError:
82
+ logger.warning("asyncpg not installed - pip install asyncpg")
83
+ return None
84
+ except Exception as e:
85
+ logger.error(f"Database connection failed: {e}")
86
+ return None
87
+
88
+
89
+ # ============================================================================
90
+ # LIFESPAN MANAGEMENT
91
+ # ============================================================================
92
+
93
+ @asynccontextmanager
94
+ async def lifespan(app: FastAPI):
95
+ """Application startup and shutdown."""
96
+ global _orchestrator, _db_pool
97
+
98
+ logger.info("=" * 60)
99
+ logger.info(" HyperClaw Server Starting")
100
+ logger.info("=" * 60)
101
+
102
+ # Create database pool
103
+ _db_pool = await create_db_pool()
104
+
105
+ # Initialize orchestrator
106
+ _orchestrator = await get_orchestrator(_db_pool)
107
+
108
+ logger.info("HyperClaw Server ONLINE")
109
+ logger.info(f" - Model: {os.environ.get('HYPERCLAW_MODEL', 'claude-sonnet-4-6')}")
110
+ logger.info(f" - Database: {'Connected' if _db_pool else 'Not configured'}")
111
+ logger.info(f" - Workspace: {HYPERCLAW_ROOT}")
112
+
113
+ yield
114
+
115
+ # Shutdown
116
+ logger.info("Shutting down HyperClaw Server...")
117
+ if _orchestrator:
118
+ await _orchestrator.shutdown()
119
+ if _db_pool:
120
+ await _db_pool.close()
121
+ logger.info("Server shutdown complete")
122
+
123
+
124
+ # ============================================================================
125
+ # FASTAPI APP
126
+ # ============================================================================
127
+
128
+ app = FastAPI(
129
+ title="HyperClaw",
130
+ description="AI Assistant Platform with Multi-Agent Orchestration",
131
+ version="1.0.0",
132
+ docs_url="/docs",
133
+ redoc_url="/redoc",
134
+ lifespan=lifespan,
135
+ )
136
+
137
+ # ============================================================================
138
+ # EXPOSURE + AUTH (fail closed)
139
+ # ============================================================================
140
+ # - Binds 127.0.0.1 unless HOST says otherwise (warning when not loopback).
141
+ # - No cross-origin access unless HYPERCLAW_CORS_ORIGINS lists origins.
142
+ # - Endpoints that act on the outside world (send a Telegram message, the
143
+ # Telegram webhook) are OFF unless their secret is configured, and then
144
+ # require it on every request.
145
+
146
+ LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"}
147
+
148
+
149
+ def bind_host() -> str:
150
+ """The address to bind: HOST if set, else loopback only."""
151
+ return (os.environ.get("HOST") or "").strip() or "127.0.0.1"
152
+
153
+
154
+ def is_loopback(host: str) -> bool:
155
+ return host in LOOPBACK_HOSTS or host.startswith("127.")
156
+
157
+
158
+ def cors_origins() -> list:
159
+ """Explicit allow-list from HYPERCLAW_CORS_ORIGINS (comma separated);
160
+ empty by default. A bare "*" is ignored: never allow every origin."""
161
+ raw = os.environ.get("HYPERCLAW_CORS_ORIGINS", "")
162
+ return [o.strip() for o in raw.split(",") if o.strip() and o.strip() != "*"]
163
+
164
+
165
+ def _secret_ok(provided: Optional[str], env_name: str) -> bool:
166
+ """Constant-time compare against a configured secret. Unset secret ->
167
+ always False (the endpoint is disabled)."""
168
+ import hmac
169
+ expected = os.environ.get(env_name, "").strip()
170
+ if not expected or not provided:
171
+ return False
172
+ return hmac.compare_digest(provided.strip().encode(), expected.encode())
173
+
174
+
175
+ def _bearer(request: Request) -> Optional[str]:
176
+ auth = request.headers.get("authorization", "")
177
+ return auth[7:] if auth.lower().startswith("bearer ") else None
178
+
179
+
180
+ _CORS = cors_origins()
181
+ if _CORS:
182
+ app.add_middleware(
183
+ CORSMiddleware,
184
+ allow_origins=_CORS,
185
+ allow_credentials=True,
186
+ allow_methods=["*"],
187
+ allow_headers=["*"],
188
+ )
189
+
190
+ # Static files
191
+ ROOT_DIR = Path(__file__).parent.parent
192
+ DASHBOARD_DIR = ROOT_DIR / "ui" / "control_center"
193
+ if DASHBOARD_DIR.exists():
194
+ app.mount("/static", StaticFiles(directory=str(DASHBOARD_DIR)), name="static")
195
+
196
+
197
+ # ============================================================================
198
+ # REQUEST MODELS
199
+ # ============================================================================
200
+
201
+ class ChatRequest(BaseModel):
202
+ message: str
203
+ session_id: Optional[str] = "default"
204
+ stream: Optional[bool] = False
205
+
206
+
207
+ class ChatResponse(BaseModel):
208
+ response: str
209
+ session_id: str
210
+ timestamp: str
211
+
212
+
213
+ class MemoryRequest(BaseModel):
214
+ content: str
215
+ memory_type: Optional[str] = "episode"
216
+ domain: Optional[str] = None
217
+ importance: Optional[float] = 0.5
218
+ is_core: Optional[bool] = False
219
+
220
+
221
+ class RecallRequest(BaseModel):
222
+ query: str
223
+ limit: Optional[int] = 5
224
+ memory_type: Optional[str] = None
225
+
226
+
227
+ # ============================================================================
228
+ # CORE ROUTES
229
+ # ============================================================================
230
+
231
+ @app.get("/")
232
+ async def root():
233
+ """Serve dashboard or API info."""
234
+ dashboard_path = DASHBOARD_DIR / "index.html"
235
+ if dashboard_path.exists():
236
+ return FileResponse(dashboard_path)
237
+ return JSONResponse({
238
+ "name": "HyperClaw",
239
+ "version": "1.0.0",
240
+ "status": "operational",
241
+ "docs": "/docs",
242
+ })
243
+
244
+
245
+ @app.get("/health")
246
+ async def health():
247
+ """Health check endpoint."""
248
+ uptime = time.time() - START_TIME
249
+
250
+ health_data = {
251
+ "status": "healthy",
252
+ "uptime_seconds": round(uptime, 1),
253
+ "timestamp": datetime.now().isoformat(),
254
+ "components": {
255
+ "api": True,
256
+ "orchestrator": _orchestrator is not None and _orchestrator._initialized,
257
+ "database": _db_pool is not None,
258
+ "memory": _orchestrator._memory is not None if _orchestrator else False,
259
+ }
260
+ }
261
+
262
+ # Check if any critical component is down
263
+ if not health_data["components"]["orchestrator"]:
264
+ health_data["status"] = "degraded"
265
+
266
+ return health_data
267
+
268
+
269
+ @app.get("/status")
270
+ async def status():
271
+ """Detailed system status."""
272
+ if not _orchestrator:
273
+ return {"error": "Orchestrator not initialized"}
274
+ return _orchestrator.get_status()
275
+
276
+
277
+ # ============================================================================
278
+ # CHAT ENDPOINTS
279
+ # ============================================================================
280
+
281
+ @app.post("/chat", response_model=ChatResponse)
282
+ async def chat(request: ChatRequest):
283
+ """Main chat endpoint."""
284
+ if not _orchestrator:
285
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
286
+
287
+ response = await _orchestrator.chat(
288
+ message=request.message,
289
+ session_id=request.session_id,
290
+ channel="api",
291
+ stream=False
292
+ )
293
+
294
+ return ChatResponse(
295
+ response=response,
296
+ session_id=request.session_id,
297
+ timestamp=datetime.now().isoformat()
298
+ )
299
+
300
+
301
+ @app.post("/chat/stream")
302
+ async def chat_stream(request: ChatRequest):
303
+ """Streaming chat endpoint."""
304
+ if not _orchestrator:
305
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
306
+
307
+ async def generate():
308
+ async for chunk in await _orchestrator.chat(
309
+ message=request.message,
310
+ session_id=request.session_id,
311
+ channel="api",
312
+ stream=True
313
+ ):
314
+ yield f"data: {chunk}\n\n"
315
+ yield "data: [DONE]\n\n"
316
+
317
+ return StreamingResponse(
318
+ generate(),
319
+ media_type="text/event-stream"
320
+ )
321
+
322
+
323
+ @app.websocket("/ws/chat")
324
+ async def websocket_chat(websocket: WebSocket):
325
+ """WebSocket chat endpoint for real-time streaming."""
326
+ await websocket.accept()
327
+ session_id = f"ws_{int(time.time())}"
328
+
329
+ try:
330
+ while True:
331
+ data = await websocket.receive_text()
332
+ message = data
333
+
334
+ if not _orchestrator:
335
+ await websocket.send_text("[Error: Orchestrator not initialized]")
336
+ continue
337
+
338
+ # Stream response
339
+ async for chunk in await _orchestrator.chat(
340
+ message=message,
341
+ session_id=session_id,
342
+ channel="websocket",
343
+ stream=True
344
+ ):
345
+ await websocket.send_text(chunk)
346
+
347
+ # Send end marker
348
+ await websocket.send_text("\n[END]")
349
+
350
+ except WebSocketDisconnect:
351
+ logger.info(f"WebSocket disconnected: {session_id}")
352
+ except Exception as e:
353
+ logger.error(f"WebSocket error: {e}")
354
+ await websocket.close()
355
+
356
+
357
+ @app.post("/reset")
358
+ async def reset_session(session_id: str = "default"):
359
+ """Reset a conversation session."""
360
+ if _orchestrator and _orchestrator._memory:
361
+ _orchestrator._memory._conversation_history.pop(session_id, None)
362
+ return {"status": "reset", "session_id": session_id}
363
+
364
+
365
+ # ============================================================================
366
+ # MEMORY ENDPOINTS
367
+ # ============================================================================
368
+
369
+ @app.post("/api/memory/remember")
370
+ async def remember(request: MemoryRequest):
371
+ """Store a memory explicitly."""
372
+ if not _orchestrator:
373
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
374
+
375
+ from hyperclaw.taint import TaintedInput
376
+ try:
377
+ memory_id = await _orchestrator.remember(
378
+ content=request.content,
379
+ memory_type=request.memory_type,
380
+ domain=request.domain,
381
+ importance=request.importance,
382
+ is_core=request.is_core
383
+ )
384
+ except TaintedInput as e: # connector content: refused, and the reason is given
385
+ raise HTTPException(status_code=403, detail=e.user_message)
386
+
387
+ return {"status": "stored", "memory_id": memory_id}
388
+
389
+
390
+ @app.post("/api/memory/recall")
391
+ async def recall(request: RecallRequest):
392
+ """Recall relevant memories."""
393
+ if not _orchestrator:
394
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
395
+
396
+ memories = await _orchestrator.recall(
397
+ query=request.query,
398
+ limit=request.limit,
399
+ memory_type=request.memory_type
400
+ )
401
+
402
+ return {
403
+ "query": request.query,
404
+ "count": len(memories),
405
+ "memories": [
406
+ {
407
+ "id": m.id,
408
+ "content": m.content,
409
+ "type": m.memory_type,
410
+ "domain": m.domain,
411
+ "importance": m.importance,
412
+ }
413
+ for m in memories
414
+ ]
415
+ }
416
+
417
+
418
+ @app.get("/api/memory/context")
419
+ async def get_context():
420
+ """Get current system context."""
421
+ if not _orchestrator or not _orchestrator._memory:
422
+ raise HTTPException(status_code=503, detail="Memory not initialized")
423
+
424
+ return {
425
+ "system_context": _orchestrator._memory.get_system_context(),
426
+ "working_memory": _orchestrator._memory.get_working_memory(),
427
+ }
428
+
429
+
430
+ # ============================================================================
431
+ # AGENT ENDPOINTS
432
+ # ============================================================================
433
+
434
+ @app.get("/api/agents")
435
+ async def list_agents():
436
+ """List all available agents."""
437
+ # Try to load from agent config
438
+ try:
439
+ import yaml
440
+ from hyperclaw.api_utils import find_config
441
+ agents_config = find_config("agents.yaml")
442
+ if agents_config.exists():
443
+ with open(agents_config) as f:
444
+ config = yaml.safe_load(f)
445
+ agents = config.get("agents", [])
446
+ return {
447
+ "count": len(agents),
448
+ "agents": [
449
+ {
450
+ "id": a.get("id", "unknown"),
451
+ "name": a.get("name", a.get("id", "Unknown")),
452
+ "domain": a.get("domain", "general"),
453
+ "role": a.get("role", ""),
454
+ }
455
+ for a in agents
456
+ ]
457
+ }
458
+ except Exception as e:
459
+ logger.warning(f"Failed to load agents config: {e}")
460
+
461
+ return {
462
+ "count": 0,
463
+ "agents": [],
464
+ "message": "Agent configuration not loaded"
465
+ }
466
+
467
+
468
+ @app.post("/api/agents/{agent_id}/dispatch")
469
+ async def dispatch_to_agent(agent_id: str, task: str):
470
+ """Dispatch a task to a specific agent."""
471
+ if not _orchestrator:
472
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
473
+
474
+ result = await _orchestrator.dispatch_task(goal=task, agent_id=agent_id)
475
+ return {
476
+ "task_id": result.id,
477
+ "agent_id": result.assigned_to,
478
+ "status": result.status.value,
479
+ }
480
+
481
+
482
+ # ============================================================================
483
+ # TASK ENDPOINTS
484
+ # ============================================================================
485
+
486
+ class TaskRequest(BaseModel):
487
+ goal: str
488
+ domain: Optional[str] = None
489
+ task_type: Optional[str] = None
490
+ agent_id: Optional[str] = None
491
+ priority: Optional[int] = 5
492
+
493
+
494
+ @app.post("/api/tasks")
495
+ async def create_task(request: TaskRequest):
496
+ """Create and queue a new task."""
497
+ if not _orchestrator:
498
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
499
+
500
+ task = await _orchestrator.dispatch_task(
501
+ goal=request.goal,
502
+ domain=request.domain,
503
+ task_type=request.task_type,
504
+ agent_id=request.agent_id,
505
+ priority=request.priority,
506
+ )
507
+
508
+ return {
509
+ "task_id": task.id,
510
+ "goal": task.goal,
511
+ "domain": task.domain,
512
+ "task_type": task.task_type,
513
+ "assigned_to": task.assigned_to,
514
+ "status": task.status.value,
515
+ }
516
+
517
+
518
+ @app.get("/api/tasks/{task_id}")
519
+ async def get_task(task_id: str):
520
+ """Get task status and result."""
521
+ if not _orchestrator or not _orchestrator._coordinator:
522
+ raise HTTPException(status_code=503, detail="Coordinator not initialized")
523
+
524
+ task = _orchestrator._coordinator.tasks.get(task_id)
525
+ if not task:
526
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
527
+
528
+ return {
529
+ "task_id": task.id,
530
+ "goal": task.goal,
531
+ "domain": task.domain,
532
+ "task_type": task.task_type,
533
+ "assigned_to": task.assigned_to,
534
+ "status": task.status.value,
535
+ "result": task.result,
536
+ "error": task.error,
537
+ "created_at": task.created_at.isoformat(),
538
+ "completed_at": task.completed_at.isoformat() if task.completed_at else None,
539
+ }
540
+
541
+
542
+ @app.post("/api/tasks/{task_id}/execute")
543
+ async def execute_task(task_id: str):
544
+ """Execute a specific task immediately."""
545
+ if not _orchestrator:
546
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
547
+
548
+ result = await _orchestrator.execute_task(task_id)
549
+ return {"task_id": task_id, "result": result}
550
+
551
+
552
+ @app.get("/api/tasks")
553
+ async def list_tasks(status: Optional[str] = None, limit: int = 50):
554
+ """List all tasks, optionally filtered by status."""
555
+ if not _orchestrator or not _orchestrator._coordinator:
556
+ raise HTTPException(status_code=503, detail="Coordinator not initialized")
557
+
558
+ tasks = list(_orchestrator._coordinator.tasks.values())
559
+
560
+ if status:
561
+ tasks = [t for t in tasks if t.status.value == status]
562
+
563
+ # Sort by created_at descending
564
+ tasks.sort(key=lambda t: t.created_at, reverse=True)
565
+
566
+ return {
567
+ "count": len(tasks[:limit]),
568
+ "tasks": [
569
+ {
570
+ "task_id": t.id,
571
+ "goal": t.goal[:100],
572
+ "domain": t.domain,
573
+ "assigned_to": t.assigned_to,
574
+ "status": t.status.value,
575
+ "created_at": t.created_at.isoformat(),
576
+ }
577
+ for t in tasks[:limit]
578
+ ]
579
+ }
580
+
581
+
582
+ class CoordinateRequest(BaseModel):
583
+ goal: str
584
+ context: Optional[dict] = None
585
+ max_agents: Optional[int] = 3
586
+
587
+
588
+ @app.post("/api/coordinate")
589
+ async def coordinate_goal(request: CoordinateRequest):
590
+ """Coordinate multiple agents to accomplish a complex goal."""
591
+ if not _orchestrator:
592
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
593
+
594
+ result = await _orchestrator.coordinate_goal(
595
+ goal=request.goal,
596
+ context=request.context,
597
+ )
598
+
599
+ return result
600
+
601
+
602
+ # ============================================================================
603
+ # COST MANAGEMENT ENDPOINTS
604
+ # ============================================================================
605
+
606
+ @app.get("/api/costs")
607
+ async def get_costs():
608
+ """Get current cost statistics."""
609
+ if not _orchestrator:
610
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
611
+
612
+ return _orchestrator.get_cost_stats()
613
+
614
+
615
+ @app.post("/api/costs/budget")
616
+ async def set_budget(budget_usd: float, request: Request):
617
+ """Set the daily cost cap. An operator action: disabled unless
618
+ HYPERCLAW_API_TOKEN is set; then requires "Authorization: Bearer <token>"."""
619
+ if not _secret_ok(_bearer(request), "HYPERCLAW_API_TOKEN"):
620
+ raise HTTPException(status_code=403, detail="Forbidden")
621
+ if not _orchestrator:
622
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
623
+
624
+ _orchestrator.set_daily_budget(budget_usd)
625
+ return {"status": "ok", "daily_budget_usd": budget_usd}
626
+
627
+
628
+ @app.get("/api/models")
629
+ async def list_models():
630
+ """List available models and their costs."""
631
+ router = get_model_router()
632
+
633
+ return {
634
+ "models": [
635
+ {
636
+ "id": m.id,
637
+ "name": m.name,
638
+ "tier": m.tier.value,
639
+ "provider": m.provider,
640
+ "cost_per_1k_input": m.cost_per_1k_input,
641
+ "cost_per_1k_output": m.cost_per_1k_output,
642
+ "max_tokens": m.max_tokens,
643
+ "latency_ms": m.latency_ms,
644
+ "capabilities": m.capabilities,
645
+ }
646
+ for m in router.models.values()
647
+ ],
648
+ "prefer_cheap": router._prefer_cheap,
649
+ "daily_budget_usd": router._daily_budget,
650
+ }
651
+
652
+
653
+ # ============================================================================
654
+ # INTEGRATION ENDPOINTS
655
+ # ============================================================================
656
+
657
+ @app.get("/api/integrations")
658
+ async def list_integrations():
659
+ """List configured integrations."""
660
+ if not _orchestrator:
661
+ return {"integrations": {}}
662
+
663
+ return {"integrations": _orchestrator._integrations}
664
+
665
+
666
+ @app.post("/api/integrations/telegram/send")
667
+ async def send_telegram(chat_id: str, message: str, request: Request):
668
+ """Send a Telegram message. Disabled unless HYPERCLAW_API_TOKEN is set;
669
+ then requires "Authorization: Bearer <token>"."""
670
+ if not _secret_ok(_bearer(request), "HYPERCLAW_API_TOKEN"):
671
+ raise HTTPException(status_code=403, detail="Forbidden")
672
+ if not _orchestrator:
673
+ raise HTTPException(status_code=503, detail="Orchestrator not initialized")
674
+
675
+ success = await _orchestrator.send_telegram(chat_id, message)
676
+ return {"success": success}
677
+
678
+
679
+ # ============================================================================
680
+ # APPROVALS (Send / Edit / Cancel for queued actions)
681
+ # ============================================================================
682
+ # Owner only: disabled unless HYPERCLAW_API_TOKEN is set, then the bearer
683
+ # token is the owner's credential. Send must name the payload hash shown on
684
+ # the card; a changed payload is refused and nothing runs.
685
+
686
+ class ApprovalDecision(BaseModel):
687
+ payload_hash: str = ""
688
+ content: str = ""
689
+
690
+
691
+ def _approval_owner(request: Request):
692
+ if not _secret_ok(_bearer(request), "HYPERCLAW_API_TOKEN"):
693
+ raise HTTPException(status_code=403, detail="Forbidden")
694
+ from hyperclaw.tools.approval import Actor
695
+ return Actor("owner", "api-token", "server")
696
+
697
+
698
+ def _approval_view(item) -> dict:
699
+ from hyperclaw.tools.approval import render
700
+ return {"id": item.id, "tool": item.tool, "status": item.status, "kind": item.kind,
701
+ "reason": item.reason, "requester": item.requester,
702
+ "payload_hash": item.short_hash, "created": item.created, "expires": item.expires,
703
+ "card": render(item)}
704
+
705
+
706
+ @app.get("/api/approvals")
707
+ async def list_approvals(request: Request):
708
+ _approval_owner(request)
709
+ from hyperclaw.tools.approval import get_queue
710
+ return {"pending": [_approval_view(i) for i in get_queue().pending()]}
711
+
712
+
713
+ @app.post("/api/approvals/{action_id}/send")
714
+ async def approval_send(action_id: str, decision: ApprovalDecision, request: Request):
715
+ actor = _approval_owner(request)
716
+ from hyperclaw.tools import approval as ap
717
+ loop = asyncio.get_running_loop()
718
+ env = await loop.run_in_executor(None, lambda: ap.send(action_id, decision.payload_hash, actor))
719
+ return env.to_dict()
720
+
721
+
722
+ @app.post("/api/approvals/{action_id}/edit")
723
+ async def approval_edit(action_id: str, decision: ApprovalDecision, request: Request):
724
+ actor = _approval_owner(request)
725
+ from hyperclaw.tools import approval as ap
726
+ try:
727
+ item = ap.edit(action_id, decision.content, actor)
728
+ except ap.ApprovalError as e:
729
+ raise HTTPException(status_code=409, detail=str(e))
730
+ return _approval_view(item)
731
+
732
+
733
+ @app.post("/api/approvals/{action_id}/cancel")
734
+ async def approval_cancel(action_id: str, request: Request):
735
+ actor = _approval_owner(request)
736
+ from hyperclaw.tools import approval as ap
737
+ return ap.cancel(action_id, actor).to_dict()
738
+
739
+
740
+ # ============================================================================
741
+ # SETUP ENDPOINT
742
+ # ============================================================================
743
+
744
+ @app.post("/api/setup")
745
+ async def setup_hyperclaw(init_db: bool = False):
746
+ """Run HyperClaw setup."""
747
+ result = await run_setup(init_db=init_db)
748
+ return result.to_dict()
749
+
750
+
751
+ # ============================================================================
752
+ # WEBHOOK ENDPOINTS (for integrations)
753
+ # ============================================================================
754
+
755
+ @app.post("/webhook/telegram")
756
+ async def telegram_webhook(request: Request):
757
+ """Handle Telegram webhook updates.
758
+
759
+ Disabled unless HYPERCLAW_TELEGRAM_WEBHOOK_SECRET is set; then every
760
+ request must carry it in X-Telegram-Bot-Api-Secret-Token (the header
761
+ Telegram sends when setWebhook is given secret_token), and only allowed
762
+ chats are answered. The polling bot (hyperclaw-telegram) needs none of
763
+ this and opens no listening socket."""
764
+ if not _secret_ok(request.headers.get("x-telegram-bot-api-secret-token"),
765
+ "HYPERCLAW_TELEGRAM_WEBHOOK_SECRET"):
766
+ raise HTTPException(status_code=403, detail="Forbidden")
767
+ try:
768
+ data = await request.json()
769
+
770
+ # Extract message
771
+ message = data.get("message", {})
772
+ text = message.get("text", "")
773
+ chat_id = message.get("chat", {}).get("id")
774
+
775
+ from hyperclaw.telegram_bot import _allowed_chat_ids
776
+ if chat_id not in _allowed_chat_ids():
777
+ logger.warning("Telegram webhook: update from a chat that is not allowed; ignored")
778
+ return {"ok": True}
779
+
780
+ if text and chat_id and _orchestrator:
781
+ # Process through orchestrator
782
+ # Guest fence: only the configured owner chat gets owner context.
783
+ from hyperclaw.chat_scope import scope_for
784
+ response = await _orchestrator.chat(
785
+ message=text,
786
+ session_id=f"telegram_{chat_id}",
787
+ channel="telegram",
788
+ scope=scope_for("telegram", chat_id),
789
+ )
790
+
791
+ # Send response back
792
+ await _orchestrator.send_telegram(str(chat_id), response)
793
+
794
+ return {"ok": True}
795
+
796
+ except Exception as e:
797
+ # Internal error text stays in the log, never in the response.
798
+ logger.error(f"Telegram webhook error: {type(e).__name__}")
799
+ return {"ok": False}
800
+
801
+
802
+ # ============================================================================
803
+ # UTILITY ENDPOINTS
804
+ # ============================================================================
805
+
806
+ @app.get("/api/config")
807
+ async def get_config():
808
+ """Get non-sensitive configuration."""
809
+ return {
810
+ "hyperclaw_root": str(HYPERCLAW_ROOT),
811
+ "model": os.environ.get("HYPERCLAW_MODEL", "claude-sonnet-4-6"),
812
+ "max_tokens": int(os.environ.get("HYPERCLAW_MAX_TOKENS", 4096)),
813
+ "database_configured": bool(os.environ.get("DATABASE_URL")),
814
+ "integrations_configured": list(_orchestrator._integrations.keys()) if _orchestrator else [],
815
+ }
816
+
817
+
818
+ # ============================================================================
819
+ # MAIN
820
+ # ============================================================================
821
+
822
+ def main():
823
+ """Run the server."""
824
+ import uvicorn
825
+ from hyperclaw.updates import on_startup
826
+ on_startup("server")
827
+
828
+ host = bind_host()
829
+ port = int(os.environ.get("PORT", 8001))
830
+ if not is_loopback(host):
831
+ logger.warning(f"HyperClaw server is bound to {host}, a NON-loopback address: it is "
832
+ f"reachable from other machines. Unset HOST to bind 127.0.0.1 only.")
833
+ if not os.environ.get("HYPERCLAW_API_TOKEN", "").strip():
834
+ logger.warning("HYPERCLAW_API_TOKEN is not set: the chat/memory API has no "
835
+ "authentication. Do not expose it beyond a trusted network.")
836
+
837
+ uvicorn.run(
838
+ "hyperclaw.server:app",
839
+ host=host,
840
+ port=port,
841
+ reload=os.environ.get("RELOAD", "").lower() == "true",
842
+ log_level="info"
843
+ )
844
+
845
+
846
+ if __name__ == "__main__":
847
+ main()