Skip to content

Commit da577cc

Browse files
Merge pull request #5 from Helixar-AI/feat/hdp-crewai-package
feat: add hdp-crewai Python middleware package
2 parents 4bfc46e + 16789af commit da577cc

7 files changed

Lines changed: 1082 additions & 0 deletions

File tree

packages/hdp-crewai/pyproject.toml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "hdp-crewai"
7+
version = "0.1.0"
8+
description = "HDP (Human Delegation Provenance) middleware for CrewAI — cryptographic audit trail for multi-agent task delegation"
9+
readme = "README.md"
10+
license = { text = "MIT" }
11+
requires-python = ">=3.10"
12+
dependencies = [
13+
"crewai>=0.80.0",
14+
"cryptography>=42.0.0",
15+
"jcs>=0.2.1",
16+
]
17+
18+
[project.optional-dependencies]
19+
dev = [
20+
"pytest>=8.0.0",
21+
"pytest-asyncio>=0.23.0",
22+
]
23+
24+
[project.urls]
25+
Homepage = "https://github.com/Helixar-AI/HDP"
26+
Repository = "https://github.com/Helixar-AI/HDP"
27+
Issues = "https://github.com/crewAIInc/crewAI/issues/5102"
28+
29+
[tool.hatch.build.targets.wheel]
30+
packages = ["src/hdp_crewai"]
31+
32+
[tool.pytest.ini_options]
33+
asyncio_mode = "auto"
34+
testpaths = ["tests"]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""hdp-crewai — HDP delegation provenance middleware for CrewAI."""
2+
3+
from ._types import HdpPrincipal, HdpScope, HdpToken, HopRecord, DataClassification
4+
from .middleware import HdpMiddleware, ScopePolicy, HDPScopeViolationError
5+
from .verify import verify_chain, VerificationResult, HopVerification
6+
7+
__all__ = [
8+
"HdpMiddleware",
9+
"ScopePolicy",
10+
"HDPScopeViolationError",
11+
"HdpPrincipal",
12+
"HdpScope",
13+
"HdpToken",
14+
"HopRecord",
15+
"DataClassification",
16+
"verify_chain",
17+
"VerificationResult",
18+
"HopVerification",
19+
]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Cryptographic primitives for HDP — Ed25519 signing/verification with RFC 8785 canonical JSON.
2+
3+
Matches the signing scheme in the TypeScript SDK (src/crypto/sign.ts + src/crypto/verify.ts):
4+
- Root: canonicalize({header, principal, scope}) → Ed25519 → base64url
5+
- Hop: canonicalize({chain: [...], root_sig: <value>}) → Ed25519 → base64url
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import base64
11+
from typing import Any
12+
13+
import jcs
14+
from cryptography.exceptions import InvalidSignature
15+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
16+
17+
18+
def _b64url(sig_bytes: bytes) -> str:
19+
"""Encode bytes as unpadded base64url (matches Buffer.toString('base64url') in Node)."""
20+
return base64.urlsafe_b64encode(sig_bytes).rstrip(b"=").decode()
21+
22+
23+
def _canonicalize(obj: Any) -> bytes:
24+
"""RFC 8785 canonical JSON bytes."""
25+
return jcs.canonicalize(obj)
26+
27+
28+
def sign_root(unsigned_token: dict, private_key_bytes: bytes, kid: str) -> dict:
29+
"""Sign the root token over {header, principal, scope} and return a signature dict."""
30+
subset = {f: unsigned_token[f] for f in ["header", "principal", "scope"] if f in unsigned_token}
31+
message = _canonicalize(subset)
32+
key = Ed25519PrivateKey.from_private_bytes(private_key_bytes)
33+
sig_bytes = key.sign(message)
34+
return {
35+
"alg": "Ed25519",
36+
"kid": kid,
37+
"value": _b64url(sig_bytes),
38+
"signed_fields": ["header", "principal", "scope"],
39+
}
40+
41+
42+
def sign_hop(cumulative_chain: list[dict], root_sig_value: str, private_key_bytes: bytes) -> str:
43+
"""Sign a hop over the cumulative chain + root signature value."""
44+
payload = {"chain": cumulative_chain, "root_sig": root_sig_value}
45+
message = _canonicalize(payload)
46+
key = Ed25519PrivateKey.from_private_bytes(private_key_bytes)
47+
sig_bytes = key.sign(message)
48+
return _b64url(sig_bytes)
49+
50+
51+
def _b64url_decode(s: str) -> bytes:
52+
"""Decode unpadded base64url string to bytes."""
53+
padding = 4 - len(s) % 4
54+
return base64.urlsafe_b64decode(s + "=" * padding)
55+
56+
57+
def verify_root(token: dict, public_key: Ed25519PublicKey) -> bool:
58+
"""Verify the root signature over {header, principal, scope}."""
59+
try:
60+
subset = {f: token[f] for f in ["header", "principal", "scope"] if f in token}
61+
message = _canonicalize(subset)
62+
sig_bytes = _b64url_decode(token["signature"]["value"])
63+
public_key.verify(sig_bytes, message)
64+
return True
65+
except (InvalidSignature, KeyError, Exception):
66+
return False
67+
68+
69+
def verify_hop(cumulative_chain: list[dict], root_sig_value: str, hop_signature: str, public_key: Ed25519PublicKey) -> bool:
70+
"""Verify a single hop signature over the cumulative chain + root sig value."""
71+
try:
72+
payload = {"chain": cumulative_chain, "root_sig": root_sig_value}
73+
message = _canonicalize(payload)
74+
sig_bytes = _b64url_decode(hop_signature)
75+
public_key.verify(sig_bytes, message)
76+
return True
77+
except (InvalidSignature, Exception):
78+
return False
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Python types mirroring the HDP TypeScript SDK schema."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass, field
6+
from typing import Any, Literal, Optional
7+
8+
DataClassification = Literal["public", "internal", "confidential", "restricted"]
9+
AgentType = Literal["orchestrator", "sub-agent", "tool-executor", "custom"]
10+
PrincipalIdType = Literal["email", "uuid", "did", "poh", "opaque"]
11+
12+
13+
@dataclass
14+
class HdpHeader:
15+
token_id: str
16+
issued_at: int
17+
expires_at: int
18+
session_id: str
19+
version: str = "0.1"
20+
parent_token_id: Optional[str] = None
21+
22+
23+
@dataclass
24+
class HdpPrincipal:
25+
id: str
26+
id_type: PrincipalIdType
27+
display_name: Optional[str] = None
28+
metadata: Optional[dict[str, Any]] = None
29+
30+
31+
@dataclass
32+
class HdpScope:
33+
intent: str
34+
data_classification: DataClassification
35+
network_egress: bool
36+
persistence: bool
37+
authorized_tools: Optional[list[str]] = None
38+
authorized_resources: Optional[list[str]] = None
39+
max_hops: Optional[int] = None
40+
41+
42+
@dataclass
43+
class HdpSignature:
44+
alg: str
45+
kid: str
46+
value: str
47+
signed_fields: list[str] = field(default_factory=lambda: ["header", "principal", "scope"])
48+
49+
50+
@dataclass
51+
class HopRecord:
52+
seq: int
53+
agent_id: str
54+
agent_type: AgentType
55+
timestamp: int
56+
action_summary: str
57+
parent_hop: int
58+
hop_signature: str
59+
agent_fingerprint: Optional[str] = None
60+
61+
62+
@dataclass
63+
class HdpToken:
64+
hdp: str
65+
header: HdpHeader
66+
principal: HdpPrincipal
67+
scope: HdpScope
68+
chain: list[HopRecord]
69+
signature: HdpSignature

0 commit comments

Comments
 (0)