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,341 @@
|
|
|
1
|
+
"""Tool-use qualification records — signed by Hyper (spec §11 + cross-cutting
|
|
2
|
+
rule 3; CEO rulings 2026-10-03).
|
|
3
|
+
|
|
4
|
+
A provider earns ``tool_use`` only by passing the qualification suite — the
|
|
5
|
+
same rule for HyperSpeed V2 and for a customer's own provider. models.yaml
|
|
6
|
+
may *declare* tool_use; it is *granted* only when a **Hyper-signed** record
|
|
7
|
+
for that exact provider + model exists and says QUALIFIED. Otherwise the
|
|
8
|
+
model is chat-only. There is no grace period: a model without a valid signed
|
|
9
|
+
record has no tools.
|
|
10
|
+
|
|
11
|
+
Record file (one per provider+model, e.g. ``anthropic--claude-sonnet-5-5.json``)::
|
|
12
|
+
|
|
13
|
+
{"record": {"provider": "anthropic", "model": "claude-sonnet-5-5",
|
|
14
|
+
"verdict": "QUALIFIED", # or NOT_QUALIFIED
|
|
15
|
+
"suite_version": "...", "benchmark_run_id": "...",
|
|
16
|
+
"harness": "<harness sha/version>",
|
|
17
|
+
"hyperclaw_version": "1.2.3rc2",
|
|
18
|
+
"issued_at": "2026-10-03T20:00:00Z",
|
|
19
|
+
"expires_at": "2027-10-03T00:00:00Z", # optional
|
|
20
|
+
"key_id": "<issuer key id>"},
|
|
21
|
+
"signature": "<base64 Ed25519 signature over canonical(record)>"}
|
|
22
|
+
|
|
23
|
+
``canonical(record)`` is ``json.dumps(record, sort_keys=True,
|
|
24
|
+
separators=(",", ":"), ensure_ascii=False).encode("utf-8")``. The signature
|
|
25
|
+
is detached from the payload: it covers the record object only.
|
|
26
|
+
|
|
27
|
+
Verification uses Hyper's public key(s) shipped in the package
|
|
28
|
+
(``hyperclaw/qualification_keys/<key_id>.pub``, base64 raw Ed25519 key),
|
|
29
|
+
selected by the record's ``key_id``. Unsigned, bad-signature, unknown-key,
|
|
30
|
+
malformed or expired records are IGNORED (logged once) and the model stays
|
|
31
|
+
chat-only.
|
|
32
|
+
|
|
33
|
+
Locations: the package (``hyperclaw/qualification_records/``, which Hyper
|
|
34
|
+
ships when a model passes) and the install (``$HYPERCLAW_ROOT/qualification/``,
|
|
35
|
+
default ``~/.hyperclaw/qualification/``). Both must verify. A Hyper-deployed
|
|
36
|
+
install honours packaged records only. The provider and model *inside* the
|
|
37
|
+
signed record must match; the filename alone proves nothing.
|
|
38
|
+
|
|
39
|
+
Signing (maintainers only): ``hyperclaw-sign-qualification`` /
|
|
40
|
+
``python -m hyperclaw.qualification sign`` reads the private key from the
|
|
41
|
+
macOS keychain (generic password, service ``hc-qual-signing-key``, account
|
|
42
|
+
``hyper``) and writes a signed record from a benchmark ``results-v2.json``.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import argparse
|
|
47
|
+
import base64
|
|
48
|
+
import datetime as _dt
|
|
49
|
+
import hashlib
|
|
50
|
+
import json
|
|
51
|
+
import logging
|
|
52
|
+
import re
|
|
53
|
+
import subprocess
|
|
54
|
+
import sys
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Optional
|
|
57
|
+
|
|
58
|
+
log = logging.getLogger("hyperclaw.qualification")
|
|
59
|
+
|
|
60
|
+
QUALIFIED = "QUALIFIED"
|
|
61
|
+
NOT_QUALIFIED = "NOT_QUALIFIED"
|
|
62
|
+
VERDICTS = (QUALIFIED, NOT_QUALIFIED)
|
|
63
|
+
RECORD_FIELDS_REQUIRED = ("provider", "model", "verdict", "suite_version",
|
|
64
|
+
"benchmark_run_id", "harness", "hyperclaw_version",
|
|
65
|
+
"issued_at", "key_id")
|
|
66
|
+
|
|
67
|
+
_PKG = Path(__file__).resolve().parent
|
|
68
|
+
PACKAGED_DIR = _PKG / "qualification_records"
|
|
69
|
+
KEYS_DIR = _PKG / "qualification_keys"
|
|
70
|
+
|
|
71
|
+
KEYCHAIN_SERVICE = "hc-qual-signing-key"
|
|
72
|
+
KEYCHAIN_ACCOUNT = "hyper"
|
|
73
|
+
|
|
74
|
+
# Extra trusted public keys {key_id: raw 32-byte key}. Empty in production;
|
|
75
|
+
# the unit tests add a throwaway test key here (never the production key).
|
|
76
|
+
EXTRA_TRUSTED_KEYS: dict = {}
|
|
77
|
+
|
|
78
|
+
_logged: set = set()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ───────────────────────── helpers ─────────────────────────
|
|
82
|
+
|
|
83
|
+
def _slug(model: str) -> str:
|
|
84
|
+
return re.sub(r"[^A-Za-z0-9._-]+", "_", model or "").strip("_") or "default"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def record_filename(provider: str, model: str) -> str:
|
|
88
|
+
return f"{_slug(provider)}--{_slug(model)}.json"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def user_dir() -> Path:
|
|
92
|
+
from hyperclaw.userconfig import root
|
|
93
|
+
return root() / "qualification"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _dirs() -> list:
|
|
97
|
+
from hyperclaw.deployment import is_hyper
|
|
98
|
+
return [PACKAGED_DIR] if is_hyper() else [PACKAGED_DIR, user_dir()]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def canonical(record: dict) -> bytes:
|
|
102
|
+
return json.dumps(record, sort_keys=True, separators=(",", ":"),
|
|
103
|
+
ensure_ascii=False).encode("utf-8")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def key_id_for(public_raw: bytes) -> str:
|
|
107
|
+
"""Key id = 'hyper-' + first 16 hex chars of sha256(raw public key)."""
|
|
108
|
+
return "hyper-" + hashlib.sha256(public_raw).hexdigest()[:16]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _now() -> _dt.datetime:
|
|
112
|
+
return _dt.datetime.now(_dt.timezone.utc)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _parse_ts(s: str) -> _dt.datetime:
|
|
116
|
+
t = _dt.datetime.fromisoformat(str(s).replace("Z", "+00:00"))
|
|
117
|
+
return t if t.tzinfo else t.replace(tzinfo=_dt.timezone.utc)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _log_once(path: Path, reason: str) -> None:
|
|
121
|
+
k = (str(path), reason)
|
|
122
|
+
if k not in _logged:
|
|
123
|
+
_logged.add(k)
|
|
124
|
+
log.warning("qualification record ignored (%s): %s — model stays chat-only",
|
|
125
|
+
reason, path)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def trusted_keys() -> dict:
|
|
129
|
+
"""{key_id: raw public key bytes} from the package (+ EXTRA_TRUSTED_KEYS)."""
|
|
130
|
+
keys: dict = {}
|
|
131
|
+
try:
|
|
132
|
+
for p in sorted(KEYS_DIR.glob("*.pub")):
|
|
133
|
+
try:
|
|
134
|
+
raw = base64.b64decode(p.read_text(encoding="utf-8").strip(), validate=True)
|
|
135
|
+
except Exception:
|
|
136
|
+
continue
|
|
137
|
+
if len(raw) == 32 and key_id_for(raw) == p.stem:
|
|
138
|
+
keys[p.stem] = raw
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
keys.update(EXTRA_TRUSTED_KEYS)
|
|
142
|
+
return keys
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ───────────────────────── verification ─────────────────────────
|
|
146
|
+
|
|
147
|
+
def verify_envelope(env, path: Path = Path("<memory>")) -> Optional[dict]:
|
|
148
|
+
"""Return the verified record, or None (and log once why it was ignored)."""
|
|
149
|
+
from cryptography.exceptions import InvalidSignature
|
|
150
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
151
|
+
|
|
152
|
+
if not isinstance(env, dict) or not isinstance(env.get("record"), dict):
|
|
153
|
+
_log_once(path, "unsigned or malformed")
|
|
154
|
+
return None
|
|
155
|
+
rec, sig = env["record"], env.get("signature")
|
|
156
|
+
if not isinstance(sig, str) or not sig:
|
|
157
|
+
_log_once(path, "unsigned")
|
|
158
|
+
return None
|
|
159
|
+
if any(not rec.get(f) for f in RECORD_FIELDS_REQUIRED) or rec.get("verdict") not in VERDICTS:
|
|
160
|
+
_log_once(path, "missing fields")
|
|
161
|
+
return None
|
|
162
|
+
raw = trusted_keys().get(rec.get("key_id"))
|
|
163
|
+
if raw is None:
|
|
164
|
+
_log_once(path, "unknown key")
|
|
165
|
+
return None
|
|
166
|
+
try:
|
|
167
|
+
Ed25519PublicKey.from_public_bytes(raw).verify(base64.b64decode(sig, validate=True),
|
|
168
|
+
canonical(rec))
|
|
169
|
+
except (InvalidSignature, ValueError, TypeError):
|
|
170
|
+
_log_once(path, "bad signature")
|
|
171
|
+
return None
|
|
172
|
+
if rec.get("expires_at"):
|
|
173
|
+
try:
|
|
174
|
+
if _now() >= _parse_ts(rec["expires_at"]):
|
|
175
|
+
_log_once(path, "expired")
|
|
176
|
+
return None
|
|
177
|
+
except Exception:
|
|
178
|
+
_log_once(path, "bad expires_at")
|
|
179
|
+
return None
|
|
180
|
+
return rec
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def find_record(provider: str, model: str) -> Optional[dict]:
|
|
184
|
+
"""The first *verified* record for this exact provider + model, or None."""
|
|
185
|
+
if not provider or not model:
|
|
186
|
+
return None
|
|
187
|
+
name = record_filename(provider, model)
|
|
188
|
+
for d in _dirs():
|
|
189
|
+
p = d / name
|
|
190
|
+
try:
|
|
191
|
+
if not p.is_file():
|
|
192
|
+
continue
|
|
193
|
+
env = json.loads(p.read_text(encoding="utf-8"))
|
|
194
|
+
except Exception:
|
|
195
|
+
_log_once(p, "unreadable")
|
|
196
|
+
continue
|
|
197
|
+
rec = verify_envelope(env, p)
|
|
198
|
+
if rec and rec.get("provider") == provider and rec.get("model") == model:
|
|
199
|
+
return rec
|
|
200
|
+
if rec:
|
|
201
|
+
_log_once(p, "provider/model mismatch")
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def is_qualified(provider: str, model: str) -> bool:
|
|
206
|
+
rec = find_record(provider, model)
|
|
207
|
+
return bool(rec) and rec.get("verdict") == QUALIFIED
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def status_label(provider: str, model: str) -> str:
|
|
211
|
+
"""'tools (qualified)' or 'chat-only (not qualified for tools)'."""
|
|
212
|
+
return ("tools (qualified)" if is_qualified(provider, model)
|
|
213
|
+
else "chat-only (not qualified for tools)")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ───────────────────────── signing ─────────────────────────
|
|
217
|
+
|
|
218
|
+
def _private_key(private_raw: bytes):
|
|
219
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
220
|
+
return Ed25519PrivateKey.from_private_bytes(private_raw)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _public_raw(private_raw: bytes) -> bytes:
|
|
224
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
225
|
+
return _private_key(private_raw).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def sign_record(record: dict, private_raw: bytes) -> dict:
|
|
229
|
+
"""Return the signed envelope. ``key_id`` is set from the key."""
|
|
230
|
+
rec = dict(record)
|
|
231
|
+
rec["key_id"] = key_id_for(_public_raw(private_raw))
|
|
232
|
+
if rec.get("verdict") not in VERDICTS:
|
|
233
|
+
raise ValueError(f"verdict must be one of {VERDICTS}")
|
|
234
|
+
sig = _private_key(private_raw).sign(canonical(rec))
|
|
235
|
+
return {"record": rec, "signature": base64.b64encode(sig).decode("ascii")}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def make_record(provider: str, model: str, verdict: str, *, suite_version: str,
|
|
239
|
+
benchmark_run_id: str, harness: str, expires_at: Optional[str] = None,
|
|
240
|
+
hyperclaw_version: Optional[str] = None) -> dict:
|
|
241
|
+
if hyperclaw_version is None:
|
|
242
|
+
from hyperclaw import __version__ as hyperclaw_version
|
|
243
|
+
rec = {"provider": provider, "model": model, "verdict": verdict,
|
|
244
|
+
"suite_version": suite_version, "benchmark_run_id": benchmark_run_id,
|
|
245
|
+
"harness": harness, "hyperclaw_version": hyperclaw_version,
|
|
246
|
+
"issued_at": _now().replace(microsecond=0).isoformat().replace("+00:00", "Z")}
|
|
247
|
+
if expires_at:
|
|
248
|
+
rec["expires_at"] = expires_at
|
|
249
|
+
return rec
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def write_envelope(env: dict, directory: Path) -> Path:
|
|
253
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
rec = env["record"]
|
|
255
|
+
p = directory / record_filename(rec["provider"], rec["model"])
|
|
256
|
+
p.write_text(json.dumps(env, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
257
|
+
return p
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def verdict_from_results(results: dict) -> str:
|
|
261
|
+
"""QUALIFIED only if the results file's qualification verdict says so."""
|
|
262
|
+
q = results.get("qualification") if isinstance(results, dict) else None
|
|
263
|
+
if (isinstance(q, dict) and q.get("qualified") is True
|
|
264
|
+
and str(q.get("line", "")).startswith(QUALIFIED) and not q.get("reasons")):
|
|
265
|
+
return QUALIFIED
|
|
266
|
+
return NOT_QUALIFIED
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def keychain_private_key() -> bytes:
|
|
270
|
+
"""Read the signing key from the macOS keychain. Never printed or logged."""
|
|
271
|
+
out = subprocess.run(["security", "find-generic-password", "-s", KEYCHAIN_SERVICE,
|
|
272
|
+
"-a", KEYCHAIN_ACCOUNT, "-w"],
|
|
273
|
+
capture_output=True, text=True, check=False)
|
|
274
|
+
if out.returncode != 0 or not out.stdout.strip():
|
|
275
|
+
raise SystemExit(f"signing key not found in keychain (service {KEYCHAIN_SERVICE!r}, "
|
|
276
|
+
f"account {KEYCHAIN_ACCOUNT!r})")
|
|
277
|
+
raw = base64.b64decode(out.stdout.strip())
|
|
278
|
+
if len(raw) != 32:
|
|
279
|
+
raise SystemExit("keychain item is not a raw Ed25519 private key")
|
|
280
|
+
return raw
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def sign_main(argv=None, *, private_key_loader=keychain_private_key) -> int:
|
|
284
|
+
ap = argparse.ArgumentParser(
|
|
285
|
+
prog="hyperclaw-sign-qualification",
|
|
286
|
+
description="Write a Hyper-signed qualification record from a benchmark "
|
|
287
|
+
"results-v2.json (maintainers only; key from the macOS keychain).")
|
|
288
|
+
ap.add_argument("results", type=Path, help="benchmark results-v2.json")
|
|
289
|
+
ap.add_argument("--provider", required=True, help="HyperClaw provider id, e.g. anthropic")
|
|
290
|
+
ap.add_argument("--model", required=True, help="exact model id HyperClaw sends")
|
|
291
|
+
ap.add_argument("--suite-version", help="default: results.suite_version")
|
|
292
|
+
ap.add_argument("--harness", help="harness sha/version; default: results.harness_sha "
|
|
293
|
+
"or results.source_commit")
|
|
294
|
+
ap.add_argument("--expires-at", help="optional ISO-8601 expiry")
|
|
295
|
+
ap.add_argument("--out", type=Path, default=PACKAGED_DIR,
|
|
296
|
+
help=f"output directory (default: the package, {PACKAGED_DIR})")
|
|
297
|
+
a = ap.parse_args(argv)
|
|
298
|
+
|
|
299
|
+
results = json.loads(a.results.read_text(encoding="utf-8"))
|
|
300
|
+
model_info = results.get("model") if isinstance(results.get("model"), dict) else {}
|
|
301
|
+
if model_info.get("production") is False:
|
|
302
|
+
print("refused: results come from a non-production (mock) run", file=sys.stderr)
|
|
303
|
+
return 2
|
|
304
|
+
run_id = str(results.get("run_id") or "").strip()
|
|
305
|
+
suite = a.suite_version or str(results.get("suite_version") or "").strip()
|
|
306
|
+
harness = a.harness or str(results.get("harness_sha") or results.get("source_commit")
|
|
307
|
+
or "").strip()
|
|
308
|
+
missing = [n for n, v in (("run_id", run_id), ("--suite-version", suite),
|
|
309
|
+
("--harness", harness)) if not v]
|
|
310
|
+
if missing:
|
|
311
|
+
print(f"refused: missing {', '.join(missing)}", file=sys.stderr)
|
|
312
|
+
return 2
|
|
313
|
+
verdict = verdict_from_results(results)
|
|
314
|
+
rec = make_record(a.provider, a.model, verdict, suite_version=suite,
|
|
315
|
+
benchmark_run_id=run_id, harness=harness, expires_at=a.expires_at)
|
|
316
|
+
env = sign_record(rec, private_key_loader())
|
|
317
|
+
if verify_envelope(env) is None:
|
|
318
|
+
print("refused: signing key is not one of the package's trusted keys", file=sys.stderr)
|
|
319
|
+
return 3
|
|
320
|
+
p = write_envelope(env, a.out)
|
|
321
|
+
print(f"{verdict}: {a.provider}/{a.model} -> {p} (key {env['record']['key_id']})")
|
|
322
|
+
return 0
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def main(argv=None) -> int:
|
|
326
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
327
|
+
if argv[:1] == ["sign"]:
|
|
328
|
+
return sign_main(argv[1:])
|
|
329
|
+
if argv[:1] == ["verify"] and len(argv) == 2:
|
|
330
|
+
p = Path(argv[1])
|
|
331
|
+
rec = verify_envelope(json.loads(p.read_text(encoding="utf-8")), p)
|
|
332
|
+
print("VALID " + rec["verdict"] if rec else "IGNORED (see warning)")
|
|
333
|
+
return 0 if rec else 1
|
|
334
|
+
print("usage: python -m hyperclaw.qualification sign RESULTS --provider P --model M ...\n"
|
|
335
|
+
" python -m hyperclaw.qualification verify RECORD.json", file=sys.stderr)
|
|
336
|
+
return 2
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
if __name__ == "__main__":
|
|
340
|
+
logging.basicConfig()
|
|
341
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
HAMPPhfeVAwcXSgTBJDz4gLJ5x8KUP5weURs4WDR9ig=
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"record": {
|
|
3
|
+
"benchmark_run_id": "20261013-205229-hc1.2.3rc4.post2-anthropic-claude-sonnet-5-5",
|
|
4
|
+
"harness": "anthropic-qual-2026-10-03b",
|
|
5
|
+
"hyperclaw_version": "1.2.3rc4.post2",
|
|
6
|
+
"issued_at": "2026-10-14T03:54:09Z",
|
|
7
|
+
"key_id": "hyper-d292fd127cef5a3e",
|
|
8
|
+
"model": "claude-sonnet-5-5",
|
|
9
|
+
"provider": "anthropic",
|
|
10
|
+
"suite_version": "hc-qual-13@235d29339fd8",
|
|
11
|
+
"verdict": "QUALIFIED"
|
|
12
|
+
},
|
|
13
|
+
"signature": "LoBWMZtwy1R1ieowq/OfPqHLqL/ZdvtkZW98iT509JIYrDeEFOpZ/nhOnbpZGdTzehu9tXgGBiG1pqGDnXSbAw=="
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Whether raw model reasoning may be shown to a person.
|
|
2
|
+
|
|
3
|
+
Vaughn P1 (2026-10-07): raw reasoning can leak internal instruction wording,
|
|
4
|
+
so until a reasoning filter lands it is OFF by default on every surface (the
|
|
5
|
+
Telegram 💠preview, the dashboard reasoning stream) and FORCED OFF on a
|
|
6
|
+
Hyper-deployed install (HYPERCLAW_DEPLOYMENT=hyper). An owner on a standard
|
|
7
|
+
install may opt in explicitly:
|
|
8
|
+
|
|
9
|
+
HYPERCLAW_SHOW_REASONING=1 every surface
|
|
10
|
+
TELEGRAM_SHOW_THINKING=1 Telegram preview only (older setting)
|
|
11
|
+
|
|
12
|
+
Reasoning is still CLASSIFIED (hyperclaw.oai_stream) so it never becomes the
|
|
13
|
+
answer; this only decides whether it is displayed.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
_TRUE = ("1", "true", "yes", "on")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _flag(name: str) -> bool:
|
|
21
|
+
return os.environ.get(name, "").strip().lower() in _TRUE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def show_reasoning(surface: str = "") -> bool:
|
|
25
|
+
from hyperclaw.deployment import is_hyper
|
|
26
|
+
try:
|
|
27
|
+
if is_hyper():
|
|
28
|
+
return False
|
|
29
|
+
except Exception:
|
|
30
|
+
return False # deployment unknown: fail closed
|
|
31
|
+
if _flag("HYPERCLAW_SHOW_REASONING"):
|
|
32
|
+
return True
|
|
33
|
+
if surface == "telegram" and _flag("TELEGRAM_SHOW_THINKING"):
|
|
34
|
+
return True
|
|
35
|
+
return False
|