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,313 @@
|
|
|
1
|
+
"""Where onboarding keeps secrets: the OS secret store if there is one, else
|
|
2
|
+
an owner-only file.
|
|
3
|
+
|
|
4
|
+
Backends (first that applies; ``HYPERCLAW_SECRET_STORE=keychain|secret-tool|file``
|
|
5
|
+
overrides):
|
|
6
|
+
|
|
7
|
+
- ``keychain`` (macOS): generic passwords, service ``hyperclaw``, account =
|
|
8
|
+
the secret's name. Written through ``security -i`` on stdin, so a value is
|
|
9
|
+
never on a command line or in the process list.
|
|
10
|
+
- ``secret-tool`` (Linux with the Secret Service): attributes
|
|
11
|
+
``service hyperclaw account <name>``; the value goes on stdin.
|
|
12
|
+
- ``file``: ``$HYPERCLAW_ROOT/secrets/team-secrets.json``, directory 0700,
|
|
13
|
+
file 0600. A file readable by group or others is refused.
|
|
14
|
+
|
|
15
|
+
Values are stored base64url-encoded, so any value (a JSON token bundle, for
|
|
16
|
+
instance) is safe on the ``security -i`` command line.
|
|
17
|
+
|
|
18
|
+
Some secrets are read by the rest of HyperAgents from the environment
|
|
19
|
+
(``ANTHROPIC_API_KEY``, ``TELEGRAM_BOT_TOKEN``). The non-secret map
|
|
20
|
+
``$HYPERCLAW_ROOT/config/secret-env.json`` names them; ``export_env()``
|
|
21
|
+
copies them into the process environment at start-up (never overriding a
|
|
22
|
+
value already set). Every entry point that loads ``.env`` calls it.
|
|
23
|
+
|
|
24
|
+
Nothing here prints, logs or returns a value in an error message.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import base64
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import shutil
|
|
34
|
+
import stat
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Callable, Optional
|
|
39
|
+
|
|
40
|
+
log = logging.getLogger("hyperclaw.teamsetup.secrets")
|
|
41
|
+
|
|
42
|
+
SERVICE = "hyperclaw"
|
|
43
|
+
STORE_ENV = "HYPERCLAW_SECRET_STORE"
|
|
44
|
+
_NAME = re.compile(r"^[a-z0-9_.-]{1,64}$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SecretStoreError(Exception):
|
|
48
|
+
"""A secret couldn't be stored or read. The text never holds a value."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _root() -> Path:
|
|
52
|
+
from hyperclaw.userconfig import root
|
|
53
|
+
return root()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _enc(value: str) -> str:
|
|
57
|
+
return base64.urlsafe_b64encode(value.encode("utf-8")).decode("ascii")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _dec(raw: str) -> str:
|
|
61
|
+
raw = (raw or "").strip()
|
|
62
|
+
if not raw:
|
|
63
|
+
return ""
|
|
64
|
+
try:
|
|
65
|
+
return base64.urlsafe_b64decode(raw.encode("ascii")).decode("utf-8")
|
|
66
|
+
except Exception:
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _check_name(name: str) -> str:
|
|
71
|
+
if not _NAME.match(name or ""):
|
|
72
|
+
raise SecretStoreError("invalid secret name")
|
|
73
|
+
return name
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── file backend ──────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
class FileStore:
|
|
79
|
+
name = "file"
|
|
80
|
+
where = "an owner-only file (0600)"
|
|
81
|
+
|
|
82
|
+
def __init__(self, path: Optional[Path] = None):
|
|
83
|
+
self.path = path or (_root() / "secrets" / "team-secrets.json")
|
|
84
|
+
|
|
85
|
+
def _read(self) -> dict:
|
|
86
|
+
p = self.path
|
|
87
|
+
if not p.exists():
|
|
88
|
+
return {}
|
|
89
|
+
if stat.S_IMODE(p.stat().st_mode) & 0o077:
|
|
90
|
+
raise SecretStoreError("the secrets file is readable by other accounts "
|
|
91
|
+
"(it must be 0600)")
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
94
|
+
except Exception:
|
|
95
|
+
raise SecretStoreError("the secrets file is not valid JSON")
|
|
96
|
+
return data if isinstance(data, dict) else {}
|
|
97
|
+
|
|
98
|
+
def _write(self, data: dict) -> None:
|
|
99
|
+
d = self.path.parent
|
|
100
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
try:
|
|
102
|
+
os.chmod(d, 0o700)
|
|
103
|
+
except OSError:
|
|
104
|
+
pass
|
|
105
|
+
tmp = self.path.with_suffix(".tmp")
|
|
106
|
+
fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
107
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
108
|
+
json.dump(data, f)
|
|
109
|
+
os.chmod(tmp, 0o600)
|
|
110
|
+
os.replace(tmp, self.path)
|
|
111
|
+
|
|
112
|
+
def get(self, name: str) -> str:
|
|
113
|
+
return _dec(str(self._read().get(_check_name(name)) or ""))
|
|
114
|
+
|
|
115
|
+
def set(self, name: str, value: str) -> None:
|
|
116
|
+
data = self._read()
|
|
117
|
+
data[_check_name(name)] = _enc(value)
|
|
118
|
+
self._write(data)
|
|
119
|
+
|
|
120
|
+
def delete(self, name: str) -> bool:
|
|
121
|
+
data = self._read()
|
|
122
|
+
had = data.pop(_check_name(name), None) is not None
|
|
123
|
+
if had:
|
|
124
|
+
self._write(data)
|
|
125
|
+
return had
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ── macOS keychain ────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
Runner = Callable[..., subprocess.CompletedProcess]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class KeychainStore:
|
|
134
|
+
name = "keychain"
|
|
135
|
+
where = "the macOS keychain"
|
|
136
|
+
|
|
137
|
+
def __init__(self, runner: Optional[Runner] = None):
|
|
138
|
+
self.run = runner or subprocess.run
|
|
139
|
+
|
|
140
|
+
def get(self, name: str) -> str:
|
|
141
|
+
r = self.run(["security", "find-generic-password", "-s", SERVICE, "-a",
|
|
142
|
+
_check_name(name), "-w"], capture_output=True, text=True)
|
|
143
|
+
if r.returncode != 0:
|
|
144
|
+
return ""
|
|
145
|
+
return _dec(r.stdout or "")
|
|
146
|
+
|
|
147
|
+
def set(self, name: str, value: str) -> None:
|
|
148
|
+
cmd = f"add-generic-password -U -s {SERVICE} -a {_check_name(name)} -w {_enc(value)}\n"
|
|
149
|
+
r = self.run(["security", "-i"], input=cmd, capture_output=True, text=True)
|
|
150
|
+
if r.returncode != 0:
|
|
151
|
+
raise SecretStoreError(f"the keychain refused the item (exit {r.returncode})")
|
|
152
|
+
|
|
153
|
+
def delete(self, name: str) -> bool:
|
|
154
|
+
r = self.run(["security", "delete-generic-password", "-s", SERVICE, "-a",
|
|
155
|
+
_check_name(name)], capture_output=True, text=True)
|
|
156
|
+
return r.returncode == 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── Linux Secret Service ──────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
class SecretToolStore:
|
|
162
|
+
name = "secret-tool"
|
|
163
|
+
where = "the system keyring"
|
|
164
|
+
|
|
165
|
+
def __init__(self, runner: Optional[Runner] = None):
|
|
166
|
+
self.run = runner or subprocess.run
|
|
167
|
+
|
|
168
|
+
def get(self, name: str) -> str:
|
|
169
|
+
r = self.run(["secret-tool", "lookup", "service", SERVICE, "account", _check_name(name)],
|
|
170
|
+
capture_output=True, text=True)
|
|
171
|
+
return _dec(r.stdout or "") if r.returncode == 0 else ""
|
|
172
|
+
|
|
173
|
+
def set(self, name: str, value: str) -> None:
|
|
174
|
+
r = self.run(["secret-tool", "store", f"--label=HyperAgents {name}", "service", SERVICE,
|
|
175
|
+
"account", _check_name(name)], input=_enc(value), capture_output=True,
|
|
176
|
+
text=True)
|
|
177
|
+
if r.returncode != 0:
|
|
178
|
+
raise SecretStoreError(f"the keyring refused the item (exit {r.returncode})")
|
|
179
|
+
|
|
180
|
+
def delete(self, name: str) -> bool:
|
|
181
|
+
r = self.run(["secret-tool", "clear", "service", SERVICE, "account", _check_name(name)],
|
|
182
|
+
capture_output=True, text=True)
|
|
183
|
+
return r.returncode == 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def backend_name() -> str:
|
|
187
|
+
env = os.environ.get(STORE_ENV, "").strip().lower()
|
|
188
|
+
if env in ("keychain", "secret-tool", "file"):
|
|
189
|
+
return env
|
|
190
|
+
if sys.platform == "darwin" and shutil.which("security"):
|
|
191
|
+
return "keychain"
|
|
192
|
+
if sys.platform.startswith("linux") and shutil.which("secret-tool") \
|
|
193
|
+
and os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
|
|
194
|
+
return "secret-tool"
|
|
195
|
+
return "file"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def store(name: str = ""):
|
|
199
|
+
n = name or backend_name()
|
|
200
|
+
if n == "keychain":
|
|
201
|
+
return KeychainStore()
|
|
202
|
+
if n == "secret-tool":
|
|
203
|
+
return SecretToolStore()
|
|
204
|
+
return FileStore()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ── the env map (non-secret) ──────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
def env_map_path() -> Path:
|
|
210
|
+
return _root() / "config" / "secret-env.json"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def env_map() -> dict:
|
|
214
|
+
"""{"store": backend, "env": {ENV_NAME: secret name}}."""
|
|
215
|
+
try:
|
|
216
|
+
data = json.loads(env_map_path().read_text(encoding="utf-8"))
|
|
217
|
+
if isinstance(data, dict) and isinstance(data.get("env"), dict):
|
|
218
|
+
return data
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
return {"store": "", "env": {}}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _write_env_map(data: dict) -> None:
|
|
225
|
+
p = env_map_path()
|
|
226
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
fd = os.open(str(p), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
228
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
229
|
+
json.dump(data, f, indent=1, sort_keys=True)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def put(name: str, value: str, *, env: str = "", backend=None) -> str:
|
|
233
|
+
"""Store a secret; with ``env``, also export it to that variable now and
|
|
234
|
+
at every start. Returns where it was stored (for the ✓ line)."""
|
|
235
|
+
st = backend or store(env_map().get("store") or "")
|
|
236
|
+
st.set(name, value)
|
|
237
|
+
if env:
|
|
238
|
+
m = env_map()
|
|
239
|
+
m["store"] = st.name
|
|
240
|
+
m["env"][env] = name
|
|
241
|
+
_write_env_map(m)
|
|
242
|
+
os.environ[env] = value
|
|
243
|
+
return st.where
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def link_env(name: str, env: str, backend=None) -> bool:
|
|
247
|
+
"""Export an already stored secret to ``env`` (now and at every start).
|
|
248
|
+
False when the secret isn't stored."""
|
|
249
|
+
st = backend or store(env_map().get("store") or "")
|
|
250
|
+
value = get(name, backend=st)
|
|
251
|
+
if not value:
|
|
252
|
+
return False
|
|
253
|
+
m = env_map()
|
|
254
|
+
m["store"] = st.name
|
|
255
|
+
m["env"][env] = name
|
|
256
|
+
_write_env_map(m)
|
|
257
|
+
os.environ[env] = value
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def unlink_env(env: str) -> None:
|
|
262
|
+
"""Stop exporting ``env`` (the secret itself stays stored)."""
|
|
263
|
+
m = env_map()
|
|
264
|
+
if m["env"].pop(env, None) is not None:
|
|
265
|
+
_write_env_map(m)
|
|
266
|
+
os.environ.pop(env, None)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get(name: str, backend=None) -> str:
|
|
270
|
+
st = backend or store(env_map().get("store") or "")
|
|
271
|
+
try:
|
|
272
|
+
return st.get(name)
|
|
273
|
+
except SecretStoreError:
|
|
274
|
+
return ""
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def remove(name: str, backend=None) -> bool:
|
|
278
|
+
st = backend or store(env_map().get("store") or "")
|
|
279
|
+
m = env_map()
|
|
280
|
+
gone = [k for k, v in m["env"].items() if v == name]
|
|
281
|
+
for k in gone:
|
|
282
|
+
m["env"].pop(k, None)
|
|
283
|
+
if gone:
|
|
284
|
+
_write_env_map(m)
|
|
285
|
+
try:
|
|
286
|
+
return st.delete(name)
|
|
287
|
+
except SecretStoreError:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def export_env(backend=None) -> list:
|
|
292
|
+
"""Copy the mapped secrets into os.environ (setdefault). Returns the
|
|
293
|
+
variable names exported. Never raises; a missing map costs nothing."""
|
|
294
|
+
try:
|
|
295
|
+
m = env_map()
|
|
296
|
+
if not m["env"]:
|
|
297
|
+
return []
|
|
298
|
+
st = backend or store(m.get("store") or "")
|
|
299
|
+
done = []
|
|
300
|
+
for env_name, secret_name in m["env"].items():
|
|
301
|
+
if os.environ.get(env_name):
|
|
302
|
+
continue
|
|
303
|
+
try:
|
|
304
|
+
v = st.get(secret_name)
|
|
305
|
+
except SecretStoreError:
|
|
306
|
+
v = ""
|
|
307
|
+
if v:
|
|
308
|
+
os.environ[env_name] = v
|
|
309
|
+
done.append(env_name)
|
|
310
|
+
return done
|
|
311
|
+
except Exception as e: # noqa: BLE001 -- start-up must never fail here
|
|
312
|
+
log.warning("secret store: couldn't export secrets (%s)", type(e).__name__)
|
|
313
|
+
return []
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""``hyperclaw self-check``: a report a team member can send Vaughn.
|
|
2
|
+
|
|
3
|
+
It writes ``$HYPERCLAW_ROOT/reports/self-check-<UTC time>.txt`` (0600) with:
|
|
4
|
+
version, OS, install method and mode, the onboarding steps, one live check
|
|
5
|
+
per piece (the same "✓ Connected" lines as onboarding), the background
|
|
6
|
+
service state, which secrets are stored (names only), and the recent
|
|
7
|
+
warnings and errors from the logs.
|
|
8
|
+
|
|
9
|
+
Removed from the whole report, whatever their source:
|
|
10
|
+
|
|
11
|
+
- keys and tokens: every value held in the secret store, the digest token
|
|
12
|
+
store, the Google token file and the secret-looking ``.env`` entries is
|
|
13
|
+
replaced literally; known shapes (Anthropic/OpenAI/xAI keys, Telegram bot
|
|
14
|
+
tokens, Slack, GitLab, Google OAuth, GitHub, AWS, JWT, bearer, password=)
|
|
15
|
+
are replaced by pattern as a second line of defence;
|
|
16
|
+
- email addresses;
|
|
17
|
+
- message content: logs keep warnings and errors only; text after message
|
|
18
|
+
markers (``User:``, ``text=``, ...) and anything in quotes is dropped;
|
|
19
|
+
conversation files are never read;
|
|
20
|
+
- file paths with the user name (home is shown as ``~``, the account name as
|
|
21
|
+
``<user>``) and long numbers such as chat ids.
|
|
22
|
+
|
|
23
|
+
``tests/unit/teamsetup/test_selfcheck.py`` plants a fake secret of every kind and
|
|
24
|
+
proves none reaches the report.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import datetime as _dt
|
|
29
|
+
import getpass
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import platform
|
|
33
|
+
import re
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Callable, Iterable, Optional
|
|
37
|
+
|
|
38
|
+
LOG_FILES = ("logs/service.log", "logs/telegram.err.log", "logs/hyperclaw.log",
|
|
39
|
+
"logs/telegram.log")
|
|
40
|
+
LOG_TAIL = 400 # lines read from the end of each log
|
|
41
|
+
LOG_KEEP = 60 # warning/error lines kept per log
|
|
42
|
+
_LEVEL = re.compile(r"\b(WARNING|ERROR|CRITICAL)\b")
|
|
43
|
+
|
|
44
|
+
_PATTERNS = [
|
|
45
|
+
(re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)",
|
|
46
|
+
re.S), "[key removed]"),
|
|
47
|
+
(re.compile(r"\bsk-ant-[A-Za-z0-9_-]{8,}"), "[key removed]"),
|
|
48
|
+
(re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}"), "[key removed]"),
|
|
49
|
+
(re.compile(r"\bxai-[A-Za-z0-9_-]{16,}"), "[key removed]"),
|
|
50
|
+
(re.compile(r"\brpa_[A-Za-z0-9_-]{8,}"), "[key removed]"),
|
|
51
|
+
(re.compile(r"\bHCT\d+\.[A-Za-z0-9_.-]{16,}"), "[item removed]"),
|
|
52
|
+
(re.compile(r"\b\d{5,}:[A-Za-z0-9_-]{30,}"), "[token removed]"),
|
|
53
|
+
(re.compile(r"\bbot\d{5,}:[A-Za-z0-9_-]{20,}"), "[token removed]"),
|
|
54
|
+
(re.compile(r"\bxox[abeoprsc]-[A-Za-z0-9-]{8,}"), "[token removed]"),
|
|
55
|
+
(re.compile(r"\bxoxe[.-][A-Za-z0-9.-]{8,}"), "[token removed]"),
|
|
56
|
+
(re.compile(r"\bxapp-[A-Za-z0-9-]{8,}"), "[token removed]"),
|
|
57
|
+
(re.compile(r"\bgl[a-z]{1,8}-[A-Za-z0-9_.-]{16,}"), "[token removed]"),
|
|
58
|
+
(re.compile(r"\bya29\.[A-Za-z0-9_.-]{8,}"), "[token removed]"),
|
|
59
|
+
(re.compile(r"\b1//[A-Za-z0-9_-]{8,}"), "[token removed]"),
|
|
60
|
+
(re.compile(r"\bGOCSPX-[A-Za-z0-9_-]{8,}"), "[secret removed]"),
|
|
61
|
+
(re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}"), "[token removed]"),
|
|
62
|
+
(re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b"), "[key removed]"),
|
|
63
|
+
(re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}"),
|
|
64
|
+
"[token removed]"),
|
|
65
|
+
(re.compile(r"(?i)\b(bearer\s+)[A-Za-z0-9._~+/=-]{8,}"), r"\1[token removed]"),
|
|
66
|
+
(re.compile(r"(?i)\b((?:password|passwd|secret|api[_-]?key|access[_-]?token|"
|
|
67
|
+
r"refresh[_-]?token|token|key|code|code_verifier)\s*[:=]\s*)"
|
|
68
|
+
r"(?!\[)(\"[^\"]*\"|'[^']*'|\S+)"), r"\1[removed]"),
|
|
69
|
+
(re.compile(r"\b([a-z][a-z0-9+.-]*://[^:/\s@]+:)[^@\s]+(@)"), r"\1[removed]\2"),
|
|
70
|
+
(re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "[email removed]"),
|
|
71
|
+
# any account's home folder, not only the current one
|
|
72
|
+
(re.compile(r"(/(?:Users|home))/(?!Shared/)[^/\s]+"), r"\1/<user>"),
|
|
73
|
+
(re.compile(r"(?<![\w.])-?\d{6,}(?![\w.])"), "[number removed]"),
|
|
74
|
+
]
|
|
75
|
+
# Text after these markers is message content.
|
|
76
|
+
_CONTENT = re.compile(r"(?i)\b(user|assistant|reply|message|text|prompt|content|said|"
|
|
77
|
+
r"caption|body|query)\s*[:=]\s*.*$")
|
|
78
|
+
_QUOTED = re.compile(r"\"[^\"]*\"|'[^']*'|“[^”]*”|‘[^’]*’")
|
|
79
|
+
_SECRET_ENV = re.compile(r"(KEY|TOKEN|SECRET|PASSWORD|PASS|DATABASE_URL|DSN|CREDENTIAL)",
|
|
80
|
+
re.I)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def known_secret_values() -> list:
|
|
84
|
+
"""Every secret value this install holds (to remove literally)."""
|
|
85
|
+
vals = set()
|
|
86
|
+
from hyperclaw.teamsetup import secrets
|
|
87
|
+
try:
|
|
88
|
+
for name in ("model_key", "hyper_key", "google_client_secret", "telegram_bot_token",
|
|
89
|
+
"slack_user_token"):
|
|
90
|
+
v = secrets.get(name)
|
|
91
|
+
if v:
|
|
92
|
+
vals.add(v)
|
|
93
|
+
if name == "slack_user_token":
|
|
94
|
+
try:
|
|
95
|
+
b = json.loads(v)
|
|
96
|
+
vals.update(str(x) for k, x in b.items()
|
|
97
|
+
if k in ("access_token", "refresh_token") and x)
|
|
98
|
+
except ValueError:
|
|
99
|
+
pass
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
try:
|
|
103
|
+
from hyperclaw.digest import tokens
|
|
104
|
+
for src in ("gitlab", "slack"):
|
|
105
|
+
for backend in {tokens.backend_name(), "file"}:
|
|
106
|
+
try:
|
|
107
|
+
v = tokens.store(backend).get(src)
|
|
108
|
+
except Exception:
|
|
109
|
+
v = ""
|
|
110
|
+
if v:
|
|
111
|
+
vals.add(v)
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
try:
|
|
115
|
+
from hyperclaw.google_workspace import auth as ga
|
|
116
|
+
tok = ga.load_token()
|
|
117
|
+
vals.update(str(tok.get(k)) for k in ("refresh_token", "access_token") if tok.get(k))
|
|
118
|
+
cp = ga.client_path()
|
|
119
|
+
if cp.is_file():
|
|
120
|
+
vals.add(str((json.loads(cp.read_text()).get("installed") or {})
|
|
121
|
+
.get("client_secret") or ""))
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
try:
|
|
125
|
+
from hyperclaw.brief import store as bstore
|
|
126
|
+
tok = bstore.read_json(bstore.token_path())
|
|
127
|
+
vals.update(str(tok.get(k)) for k in ("refresh_token", "access_token") if tok.get(k))
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
try:
|
|
131
|
+
from hyperclaw.userconfig import env_read
|
|
132
|
+
for k, v in env_read().items():
|
|
133
|
+
if v and _SECRET_ENV.search(k):
|
|
134
|
+
vals.add(v)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
for k, v in os.environ.items():
|
|
138
|
+
if v and _SECRET_ENV.search(k) and len(v) >= 8:
|
|
139
|
+
vals.add(v)
|
|
140
|
+
return sorted((v for v in vals if len(v) >= 6), key=len, reverse=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Redactor:
|
|
144
|
+
def __init__(self, values: Optional[Iterable[str]] = None, home: str = "",
|
|
145
|
+
user: str = ""):
|
|
146
|
+
self.values = list(values if values is not None else known_secret_values())
|
|
147
|
+
self.home = home or str(Path.home())
|
|
148
|
+
self.user = user or _user()
|
|
149
|
+
|
|
150
|
+
def __call__(self, text: str) -> str:
|
|
151
|
+
out = str(text or "")
|
|
152
|
+
for v in self.values:
|
|
153
|
+
out = out.replace(v, "[secret removed]")
|
|
154
|
+
if self.home and self.home not in ("/", ""):
|
|
155
|
+
out = out.replace(self.home, "~")
|
|
156
|
+
for pat, repl in _PATTERNS:
|
|
157
|
+
out = pat.sub(repl, out)
|
|
158
|
+
if self.user and len(self.user) >= 3:
|
|
159
|
+
out = re.sub(rf"(?<![A-Za-z0-9]){re.escape(self.user)}(?![A-Za-z0-9])", "<user>",
|
|
160
|
+
out)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
def log_line(self, line: str) -> str:
|
|
164
|
+
"""A log line with message content removed, then redacted."""
|
|
165
|
+
s = _CONTENT.sub(lambda m: m.group(1) + ": [message removed]", line)
|
|
166
|
+
s = _QUOTED.sub("[text removed]", s)
|
|
167
|
+
return self(s)[:300]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _user() -> str:
|
|
171
|
+
try:
|
|
172
|
+
return getpass.getuser()
|
|
173
|
+
except Exception:
|
|
174
|
+
return os.environ.get("USER", "")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def log_section(redact: Redactor) -> list:
|
|
178
|
+
from hyperclaw.userconfig import root
|
|
179
|
+
out = []
|
|
180
|
+
for rel in LOG_FILES:
|
|
181
|
+
p = root() / rel
|
|
182
|
+
if not p.exists():
|
|
183
|
+
continue
|
|
184
|
+
try:
|
|
185
|
+
with open(p, "rb") as f:
|
|
186
|
+
f.seek(0, 2)
|
|
187
|
+
size = f.tell()
|
|
188
|
+
f.seek(max(0, size - 256 * 1024))
|
|
189
|
+
lines = f.read().decode("utf-8", errors="replace").splitlines()[-LOG_TAIL:]
|
|
190
|
+
except OSError:
|
|
191
|
+
continue
|
|
192
|
+
kept = [redact.log_line(x) for x in lines if _LEVEL.search(x)][-LOG_KEEP:]
|
|
193
|
+
out.append(f"--- {rel}: {len(lines)} recent lines, {len(kept)} warnings/errors shown "
|
|
194
|
+
"(message text removed)")
|
|
195
|
+
out += kept
|
|
196
|
+
return out or ["(no logs yet)"]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _secret_names() -> list:
|
|
200
|
+
from hyperclaw.teamsetup import secrets
|
|
201
|
+
out = []
|
|
202
|
+
st = secrets.env_map().get("store") or secrets.backend_name()
|
|
203
|
+
for name in ("model_key", "hyper_key", "google_client_secret", "telegram_bot_token",
|
|
204
|
+
"slack_user_token"):
|
|
205
|
+
try:
|
|
206
|
+
present = bool(secrets.get(name))
|
|
207
|
+
except Exception:
|
|
208
|
+
present = False
|
|
209
|
+
out.append(f"{name}: {'stored' if present else 'not stored'}")
|
|
210
|
+
out.append(f"secret store: {st}")
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build(*, checks_fn: Optional[Callable[[], list]] = None,
|
|
215
|
+
service_fn: Optional[Callable[[], dict]] = None,
|
|
216
|
+
redact: Optional[Redactor] = None, now: Optional[_dt.datetime] = None) -> str:
|
|
217
|
+
from hyperclaw import __version__, distribution
|
|
218
|
+
from hyperclaw.deployment import mode
|
|
219
|
+
from hyperclaw.teamsetup import onboard, profile, service
|
|
220
|
+
redact = redact or Redactor()
|
|
221
|
+
now = now or _dt.datetime.now(_dt.timezone.utc)
|
|
222
|
+
try:
|
|
223
|
+
from hyperclaw.updates.install import detect
|
|
224
|
+
method = detect().kind
|
|
225
|
+
except Exception:
|
|
226
|
+
method = "other"
|
|
227
|
+
prof = profile.load()
|
|
228
|
+
lines = [
|
|
229
|
+
"HyperAgents self-check",
|
|
230
|
+
f"time (UTC): {now.strftime('%Y-%m-%d %H:%M')}",
|
|
231
|
+
f"version: {__version__}",
|
|
232
|
+
f"python: {platform.python_version()}",
|
|
233
|
+
f"system: {platform.system()} {platform.mac_ver()[0] or platform.release()} "
|
|
234
|
+
f"({platform.machine()})",
|
|
235
|
+
f"install: {method}; mode: {mode()}; team profile: "
|
|
236
|
+
f"{prof.get('name', 'none') if prof else 'none or not verified'}",
|
|
237
|
+
f"install command: {distribution.install_command(__version__)}"
|
|
238
|
+
+ (f" (this install is the old package; {distribution.migration_line(__version__)})"
|
|
239
|
+
if distribution.is_legacy_install() else ""),
|
|
240
|
+
"",
|
|
241
|
+
"Onboarding steps:",
|
|
242
|
+
]
|
|
243
|
+
st = onboard.load_state()["steps"]
|
|
244
|
+
for s in onboard.STEPS:
|
|
245
|
+
info = st.get(s) or {}
|
|
246
|
+
lines.append(f" {s}: {info.get('status') or 'not started'}"
|
|
247
|
+
+ (f" ({info['code']})" if info.get("code") else ""))
|
|
248
|
+
lines += ["", "Connections:"]
|
|
249
|
+
try:
|
|
250
|
+
conn = checks_fn() if checks_fn else onboard.summary_lines()
|
|
251
|
+
except Exception as e: # noqa: BLE001
|
|
252
|
+
conn = [f"(checks couldn't run: {type(e).__name__})"]
|
|
253
|
+
lines += [f" {x}" for x in conn]
|
|
254
|
+
try:
|
|
255
|
+
svc = service_fn() if service_fn else service.status()
|
|
256
|
+
except Exception as e: # noqa: BLE001
|
|
257
|
+
svc = {"error": type(e).__name__}
|
|
258
|
+
lines += ["", "Background service: " + ", ".join(f"{k}={v}" for k, v in svc.items()),
|
|
259
|
+
"scheduler jobs: " + (os.environ.get("HYPERCLAW_SCHEDULER_JOBS", "")
|
|
260
|
+
or "(set in the service only)"),
|
|
261
|
+
"", "Stored secrets (names only):"]
|
|
262
|
+
lines += [f" {x}" for x in _secret_names()]
|
|
263
|
+
lines += ["", "Recent warnings and errors:"]
|
|
264
|
+
lines += log_section(redact)
|
|
265
|
+
lines += ["", "Removed from this report: keys, tokens, email addresses, message text, "
|
|
266
|
+
"and your user name in file paths."]
|
|
267
|
+
return redact("\n".join(lines)) + "\n"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def write(text: str, out: Optional[Path] = None) -> Path:
|
|
271
|
+
from hyperclaw.userconfig import root
|
|
272
|
+
if out is None:
|
|
273
|
+
stamp = _dt.datetime.now(_dt.timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
274
|
+
out = root() / "reports" / f"self-check-{stamp}.txt"
|
|
275
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
fd = os.open(str(out), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
277
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
278
|
+
f.write(text)
|
|
279
|
+
return out
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main(out: Optional[str] = None, *, echo: Callable[[str], None] = print) -> Path:
|
|
283
|
+
from hyperclaw.userconfig import load_env_into_process
|
|
284
|
+
load_env_into_process()
|
|
285
|
+
text = build()
|
|
286
|
+
p = write(text, Path(out).expanduser() if out else None)
|
|
287
|
+
shown = str(p).replace(str(Path.home()), "~")
|
|
288
|
+
echo(f"Self-check written to {shown}")
|
|
289
|
+
echo("It has no keys, tokens, email addresses or messages in it. Send that file to Vaughn "
|
|
290
|
+
"(attach it in a direct message).")
|
|
291
|
+
return p
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
main(sys.argv[1] if len(sys.argv) > 1 else None)
|