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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- 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()
|