aroha 1.0.0__tar.gz

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.
aroha-1.0.0/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
aroha-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: aroha
3
+ Version: 1.0.0
4
+ Summary: Aroha Protocol — Python SDK for agent-to-agent discovery, cryptographic identity, and atomic transactions
5
+ Project-URL: Homepage, https://aroha-labs.com
6
+ Project-URL: Repository, https://github.com/projectmed99/aroha
7
+ Project-URL: Documentation, https://aroha-labs.com/docs
8
+ License: MIT
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: base58>=2.1.1
11
+ Requires-Dist: cryptography>=42.0.0
12
+ Requires-Dist: fastapi>=0.111.0
13
+ Requires-Dist: httpx>=0.27.0
14
+ Requires-Dist: pydantic>=2.7.0
15
+ Requires-Dist: uvicorn[standard]>=0.30.0
16
+ Requires-Dist: websockets>=12.0
17
+ Provides-Extra: autogen
18
+ Requires-Dist: pyautogen>=0.2.0; extra == 'autogen'
19
+ Provides-Extra: crewai
20
+ Requires-Dist: crewai>=0.28.0; extra == 'crewai'
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
23
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
24
+ Provides-Extra: langchain
25
+ Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
26
+ Requires-Dist: langchain>=0.2.0; extra == 'langchain'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # aroha
30
+
31
+ Python SDK for the Aroha Protocol — decentralized agent-to-agent communication.
aroha-1.0.0/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # aroha
2
+
3
+ Python SDK for the Aroha Protocol — decentralized agent-to-agent communication.
@@ -0,0 +1,33 @@
1
+ """
2
+ aroha — Aroha Protocol, Python SDK
3
+
4
+ aroha.identity → Layer 1 (DID, Ed25519, Verifiable Credentials)
5
+ aroha.crypto → Signing, verification, encryption
6
+ aroha.messages → Layer 3 (envelope builder/validator, all message types)
7
+ aroha.transport → Layer 0 (HTTP + WebSocket server/client)
8
+ aroha.orchestrator→ Layer 4 (saga engine, agent selector)
9
+ aroha.csn → Capability Specification Notation
10
+ aroha.mandate → Spending mandates / RBAC
11
+ aroha.reputation → Stake-weighted reputation engine
12
+
13
+ Framework bridges (optional — install extras):
14
+ aroha.bridges.langchain (pip install aroha[langchain])
15
+ aroha.bridges.crewai (pip install aroha[crewai])
16
+ aroha.bridges.autogen (pip install aroha[autogen])
17
+ """
18
+
19
+ from aroha.identity import generate_did, AgentKeyPair, DIDDocument
20
+ from aroha.messages import build_envelope, validate_envelope, ArohaEnvelope
21
+ from aroha.transport import ArohaServer, ArohaClient
22
+
23
+ __version__ = "1.0.0"
24
+ __all__ = [
25
+ "generate_did",
26
+ "AgentKeyPair",
27
+ "DIDDocument",
28
+ "build_envelope",
29
+ "validate_envelope",
30
+ "ArohaEnvelope",
31
+ "ArohaServer",
32
+ "ArohaClient",
33
+ ]
@@ -0,0 +1,5 @@
1
+ # Framework bridges — optional, installed via extras:
2
+ # pip install aroha[langchain] → aroha.bridges.langchain_bridge
3
+ # pip install aroha[crewai] → aroha.bridges.crewai_bridge
4
+ # pip install aroha[autogen] → aroha.bridges.autogen_bridge
5
+
@@ -0,0 +1,122 @@
1
+ """
2
+ Aroha ↔ AutoGen Bridge
3
+
4
+ Wraps Aroha capabilities as AutoGen FunctionTool objects so AutoGen agents
5
+ (including AssistantAgent, ConversableAgent) can call Aroha capabilities as
6
+ part of their conversations.
7
+
8
+ Usage:
9
+ from aroha.bridges.autogen_bridge import aroha_tools_for_autogen
10
+ from autogen import AssistantAgent, UserProxyAgent
11
+
12
+ tools = aroha_tools_for_autogen(
13
+ agents=discovered_agents,
14
+ capability_ids=["search-flights", "book-flight"],
15
+ orchestrator_did=identity.did,
16
+ private_key=identity.private_key,
17
+ client=ArohaClient(),
18
+ )
19
+
20
+ assistant = AssistantAgent("assistant", llm_config={"tools": tools})
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import json
27
+ from typing import Any
28
+
29
+ from aroha.messages import build_envelope, new_correlation_id
30
+ from aroha.transport import ArohaClient
31
+
32
+ try:
33
+ from autogen import FunctionTool
34
+ except ImportError:
35
+ raise ImportError("Install AutoGen support: pip install aroha[autogen]")
36
+
37
+
38
+ def aroha_tools_for_autogen(
39
+ agents: list[dict[str, Any]], # [{"did", "endpoint", "manifest"}]
40
+ capability_ids: list[str],
41
+ orchestrator_did: str,
42
+ private_key: Any, # Ed25519PrivateKey
43
+ client: ArohaClient,
44
+ ) -> list[FunctionTool]:
45
+ """
46
+ Convert discovered Aroha agent capabilities into AutoGen FunctionTool objects.
47
+
48
+ Args:
49
+ agents: List of discovered Aroha agents
50
+ capability_ids: Which capabilities to expose as tools
51
+ orchestrator_did: DID of the orchestrating agent
52
+ private_key: Ed25519PrivateKey of the orchestrator
53
+ client: ArohaClient instance
54
+
55
+ Returns:
56
+ List of FunctionTool objects ready to pass as ``llm_config["tools"]``
57
+ """
58
+ tools: list[FunctionTool] = []
59
+ correlation_id = new_correlation_id()
60
+
61
+ for agent in agents:
62
+ manifest = agent.get("manifest", {})
63
+ for cap in manifest.get("capabilities", []):
64
+ if cap["id"] not in capability_ids:
65
+ continue
66
+
67
+ # Capture loop variables for closure
68
+ cap_id = cap["id"]
69
+ agent_did = agent["did"]
70
+ agent_endpoint = agent["endpoint"]
71
+ description = cap.get("description", f"Aroha capability: {cap_id}")
72
+
73
+ def make_func(
74
+ _cap_id: str,
75
+ _agent_did: str,
76
+ _endpoint: str,
77
+ _corr_id: str,
78
+ _priv: Any,
79
+ _client: ArohaClient,
80
+ ):
81
+ async def _call_capability(**kwargs: Any) -> str:
82
+ envelope = build_envelope(
83
+ "ArohaRequest",
84
+ orchestrator_did,
85
+ _agent_did,
86
+ {"capability": _cap_id, "params": kwargs},
87
+ _corr_id,
88
+ _priv,
89
+ )
90
+ response = await _client.send(_endpoint, envelope)
91
+ if response is None:
92
+ return "Request accepted (async)"
93
+ if response.get("type") == "ArohaError":
94
+ return f"Error: {response['body'].get('message')}"
95
+ return json.dumps(response["body"].get("result", {}), indent=2)
96
+
97
+ def call_capability(**kwargs: Any) -> str:
98
+ """Synchronous wrapper for AutoGen's function calling."""
99
+ return asyncio.run(_call_capability(**kwargs))
100
+
101
+ call_capability.__name__ = "aroha_" + _cap_id.replace("-", "_")
102
+ call_capability.__doc__ = description
103
+ return call_capability
104
+
105
+ func = make_func(
106
+ cap_id, agent_did, agent_endpoint,
107
+ correlation_id, private_key, client,
108
+ )
109
+
110
+ # Build a minimal JSON Schema for the input params from the manifest
111
+ input_schema = cap.get("inputSchema", {"type": "object", "properties": {}})
112
+
113
+ tool = FunctionTool(
114
+ func,
115
+ description=description,
116
+ name="aroha_" + cap_id.replace("-", "_"),
117
+ input_schema=input_schema,
118
+ )
119
+ tools.append(tool)
120
+
121
+ return tools
122
+
@@ -0,0 +1,87 @@
1
+ """
2
+ Aroha ↔ CrewAI Bridge
3
+
4
+ Wraps a Aroha provider agent as a CrewAI Tool so CrewAI agents
5
+ can call Aroha capabilities as part of their crew workflows.
6
+
7
+ Usage:
8
+ from aroha.bridges.crewai_bridge import ArohaCrewTool
9
+ from crewai import Agent, Task, Crew
10
+
11
+ flight_tool = ArohaCrewTool(
12
+ capability_id="search-flights",
13
+ agent_did="did:aroha:expedia-flights-v2",
14
+ agent_endpoint="https://agents.expedia.com/aroha/v1",
15
+ orchestrator_did=identity.did,
16
+ private_key=identity.private_key,
17
+ )
18
+
19
+ researcher = Agent(role="Travel Researcher", tools=[flight_tool], ...)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import json
26
+ from typing import Any
27
+
28
+ from aroha.messages import build_envelope, new_correlation_id
29
+ from aroha.transport import ArohaClient
30
+
31
+ try:
32
+ from crewai.tools import BaseTool as CrewBaseTool
33
+ except ImportError:
34
+ raise ImportError("Install CrewAI support: pip install aroha[crewai]")
35
+
36
+
37
+ class ArohaCrewTool(CrewBaseTool):
38
+ """CrewAI tool backed by a Aroha capability."""
39
+
40
+ name: str
41
+ description: str
42
+ agent_did: str
43
+ agent_endpoint: str
44
+ capability_id: str
45
+ orchestrator_did: str
46
+ _private_key: Any
47
+ _client: ArohaClient
48
+
49
+ def __init__(
50
+ self,
51
+ capability_id: str,
52
+ agent_did: str,
53
+ agent_endpoint: str,
54
+ orchestrator_did: str,
55
+ private_key: Any,
56
+ description: str = "",
57
+ ) -> None:
58
+ super().__init__(
59
+ name=f"aroha_{capability_id.replace('-', '_')}",
60
+ description=description or f"Aroha capability: {capability_id}",
61
+ )
62
+ self.capability_id = capability_id
63
+ self.agent_did = agent_did
64
+ self.agent_endpoint = agent_endpoint
65
+ self.orchestrator_did = orchestrator_did
66
+ self._private_key = private_key
67
+ self._client = ArohaClient()
68
+
69
+ def _run(self, **kwargs: Any) -> str:
70
+ return asyncio.run(self._execute(kwargs))
71
+
72
+ async def _execute(self, params: dict[str, Any]) -> str:
73
+ envelope = build_envelope(
74
+ "ArohaRequest",
75
+ self.orchestrator_did,
76
+ self.agent_did,
77
+ {"capability": self.capability_id, "params": params},
78
+ new_correlation_id(),
79
+ self._private_key,
80
+ )
81
+ response = await self._client.send(self.agent_endpoint, envelope)
82
+ if response is None:
83
+ return "Request accepted (async)"
84
+ if response.get("type") == "ArohaError":
85
+ return f"Error: {response['body'].get('message')}"
86
+ return json.dumps(response["body"].get("result", {}), indent=2)
87
+
@@ -0,0 +1,114 @@
1
+ """
2
+ Aroha ↔ LangChain Bridge
3
+
4
+ Exposes Aroha capabilities as LangChain Tools so any LangChain/LangGraph
5
+ agent can call them without knowing about the Aroha protocol.
6
+
7
+ Usage:
8
+ from aroha.bridges.langchain_bridge import aroha_tools_for_agent
9
+
10
+ tools = await aroha_tools_for_agent(
11
+ discovery=discovery,
12
+ capability_ids=["search-flights", "book-flight"],
13
+ orchestrator_did=identity.did,
14
+ private_key=identity.private_key,
15
+ client=ArohaClient(),
16
+ )
17
+ agent = create_react_agent(llm, tools)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import json
24
+ import uuid
25
+ from typing import Any, Optional, Type
26
+
27
+ from aroha.messages import build_envelope, new_correlation_id
28
+ from aroha.transport import ArohaClient
29
+
30
+ try:
31
+ from langchain_core.tools import BaseTool
32
+ from pydantic import BaseModel, Field
33
+ except ImportError:
34
+ raise ImportError("Install langchain support: pip install aroha[langchain]")
35
+
36
+
37
+ class ArohaCapabilityTool(BaseTool):
38
+ """A LangChain Tool backed by a Aroha capability."""
39
+
40
+ name: str
41
+ description: str
42
+ agent_did: str
43
+ agent_endpoint: str
44
+ capability_id: str
45
+ orchestrator_did: str
46
+ correlation_id: str
47
+ _private_key: Any # Ed25519PrivateKey
48
+ _client: Any # ArohaClient
49
+
50
+ class Config:
51
+ arbitrary_types_allowed = True
52
+
53
+ def _run(self, **kwargs: Any) -> str:
54
+ """Synchronous wrapper — runs the async send in a new event loop."""
55
+ return asyncio.run(self._arun(**kwargs))
56
+
57
+ async def _arun(self, **kwargs: Any) -> str:
58
+ envelope = build_envelope(
59
+ "ArohaRequest",
60
+ self.orchestrator_did,
61
+ self.agent_did,
62
+ {"capability": self.capability_id, "params": kwargs},
63
+ self.correlation_id,
64
+ self._private_key,
65
+ )
66
+ response = await self._client.send(self.agent_endpoint, envelope)
67
+ if response is None:
68
+ return "Agent accepted request (async — check WebSocket stream)"
69
+ if response.get("type") == "ArohaError":
70
+ return f"Error: {response['body'].get('message')}"
71
+ return json.dumps(response["body"].get("result", {}), indent=2)
72
+
73
+
74
+ async def aroha_tools_for_agent(
75
+ agents: list[dict[str, Any]], # [{"did", "endpoint", "manifest"}]
76
+ capability_ids: list[str],
77
+ orchestrator_did: str,
78
+ private_key: Any,
79
+ client: ArohaClient,
80
+ ) -> list[ArohaCapabilityTool]:
81
+ """
82
+ Convert Aroha agent capabilities into LangChain tools.
83
+
84
+ Args:
85
+ agents: List of discovered Aroha agents
86
+ capability_ids: Which capabilities to expose as tools
87
+ orchestrator_did: DID of the orchestrating agent
88
+ private_key: Ed25519PrivateKey of the orchestrator
89
+ client: ArohaClient instance
90
+ """
91
+ tools = []
92
+ correlation_id = new_correlation_id()
93
+
94
+ for agent in agents:
95
+ manifest = agent.get("manifest", {})
96
+ for cap in manifest.get("capabilities", []):
97
+ if cap["id"] not in capability_ids:
98
+ continue
99
+
100
+ tool = ArohaCapabilityTool(
101
+ name=f"aroha_{cap['id'].replace('-', '_')}",
102
+ description=cap.get("description", cap["id"]),
103
+ agent_did=agent["did"],
104
+ agent_endpoint=agent["endpoint"],
105
+ capability_id=cap["id"],
106
+ orchestrator_did=orchestrator_did,
107
+ correlation_id=correlation_id,
108
+ )
109
+ tool._private_key = private_key
110
+ tool._client = client
111
+ tools.append(tool)
112
+
113
+ return tools
114
+
@@ -0,0 +1,38 @@
1
+ import uuid
2
+ import httpx
3
+ from .envelope import build_envelope
4
+
5
+
6
+ class ArohaClient:
7
+ """Minimal async HTTP client for sending Aroha messages."""
8
+
9
+ def __init__(self, agent: dict, *, timeout: float = 30.0):
10
+ self.agent = agent
11
+ self.timeout = timeout
12
+
13
+ async def request(
14
+ self,
15
+ endpoint: str,
16
+ to_did: str,
17
+ capability: str,
18
+ params: dict,
19
+ *,
20
+ correlation_id: str | None = None,
21
+ ) -> dict:
22
+ cid = correlation_id or f"saga-{uuid.uuid4()}"
23
+ envelope = build_envelope(
24
+ "ArohaRequest",
25
+ self.agent["did"],
26
+ to_did,
27
+ {"capability": capability, "params": params},
28
+ cid,
29
+ self.agent["private_key"],
30
+ )
31
+ async with httpx.AsyncClient(timeout=self.timeout) as http:
32
+ res = await http.post(
33
+ endpoint,
34
+ json=envelope,
35
+ headers={"Content-Type": "application/json"},
36
+ )
37
+ res.raise_for_status()
38
+ return res.json()
@@ -0,0 +1,143 @@
1
+ """
2
+ Aroha Python SDK — Crypto: Signing + Encryption
3
+
4
+ Ed25519 signing (per spec: canonical JSON, keys sorted, proof excluded).
5
+ AES-256-GCM encryption for sensitive body fields (DIDComm-style).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import json
12
+ import os
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
17
+ Ed25519PrivateKey,
18
+ Ed25519PublicKey,
19
+ )
20
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
21
+ from cryptography.hazmat.primitives import hashes
22
+ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
23
+
24
+
25
+ # ── Signing ────────────────────────────────────────────────────────────────────
26
+
27
+ def sign_message(
28
+ message: dict[str, Any],
29
+ private_key: Ed25519PrivateKey,
30
+ verification_method_id: str,
31
+ ) -> dict[str, str]:
32
+ """
33
+ Sign a Aroha message. Returns a proof dict to embed in the envelope.
34
+
35
+ Per spec: signature covers canonical JSON with 'proof' field excluded,
36
+ all keys sorted lexicographically at every nesting level.
37
+ """
38
+ canonical = canonicalize_message(message)
39
+ signature = private_key.sign(canonical.encode("utf-8"))
40
+ proof_value = base64.urlsafe_b64encode(signature).rstrip(b"=").decode()
41
+
42
+ return {
43
+ "type": "Ed25519Signature2020",
44
+ "created": datetime.now(timezone.utc).isoformat(),
45
+ "verificationMethod": verification_method_id,
46
+ "proofPurpose": "authentication",
47
+ "proofValue": proof_value,
48
+ }
49
+
50
+
51
+ def verify_message_signature(
52
+ message: dict[str, Any],
53
+ public_key: Ed25519PublicKey,
54
+ ) -> bool:
55
+ """
56
+ Verify a Aroha message signature.
57
+ Returns True if valid, False otherwise.
58
+ """
59
+ proof = message.get("proof")
60
+ if not proof or not proof.get("proofValue"):
61
+ return False
62
+
63
+ canonical = canonicalize_message(message)
64
+ proof_value = proof["proofValue"]
65
+
66
+ # Restore base64url padding
67
+ padding = 4 - len(proof_value) % 4
68
+ if padding != 4:
69
+ proof_value += "=" * padding
70
+ signature = base64.urlsafe_b64decode(proof_value)
71
+
72
+ try:
73
+ public_key.verify(signature, canonical.encode("utf-8"))
74
+ return True
75
+ except Exception:
76
+ return False
77
+
78
+
79
+ def canonicalize_message(message: dict[str, Any]) -> str:
80
+ """
81
+ Produce canonical JSON string for signing.
82
+ Excludes 'proof' field, sorts all keys lexicographically.
83
+ """
84
+ without_proof = {k: v for k, v in message.items() if k != "proof"}
85
+ return json.dumps(_sort_keys_deep(without_proof), separators=(",", ":"))
86
+
87
+
88
+ def _sort_keys_deep(obj: Any) -> Any:
89
+ if isinstance(obj, dict):
90
+ return {k: _sort_keys_deep(v) for k, v in sorted(obj.items())}
91
+ if isinstance(obj, list):
92
+ return [_sort_keys_deep(item) for item in obj]
93
+ return obj
94
+
95
+
96
+ # ── Encryption (DIDComm-style AES-256-GCM) ────────────────────────────────────
97
+
98
+ def encrypt_body(plaintext: dict[str, Any], recipient_pub_bytes: bytes) -> dict[str, str]:
99
+ """
100
+ Encrypt a message body for a specific recipient.
101
+ Uses AES-256-GCM with a random key + IV.
102
+
103
+ Returns an EncryptedBody dict with alg, iv, ciphertext, tag.
104
+
105
+ Note: In production use full ECDH-ES+A256GCM. This implementation
106
+ uses a simplified symmetric approach suitable for testing.
107
+ For full DIDComm v2 compliance use the `didcomm` PyPI package.
108
+ """
109
+ key = os.urandom(32)
110
+ iv = os.urandom(12)
111
+ aesgcm = AESGCM(key)
112
+ payload = json.dumps(plaintext).encode("utf-8")
113
+ encrypted = aesgcm.encrypt(iv, payload, None)
114
+ ciphertext = encrypted[:-16]
115
+ tag = encrypted[-16:]
116
+
117
+ def b64(b: bytes) -> str:
118
+ return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
119
+
120
+ return {
121
+ "alg": "A256GCM",
122
+ "iv": b64(iv),
123
+ "ciphertext": b64(ciphertext),
124
+ "tag": b64(tag),
125
+ # In full ECDH-ES: encrypted_key and epk would be included here
126
+ "_key": b64(key), # DEV ONLY — remove in production
127
+ }
128
+
129
+
130
+ def decrypt_body(encrypted: dict[str, str], private_key_bytes: bytes) -> dict[str, Any]:
131
+ """Decrypt an encrypted message body."""
132
+ def b64d(s: str) -> bytes:
133
+ s += "=" * (4 - len(s) % 4)
134
+ return base64.urlsafe_b64decode(s)
135
+
136
+ key = b64d(encrypted["_key"]) # DEV ONLY
137
+ iv = b64d(encrypted["iv"])
138
+ ciphertext = b64d(encrypted["ciphertext"])
139
+ tag = b64d(encrypted["tag"])
140
+
141
+ aesgcm = AESGCM(key)
142
+ plaintext = aesgcm.decrypt(iv, ciphertext + tag, None)
143
+ return json.loads(plaintext.decode("utf-8"))