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
|
@@ -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
|