hypermind 0.11.0__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.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Cloud KMS backends for the v0.10.4 production profile (T6-03).
|
|
2
|
+
|
|
3
|
+
Each backend implements :class:`hypermind.crypto.kms.Signer` and lives in
|
|
4
|
+
its own module so the cloud SDK stays an *optional* import. Calling
|
|
5
|
+
:class:`AWSKMSSigner` / :class:`GCPKMSSigner` / :class:`AzureKMSSigner` /
|
|
6
|
+
:class:`VaultTransitSigner` without the matching extra installed raises
|
|
7
|
+
:class:`KMSBackendNotInstalled` with the exact ``pip install`` invocation.
|
|
8
|
+
|
|
9
|
+
Backends:
|
|
10
|
+
|
|
11
|
+
* AWS — ``hypermind[kms-aws]`` → ``boto3``
|
|
12
|
+
* GCP — ``hypermind[kms-gcp]`` → ``google-cloud-kms``
|
|
13
|
+
* Azure — ``hypermind[kms-azure]`` → ``azure-keyvault-keys``, ``azure-identity``
|
|
14
|
+
* Vault — ``hypermind[kms-vault]`` → ``hvac``
|
|
15
|
+
|
|
16
|
+
Real cloud credentials are explicitly out of scope for v0.10.4 — see
|
|
17
|
+
``§External & human deliverables`` in ROADMAP.md. Each backend raises
|
|
18
|
+
``KMSCredentialsMissing`` early when the documented env vars are absent
|
|
19
|
+
so production wiring fails fast and clearly.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KMSBackendNotInstalled(RuntimeError):
|
|
26
|
+
"""Raised when a cloud SDK extra is missing.
|
|
27
|
+
|
|
28
|
+
Inherits :class:`RuntimeError` (not :class:`ImportError`) so the
|
|
29
|
+
``hypermind.crypto.kms_backends`` package itself imports cleanly even
|
|
30
|
+
when no cloud SDK is installed — only *use* of a backend fails.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, backend: str, extra: str) -> None:
|
|
34
|
+
super().__init__(
|
|
35
|
+
f"{backend} KMS backend requires an optional dependency; install via "
|
|
36
|
+
f"`pip install 'hypermind[{extra}]'` (or `pip install 'hypermind[kms-all]'`)"
|
|
37
|
+
)
|
|
38
|
+
self.backend = backend
|
|
39
|
+
self.extra = extra
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class KMSCredentialsMissing(RuntimeError):
|
|
43
|
+
"""Raised when a backend is constructed without required credentials.
|
|
44
|
+
|
|
45
|
+
The message names the missing env var(s) so operators can self-resolve.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, backend: str, missing: list[str]) -> None:
|
|
49
|
+
super().__init__(
|
|
50
|
+
f"{backend} KMS backend missing required credentials: {', '.join(missing)}"
|
|
51
|
+
)
|
|
52
|
+
self.backend = backend
|
|
53
|
+
self.missing = missing
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["KMSBackendNotInstalled", "KMSCredentialsMissing"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""AWS KMS Signer (T6-03).
|
|
2
|
+
|
|
3
|
+
Lazy-imports ``boto3``. The HyperMind library MUST be importable without
|
|
4
|
+
``boto3`` installed; only constructing :class:`AWSKMSSigner` triggers the
|
|
5
|
+
import (and raises :class:`KMSBackendNotInstalled` if ``boto3`` is absent).
|
|
6
|
+
|
|
7
|
+
Production wiring (real AWS credentials, KMS key ARN, signature DER
|
|
8
|
+
unwrap) is out of scope for v0.10.4 — see ROADMAP §External & human
|
|
9
|
+
deliverables. This file implements the contract and the fast-fail path
|
|
10
|
+
so downstream code can program against the same Signer Protocol it uses
|
|
11
|
+
for ``LocalSigner``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from hypermind.crypto.kms_backends import (
|
|
20
|
+
KMSBackendNotInstalled,
|
|
21
|
+
KMSCredentialsMissing,
|
|
22
|
+
)
|
|
23
|
+
from hypermind.crypto.signing import ALG_ED25519
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _try_import_boto3() -> Any:
|
|
27
|
+
try:
|
|
28
|
+
import boto3 # type: ignore[import-not-found]
|
|
29
|
+
|
|
30
|
+
return boto3
|
|
31
|
+
except ImportError as exc:
|
|
32
|
+
raise KMSBackendNotInstalled("AWS", "kms-aws") from exc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AWSKMSSigner:
|
|
36
|
+
"""Signer backed by AWS KMS ``Sign`` API.
|
|
37
|
+
|
|
38
|
+
Construction parameters
|
|
39
|
+
-----------------------
|
|
40
|
+
key_id
|
|
41
|
+
AWS KMS Key ID or ARN (``arn:aws:kms:<region>:<acct>:key/<uuid>``).
|
|
42
|
+
published_kid
|
|
43
|
+
The 32-byte Ed25519 public-key half pinned at construction. Pulled
|
|
44
|
+
out-of-band from ``GetPublicKey`` and stored alongside the agent's
|
|
45
|
+
published identity. The Signer refuses to ``sign()`` if rotation
|
|
46
|
+
invalidates this pin.
|
|
47
|
+
region
|
|
48
|
+
Optional AWS region. Falls back to the standard boto3 resolution
|
|
49
|
+
chain (``AWS_REGION`` env var, ``~/.aws/config``).
|
|
50
|
+
boto3_session
|
|
51
|
+
Optional pre-built :class:`boto3.Session`; useful for tests and
|
|
52
|
+
for SDK consumers that already manage a session.
|
|
53
|
+
|
|
54
|
+
Required env vars (when no explicit session is supplied)
|
|
55
|
+
--------------------------------------------------------
|
|
56
|
+
Either ``AWS_ACCESS_KEY_ID`` + ``AWS_SECRET_ACCESS_KEY`` or
|
|
57
|
+
``AWS_PROFILE``. ``KMSCredentialsMissing`` is raised at *first sign()*
|
|
58
|
+
when neither is configured — construction stays cheap so unit tests
|
|
59
|
+
can introspect the Signer without hitting AWS.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
key_id: str,
|
|
66
|
+
published_kid: bytes,
|
|
67
|
+
region: str | None = None,
|
|
68
|
+
boto3_session: Any | None = None,
|
|
69
|
+
alg: int = ALG_ED25519,
|
|
70
|
+
) -> None:
|
|
71
|
+
if len(published_kid) != 32:
|
|
72
|
+
raise ValueError("published_kid must be 32 bytes (Ed25519 public)")
|
|
73
|
+
self._key_id = key_id
|
|
74
|
+
self._kid = published_kid
|
|
75
|
+
self._alg = alg
|
|
76
|
+
self._region = region
|
|
77
|
+
self._session = boto3_session
|
|
78
|
+
self._client: Any | None = None
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def public_key(self) -> bytes:
|
|
82
|
+
return self._kid
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def kid(self) -> bytes:
|
|
86
|
+
return self._kid
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def alg(self) -> int:
|
|
90
|
+
return self._alg
|
|
91
|
+
|
|
92
|
+
def _client_or_create(self) -> Any:
|
|
93
|
+
if self._client is not None:
|
|
94
|
+
return self._client
|
|
95
|
+
boto3 = _try_import_boto3()
|
|
96
|
+
if self._session is None:
|
|
97
|
+
missing: list[str] = []
|
|
98
|
+
if not (
|
|
99
|
+
os.getenv("AWS_ACCESS_KEY_ID") and os.getenv("AWS_SECRET_ACCESS_KEY")
|
|
100
|
+
) and not os.getenv("AWS_PROFILE"):
|
|
101
|
+
missing.append("AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (or AWS_PROFILE)")
|
|
102
|
+
if missing:
|
|
103
|
+
raise KMSCredentialsMissing("AWS", missing)
|
|
104
|
+
self._session = boto3.Session(region_name=self._region)
|
|
105
|
+
self._client = self._session.client("kms")
|
|
106
|
+
return self._client
|
|
107
|
+
|
|
108
|
+
def sign(self, message: bytes) -> bytes: # pragma: no cover - real AWS path
|
|
109
|
+
client = self._client_or_create()
|
|
110
|
+
response = client.sign(
|
|
111
|
+
KeyId=self._key_id,
|
|
112
|
+
Message=message,
|
|
113
|
+
MessageType="RAW",
|
|
114
|
+
SigningAlgorithm="EDDSA",
|
|
115
|
+
)
|
|
116
|
+
sig = response["Signature"]
|
|
117
|
+
# AWS returns DER-wrapped ECDSA but raw Ed25519; defensive normalise.
|
|
118
|
+
if not isinstance(sig, (bytes, bytearray)):
|
|
119
|
+
raise RuntimeError("AWS KMS returned non-bytes Signature")
|
|
120
|
+
return bytes(sig)
|
|
121
|
+
|
|
122
|
+
def __repr__(self) -> str:
|
|
123
|
+
return (
|
|
124
|
+
f"AWSKMSSigner(key_id={self._key_id!r}, kid={self._kid.hex()[:16]}..., alg={self._alg})"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = ["AWSKMSSigner"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Azure Key Vault Signer (T6-03).
|
|
2
|
+
|
|
3
|
+
Lazy-imports ``azure-keyvault-keys`` and ``azure-identity``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from hypermind.crypto.kms_backends import (
|
|
12
|
+
KMSBackendNotInstalled,
|
|
13
|
+
KMSCredentialsMissing,
|
|
14
|
+
)
|
|
15
|
+
from hypermind.crypto.signing import ALG_ED25519
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _try_import_azure() -> tuple[Any, Any, Any]:
|
|
19
|
+
try:
|
|
20
|
+
from azure.identity import DefaultAzureCredential # type: ignore[import-not-found]
|
|
21
|
+
from azure.keyvault.keys.crypto import ( # type: ignore[import-not-found]
|
|
22
|
+
CryptographyClient,
|
|
23
|
+
SignatureAlgorithm,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return DefaultAzureCredential, CryptographyClient, SignatureAlgorithm
|
|
27
|
+
except ImportError as exc:
|
|
28
|
+
raise KMSBackendNotInstalled("Azure", "kms-azure") from exc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AzureKMSSigner:
|
|
32
|
+
"""Signer backed by Azure Key Vault ``sign`` operation.
|
|
33
|
+
|
|
34
|
+
Construction parameters
|
|
35
|
+
-----------------------
|
|
36
|
+
key_id
|
|
37
|
+
Full Azure Key Vault key identifier —
|
|
38
|
+
``https://<vault>.vault.azure.net/keys/<name>/<version>``.
|
|
39
|
+
published_kid
|
|
40
|
+
Pinned 32-byte Ed25519 public-key half.
|
|
41
|
+
|
|
42
|
+
Required env vars
|
|
43
|
+
-----------------
|
|
44
|
+
Standard Azure auth chain — at least one of:
|
|
45
|
+
* ``AZURE_TENANT_ID`` + ``AZURE_CLIENT_ID`` + ``AZURE_CLIENT_SECRET``
|
|
46
|
+
* managed-identity / workload-identity environment
|
|
47
|
+
Validated at first ``sign()`` call.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
key_id: str,
|
|
54
|
+
published_kid: bytes,
|
|
55
|
+
credential: Any | None = None,
|
|
56
|
+
alg: int = ALG_ED25519,
|
|
57
|
+
) -> None:
|
|
58
|
+
if len(published_kid) != 32:
|
|
59
|
+
raise ValueError("published_kid must be 32 bytes (Ed25519 public)")
|
|
60
|
+
self._key_id = key_id
|
|
61
|
+
self._kid = published_kid
|
|
62
|
+
self._alg = alg
|
|
63
|
+
self._credential = credential
|
|
64
|
+
self._client: Any | None = None
|
|
65
|
+
self._sig_alg: Any | None = None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def public_key(self) -> bytes:
|
|
69
|
+
return self._kid
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def kid(self) -> bytes:
|
|
73
|
+
return self._kid
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def alg(self) -> int:
|
|
77
|
+
return self._alg
|
|
78
|
+
|
|
79
|
+
def _client_or_create(self) -> Any:
|
|
80
|
+
if self._client is not None:
|
|
81
|
+
return self._client
|
|
82
|
+
DefaultAzureCredential, CryptographyClient, SignatureAlgorithm = _try_import_azure()
|
|
83
|
+
if self._credential is None:
|
|
84
|
+
if not (
|
|
85
|
+
os.getenv("AZURE_TENANT_ID")
|
|
86
|
+
or os.getenv("AZURE_CLIENT_ID")
|
|
87
|
+
or os.getenv("MSI_ENDPOINT")
|
|
88
|
+
or os.getenv("IDENTITY_ENDPOINT")
|
|
89
|
+
):
|
|
90
|
+
raise KMSCredentialsMissing(
|
|
91
|
+
"Azure",
|
|
92
|
+
["AZURE_TENANT_ID/AZURE_CLIENT_ID or managed-identity"],
|
|
93
|
+
)
|
|
94
|
+
self._credential = DefaultAzureCredential()
|
|
95
|
+
self._client = CryptographyClient(self._key_id, credential=self._credential)
|
|
96
|
+
# Azure exposes EdDSA via SignatureAlgorithm.eddsa.
|
|
97
|
+
self._sig_alg = getattr(SignatureAlgorithm, "eddsa", None)
|
|
98
|
+
return self._client
|
|
99
|
+
|
|
100
|
+
def sign(self, message: bytes) -> bytes: # pragma: no cover - real Azure path
|
|
101
|
+
client = self._client_or_create()
|
|
102
|
+
if self._sig_alg is None:
|
|
103
|
+
raise RuntimeError("Azure SDK does not expose SignatureAlgorithm.eddsa")
|
|
104
|
+
result = client.sign(self._sig_alg, message)
|
|
105
|
+
sig = result.signature
|
|
106
|
+
if not isinstance(sig, (bytes, bytearray)):
|
|
107
|
+
raise RuntimeError("Azure Key Vault returned non-bytes signature")
|
|
108
|
+
return bytes(sig)
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
return f"AzureKMSSigner(key_id={self._key_id!r}, kid={self._kid.hex()[:16]}..., alg={self._alg})"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = ["AzureKMSSigner"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""GCP Cloud KMS Signer (T6-03).
|
|
2
|
+
|
|
3
|
+
Lazy-imports ``google-cloud-kms``. See ``aws.py`` for the lazy-import
|
|
4
|
+
discipline rationale.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from hypermind.crypto.kms_backends import (
|
|
13
|
+
KMSBackendNotInstalled,
|
|
14
|
+
KMSCredentialsMissing,
|
|
15
|
+
)
|
|
16
|
+
from hypermind.crypto.signing import ALG_ED25519
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _try_import_kms() -> Any:
|
|
20
|
+
try:
|
|
21
|
+
from google.cloud import kms # type: ignore[import-not-found]
|
|
22
|
+
|
|
23
|
+
return kms
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
raise KMSBackendNotInstalled("GCP", "kms-gcp") from exc
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GCPKMSSigner:
|
|
29
|
+
"""Signer backed by GCP Cloud KMS ``AsymmetricSign`` RPC.
|
|
30
|
+
|
|
31
|
+
Construction parameters
|
|
32
|
+
-----------------------
|
|
33
|
+
key_uri
|
|
34
|
+
GCP key version resource name —
|
|
35
|
+
``projects/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k>/cryptoKeyVersions/<v>``.
|
|
36
|
+
published_kid
|
|
37
|
+
Pinned 32-byte Ed25519 public-key half (see ``aws.py``).
|
|
38
|
+
|
|
39
|
+
Required env vars
|
|
40
|
+
-----------------
|
|
41
|
+
``GOOGLE_APPLICATION_CREDENTIALS`` pointing at a service-account JSON,
|
|
42
|
+
or a workload-identity environment (GKE, Cloud Run). Validated at
|
|
43
|
+
first ``sign()`` call.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
key_uri: str,
|
|
50
|
+
published_kid: bytes,
|
|
51
|
+
client: Any | None = None,
|
|
52
|
+
alg: int = ALG_ED25519,
|
|
53
|
+
) -> None:
|
|
54
|
+
if len(published_kid) != 32:
|
|
55
|
+
raise ValueError("published_kid must be 32 bytes (Ed25519 public)")
|
|
56
|
+
self._key_uri = key_uri
|
|
57
|
+
self._kid = published_kid
|
|
58
|
+
self._alg = alg
|
|
59
|
+
self._client = client
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def public_key(self) -> bytes:
|
|
63
|
+
return self._kid
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def kid(self) -> bytes:
|
|
67
|
+
return self._kid
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def alg(self) -> int:
|
|
71
|
+
return self._alg
|
|
72
|
+
|
|
73
|
+
def _client_or_create(self) -> Any:
|
|
74
|
+
if self._client is not None:
|
|
75
|
+
return self._client
|
|
76
|
+
kms = _try_import_kms()
|
|
77
|
+
if not (os.getenv("GOOGLE_APPLICATION_CREDENTIALS") or os.getenv("GOOGLE_CLOUD_PROJECT")):
|
|
78
|
+
raise KMSCredentialsMissing(
|
|
79
|
+
"GCP",
|
|
80
|
+
["GOOGLE_APPLICATION_CREDENTIALS or workload-identity (GOOGLE_CLOUD_PROJECT)"],
|
|
81
|
+
)
|
|
82
|
+
self._client = kms.KeyManagementServiceClient()
|
|
83
|
+
return self._client
|
|
84
|
+
|
|
85
|
+
def sign(self, message: bytes) -> bytes: # pragma: no cover - real GCP path
|
|
86
|
+
client = self._client_or_create()
|
|
87
|
+
response = client.asymmetric_sign(request={"name": self._key_uri, "data": message})
|
|
88
|
+
sig = response.signature
|
|
89
|
+
if not isinstance(sig, (bytes, bytearray)):
|
|
90
|
+
raise RuntimeError("GCP KMS returned non-bytes signature")
|
|
91
|
+
return bytes(sig)
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
return f"GCPKMSSigner(key_uri={self._key_uri!r}, kid={self._kid.hex()[:16]}..., alg={self._alg})"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["GCPKMSSigner"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""HashiCorp Vault Transit Signer (T6-03).
|
|
2
|
+
|
|
3
|
+
Lazy-imports ``hvac``. Uses the Transit Secrets Engine ``sign/<key>``
|
|
4
|
+
endpoint with Ed25519 keys.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from hypermind.crypto.kms_backends import (
|
|
14
|
+
KMSBackendNotInstalled,
|
|
15
|
+
KMSCredentialsMissing,
|
|
16
|
+
)
|
|
17
|
+
from hypermind.crypto.signing import ALG_ED25519
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _try_import_hvac() -> Any:
|
|
21
|
+
try:
|
|
22
|
+
import hvac # type: ignore[import-not-found]
|
|
23
|
+
|
|
24
|
+
return hvac
|
|
25
|
+
except ImportError as exc:
|
|
26
|
+
raise KMSBackendNotInstalled("Vault", "kms-vault") from exc
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VaultTransitSigner:
|
|
30
|
+
"""Signer backed by HashiCorp Vault Transit Secrets Engine.
|
|
31
|
+
|
|
32
|
+
Construction parameters
|
|
33
|
+
-----------------------
|
|
34
|
+
vault_addr
|
|
35
|
+
Vault server URL (``https://vault.internal:8200``). Falls back to
|
|
36
|
+
the ``VAULT_ADDR`` env var.
|
|
37
|
+
key_name
|
|
38
|
+
Transit key name (the ``<key>`` in ``transit/keys/<key>``).
|
|
39
|
+
published_kid
|
|
40
|
+
Pinned 32-byte Ed25519 public-key half.
|
|
41
|
+
transit_mount
|
|
42
|
+
Transit mount path (default: ``transit``).
|
|
43
|
+
token
|
|
44
|
+
Vault token. Falls back to ``VAULT_TOKEN``.
|
|
45
|
+
|
|
46
|
+
Required env vars (when not supplied via constructor)
|
|
47
|
+
-----------------------------------------------------
|
|
48
|
+
``VAULT_ADDR`` and ``VAULT_TOKEN`` (or an AppRole / k8s auth token
|
|
49
|
+
pre-configured on the supplied ``hvac.Client``). Validated at first
|
|
50
|
+
``sign()`` call.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
key_name: str,
|
|
57
|
+
published_kid: bytes,
|
|
58
|
+
vault_addr: str | None = None,
|
|
59
|
+
transit_mount: str = "transit",
|
|
60
|
+
token: str | None = None,
|
|
61
|
+
client: Any | None = None,
|
|
62
|
+
alg: int = ALG_ED25519,
|
|
63
|
+
) -> None:
|
|
64
|
+
if len(published_kid) != 32:
|
|
65
|
+
raise ValueError("published_kid must be 32 bytes (Ed25519 public)")
|
|
66
|
+
self._key_name = key_name
|
|
67
|
+
self._kid = published_kid
|
|
68
|
+
self._alg = alg
|
|
69
|
+
self._vault_addr = vault_addr
|
|
70
|
+
self._mount = transit_mount
|
|
71
|
+
self._token = token
|
|
72
|
+
self._client = client
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def public_key(self) -> bytes:
|
|
76
|
+
return self._kid
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def kid(self) -> bytes:
|
|
80
|
+
return self._kid
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def alg(self) -> int:
|
|
84
|
+
return self._alg
|
|
85
|
+
|
|
86
|
+
def _client_or_create(self) -> Any:
|
|
87
|
+
if self._client is not None:
|
|
88
|
+
return self._client
|
|
89
|
+
hvac = _try_import_hvac()
|
|
90
|
+
addr = self._vault_addr or os.getenv("VAULT_ADDR")
|
|
91
|
+
token = self._token or os.getenv("VAULT_TOKEN")
|
|
92
|
+
missing: list[str] = []
|
|
93
|
+
if not addr:
|
|
94
|
+
missing.append("VAULT_ADDR")
|
|
95
|
+
if not token:
|
|
96
|
+
missing.append("VAULT_TOKEN")
|
|
97
|
+
if missing:
|
|
98
|
+
raise KMSCredentialsMissing("Vault", missing)
|
|
99
|
+
self._client = hvac.Client(url=addr, token=token)
|
|
100
|
+
return self._client
|
|
101
|
+
|
|
102
|
+
def sign(self, message: bytes) -> bytes: # pragma: no cover - real Vault path
|
|
103
|
+
client = self._client_or_create()
|
|
104
|
+
response = client.secrets.transit.sign_data(
|
|
105
|
+
name=self._key_name,
|
|
106
|
+
hash_input=base64.b64encode(message).decode("ascii"),
|
|
107
|
+
mount_point=self._mount,
|
|
108
|
+
)
|
|
109
|
+
# Vault returns "vault:v1:<base64-sig>".
|
|
110
|
+
raw = response["data"]["signature"]
|
|
111
|
+
prefix, _, b64 = raw.rpartition(":")
|
|
112
|
+
if not prefix.startswith("vault:"):
|
|
113
|
+
raise RuntimeError(f"unexpected Vault signature envelope: {raw!r}")
|
|
114
|
+
return base64.b64decode(b64)
|
|
115
|
+
|
|
116
|
+
def __repr__(self) -> str:
|
|
117
|
+
return f"VaultTransitSigner(key={self._key_name!r}, kid={self._kid.hex()[:16]}..., alg={self._alg})"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = ["VaultTransitSigner"]
|