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,391 @@
1
+ """The team 1Password item: a compact, versioned format and its parser.
2
+
3
+ One line of text, pasted at a hidden prompt::
4
+
5
+ HCT1.<payload>.<check>
6
+
7
+ - ``HCT1``: format version 1. A newer version is refused with "upgrade first".
8
+ - ``payload``: base64url (no padding) of the zlib-compressed JSON object::
9
+
10
+ {"model_key": "<personal Claude Console workspace key>",
11
+ "hyper_key": "<Hyper access key, optional>",
12
+ "google_client_secret": "<team Google client secret, optional>",
13
+ "profile": {"profile": {...}, "signature": "<b64 Ed25519>"}}
14
+
15
+ - ``check``: the first 8 hex characters of sha256(payload), so a paste that
16
+ lost characters is caught before anything is read.
17
+
18
+ The **team profile** is the non-secret policy for the install, signed by Hyper
19
+ with the same Ed25519 keys as qualification records (the key ships in
20
+ ``hyperclaw/qualification_keys``). A profile that doesn't verify is refused,
21
+ so nobody can widen their allowed models by editing an item::
22
+
23
+ {"type": "hyperclaw-team-profile", "schema": 1, "name": "hyper-team",
24
+ "install_mode": "hyper",
25
+ "allowed_models": ["anthropic/claude-sonnet-5-5", "anthropic/claude-opus-5-5", "hyperspeed/hospitality"],
26
+ "default_model": "anthropic/claude-sonnet-5-5",
27
+ "model_names": {"anthropic/claude-sonnet-5-5": "Claude Sonnet 5.5", "anthropic/claude-opus-5-5": "Claude Opus 5.5"},
28
+ "hyper_issued_keys": ["anthropic"],
29
+ "telegram": true,
30
+ "google": {"client_id": "<id>.apps.googleusercontent.com"},
31
+ "slack": {"app_id": "A0C8HG64MM3", "client_id": "<id>", "redirect_port": 8766,
32
+ "redirect_path": "/slack/callback"},
33
+ "slack_team_id": "T07GE79LBBJ",
34
+ "gitlab": {"url": "https://gitlab.com"},
35
+ "restricted_clients": [{"name": "...", "aliases": [...], "domains": [...],
36
+ "projects": [...]}],
37
+ "restricted_clients_required": true,
38
+ "google_calendar_write": false,
39
+ "issued_at": "...", "key_id": "hyper-..."}
40
+
41
+ ``slack_team_id`` is the one Slack workspace a sign-in may belong to (Hyper
42
+ Nimbus: ``T07GE79LBBJ``); ``slack_enterprise_id`` is set only for an
43
+ Enterprise Grid org. There is no default: a profile without
44
+ ``slack_team_id`` still verifies, but Slack sign-in is refused on it
45
+ (hyperclaw.teamsetup.slack_oauth).
46
+
47
+ ``restricted_clients`` / ``restricted_clients_required`` feed the
48
+ restricted-client guard (hyperclaw.client_guard); a Hyper-deployed profile
49
+ must set ``restricted_clients_required`` to true. ``google_calendar_write``
50
+ adds Calendar write to the Google sign-in (hyperclaw.google_workspace). The
51
+ Google client is ``google.client_id`` here plus ``google_client_secret`` in the
52
+ item; onboarding writes them as the install's Desktop client file.
53
+
54
+ Errors never contain any part of the pasted text.
55
+
56
+ Maintainers (key from the macOS keychain, never printed)::
57
+
58
+ python -m hyperclaw.teamsetup.item sign-profile profile.json --out team-profile.signed.json
59
+ python -m hyperclaw.teamsetup.item make --profile team-profile.signed.json --out item.txt
60
+ """
61
+ from __future__ import annotations
62
+
63
+ import argparse
64
+ import base64
65
+ import datetime as _dt
66
+ import hashlib
67
+ import json
68
+ import os
69
+ import re
70
+ import sys
71
+ import zlib
72
+ from dataclasses import dataclass, field
73
+ from pathlib import Path
74
+ from typing import Optional
75
+
76
+ PREFIX = "HCT"
77
+ VERSION = 1
78
+ PROFILE_TYPE = "hyperclaw-team-profile"
79
+ PROFILE_SCHEMA = 1
80
+ INSTALL_MODES = ("hyper", "standard")
81
+ DEFAULT_SLACK_PORT = 8766
82
+ DEFAULT_SLACK_PATH = "/slack/callback"
83
+ # Exactly the user scopes of the installed Slack app A0C8HG64MM3 (Wil, 13 Oct):
84
+ # read mentions, DMs and channels, and post on tap (chat:write; every post still
85
+ # waits for the owner's Send in the approval queue). Nothing else is requested
86
+ # or accepted.
87
+ SLACK_USER_SCOPES = ("channels:history", "channels:read", "chat:write", "groups:history",
88
+ "groups:read", "im:history", "im:read", "mpim:history", "mpim:read",
89
+ "search:read", "users:read")
90
+ _SLACK_TEAM = re.compile(r"^T[A-Z0-9]{2,20}$")
91
+ _SLACK_ENTERPRISE = re.compile(r"^E[A-Z0-9]{2,20}$")
92
+ _ENTRY = re.compile(r"^[a-z0-9_]{1,40}/(\*|[A-Za-z0-9._:-][A-Za-z0-9._:/-]{0,127})$")
93
+ _ITEM = re.compile(r"^HCT(\d{1,3})\.([A-Za-z0-9_-]{16,})\.([0-9a-f]{8})$")
94
+ _PRINTABLE = re.compile(r"^[\x21-\x7e]{8,512}$")
95
+
96
+
97
+ class ItemError(Exception):
98
+ """A refused item. ``code`` names the reason; the text is fixed and never
99
+ holds any of the pasted value."""
100
+
101
+ def __init__(self, code: str):
102
+ self.code = code
103
+ super().__init__(TEXT[code])
104
+
105
+ @property
106
+ def text(self) -> str:
107
+ return TEXT[self.code]
108
+
109
+
110
+ TEXT = {
111
+ "empty": "Nothing was pasted.",
112
+ "shape": ("That isn't a HyperAgents team item. In 1Password, copy the whole item text "
113
+ "(it starts with HCT1.) and paste it again."),
114
+ "truncated": ("The pasted item is incomplete or changed (its check didn't match). Copy it "
115
+ "from 1Password again and paste the whole line."),
116
+ "newer": ("This item is for a newer HyperAgents. Run 'hyperclaw upgrade' first, then "
117
+ "onboard again."),
118
+ "payload": "The item's contents couldn't be read. Ask Vaughn for a new 1Password item.",
119
+ "model_key": ("The item has no usable model key. Ask Vaughn for a new 1Password item."),
120
+ "runpod": ("The item holds a key that can't be used here. Nothing was saved. Tell Vaughn."),
121
+ "unsigned": ("The team profile in this item isn't signed by Hyper, so nothing was saved. "
122
+ "Ask Vaughn for a new 1Password item."),
123
+ "profile": ("The team profile in this item is incomplete. Nothing was saved. Ask Vaughn "
124
+ "for a new 1Password item."),
125
+ }
126
+
127
+
128
+ @dataclass
129
+ class TeamItem:
130
+ model_key: str
131
+ profile: dict # verified, non-secret
132
+ envelope: dict # signed profile, as issued
133
+ hyper_key: str = ""
134
+ google_client_secret: str = ""
135
+ extras: dict = field(default_factory=dict)
136
+
137
+ def __repr__(self) -> str: # never show a secret
138
+ return f"TeamItem(profile={self.profile.get('name', '')!r}, secrets=hidden)"
139
+
140
+ __str__ = __repr__
141
+
142
+
143
+ def _b64d(s: str) -> bytes:
144
+ return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
145
+
146
+
147
+ def _b64e(b: bytes) -> str:
148
+ return base64.urlsafe_b64encode(b).decode("ascii").rstrip("=")
149
+
150
+
151
+ def checksum(payload_b64: str) -> str:
152
+ return hashlib.sha256(payload_b64.encode("ascii")).hexdigest()[:8]
153
+
154
+
155
+ def _secret(v) -> str:
156
+ v = v.strip() if isinstance(v, str) else ""
157
+ return v if _PRINTABLE.match(v) else ""
158
+
159
+
160
+ # ── profile ───────────────────────────────────────────────────────────────
161
+
162
+ def validate_profile(p: dict) -> dict:
163
+ """The profile's fields, checked and normalized. Raises ItemError('profile')."""
164
+ if not isinstance(p, dict) or p.get("type") != PROFILE_TYPE \
165
+ or p.get("schema") != PROFILE_SCHEMA:
166
+ raise ItemError("profile")
167
+ mode = p.get("install_mode")
168
+ allowed = p.get("allowed_models")
169
+ if mode not in INSTALL_MODES or not isinstance(allowed, list) or not allowed \
170
+ or not all(isinstance(e, str) and _ENTRY.match(e) for e in allowed):
171
+ raise ItemError("profile")
172
+ default = p.get("default_model") or allowed[0]
173
+ if default not in allowed:
174
+ raise ItemError("profile")
175
+ google = p.get("google") or {}
176
+ slack = p.get("slack") or {}
177
+ if not isinstance(google, dict) or not isinstance(slack, dict):
178
+ raise ItemError("profile")
179
+ scopes = slack.get("user_scopes") or list(SLACK_USER_SCOPES)
180
+ if not isinstance(scopes, list) or \
181
+ sorted({str(s).strip() for s in scopes}) != sorted(SLACK_USER_SCOPES):
182
+ raise ItemError("profile") # exactly the app's scope set, nothing else
183
+ port = slack.get("redirect_port", DEFAULT_SLACK_PORT)
184
+ if not isinstance(port, int) or not 1024 <= port <= 65535:
185
+ raise ItemError("profile")
186
+ path = str(slack.get("redirect_path") or DEFAULT_SLACK_PATH)
187
+ if not path.startswith("/"):
188
+ raise ItemError("profile")
189
+ team_id = p.get("slack_team_id") or ""
190
+ enterprise_id = p.get("slack_enterprise_id") or ""
191
+ if not isinstance(team_id, str) or (team_id and not _SLACK_TEAM.match(team_id)):
192
+ raise ItemError("profile")
193
+ if not isinstance(enterprise_id, str) or \
194
+ (enterprise_id and not _SLACK_ENTERPRISE.match(enterprise_id)):
195
+ raise ItemError("profile")
196
+ restricted = p.get("restricted_clients") or []
197
+ if not isinstance(restricted, list) or not all(_client_entry_ok(c) for c in restricted):
198
+ raise ItemError("profile")
199
+ required = p.get("restricted_clients_required", True)
200
+ if not isinstance(required, bool) or (mode == "hyper" and required is not True):
201
+ raise ItemError("profile") # team and client installs always require the list
202
+ cal_write = p.get("google_calendar_write", False)
203
+ if not isinstance(cal_write, bool):
204
+ raise ItemError("profile")
205
+ names = p.get("model_names") or {}
206
+ out = {
207
+ "type": PROFILE_TYPE, "schema": PROFILE_SCHEMA,
208
+ "name": str(p.get("name") or "hyper-team")[:60],
209
+ "install_mode": mode,
210
+ "allowed_models": list(dict.fromkeys(allowed)),
211
+ "default_model": default,
212
+ "model_names": {k: str(v)[:60] for k, v in names.items()
213
+ if isinstance(k, str) and _ENTRY.match(k) and isinstance(v, str)},
214
+ "hyper_issued_keys": sorted({x for x in (p.get("hyper_issued_keys") or [])
215
+ if isinstance(x, str) and re.match(r"^[a-z0-9_]{1,40}$", x)}),
216
+ "telegram": p.get("telegram") is True,
217
+ "google": {"client_id": str(google.get("client_id") or "")},
218
+ "slack": {"app_id": str(slack.get("app_id") or ""),
219
+ "client_id": str(slack.get("client_id") or ""),
220
+ "redirect_port": port, "redirect_path": path,
221
+ "user_scopes": sorted({str(s).strip() for s in scopes if str(s).strip()}),
222
+ # the only workspace (and Grid org) a Slack sign-in may belong to
223
+ "team_id": team_id, "enterprise_id": enterprise_id},
224
+ "slack_team_id": team_id,
225
+ "slack_enterprise_id": enterprise_id,
226
+ "gitlab": {"url": str((p.get("gitlab") or {}).get("url") or "https://gitlab.com")},
227
+ "restricted_clients": [_client_entry(c) for c in restricted],
228
+ "restricted_clients_required": required,
229
+ "google_calendar_write": cal_write,
230
+ "issued_at": str(p.get("issued_at") or ""),
231
+ "key_id": str(p.get("key_id") or ""),
232
+ }
233
+ if out["gitlab"]["url"] and not out["gitlab"]["url"].startswith("https://"):
234
+ raise ItemError("profile")
235
+ return out
236
+
237
+
238
+ def _client_entry_ok(c) -> bool:
239
+ """A restricted-client entry (hyperclaw.client_guard): a name, plus
240
+ optional lists of aliases, domains and projects (strings)."""
241
+ if not isinstance(c, dict) or not isinstance(c.get("name"), str) or not c["name"].strip():
242
+ return False
243
+ return all(isinstance(c.get(k, []), list) and all(isinstance(x, str) for x in c.get(k, []))
244
+ for k in ("aliases", "domains", "projects"))
245
+
246
+
247
+ def _client_entry(c: dict) -> dict:
248
+ return {"name": c["name"].strip(), **{k: list(c.get(k, [])) for k in
249
+ ("aliases", "domains", "projects") if c.get(k)}}
250
+
251
+
252
+ def verify_profile(envelope) -> dict:
253
+ """The verified, validated profile, or ItemError('unsigned'/'profile')."""
254
+ from cryptography.exceptions import InvalidSignature
255
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
256
+
257
+ from hyperclaw import qualification as q
258
+ if not isinstance(envelope, dict) or not isinstance(envelope.get("profile"), dict):
259
+ raise ItemError("unsigned")
260
+ prof, sig = envelope["profile"], envelope.get("signature")
261
+ if not isinstance(sig, str) or not sig:
262
+ raise ItemError("unsigned")
263
+ raw = q.trusted_keys().get(prof.get("key_id"))
264
+ if raw is None:
265
+ raise ItemError("unsigned")
266
+ try:
267
+ Ed25519PublicKey.from_public_bytes(raw).verify(base64.b64decode(sig, validate=True),
268
+ q.canonical(prof))
269
+ except (InvalidSignature, ValueError, TypeError):
270
+ raise ItemError("unsigned")
271
+ return validate_profile(prof)
272
+
273
+
274
+ def sign_profile(profile: dict, private_raw: bytes) -> dict:
275
+ """Maintainers: the signed envelope for a profile."""
276
+ from hyperclaw import qualification as q
277
+ prof = dict(profile)
278
+ prof.setdefault("type", PROFILE_TYPE)
279
+ prof.setdefault("schema", PROFILE_SCHEMA)
280
+ prof.setdefault("issued_at", _dt.datetime.now(_dt.timezone.utc).replace(microsecond=0)
281
+ .isoformat().replace("+00:00", "Z"))
282
+ prof["key_id"] = q.key_id_for(q._public_raw(private_raw))
283
+ validate_profile(prof)
284
+ sig = q._private_key(private_raw).sign(q.canonical(prof))
285
+ return {"profile": prof, "signature": base64.b64encode(sig).decode("ascii")}
286
+
287
+
288
+ # ── the item ──────────────────────────────────────────────────────────────
289
+
290
+ def encode(payload: dict, version: int = VERSION) -> str:
291
+ raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
292
+ body = _b64e(zlib.compress(raw, 9))
293
+ return f"{PREFIX}{version}.{body}.{checksum(body)}"
294
+
295
+
296
+ def parse(text: str) -> TeamItem:
297
+ """Parse and verify a pasted item. Raises ItemError; never echoes input."""
298
+ raw = "".join((text or "").split()) # 1Password may wrap long lines
299
+ if not raw:
300
+ raise ItemError("empty")
301
+ m = _ITEM.match(raw)
302
+ if not m:
303
+ raise ItemError("shape")
304
+ ver, body, check = int(m.group(1)), m.group(2), m.group(3)
305
+ if ver > VERSION:
306
+ raise ItemError("newer")
307
+ if ver < 1:
308
+ raise ItemError("shape")
309
+ if checksum(body) != check:
310
+ raise ItemError("truncated")
311
+ try:
312
+ d = zlib.decompressobj()
313
+ raw = d.decompress(_b64d(body), 1 << 16) # an item is a few KB at most
314
+ if d.unconsumed_tail:
315
+ raise ValueError("too large")
316
+ data = json.loads(raw.decode("utf-8"))
317
+ except Exception:
318
+ raise ItemError("payload") from None
319
+ if not isinstance(data, dict):
320
+ raise ItemError("payload")
321
+ from hyperclaw.model_policy import check_key
322
+ secrets = {k: data.get(k) for k in ("model_key", "hyper_key", "google_client_secret")}
323
+ if any(isinstance(v, str) and check_key(v) for v in secrets.values()):
324
+ raise ItemError("runpod")
325
+ model_key = _secret(secrets["model_key"])
326
+ if not model_key.startswith("sk-ant-") or len(model_key) < 24:
327
+ raise ItemError("model_key")
328
+ profile = verify_profile(data.get("profile"))
329
+ return TeamItem(model_key=model_key, profile=profile, envelope=data["profile"],
330
+ hyper_key=_secret(secrets["hyper_key"]),
331
+ google_client_secret=_secret(secrets["google_client_secret"]))
332
+
333
+
334
+ # ── maintainer command ────────────────────────────────────────────────────
335
+
336
+ def _write_private(path: Path, text: str) -> None:
337
+ path.parent.mkdir(parents=True, exist_ok=True)
338
+ fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
339
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
340
+ f.write(text)
341
+ os.chmod(path, 0o600)
342
+
343
+
344
+ def main(argv=None, *, private_key_loader=None, ask=None) -> int:
345
+ import getpass
346
+ ap = argparse.ArgumentParser(prog="python -m hyperclaw.teamsetup.item",
347
+ description="Maintainers: sign a team profile and make "
348
+ "per-person 1Password items. Never prints a secret.")
349
+ sub = ap.add_subparsers(dest="cmd", required=True)
350
+ s = sub.add_parser("sign-profile")
351
+ s.add_argument("profile", type=Path)
352
+ s.add_argument("--out", type=Path, required=True)
353
+ mk = sub.add_parser("make")
354
+ mk.add_argument("--profile", type=Path, required=True, help="signed profile (sign-profile)")
355
+ mk.add_argument("--out", type=Path, required=True, help="item file to create (0600)")
356
+ a = ap.parse_args(argv)
357
+ if a.cmd == "sign-profile":
358
+ if private_key_loader is None:
359
+ from hyperclaw.qualification import keychain_private_key as private_key_loader
360
+ try:
361
+ env = sign_profile(json.loads(a.profile.read_text(encoding="utf-8")),
362
+ private_key_loader())
363
+ verify_profile(env)
364
+ except ItemError as e:
365
+ print(f"refused: {e.text}", file=sys.stderr)
366
+ return 2
367
+ _write_private(a.out, json.dumps(env, indent=2, sort_keys=True) + "\n")
368
+ print(f"signed team profile -> {a.out} (key {env['profile']['key_id']})")
369
+ return 0
370
+ ask = ask or (lambda label: getpass.getpass(f"{label} (hidden): "))
371
+ env = json.loads(a.profile.read_text(encoding="utf-8"))
372
+ payload = {"profile": env, "model_key": ask("Personal Claude Console workspace key")}
373
+ for k, label in (("hyper_key", "Hyper access key (Enter for none)"),
374
+ ("google_client_secret", "Team Google client secret (Enter for none)")):
375
+ v = ask(label).strip()
376
+ if v:
377
+ payload[k] = v
378
+ item = encode(payload)
379
+ try:
380
+ parse(item)
381
+ except ItemError as e:
382
+ print(f"refused: {e.text}", file=sys.stderr)
383
+ return 2
384
+ _write_private(a.out, item + "\n")
385
+ print(f"item written to {a.out} (0600). Put its text in the person's 1Password item, "
386
+ f"then delete the file.")
387
+ return 0
388
+
389
+
390
+ if __name__ == "__main__":
391
+ raise SystemExit(main())
@@ -0,0 +1,84 @@
1
+ """A one-shot loopback listener for an OAuth redirect (Slack sign-in).
2
+
3
+ It listens on a fixed local port (the one registered with the app, e.g.
4
+ 8766), on 127.0.0.1 and, when available, ::1, so ``http://localhost:<port>``
5
+ works whichever address the browser picks. Nothing is logged: the query holds
6
+ the authorization code.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import socket
11
+ import threading
12
+ from http.server import BaseHTTPRequestHandler, HTTPServer
13
+ from urllib.parse import parse_qs, urlsplit
14
+
15
+ DONE_PAGE = (b"<!doctype html><meta charset=utf-8><title>HyperAgents</title>"
16
+ b"<p>HyperAgents has your answer. You can close this tab and go back to the "
17
+ b"terminal.</p>")
18
+
19
+
20
+ class PortBusy(Exception):
21
+ pass
22
+
23
+
24
+ class _V6Server(HTTPServer):
25
+ address_family = socket.AF_INET6
26
+
27
+
28
+ class Catcher:
29
+ def __init__(self, port: int, path: str = "/"):
30
+ self.result: dict = {}
31
+ self.event = threading.Event()
32
+ self.path = path
33
+ outer = self
34
+
35
+ class H(BaseHTTPRequestHandler):
36
+ def log_message(self, *a): # never log the query (it holds the code)
37
+ pass
38
+
39
+ def do_GET(self):
40
+ u = urlsplit(self.path)
41
+ q = {k: v[0] for k, v in parse_qs(u.query).items()}
42
+ if u.path == outer.path and ("code" in q or "error" in q) \
43
+ and not outer.event.is_set():
44
+ outer.result = q
45
+ outer.event.set()
46
+ self.send_response(200)
47
+ self.send_header("Content-Type", "text/html; charset=utf-8")
48
+ self.send_header("Content-Length", str(len(DONE_PAGE)))
49
+ self.end_headers()
50
+ self.wfile.write(DONE_PAGE)
51
+
52
+ self.servers = []
53
+ try:
54
+ self.servers.append(HTTPServer(("127.0.0.1", port), H))
55
+ except OSError:
56
+ raise PortBusy(port) from None
57
+ try:
58
+ self.servers.append(_V6Server(("::1", port), H))
59
+ except OSError:
60
+ pass # no IPv6 loopback: 127.0.0.1 is enough
61
+ self.port = self.servers[0].server_address[1]
62
+ self.threads = [threading.Thread(target=self._serve, args=(s,), daemon=True)
63
+ for s in self.servers]
64
+
65
+ def _serve(self, srv):
66
+ while not self.event.is_set():
67
+ srv.handle_request()
68
+
69
+ def start(self):
70
+ for s in self.servers:
71
+ s.timeout = 0.3
72
+ for t in self.threads:
73
+ t.start()
74
+
75
+ def wait(self, timeout: float) -> bool:
76
+ return self.event.wait(timeout)
77
+
78
+ def close(self):
79
+ self.event.set()
80
+ for s in self.servers:
81
+ try:
82
+ s.server_close()
83
+ except Exception:
84
+ pass