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
@@ -0,0 +1,550 @@
1
+ """``hyperclaw onboard --team``: a team member sets up HyperAgents alone.
2
+
3
+ Steps, in order (each can be re-run alone with ``--step <id>``)::
4
+
5
+ item paste the 1Password item (hidden): team profile + personal keys
6
+ name name the agent
7
+ consent the one consent line for the outside model, then a live check
8
+ google sign in to Google in their own browser (read-only Docs, Sheets)
9
+ slack sign in to Slack in their own browser (PKCE; reads, posts only on Send)
10
+ telegram create the bot (named after the agent) and pair it by code
11
+ brief pick the morning brief time (and, optionally, its Doc and Sheet)
12
+ gitlab add GitLab with a read_api token (optional)
13
+ service the background service at login (Telegram + scheduler)
14
+
15
+ It ends with one line per piece: ``✓ Connected: ...`` or the plain reason
16
+ and the fix. Progress is saved after every step in
17
+ ``$HYPERCLAW_ROOT/config/onboard-team.json`` (no secrets), so quitting
18
+ halfway is safe: running it again carries on where they left off.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import datetime as _dt
23
+ import json
24
+ import os
25
+ import re
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+ from typing import Callable, Optional
29
+
30
+ STEPS = ("item", "name", "consent", "google", "slack", "telegram", "brief", "gitlab",
31
+ "service")
32
+ TITLES = {
33
+ "item": "Your 1Password item", "name": "Name your agent", "consent": "Your model",
34
+ "google": "Google sign-in", "slack": "Slack sign-in", "telegram": "Telegram",
35
+ "brief": "Morning brief", "gitlab": "GitLab (optional)", "service": "Background service",
36
+ }
37
+ DONE, FAILED, SKIPPED = "done", "failed", "skipped"
38
+ STOPPED = ("Stopped. Everything finished so far is saved. Run 'hyperclaw onboard --team' "
39
+ "to carry on from where you left off.")
40
+
41
+
42
+ # ── state (non-secret) ────────────────────────────────────────────────────
43
+
44
+ def state_path() -> Path:
45
+ from hyperclaw.userconfig import root
46
+ return root() / "config" / "onboard-team.json"
47
+
48
+
49
+ def load_state() -> dict:
50
+ try:
51
+ d = json.loads(state_path().read_text(encoding="utf-8"))
52
+ if isinstance(d, dict) and isinstance(d.get("steps"), dict):
53
+ return d
54
+ except Exception:
55
+ pass
56
+ return {"steps": {}}
57
+
58
+
59
+ def mark(step: str, status: str, code: str = "") -> None:
60
+ d = load_state()
61
+ d["steps"][step] = {"status": status, "code": code,
62
+ "at": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds")}
63
+ p = state_path()
64
+ p.parent.mkdir(parents=True, exist_ok=True)
65
+ tmp = p.with_suffix(".tmp")
66
+ fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
67
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
68
+ json.dump(d, f, indent=1)
69
+ os.replace(tmp, p)
70
+
71
+
72
+ def status_of(step: str) -> str:
73
+ return (load_state()["steps"].get(step) or {}).get("status", "")
74
+
75
+
76
+ def next_steps() -> list:
77
+ """Steps still to do, in order (failed ones are tried again)."""
78
+ st = load_state()["steps"]
79
+ return [s for s in STEPS if (st.get(s) or {}).get("status") not in (DONE, SKIPPED)]
80
+
81
+
82
+ # ── terminal I/O (tests pass a fake) ──────────────────────────────────────
83
+
84
+ @dataclass
85
+ class IO:
86
+ say: Callable[[str], None]
87
+ ask: Callable[..., str] # ask(label, default="") -> str
88
+ hidden: Callable[[str], str] # hidden(label) -> str (never echoed)
89
+ confirm: Callable[..., bool] # confirm(label, default=False) -> bool
90
+ open_browser: Optional[Callable[[str], object]] = None
91
+
92
+
93
+ def terminal_io() -> IO:
94
+ import webbrowser
95
+
96
+ from rich.console import Console
97
+ from rich.prompt import Confirm, Prompt
98
+
99
+ from hyperclaw.teamsetup.tty import read_hidden
100
+ con = Console()
101
+ return IO(
102
+ say=lambda t: con.print(t, markup=False, highlight=False),
103
+ ask=lambda label, default="": Prompt.ask(label, default=default,
104
+ show_default=bool(default)),
105
+ hidden=lambda label: read_hidden(label + ": "),
106
+ confirm=lambda label, default=False: Confirm.ask(label, default=default),
107
+ open_browser=webbrowser.open,
108
+ )
109
+
110
+
111
+ @dataclass
112
+ class StepResult:
113
+ status: str
114
+ line: str = ""
115
+ code: str = ""
116
+
117
+
118
+ # ── the steps ─────────────────────────────────────────────────────────────
119
+
120
+ def _profile() -> dict:
121
+ from hyperclaw.teamsetup import profile
122
+ return profile.load()
123
+
124
+
125
+ def _need_profile() -> Optional[StepResult]:
126
+ if not _profile():
127
+ return StepResult(FAILED, "✗ This step needs your team item first. Fix: hyperclaw "
128
+ "onboard --team --step item", "no_item")
129
+ return None
130
+
131
+
132
+ def step_item(io: IO) -> StepResult:
133
+ from hyperclaw.teamsetup import profile, secrets
134
+ from hyperclaw.teamsetup.item import ItemError, parse
135
+ from hyperclaw.userconfig import update_config
136
+ io.say("Open 1Password, find the HyperAgents item Vaughn shared with you, and copy its "
137
+ "text. Paste it here and press Enter. Nothing will show on screen as you paste; "
138
+ "that is on purpose.")
139
+ item = None
140
+ for _ in range(3):
141
+ raw = io.hidden("Paste your 1Password item (hidden)")
142
+ try:
143
+ item = parse(raw)
144
+ break
145
+ except ItemError as e:
146
+ io.say(e.text)
147
+ finally:
148
+ raw = ""
149
+ if item is None:
150
+ return StepResult(FAILED, "✗ Team item: not saved. Fix: copy the item from 1Password "
151
+ "again, then hyperclaw onboard --team --step item", "item")
152
+ profile.save(item.envelope)
153
+ where = secrets.put("model_key", item.model_key)
154
+ if item.hyper_key:
155
+ secrets.put("hyper_key", item.hyper_key, env="HYPERSPEED_API_KEY")
156
+ if item.google_client_secret:
157
+ secrets.put("google_client_secret", item.google_client_secret)
158
+ if item.profile["google"]["client_id"]:
159
+ write_google_client(item.profile["google"]["client_id"], item.google_client_secret)
160
+ update_config(deployment="hyper" if item.profile["install_mode"] == "hyper" else "standard",
161
+ team_profile=item.profile["name"])
162
+ if secrets.env_map()["env"].get("ANTHROPIC_API_KEY") == "model_key":
163
+ secrets.link_env("model_key", "ANTHROPIC_API_KEY") # re-paste: keep it current
164
+ del item
165
+ return StepResult(DONE, f"✓ Team item saved. Your keys are kept in {where}.")
166
+
167
+
168
+ def step_name(io: IO) -> StepResult:
169
+ from hyperclaw import naming
170
+ current = naming.current_name()
171
+ if current and io.confirm(f"Your agent is called {current}. Keep that name?", default=True):
172
+ return StepResult(DONE, f"✓ Your agent is called {current}.")
173
+ name = naming.run_interactive(lambda label: io.ask(label, ""), io.say, model_call=None)
174
+ if not name:
175
+ return StepResult(FAILED, "✗ No name chosen. Fix: hyperclaw onboard --team --step "
176
+ "name", "no_name")
177
+ return StepResult(DONE, f"✓ Your agent is called {name}.")
178
+
179
+
180
+ def step_consent(io: IO, *, http=None) -> StepResult:
181
+ from hyperclaw.model_policy import consent_line, has_consent, record_consent
182
+ from hyperclaw.teamsetup import checks, secrets
183
+ need = _need_profile()
184
+ if need:
185
+ return need
186
+ provider, _, model = _profile()["default_model"].partition("/")
187
+ label = checks.model_label(provider, model)
188
+ if provider == "hyperspeed":
189
+ return StepResult(DONE, f"✓ Your model is {label} (Hyper's own; no consent needed).")
190
+ io.say(f"Your model: {label}.")
191
+ io.say(consent_line(provider))
192
+ if not io.confirm("Continue?", default=False):
193
+ secrets.unlink_env("ANTHROPIC_API_KEY")
194
+ return StepResult(FAILED, f"✗ {label}: not switched on, because you didn't agree. "
195
+ "Fix: when you're ready, hyperclaw onboard --team --step "
196
+ "consent", "declined")
197
+ if not has_consent(provider):
198
+ record_consent(provider, model)
199
+ if not secrets.link_env("model_key", "ANTHROPIC_API_KEY"):
200
+ return StepResult(FAILED, f"✗ {label}: no model key is stored. Fix: hyperclaw onboard "
201
+ "--team --step item", "no_key")
202
+ try:
203
+ from hyperclaw.providers import reset_registry
204
+ reset_registry()
205
+ except Exception:
206
+ pass
207
+ c = checks.check_model(provider, model, http=http)
208
+ return StepResult(DONE if c.ok else FAILED, c.line, c.code)
209
+
210
+
211
+ def write_google_client(client_id: str, client_secret: str) -> Path:
212
+ """The install's Desktop client file for the Google sign-in
213
+ (hyperclaw.google_workspace.auth.client_path), owner-only."""
214
+ from hyperclaw.google_workspace import auth as ga
215
+ p = ga.client_path()
216
+ p.parent.mkdir(parents=True, exist_ok=True)
217
+ try:
218
+ os.chmod(p.parent, 0o700)
219
+ except OSError:
220
+ pass
221
+ body = {"installed": {"client_id": client_id, "client_secret": client_secret,
222
+ "auth_uri": ga.AUTH_URI, "token_uri": ga.TOKEN_URI,
223
+ "redirect_uris": ["http://127.0.0.1"]}}
224
+ tmp = p.with_suffix(".tmp")
225
+ fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
226
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
227
+ json.dump(body, f)
228
+ os.replace(tmp, p)
229
+ return p
230
+
231
+
232
+ def step_google(io: IO, *, http=None, connect=None) -> StepResult:
233
+ """`hyperclaw google connect` (hyperclaw.google_workspace): one sign-in in
234
+ their own browser, loopback PKCE, the full team scopes (Gmail read,
235
+ compose and send; read-only Calendar, Meet, Drive, Docs, Sheets, Slides;
236
+ Calendar write only when the profile allows it); token in the OS secret
237
+ store."""
238
+ from hyperclaw.google_workspace import auth as ga
239
+ from hyperclaw.google_workspace.cli import _whoami
240
+ from hyperclaw.google_workspace.errors import GoogleError
241
+ from hyperclaw.teamsetup import checks, profile
242
+ need = _need_profile()
243
+ if need:
244
+ return need
245
+ client = profile.google_client()
246
+ if client:
247
+ write_google_client(client["client_id"], client["client_secret"])
248
+ elif not ga.client_path().is_file():
249
+ return StepResult(FAILED, "✗ Google: your team item has no Google sign-in details. "
250
+ "Fix: ask Vaughn for an updated item, then hyperclaw onboard "
251
+ "--team --step item", "no_client")
252
+ io.say("Next, Google. Your browser will open: sign in with your work Google account and "
253
+ "choose Allow. Your agent can then read your email and draft or send mail you "
254
+ "approve, and read your Calendar, Meet notes, Drive, Docs, Sheets and Slides.")
255
+ try:
256
+ (connect or ga.connect)(show=io.say, open_browser=io.open_browser, http=http,
257
+ whoami=_whoami)
258
+ except GoogleError as e:
259
+ return StepResult(FAILED, f"✗ Google: {e.text.split('. ')[0].rstrip('.')}. Nothing was "
260
+ "saved. Fix: hyperclaw onboard --team --step google",
261
+ e.reason.value)
262
+ c = checks.check_google(http=http)
263
+ return StepResult(DONE if c.ok else FAILED, c.line, c.code)
264
+
265
+
266
+ def step_slack(io: IO, *, http=None, sign_in=None) -> StepResult:
267
+ from hyperclaw.teamsetup import checks, slack_oauth
268
+ need = _need_profile()
269
+ if need:
270
+ return need
271
+ cfg = _profile().get("slack") or {}
272
+ if not cfg.get("client_id"):
273
+ return StepResult(SKIPPED, "– Slack: not part of your team profile.", "no_app")
274
+ io.say("Next, Slack.")
275
+ try:
276
+ (sign_in or slack_oauth.sign_in)(cfg, show=io.say, open_browser=io.open_browser,
277
+ http=http)
278
+ except slack_oauth.SlackAuthError as e:
279
+ saved = "" if "nothing was saved" in e.text else " Nothing was saved."
280
+ return StepResult(FAILED, f"✗ Slack: {e.text}.{saved} Fix: {e.fix}", e.code)
281
+ c = checks.check_slack(cfg, http=http)
282
+ return StepResult(DONE if c.ok else FAILED, c.line, c.code)
283
+
284
+
285
+ def bot_username_hint(agent: str) -> str:
286
+ base = re.sub(r"[^a-z0-9]+", "_", agent.lower()).strip("_") or "my"
287
+ return f"{base[:20]}_hyperagents_bot"
288
+
289
+
290
+ def step_telegram(io: IO, *, client=None, pair=None, paused=None) -> StepResult:
291
+ from hyperclaw import naming
292
+ from hyperclaw import telegram_pairing as tp
293
+ from hyperclaw.teamsetup import checks, secrets, service
294
+ need = _need_profile()
295
+ if need:
296
+ return need
297
+ if not _profile().get("telegram"):
298
+ return StepResult(SKIPPED, "– Telegram: not part of your team profile.", "off")
299
+ agent = naming.current_name() or "HyperAgents"
300
+ io.say("Next, Telegram: your agent gets its own bot, and only your chat can use it.")
301
+ io.say("1. In Telegram, open @BotFather and send /newbot.")
302
+ io.say(f"2. When it asks for a name, send: {agent}")
303
+ io.say(f"3. When it asks for a username, send one ending in 'bot', for example "
304
+ f"{bot_username_hint(agent)}")
305
+ io.say("4. BotFather replies with a token. Copy it and paste it here.")
306
+ token = ""
307
+ for _ in range(3):
308
+ token = "".join(io.hidden("Bot token (hidden)").split())
309
+ if re.fullmatch(tp._TOKEN_SHAPE, token):
310
+ break
311
+ io.say("That doesn't look like a Telegram bot token, so it wasn't saved. Copy the "
312
+ "whole token from BotFather's message and paste it again.")
313
+ token = ""
314
+ if not token:
315
+ return StepResult(FAILED, "✗ Telegram: no bot token saved. Fix: hyperclaw onboard "
316
+ "--team --step telegram", "token")
317
+ secrets.put("telegram_bot_token", token, env="TELEGRAM_BOT_TOKEN")
318
+ with (paused or service.paused)():
319
+ chat = (pair or tp.run_owner_pairing)(token, io.say, client=client)
320
+ if chat is None:
321
+ return StepResult(FAILED, "✗ Telegram: not paired. Fix: hyperclaw onboard --team "
322
+ "--step telegram", "unpaired")
323
+ try:
324
+ tp._call(client or tp._client(), token, "setMyName", {"name": agent[:64]}, timeout=15)
325
+ except Exception:
326
+ io.say(f"Telegram didn't take the display name {agent} just now; the bot works anyway, "
327
+ "and you can try again later with /name.")
328
+ c = checks.check_telegram(client=client)
329
+ return StepResult(DONE if c.ok else FAILED, c.line, c.code)
330
+
331
+
332
+ def local_timezone() -> str:
333
+ tz = os.environ.get("TZ", "").strip().lstrip(":")
334
+ if tz and "/" in tz:
335
+ return tz
336
+ try:
337
+ target = os.path.realpath("/etc/localtime")
338
+ if "zoneinfo/" in target:
339
+ return target.split("zoneinfo/", 1)[1]
340
+ except OSError:
341
+ pass
342
+ return ""
343
+
344
+
345
+ def _brief_ready_line(when: str, tz: str) -> StepResult:
346
+ from hyperclaw.brief import compose, store
347
+ from hyperclaw.brief.errors import BriefError
348
+ from hyperclaw.digest.errors import DigestError
349
+ try:
350
+ from hyperclaw.providers import reset_registry
351
+ reset_registry()
352
+ except Exception:
353
+ pass
354
+ try:
355
+ compose.v2_route()
356
+ except (BriefError, DigestError):
357
+ return StepResult(FAILED, f"✗ Morning brief: set for {when} {tz}, but it is written only "
358
+ "by HyperSpeed V2 and this install has no Hyper access key. "
359
+ "Fix: ask Vaughn for an item that includes one, then "
360
+ "hyperclaw onboard --team --step item", "no_v2")
361
+ from hyperclaw.brief import google_auth
362
+ if store.source_id("doc") and not google_auth.connected():
363
+ return StepResult(FAILED, f"✗ Morning brief: set for {when} {tz}, but it can't read its "
364
+ "Doc yet (no Google sign-in for the brief). Fix: hyperclaw "
365
+ "onboard --team --step brief", "brief_google")
366
+ if not (store.source_id("doc") and store.source_id("sheet")):
367
+ return StepResult(FAILED, f"✗ Morning brief: set for {when} {tz}, but its Google Doc "
368
+ "and Sheet aren't set. Fix: hyperclaw onboard --team --step "
369
+ "brief", "no_sources")
370
+ return StepResult(DONE, f"✓ Morning brief: every day at {when} {tz}")
371
+
372
+
373
+ def step_brief(io: IO, *, brief_connect=None, http=None) -> StepResult:
374
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
375
+
376
+ from hyperclaw.brief import links, store
377
+ when = None
378
+ for _ in range(3):
379
+ raw = io.ask("What time should your morning brief arrive? (24-hour, e.g. 07:30)",
380
+ str(store.config().get("time") or store.DEFAULT_TIME))
381
+ when = store.parse_hhmm(raw)
382
+ if when:
383
+ break
384
+ io.say("Use the 24-hour form HH:MM, for example 07:30.")
385
+ if not when:
386
+ return StepResult(FAILED, "✗ Morning brief: no time set. Fix: hyperclaw onboard --team "
387
+ "--step brief", "time")
388
+ tz = ""
389
+ for _ in range(3):
390
+ tz = io.ask("Your time zone", str(store.config().get("tz") or local_timezone()
391
+ or "America/Chicago")).strip()
392
+ try:
393
+ ZoneInfo(tz)
394
+ break
395
+ except (ZoneInfoNotFoundError, ValueError):
396
+ io.say("That isn't a time zone name. Use one like America/Chicago or Europe/London.")
397
+ tz = ""
398
+ if not tz:
399
+ return StepResult(FAILED, "✗ Morning brief: no time zone set. Fix: hyperclaw onboard "
400
+ "--team --step brief", "tz")
401
+ hm = f"{when[0]:02d}:{when[1]:02d}"
402
+ store.set_config(scheduled=True, time=hm, tz=tz,
403
+ scheduled_at=_dt.datetime.now(ZoneInfo(tz)).isoformat(timespec="seconds"))
404
+ have = bool(store.source_id("doc") and store.source_id("sheet"))
405
+ if io.confirm("Set the Google Doc and Sheet your brief reads now? (you can do it later)",
406
+ default=not have):
407
+ for kind, want, label in (("doc", links.DOC, "Doc"), ("sheet", links.SHEET, "Sheet")):
408
+ raw = io.hidden(f"Link to your {label} (hidden; Enter to keep the current one)")
409
+ if not raw.strip():
410
+ continue
411
+ got, fid = links.parse(raw.strip())
412
+ raw = ""
413
+ if got != want or not fid:
414
+ io.say(f"That isn't a Google {label} link, so the {label} wasn't changed.")
415
+ continue
416
+ store.set_source(kind, fid, "")
417
+ io.say(f"{label}: set.")
418
+ from hyperclaw.brief import google_auth
419
+ from hyperclaw.brief.errors import BriefError
420
+ if store.source_id("doc") and not google_auth.connected():
421
+ # The brief reads its Doc and Sheet with its own read-only sign-in.
422
+ io.say("Your brief reads that Doc and Sheet with its own read-only Google sign-in. Your "
423
+ "browser will open once more: choose the same account and Allow.")
424
+ try:
425
+ (brief_connect or google_auth.connect)(show=io.say, open_browser=io.open_browser,
426
+ http=http)
427
+ except BriefError as e:
428
+ return StepResult(FAILED, f"✗ Morning brief: set for {hm} {tz}, but {e.text}. Fix: "
429
+ "hyperclaw onboard --team --step brief", e.reason.value)
430
+ return _brief_ready_line(hm, tz)
431
+
432
+
433
+ def step_gitlab(io: IO, *, client=None) -> StepResult:
434
+ from hyperclaw.digest.errors import DigestError
435
+ from hyperclaw.digest.service import connect
436
+ from hyperclaw.teamsetup import checks
437
+ if not io.confirm("Add GitLab, so your agent can tell you about your merge requests and "
438
+ "to-dos? (optional, read-only)", default=False):
439
+ return StepResult(SKIPPED, "– GitLab: not added (optional). Add it any time: hyperclaw "
440
+ "onboard --team --step gitlab", "declined")
441
+ url = (_profile().get("gitlab") or {}).get("url") or "https://gitlab.com"
442
+ io.say(f"In GitLab ({url}): click your avatar, then Edit profile, then Access tokens, then "
443
+ "Add new token. Tick only read_api, pick an expiry date, create it and copy it.")
444
+ token = io.hidden("GitLab token (hidden)")
445
+ try:
446
+ connect("gitlab", token, gitlab_url=url, client=client)
447
+ except DigestError as e:
448
+ return StepResult(FAILED, f"✗ GitLab: {e.text}. Nothing was saved. Fix: "
449
+ + (e.fix or "hyperclaw onboard --team --step gitlab"),
450
+ e.reason.value)
451
+ finally:
452
+ token = ""
453
+ c = checks.check_gitlab(client=client)
454
+ return StepResult(DONE if c.ok else FAILED, c.line, c.code)
455
+
456
+
457
+ def step_service(io: IO, *, run=None) -> StepResult:
458
+ from hyperclaw.teamsetup import service
459
+ io.say("Last, the background service: it starts when you log in and keeps your agent "
460
+ "on Telegram and your brief on time, even after a restart or sleep.")
461
+ r = service.install(**({"run": run} if run else {}))
462
+ return StepResult(DONE if r.ok else FAILED, r.line, "" if r.ok else "service")
463
+
464
+
465
+ RUNNERS = {"item": step_item, "name": step_name, "consent": step_consent,
466
+ "google": step_google, "slack": step_slack, "telegram": step_telegram,
467
+ "brief": step_brief, "gitlab": step_gitlab, "service": step_service}
468
+
469
+
470
+ # ── summary ───────────────────────────────────────────────────────────────
471
+
472
+ def summary_lines(*, http=None, client=None, run=None) -> list:
473
+ """One line per piece, from live checks (and the saved state for skipped
474
+ optional pieces)."""
475
+ from hyperclaw.teamsetup import checks, service
476
+ steps = load_state()["steps"]
477
+ lines = []
478
+ prof = _profile()
479
+ if not prof:
480
+ return ["✗ Team item: not set up. Fix: hyperclaw onboard --team --step item"]
481
+ provider, _, model = prof["default_model"].partition("/")
482
+ if (steps.get("consent") or {}).get("code") == "declined":
483
+ lines.append(f"✗ {checks.model_label(provider, model)}: not switched on, because you "
484
+ "didn't agree. Fix: hyperclaw onboard --team --step consent")
485
+ else:
486
+ lines.append(checks.check_model(provider, model, http=http).line)
487
+ lines.append(checks.check_google(http=http).line)
488
+ if (prof.get("slack") or {}).get("client_id"):
489
+ lines.append(checks.check_slack(prof["slack"], http=http).line)
490
+ if prof.get("telegram"):
491
+ lines.append(checks.check_telegram(client=client).line)
492
+ from hyperclaw.brief import store
493
+ if store.opted_in_by_config():
494
+ lines.append(_brief_ready_line(str(store.config().get("time")), store.tz_name()).line)
495
+ else:
496
+ lines.append("✗ Morning brief: no time set. Fix: hyperclaw onboard --team --step brief")
497
+ if (steps.get("gitlab") or {}).get("status") == SKIPPED:
498
+ lines.append("– GitLab: not added (optional)")
499
+ else:
500
+ lines.append(checks.check_gitlab(client=client).line)
501
+ lines.append(service.check(**({"run": run} if run else {})).line)
502
+ return lines
503
+
504
+
505
+ # ── runner ────────────────────────────────────────────────────────────────
506
+
507
+ def run(io: Optional[IO] = None, *, only: Optional[str] = None, deps: Optional[dict] = None,
508
+ summary: bool = True) -> int:
509
+ """Run the remaining steps (or one, with ``only``). Returns an exit code:
510
+ 0 when every line is ✓ (or an optional piece was skipped)."""
511
+ io = io or terminal_io()
512
+ deps = deps or {}
513
+ todo = [only] if only else next_steps()
514
+ if only and only not in RUNNERS:
515
+ io.say(f"Unknown step '{only}'. Steps: {', '.join(STEPS)}")
516
+ return 2
517
+ if not only:
518
+ done = [s for s in STEPS if s not in todo]
519
+ if done and todo:
520
+ io.say(f"Picking up where you left off: {TITLES[todo[0]]}.")
521
+ try:
522
+ for step in todo:
523
+ io.say("")
524
+ io.say(f"── {TITLES[step]} ──")
525
+ res = RUNNERS[step](io, **deps.get(step, {}))
526
+ mark(step, res.status, res.code)
527
+ if res.line:
528
+ io.say(res.line)
529
+ except (KeyboardInterrupt, EOFError):
530
+ io.say("")
531
+ io.say(STOPPED)
532
+ return 130
533
+ if only and status_of("service") == DONE and only != "service":
534
+ from hyperclaw.teamsetup import service
535
+ service.install(**({"run": deps["service"]["run"]} if deps.get("service") else {}))
536
+ if not summary:
537
+ return 0
538
+ io.say("")
539
+ io.say("Where everything stands:")
540
+ lines = summary_lines(**deps.get("summary", {}))
541
+ for line in lines:
542
+ io.say(" " + line)
543
+ bad = [x for x in lines if x.startswith("✗")]
544
+ io.say("")
545
+ if bad:
546
+ io.say("Follow each Fix above. If you're stuck, run 'hyperclaw self-check' and send "
547
+ "Vaughn the report it writes (it has no keys, tokens, emails or messages in it).")
548
+ else:
549
+ io.say("All set. Say hello to your agent on Telegram.")
550
+ return 1 if bad else 0
@@ -0,0 +1,82 @@
1
+ """The team profile on this install: the Hyper-signed policy from the team
2
+ item (hyperclaw.teamsetup.item), kept as issued in
3
+ ``$HYPERCLAW_ROOT/config/team-profile.json`` (0600).
4
+
5
+ It is verified again every time it is read, so editing the file only makes
6
+ it invalid (then the strictest defaults apply). ``hyperclaw.managed_config``
7
+ reads it as the install's managed settings when no system managed file is
8
+ present (docs/MODEL-POLICY.md, "Team install profile").
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ from pathlib import Path
16
+
17
+ log = logging.getLogger("hyperclaw.teamsetup.profile")
18
+
19
+ _cache: dict = {}
20
+
21
+
22
+ def path() -> Path:
23
+ from hyperclaw.userconfig import root
24
+ return root() / "config" / "team-profile.json"
25
+
26
+
27
+ def save(envelope: dict) -> None:
28
+ p = path()
29
+ p.parent.mkdir(parents=True, exist_ok=True)
30
+ tmp = p.with_suffix(".tmp")
31
+ fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
32
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
33
+ json.dump(envelope, f, indent=1, sort_keys=True)
34
+ os.replace(tmp, p)
35
+ _cache.clear()
36
+
37
+
38
+ def load() -> dict:
39
+ """The verified profile, or {} (no file, or it doesn't verify)."""
40
+ from hyperclaw.teamsetup.item import ItemError, verify_profile
41
+ p = path()
42
+ try:
43
+ st = p.stat()
44
+ except OSError:
45
+ return {}
46
+ key = (str(p), st.st_mtime_ns, st.st_size)
47
+ if _cache.get("key") == key:
48
+ return dict(_cache["profile"])
49
+ try:
50
+ prof = verify_profile(json.loads(p.read_text(encoding="utf-8")))
51
+ except (ItemError, ValueError, OSError) as e:
52
+ log.warning("team profile %s ignored (%s); defaults apply", p,
53
+ getattr(e, "code", type(e).__name__))
54
+ prof = {}
55
+ _cache.update(key=key, profile=prof)
56
+ return dict(prof)
57
+
58
+
59
+ def managed_view() -> dict:
60
+ """The profile's managed-config fields (same shape as the managed config file)."""
61
+ prof = load()
62
+ if not prof:
63
+ return {}
64
+ return {"allowed_models": list(prof["allowed_models"]),
65
+ "default_model": prof["default_model"],
66
+ "hyper_issued_keys": list(prof["hyper_issued_keys"]),
67
+ "model_names": dict(prof["model_names"]),
68
+ "telegram": prof["telegram"] is True,
69
+ "restricted_clients": [dict(c) for c in prof["restricted_clients"]],
70
+ "restricted_clients_required": prof["restricted_clients_required"] is True,
71
+ "google_calendar_write": prof["google_calendar_write"] is True}
72
+
73
+
74
+ def google_client() -> dict:
75
+ """{"client_id", "client_secret"} for the team Google client, or {}."""
76
+ prof = load()
77
+ cid = (prof.get("google") or {}).get("client_id") or ""
78
+ if not cid:
79
+ return {}
80
+ from hyperclaw.teamsetup import secrets
81
+ sec = secrets.get("google_client_secret")
82
+ return {"client_id": cid, "client_secret": sec} if sec else {}