chore: consolidate local working changes

This commit is contained in:
Hanzo Dev
2026-07-21 19:16:37 -07:00
parent b54d30106d
commit 663390065e
56 changed files with 15697 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
rust:
name: Rust Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-action@stable
with:
components: rustfmt, clippy
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Cap'n Proto
run: |
sudo apt-get update
sudo apt-get install -y capnproto
- name: Check formatting
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Build
run: cargo build --all-features
- name: Run tests
run: cargo test --all-features --verbose
- name: Install cargo-tarpaulin
run: cargo install cargo-tarpaulin
- name: Coverage
run: cargo tarpaulin --all-features --out Xml --output-dir coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: coverage/cobertura.xml
flags: rust
fail_ci_if_error: false
python:
name: Python Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
working-directory: python
run: |
uv venv
uv pip install -e ".[dev]"
uv pip install pytest-cov
- name: Lint with ruff
working-directory: python
run: uv run ruff check src
- name: Type check with mypy
working-directory: python
run: uv run mypy src --ignore-missing-imports
- name: Run tests with coverage
working-directory: python
run: |
uv run pytest tests/ -v --cov=src/hanzo_zap --cov-report=xml --cov-report=term --cov-fail-under=80
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: python/coverage.xml
flags: python
fail_ci_if_error: false
typescript:
name: TypeScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
working-directory: typescript
run: pnpm install
- name: Lint
working-directory: typescript
run: pnpm lint
- name: Build
working-directory: typescript
run: pnpm build
- name: Run tests with coverage
working-directory: typescript
run: pnpm test -- --coverage --coverage.thresholds.lines=80
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: typescript/coverage/lcov.info
flags: typescript
fail_ci_if_error: false
coverage-gate:
name: Coverage Gate
needs: [rust, python, typescript]
runs-on: ubuntu-latest
steps:
- name: Coverage gate passed
run: echo "All coverage gates passed (80%+ required)"
+24
View File
@@ -0,0 +1,24 @@
# Rust
/target
Cargo.lock
# Python
__pycache__/
*.pyc
.venv/
*.egg-info/
dist/
.pytest_cache/
# TypeScript
node_modules/
dist/
*.tsbuildinfo
# IDE
.idea/
.vscode/
*.swp
# OS
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Hanzo AI Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+286
View File
@@ -0,0 +1,286 @@
# ZAP - Zero-Copy App Proto
> **Docs:** [ZAP wire spec](https://zap-proto.dev/docs/protocol) · part of the [ZAP Protocol](https://zap-proto.io); also: [Native ZAP RPC](https://zap-proto.dev/docs/protocols/native)
High-performance Cap'n Proto RPC for AI agent communication.
ZAP provides a unified protocol for connecting to and aggregating MCP (Model Context Protocol) servers, enabling efficient tool calling, resource access, and prompt management for AI agents.
**"Infinity Times Faster"** - When decode time is zero, the speedup is mathematically infinite.
## Features
- **Zero-copy Serialization**: Cap'n Proto wire format = memory format
- **Promise Pipelining**: N dependent calls in 1 round trip
- **Capability Security**: Possession = permission, no ambient authority
- **Multi-transport**: Unix sockets, TCP, TLS, QUIC, WebSocket, shared memory
- **MCP Gateway**: Aggregate multiple MCP servers behind a single endpoint
- **Agent Consensus**: Built-in metastable voting for multi-agent coordination
- **Post-Quantum Ready**: ML-KEM-768, ML-DSA-65, Ringtail signatures
- **Cross-language**: 17 language bindings
## Official Packages
| Package | Language | Install |
|---------|----------|---------|
| `zap-protocol` | Rust | `cargo add zap-protocol` |
| `zap-protocol` | Python | `pip install zap-protocol` |
| `@zap-protocol/zap` | TypeScript | `npm install @zap-protocol/zap` |
| `zap-protocol/zap-go` | Go | `go get github.com/zap-protocol/zap-go` |
## Language Bindings
All language bindings are maintained in the [zap-protocol](https://github.com/zap-protocol) GitHub organization:
| Language | Repository | Status |
|----------|------------|--------|
| **Rust** | [zap-protocol/zap](https://github.com/zap-protocol/zap) | Production |
| **Go** | [zap-protocol/zap-go](https://github.com/zap-protocol/zap-go) | Production |
| **Python** | [zap-protocol/zap-py](https://github.com/zap-protocol/zap-py) | Production |
| **JavaScript/TypeScript** | [zap-protocol/zap-js](https://github.com/zap-protocol/zap-js) | Production |
| **C++** | [zap-protocol/zap-cpp](https://github.com/zap-protocol/zap-cpp) | Stable |
| **C** | [zap-protocol/zap-c](https://github.com/zap-protocol/zap-c) | Stable |
| **C#** | [zap-protocol/zap-cs](https://github.com/zap-protocol/zap-cs) | Development |
| **Java** | [zap-protocol/zap-java](https://github.com/zap-protocol/zap-java) | Development |
| **Haskell** | [zap-protocol/zap-haskell](https://github.com/zap-protocol/zap-haskell) | Development |
| **OCaml** | [zap-protocol/zap-ocaml](https://github.com/zap-protocol/zap-ocaml) | Development |
| **Erlang** | [zap-protocol/zap-erlang](https://github.com/zap-protocol/zap-erlang) | Development |
| **D** | [zap-protocol/zap-d](https://github.com/zap-protocol/zap-d) | Development |
| **Lua** | [zap-protocol/zap-lua](https://github.com/zap-protocol/zap-lua) | Development |
| **Nim** | [zap-protocol/zap-nim](https://github.com/zap-protocol/zap-nim) | Development |
| **Ruby** | [zap-protocol/zap-ruby](https://github.com/zap-protocol/zap-ruby) | Development |
| **Scala** | [zap-protocol/zap-scala](https://github.com/zap-protocol/zap-scala) | Development |
## Quick Start
### Rust
```rust
use zap_protocol::{Client, Gateway};
// Connect to a ZAP gateway
let client = Client::connect("zap://localhost:9999").await?;
// List available tools
let tools = client.list_tools().await?;
// Call a tool
let result = client.call_tool("search", json!({"query": "hello"})).await?;
```
### Go
```go
import "github.com/zap-protocol/zap-go"
// Connect to a ZAP gateway
client, err := zap.Connect("zap://localhost:9999")
// List available tools
tools, err := client.ListTools(ctx)
// Call a tool
result, err := client.CallTool(ctx, "search", map[string]any{"query": "hello"})
```
### Python
```python
from zap import ZAP
# FastMCP-style decorator API
zap = ZAP("my-agent")
@zap.tool
def search(query: str) -> str:
"""Search for information."""
return f"Results for: {query}"
# Or connect as client
from zap import Client
client = await Client.connect("zap://localhost:9999")
tools = await client.list_tools()
```
### TypeScript
```typescript
import { Client, Gateway } from '@zap-protocol/zap';
// Connect to a ZAP gateway
const client = await Client.connect('zap://localhost:9999');
// List available tools
const tools = await client.listTools();
// Call a tool
const result = await client.callTool('search', { query: 'hello' });
```
## CLI Tools
### zap - Command Line Client
```bash
# List tools from a gateway
zap tools list
# Call a tool
zap call search --query "hello world"
# List resources
zap resources list
# Read a resource
zap read file:///path/to/file
```
### zapd - Gateway Daemon
```bash
# Start gateway with config file
zapd --config /etc/zap/config.toml
# Start with inline servers
zapd --server "stdio://npx @modelcontextprotocol/server-filesystem"
```
## Configuration
Create a `zap.toml` configuration file:
```toml
[gateway]
listen = "0.0.0.0"
port = 9999
log_level = "info"
[[servers]]
name = "filesystem"
transport = "stdio"
command = "npx"
args = ["@modelcontextprotocol/server-filesystem", "/path/to/files"]
[[servers]]
name = "database"
transport = "http"
url = "http://localhost:8080/mcp"
[[servers]]
name = "search"
transport = "websocket"
url = "ws://localhost:9000/ws"
```
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ AI Client │
│ (Claude, GPT, etc.) │
└──────────────────────────┬──────────────────────────────────┘
│ ZAP Protocol (Cap'n Proto RPC)
┌─────────────────────────────────────────────────────────────┐
│ ZAP Gateway │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Server Registry │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │Server A │ │Server B │ │Server C │ │Server D │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └───────┼────────────┼────────────┼────────────┼───────┘ │
│ │ │ │ │ │
└──────────┼────────────┼────────────┼────────────┼──────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ stdio │ │ HTTP │ │ WS │ │ Unix │
│ MCP │ │ MCP │ │ MCP │ │ Socket │
│ Server │ │ Server │ │ Server │ │ Server │
└────────┘ └────────┘ └────────┘ └────────┘
```
## Protocol
ZAP uses Cap'n Proto for efficient serialization and RPC:
```capnp
interface Zap {
# Server discovery
initialize @0 (info :ServerInfo) -> (info :ServerInfo);
# Tools
listTools @1 () -> (tools :List(Tool));
callTool @2 (name :Text, arguments :Text) -> (result :ToolResult);
# Resources
listResources @3 () -> (resources :List(Resource));
readResource @4 (uri :Text) -> (content :ResourceContent);
# Prompts
listPrompts @5 () -> (prompts :List(Prompt));
getPrompt @6 (name :Text, arguments :Text) -> (messages :List(PromptMessage));
}
```
## Development
### Rust
```bash
cd /path/to/hanzo-zap
cargo build
cargo test
```
### Python
```bash
cd /path/to/hanzo-zap/python
uv sync
uv run pytest
```
### TypeScript
```bash
cd /path/to/hanzo-zap/typescript
npm install
npm run build
npm test
```
## Tools
| Tool | Repository | Description |
|------|------------|-------------|
| **VS Code** | [zap-protocol/zap-vscode](https://github.com/zap-protocol/zap-vscode) | VS Code extension |
| **Vim** | [zap-protocol/zap-vim](https://github.com/zap-protocol/zap-vim) | Vim plugin |
| **IntelliJ** | [zap-protocol/zap-intellij](https://github.com/zap-protocol/zap-intellij) | JetBrains plugin |
| **LSP** | [zap-protocol/zap-lsp](https://github.com/zap-protocol/zap-lsp) | Language server |
| **Wireshark** | [zap-protocol/zap-wireshark](https://github.com/zap-protocol/zap-wireshark) | Protocol dissector |
## Documentation
Full documentation available at: https://zap.hanzo.ai
- [Quick Start](https://zap.hanzo.ai/docs/quickstart)
- [Architecture](https://zap.hanzo.ai/docs/architecture)
- [Protocol Deep Dive](https://zap.hanzo.ai/docs/protocol)
- [Transport Layer](https://zap.hanzo.ai/docs/transports)
- [Why ZAP over MCP?](https://zap.hanzo.ai/docs/concepts/why-zap)
## License
MIT OR Apache-2.0
## Links
- [GitHub Organization](https://github.com/zap-protocol)
- [Documentation](https://zap.hanzo.ai)
- [Hanzo AI](https://hanzo.ai)
- [HIP-007 Whitepaper](https://zap.hanzo.ai/docs/whitepaper)
+36
View File
@@ -0,0 +1,36 @@
# hanzo-zap
ZAP - Zero-Copy App Proto for Python
High-performance Cap'n Proto RPC for AI agent communication.
## Installation
```bash
pip install hanzo-zap
```
## Quick Start
```python
import asyncio
from hanzo_zap import Client
async def main():
client = await Client.connect("zap://localhost:9999")
tools = await client.list_tools()
result = await client.call_tool("search", {"query": "hello"})
asyncio.run(main())
```
## Features
- **Zero-copy serialization** via Cap'n Proto
- **Post-quantum cryptography** with ML-KEM and ML-DSA
- **W3C DID identity** for decentralized agent authentication
- **Agentic consensus** for trustless response voting
## License
MIT
+72
View File
@@ -0,0 +1,72 @@
[project]
name = "hanzo-zap"
version = "0.2.1"
description = "ZAP - Zero-Copy App Proto for Python"
readme = "README.md"
license = "MIT"
authors = [{ name = "Hanzo AI", email = "dev@hanzo.ai" }]
requires-python = ">=3.10"
keywords = ["capnproto", "rpc", "agents", "mcp", "ai", "hanzo"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries",
]
dependencies = [
"pycapnp>=2.0.0",
"anyio>=4.0",
"httpx>=0.27",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"pytest-cov>=5.0",
"ruff>=0.8",
"mypy>=1.13",
]
[project.urls]
Homepage = "https://hanzo.ai/zap"
Documentation = "https://hanzoai.github.io/zap/docs"
Repository = "https://github.com/hanzoai/zap"
[project.scripts]
zap = "hanzo_zap.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/hanzo_zap"]
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
[tool.mypy]
python_version = "3.10"
strict = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
pythonpath = ["src"]
[tool.coverage.run]
source = ["src/hanzo_zap"]
branch = true
[tool.coverage.report]
fail_under = 80
show_missing = true
@@ -0,0 +1,40 @@
"""
ZAP - Zero-Copy App Proto
High-performance Cap'n Proto RPC for AI agent communication.
Example:
>>> import asyncio
>>> from hanzo_zap import Client
>>>
>>> async def main():
... client = await Client.connect("zap://localhost:9999")
... tools = await client.list_tools()
... result = await client.call_tool("search", {"query": "hello"})
...
>>> asyncio.run(main())
"""
from .client import Client
from .server import Server
from .gateway import Gateway
from .config import Config, ServerConfig
from .error import ZapError
from . import crypto
from . import identity
from . import agent_consensus
__version__ = "0.2.1"
__all__ = [
"Client",
"Server",
"Gateway",
"Config",
"ServerConfig",
"ZapError",
"crypto",
"identity",
"agent_consensus",
]
DEFAULT_PORT = 9999
@@ -0,0 +1,269 @@
"""
Agentic consensus for response voting.
Agents vote on responses to queries. No trust needed - majority wins.
As long as majority are honest, you get correct results.
"""
from __future__ import annotations
import asyncio
import hashlib
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from .identity import Did
@dataclass
class Query:
"""A query submitted to the agent network."""
id: bytes
content: str
submitter: Did
timestamp: int
@classmethod
def create(cls, content: str, submitter: Did) -> "Query":
"""Create a new query with auto-generated ID."""
timestamp = int(time.time())
hasher = hashlib.blake2b(digest_size=32)
hasher.update(content.encode())
hasher.update(submitter.uri().encode())
hasher.update(timestamp.to_bytes(8, 'little'))
return cls(
id=hasher.digest(),
content=content,
submitter=submitter,
timestamp=timestamp,
)
@dataclass
class Response:
"""A response to a query."""
id: bytes
query_id: bytes
content: str
responder: Did
timestamp: int
@classmethod
def create(cls, query_id: bytes, content: str, responder: Did) -> "Response":
"""Create a new response with auto-generated ID."""
timestamp = int(time.time())
hasher = hashlib.blake2b(digest_size=32)
hasher.update(query_id)
hasher.update(content.encode())
hasher.update(responder.uri().encode())
hasher.update(timestamp.to_bytes(8, 'little'))
return cls(
id=hasher.digest(),
query_id=query_id,
content=content,
responder=responder,
timestamp=timestamp,
)
@dataclass
class ConsensusResult:
"""Result of consensus voting."""
response: Response
votes: int
total_voters: int
confidence: float
@dataclass
class QueryState:
"""Internal state for a query."""
query: Query
responses: Dict[bytes, Response] = field(default_factory=dict)
votes: Dict[bytes, List[Did]] = field(default_factory=dict)
finalized: Optional[bytes] = None
class AgentConsensusVoting:
"""
Agentic consensus for response voting.
Agents submit responses and vote. Majority wins.
Args:
threshold: Fraction of votes needed (0.5 = majority)
min_responses: Minimum responses before checking consensus
min_votes: Minimum votes before checking consensus
"""
def __init__(
self,
threshold: float = 0.5,
min_responses: int = 1,
min_votes: int = 1,
):
self.threshold = max(0.0, min(1.0, threshold))
self.min_responses = min_responses
self.min_votes = min_votes
self._queries: Dict[bytes, QueryState] = {}
self._lock = asyncio.Lock()
async def submit_query(self, query: Query) -> bytes:
"""Submit a new query."""
async with self._lock:
self._queries[query.id] = QueryState(query=query)
return query.id
async def submit_response(self, response: Response) -> bytes:
"""Submit a response to a query."""
async with self._lock:
state = self._queries.get(response.query_id)
if state is None:
raise ValueError("Query not found")
if state.finalized is not None:
raise ValueError("Query already finalized")
state.responses[response.id] = response
state.votes[response.id] = []
return response.id
async def vote(self, query_id: bytes, response_id: bytes, voter: Did) -> None:
"""
Vote for a response.
Each agent can only vote once per query (across all responses).
"""
async with self._lock:
state = self._queries.get(query_id)
if state is None:
raise ValueError("Query not found")
if state.finalized is not None:
raise ValueError("Query already finalized")
if response_id not in state.responses:
raise ValueError("Response not found")
# Check if voter already voted
for voters in state.votes.values():
if any(v.uri() == voter.uri() for v in voters):
raise ValueError("Already voted on this query")
state.votes[response_id].append(voter)
self._check_consensus(state)
def _check_consensus(self, state: QueryState) -> None:
"""Check if consensus has been reached."""
if state.finalized is not None:
return
if len(state.responses) < self.min_responses:
return
total_votes = sum(len(v) for v in state.votes.values())
if total_votes < self.min_votes:
return
# Find response with most votes that meets threshold
best: Optional[tuple[bytes, int]] = None
for response_id, voters in state.votes.items():
vote_count = len(voters)
confidence = vote_count / total_votes if total_votes > 0 else 0
if confidence >= self.threshold:
if best is None or vote_count > best[1]:
best = (response_id, vote_count)
if best is not None:
state.finalized = best[0]
async def get_result(self, query_id: bytes) -> Optional[ConsensusResult]:
"""Get the consensus result for a query."""
async with self._lock:
state = self._queries.get(query_id)
if state is None or state.finalized is None:
return None
response = state.responses[state.finalized]
votes = len(state.votes[state.finalized])
total_voters = sum(len(v) for v in state.votes.values())
return ConsensusResult(
response=response,
votes=votes,
total_voters=total_voters,
confidence=votes / total_voters if total_voters > 0 else 0,
)
async def is_finalized(self, query_id: bytes) -> bool:
"""Check if a query has reached consensus."""
async with self._lock:
state = self._queries.get(query_id)
return state is not None and state.finalized is not None
async def get_responses(self, query_id: bytes) -> Optional[List[Response]]:
"""Get all responses for a query."""
async with self._lock:
state = self._queries.get(query_id)
if state is None:
return None
return list(state.responses.values())
async def get_vote_counts(self, query_id: bytes) -> Optional[Dict[bytes, int]]:
"""Get vote counts for a query."""
async with self._lock:
state = self._queries.get(query_id)
if state is None:
return None
return {rid: len(voters) for rid, voters in state.votes.items()}
# Convenience functions
async def consensus_decide(
query: str,
submitter: Did,
responses: List[tuple[str, Did]],
votes: List[tuple[int, Did]],
threshold: float = 0.5,
) -> Optional[ConsensusResult]:
"""
One-shot consensus decision.
Args:
query: The query content
submitter: Who submitted the query
responses: List of (content, responder) tuples
votes: List of (response_index, voter) tuples
threshold: Voting threshold
Returns:
ConsensusResult if consensus reached, None otherwise
"""
consensus = AgentConsensusVoting(
threshold=threshold,
min_responses=1,
min_votes=1,
)
q = Query.create(query, submitter)
await consensus.submit_query(q)
response_ids = []
for content, responder in responses:
r = Response.create(q.id, content, responder)
await consensus.submit_response(r)
response_ids.append(r.id)
for response_idx, voter in votes:
await consensus.vote(q.id, response_ids[response_idx], voter)
return await consensus.get_result(q.id)
__all__ = [
'Query',
'Response',
'ConsensusResult',
'AgentConsensusVoting',
'consensus_decide',
]
+84
View File
@@ -0,0 +1,84 @@
"""ZAP client implementation."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from .error import ZapError
@dataclass
class Tool:
"""Tool definition."""
name: str
description: str
schema: dict[str, Any]
@dataclass
class Resource:
"""Resource definition."""
uri: str
name: str
description: str
mime_type: str
@dataclass
class ResourceContent:
"""Resource content."""
uri: str
mime_type: str
content: str | bytes
class Client:
"""ZAP client for connecting to ZAP gateways."""
def __init__(self, url: str) -> None:
self.url = url
self._connected = False
@classmethod
async def connect(cls, url: str) -> Client:
"""Connect to a ZAP gateway."""
client = cls(url)
# TODO: Establish Cap'n Proto RPC connection
client._connected = True
return client
async def list_tools(self) -> list[Tool]:
"""List available tools."""
if not self._connected:
raise ZapError("Not connected")
# TODO: Implement RPC call
return []
async def call_tool(self, name: str, args: dict[str, Any]) -> Any:
"""Call a tool."""
if not self._connected:
raise ZapError("Not connected")
# TODO: Implement RPC call
return None
async def list_resources(self) -> list[Resource]:
"""List available resources."""
if not self._connected:
raise ZapError("Not connected")
# TODO: Implement RPC call
return []
async def read_resource(self, uri: str) -> ResourceContent:
"""Read a resource."""
if not self._connected:
raise ZapError("Not connected")
# TODO: Implement RPC call
return ResourceContent(uri=uri, mime_type="text/plain", content="")
async def close(self) -> None:
"""Close the connection."""
self._connected = False
+89
View File
@@ -0,0 +1,89 @@
"""ZAP configuration."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any
import tomllib
class Transport(Enum):
"""Transport type."""
STDIO = "stdio"
HTTP = "http"
WEBSOCKET = "websocket"
ZAP = "zap"
UNIX = "unix"
@dataclass
class Auth:
"""Authentication config."""
type: str = "none"
token: str | None = None
username: str | None = None
password: str | None = None
@dataclass
class ServerConfig:
"""Server configuration."""
name: str
url: str
transport: Transport = Transport.STDIO
timeout: int = 30000
auth: Auth | None = None
@dataclass
class Config:
"""ZAP configuration."""
listen: str = "0.0.0.0"
port: int = 9999
servers: list[ServerConfig] = field(default_factory=list)
log_level: str = "info"
@classmethod
def load(cls, path: Path) -> Config:
"""Load config from file."""
with open(path, "rb") as f:
data = tomllib.load(f)
return cls.from_dict(data)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> Config:
"""Create config from dict."""
servers = [
ServerConfig(
name=s["name"],
url=s["url"],
transport=Transport(s.get("transport", "stdio")),
timeout=s.get("timeout", 30000),
)
for s in data.get("servers", [])
]
return cls(
listen=data.get("listen", "0.0.0.0"),
port=data.get("port", 9999),
servers=servers,
log_level=data.get("log_level", "info"),
)
@staticmethod
def default_path() -> Path:
"""Get default config path."""
import platform
if platform.system() == "Darwin":
return Path.home() / "Library" / "Application Support" / "zap" / "config.toml"
elif platform.system() == "Windows":
return Path.home() / "AppData" / "Roaming" / "zap" / "config.toml"
else:
return Path.home() / ".config" / "zap" / "config.toml"
+521
View File
@@ -0,0 +1,521 @@
"""
Post-Quantum Cryptography Module for ZAP
Provides ML-KEM-768 key exchange, ML-DSA-65 signatures, and hybrid X25519+ML-KEM handshake.
Security:
This module implements NIST FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) standards
for post-quantum cryptographic protection. The hybrid handshake combines
classical X25519 with ML-KEM-768 for defense-in-depth.
Example:
>>> from hanzo_zap.crypto import PQKeyExchange, PQSignature, HybridHandshake
# Key exchange
>>> alice = PQKeyExchange.generate()
>>> bob = PQKeyExchange.generate()
>>> ciphertext, shared_alice = alice.encapsulate(bob.public_key)
>>> shared_bob = bob.decapsulate(ciphertext)
>>> assert shared_alice == shared_bob
# Signatures
>>> signer = PQSignature.generate()
>>> sig = signer.sign(b"message")
>>> signer.verify(b"message", sig) # Returns True
# Hybrid handshake
>>> initiator = HybridHandshake.initiate()
>>> responder, response = HybridHandshake.respond(initiator.public_data)
>>> shared_init = initiator.finalize(response)
>>> shared_resp = responder.complete(initiator.public_data)
"""
from __future__ import annotations
import hashlib
import hmac
import os
import secrets
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Tuple
# Attempt to import pqcrypto bindings
# Falls back to stub implementations if not available
try:
from pqcrypto.kem.kyber768 import (
generate_keypair as mlkem_keypair,
encrypt as mlkem_encapsulate,
decrypt as mlkem_decapsulate,
PUBLIC_KEY_SIZE as MLKEM_PUBLIC_KEY_SIZE,
CIPHERTEXT_SIZE as MLKEM_CIPHERTEXT_SIZE,
)
from pqcrypto.sign.dilithium3 import (
generate_keypair as mldsa_keypair,
sign as mldsa_sign,
verify as mldsa_verify,
PUBLIC_KEY_SIZE as MLDSA_PUBLIC_KEY_SIZE,
SIGNATURE_SIZE as MLDSA_SIGNATURE_SIZE,
)
PQ_AVAILABLE = True
except ImportError:
PQ_AVAILABLE = False
# Placeholder sizes for when pqcrypto is not available
MLKEM_PUBLIC_KEY_SIZE = 1184
MLKEM_CIPHERTEXT_SIZE = 1088
MLDSA_PUBLIC_KEY_SIZE = 1952
MLDSA_SIGNATURE_SIZE = 3293
# Attempt to import X25519 from cryptography
try:
from cryptography.hazmat.primitives.asymmetric.x25519 import (
X25519PrivateKey,
X25519PublicKey,
)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
X25519_AVAILABLE = True
except ImportError:
X25519_AVAILABLE = False
# Constants
X25519_PUBLIC_KEY_SIZE = 32
SHARED_SECRET_SIZE = 32
HYBRID_SHARED_SECRET_SIZE = 32
class CryptoError(Exception):
"""Cryptographic operation error."""
pass
class PQKeyExchange:
"""
ML-KEM-768 Key Encapsulation Mechanism.
Implements NIST FIPS 203 ML-KEM-768 for post-quantum key exchange.
Security level: NIST Level 3 (~AES-192 equivalent).
"""
def __init__(self, public_key: bytes, secret_key: Optional[bytes] = None):
self._public_key = public_key
self._secret_key = secret_key
@classmethod
def generate(cls) -> "PQKeyExchange":
"""Generate a new ML-KEM-768 keypair."""
if not PQ_AVAILABLE:
raise CryptoError("pqcrypto not available - install with: pip install pqcrypto")
pk, sk = mlkem_keypair()
return cls(pk, sk)
@classmethod
def from_public_key(cls, public_key: bytes) -> "PQKeyExchange":
"""Create instance from public key (for encapsulation only)."""
if len(public_key) != MLKEM_PUBLIC_KEY_SIZE:
raise CryptoError(
f"Invalid ML-KEM public key size: expected {MLKEM_PUBLIC_KEY_SIZE}, "
f"got {len(public_key)}"
)
return cls(public_key, None)
@property
def public_key(self) -> bytes:
"""Get the public key bytes."""
return self._public_key
def encapsulate(self, recipient_pk: bytes) -> Tuple[bytes, bytes]:
"""
Encapsulate: generate ciphertext and shared secret for a recipient's public key.
Args:
recipient_pk: The recipient's ML-KEM public key.
Returns:
Tuple of (ciphertext, shared_secret).
"""
if not PQ_AVAILABLE:
raise CryptoError("pqcrypto not available")
if len(recipient_pk) != MLKEM_PUBLIC_KEY_SIZE:
raise CryptoError(
f"Invalid recipient public key size: expected {MLKEM_PUBLIC_KEY_SIZE}, "
f"got {len(recipient_pk)}"
)
ciphertext, shared_secret = mlkem_encapsulate(recipient_pk)
return ciphertext, shared_secret
def decapsulate(self, ciphertext: bytes) -> bytes:
"""
Decapsulate: recover shared secret from ciphertext.
Args:
ciphertext: The ML-KEM ciphertext.
Returns:
The shared secret bytes.
"""
if not PQ_AVAILABLE:
raise CryptoError("pqcrypto not available")
if self._secret_key is None:
raise CryptoError("No secret key available for decapsulation")
if len(ciphertext) != MLKEM_CIPHERTEXT_SIZE:
raise CryptoError(
f"Invalid ML-KEM ciphertext size: expected {MLKEM_CIPHERTEXT_SIZE}, "
f"got {len(ciphertext)}"
)
return mlkem_decapsulate(ciphertext, self._secret_key)
class PQSignature:
"""
ML-DSA-65 Digital Signature Algorithm.
Implements NIST FIPS 204 ML-DSA-65 (Dilithium3) for post-quantum signatures.
Security level: NIST Level 3 (~AES-192 equivalent).
"""
def __init__(self, public_key: bytes, secret_key: Optional[bytes] = None):
self._public_key = public_key
self._secret_key = secret_key
@classmethod
def generate(cls) -> "PQSignature":
"""Generate a new ML-DSA-65 keypair."""
if not PQ_AVAILABLE:
raise CryptoError("pqcrypto not available - install with: pip install pqcrypto")
pk, sk = mldsa_keypair()
return cls(pk, sk)
@classmethod
def from_public_key(cls, public_key: bytes) -> "PQSignature":
"""Create instance from public key (for verification only)."""
if len(public_key) != MLDSA_PUBLIC_KEY_SIZE:
raise CryptoError(
f"Invalid ML-DSA public key size: expected {MLDSA_PUBLIC_KEY_SIZE}, "
f"got {len(public_key)}"
)
return cls(public_key, None)
@property
def public_key(self) -> bytes:
"""Get the public key bytes."""
return self._public_key
def sign(self, message: bytes) -> bytes:
"""
Sign a message.
Args:
message: The message bytes to sign.
Returns:
The signature bytes.
"""
if not PQ_AVAILABLE:
raise CryptoError("pqcrypto not available")
if self._secret_key is None:
raise CryptoError("No secret key available for signing")
return mldsa_sign(message, self._secret_key)
def verify(self, message: bytes, signature: bytes) -> bool:
"""
Verify a signature.
Args:
message: The original message bytes.
signature: The signature bytes.
Returns:
True if valid, raises CryptoError if invalid.
"""
if not PQ_AVAILABLE:
raise CryptoError("pqcrypto not available")
if len(signature) != MLDSA_SIGNATURE_SIZE:
raise CryptoError(
f"Invalid ML-DSA signature size: expected {MLDSA_SIGNATURE_SIZE}, "
f"got {len(signature)}"
)
try:
mldsa_verify(message, signature, self._public_key)
return True
except Exception:
raise CryptoError("Signature verification failed")
@dataclass
class HybridInitiatorData:
"""Public data from the initiator for the responder."""
x25519_public_key: bytes
mlkem_public_key: bytes
@dataclass
class HybridResponderData:
"""Response data from the responder for the initiator."""
x25519_public_key: bytes
mlkem_ciphertext: bytes
class HandshakeRole(Enum):
"""Role in the handshake."""
INITIATOR = "initiator"
RESPONDER = "responder"
class HybridHandshake:
"""
Hybrid X25519 + ML-KEM-768 Handshake.
Combines classical elliptic curve Diffie-Hellman (X25519) with post-quantum
ML-KEM-768 for defense-in-depth. Even if one algorithm is broken, the other
provides protection.
The final shared secret is derived using HKDF-SHA256 over both shared secrets.
"""
def __init__(
self,
x25519_private: Optional[bytes],
x25519_public: bytes,
mlkem: PQKeyExchange,
role: HandshakeRole,
):
self._x25519_private = x25519_private
self._x25519_public = x25519_public
self._mlkem = mlkem
self._role = role
@classmethod
def initiate(cls) -> "HybridHandshake":
"""
Initiate a hybrid handshake (client side).
Returns:
A new HybridHandshake instance ready to send public data.
"""
if not X25519_AVAILABLE:
raise CryptoError(
"cryptography not available - install with: pip install cryptography"
)
if not PQ_AVAILABLE:
raise CryptoError(
"pqcrypto not available - install with: pip install pqcrypto"
)
# Generate X25519 keypair
x25519_private = X25519PrivateKey.generate()
x25519_public = x25519_private.public_key().public_bytes_raw()
x25519_private_bytes = x25519_private.private_bytes_raw()
# Generate ML-KEM keypair
mlkem = PQKeyExchange.generate()
return cls(
x25519_private=x25519_private_bytes,
x25519_public=x25519_public,
mlkem=mlkem,
role=HandshakeRole.INITIATOR,
)
@property
def public_data(self) -> HybridInitiatorData:
"""Get the public data to send to the responder."""
return HybridInitiatorData(
x25519_public_key=self._x25519_public,
mlkem_public_key=self._mlkem.public_key,
)
@classmethod
def respond(
cls, initiator_data: HybridInitiatorData
) -> Tuple["HybridHandshake", HybridResponderData]:
"""
Respond to a hybrid handshake (server side).
Args:
initiator_data: Public data from the initiator.
Returns:
Tuple of (HybridHandshake, HybridResponderData) to send back.
"""
if not X25519_AVAILABLE:
raise CryptoError("cryptography not available")
if not PQ_AVAILABLE:
raise CryptoError("pqcrypto not available")
# Validate input
if len(initiator_data.x25519_public_key) != X25519_PUBLIC_KEY_SIZE:
raise CryptoError(
f"Invalid X25519 public key size: expected {X25519_PUBLIC_KEY_SIZE}, "
f"got {len(initiator_data.x25519_public_key)}"
)
if len(initiator_data.mlkem_public_key) != MLKEM_PUBLIC_KEY_SIZE:
raise CryptoError(
f"Invalid ML-KEM public key size: expected {MLKEM_PUBLIC_KEY_SIZE}, "
f"got {len(initiator_data.mlkem_public_key)}"
)
# Generate responder's X25519 keypair
x25519_private = X25519PrivateKey.generate()
x25519_public = x25519_private.public_key().public_bytes_raw()
x25519_private_bytes = x25519_private.private_bytes_raw()
# Generate ML-KEM keypair and encapsulate to initiator
mlkem = PQKeyExchange.generate()
mlkem_ciphertext, _ = mlkem.encapsulate(initiator_data.mlkem_public_key)
response = HybridResponderData(
x25519_public_key=x25519_public,
mlkem_ciphertext=mlkem_ciphertext,
)
handshake = cls(
x25519_private=x25519_private_bytes,
x25519_public=x25519_public,
mlkem=mlkem,
role=HandshakeRole.RESPONDER,
)
return handshake, response
def finalize(self, responder_data: HybridResponderData) -> bytes:
"""
Finalize the handshake and derive the shared secret (initiator side).
Args:
responder_data: Response data from the responder.
Returns:
The derived shared secret (32 bytes).
"""
if self._role != HandshakeRole.INITIATOR:
raise CryptoError("finalize() can only be called by initiator")
if self._x25519_private is None:
raise CryptoError("X25519 private key not available")
# X25519 key exchange
x25519_private = X25519PrivateKey.from_private_bytes(self._x25519_private)
peer_x25519_public = X25519PublicKey.from_public_bytes(
responder_data.x25519_public_key
)
x25519_shared = x25519_private.exchange(peer_x25519_public)
# ML-KEM decapsulation
mlkem_shared = self._mlkem.decapsulate(responder_data.mlkem_ciphertext)
# Clear private key
self._x25519_private = None
# Combine shared secrets with HKDF
return self._derive_hybrid_secret(x25519_shared, mlkem_shared)
def complete(
self,
initiator_data: HybridInitiatorData,
mlkem_shared: Optional[bytes] = None,
) -> bytes:
"""
Complete the handshake and derive the shared secret (responder side).
Args:
initiator_data: Public data from the initiator.
mlkem_shared: Optional pre-computed ML-KEM shared secret.
Returns:
The derived shared secret (32 bytes).
"""
if self._role != HandshakeRole.RESPONDER:
raise CryptoError("complete() can only be called by responder")
if self._x25519_private is None:
raise CryptoError("X25519 private key not available")
# X25519 key exchange
x25519_private = X25519PrivateKey.from_private_bytes(self._x25519_private)
peer_x25519_public = X25519PublicKey.from_public_bytes(
initiator_data.x25519_public_key
)
x25519_shared = x25519_private.exchange(peer_x25519_public)
# Use provided ML-KEM shared secret or compute it
if mlkem_shared is None:
# Responder needs to encapsulate to initiator's key to get same shared secret
_, mlkem_shared = self._mlkem.encapsulate(initiator_data.mlkem_public_key)
# Clear private key
self._x25519_private = None
# Combine shared secrets with HKDF
return self._derive_hybrid_secret(x25519_shared, mlkem_shared)
@staticmethod
def _derive_hybrid_secret(x25519_shared: bytes, mlkem_shared: bytes) -> bytes:
"""
Derive hybrid shared secret using HKDF-SHA256.
Args:
x25519_shared: X25519 shared secret.
mlkem_shared: ML-KEM shared secret.
Returns:
The derived shared secret (32 bytes).
"""
# Concatenate both shared secrets
ikm = x25519_shared + mlkem_shared
# HKDF extract and expand
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=HYBRID_SHARED_SECRET_SIZE,
salt=b"ZAP-HYBRID-HANDSHAKE-v1",
info=b"shared-secret",
)
return hkdf.derive(ikm)
def hybrid_handshake() -> Tuple[bytes, bytes]:
"""
Perform a complete hybrid handshake between two parties.
This is a convenience function for testing and simple use cases.
Returns:
Tuple of (initiator_secret, responder_secret) - both should be equal.
"""
# Initiator starts
initiator = HybridHandshake.initiate()
init_data = initiator.public_data
# Responder receives and responds
responder, resp_data = HybridHandshake.respond(init_data)
# Responder also encapsulates to get shared secret
_, mlkem_shared = PQKeyExchange.generate().encapsulate(init_data.mlkem_public_key)
# Initiator finalizes
initiator_secret = initiator.finalize(resp_data)
# Responder completes
responder_secret = responder.complete(init_data, mlkem_shared)
return initiator_secret, responder_secret
# Export public API
__all__ = [
"PQ_AVAILABLE",
"X25519_AVAILABLE",
"MLKEM_PUBLIC_KEY_SIZE",
"MLKEM_CIPHERTEXT_SIZE",
"MLDSA_PUBLIC_KEY_SIZE",
"MLDSA_SIGNATURE_SIZE",
"X25519_PUBLIC_KEY_SIZE",
"SHARED_SECRET_SIZE",
"HYBRID_SHARED_SECRET_SIZE",
"CryptoError",
"PQKeyExchange",
"PQSignature",
"HybridInitiatorData",
"HybridResponderData",
"HybridHandshake",
"hybrid_handshake",
]
+31
View File
@@ -0,0 +1,31 @@
"""ZAP error types."""
class ZapError(Exception):
"""Base ZAP error."""
pass
class ConnectionError(ZapError):
"""Connection error."""
pass
class ProtocolError(ZapError):
"""Protocol error."""
pass
class ToolNotFoundError(ZapError):
"""Tool not found."""
pass
class ResourceNotFoundError(ZapError):
"""Resource not found."""
pass
+78
View File
@@ -0,0 +1,78 @@
"""ZAP gateway for MCP bridging."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Any
import uuid
from .config import Config, ServerConfig
class ServerStatus(Enum):
"""Server connection status."""
CONNECTING = "connecting"
CONNECTED = "connected"
DISCONNECTED = "disconnected"
ERROR = "error"
@dataclass
class ServerInfo:
"""Connected server info."""
id: str
name: str
url: str
status: ServerStatus
class Gateway:
"""ZAP gateway that bridges MCP servers."""
def __init__(self, config: Config | None = None) -> None:
self.config = config or Config()
self._servers: dict[str, tuple[ServerConfig, ServerStatus]] = {}
async def add_server(self, name: str, url: str, config: ServerConfig) -> str:
"""Add an MCP server."""
server_id = str(uuid.uuid4())[:8]
self._servers[server_id] = (config, ServerStatus.CONNECTING)
# TODO: Connect to MCP server
self._servers[server_id] = (config, ServerStatus.CONNECTED)
return server_id
def remove_server(self, server_id: str) -> None:
"""Remove a server."""
self._servers.pop(server_id, None)
def list_servers(self) -> list[ServerInfo]:
"""List connected servers."""
return [
ServerInfo(
id=sid,
name=cfg.name,
url=cfg.url,
status=status,
)
for sid, (cfg, status) in self._servers.items()
]
async def run(self) -> None:
"""Run the gateway."""
addr = f"{self.config.listen}:{self.config.port}"
print(f"ZAP gateway listening on {addr}")
# Connect to configured servers
for server_config in self.config.servers:
await self.add_server(
server_config.name,
server_config.url,
server_config,
)
# TODO: Start Cap'n Proto RPC server
import asyncio
await asyncio.Event().wait()
+639
View File
@@ -0,0 +1,639 @@
"""
W3C Decentralized Identifier (DID) Implementation
Implements W3C DID Core 1.0 specification with support for:
- did:lux - Lux blockchain-anchored DIDs
- did:key - Self-certifying DIDs from cryptographic keys
- did:web - DNS-based DIDs
Example:
>>> from hanzo_zap.identity import Did, DidMethod, NodeIdentity
# Parse existing DID
>>> did = parse_did("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
>>> did.method
<DidMethod.LUX: 'lux'>
# Create from ML-DSA public key
>>> did = create_did_from_key(public_key_bytes)
# Generate DID Document
>>> doc = did.document()
# Generate node identity
>>> identity = generate_identity()
"""
from __future__ import annotations
import hashlib
import json
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union
# Base58 alphabet (Bitcoin style)
BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
# Multibase prefix for base58btc
MULTIBASE_BASE58BTC = "z"
# Multicodec prefix for ML-DSA-65 public key (provisional)
MULTICODEC_MLDSA65 = bytes([0x13, 0x09])
# Expected ML-DSA-65 public key size
MLDSA_PUBLIC_KEY_SIZE = 1952
class IdentityError(Exception):
"""Identity-related error."""
pass
def base58_encode(data: bytes) -> str:
"""Encode bytes to base58 (Bitcoin alphabet)."""
num = int.from_bytes(data, "big")
result = []
while num > 0:
num, remainder = divmod(num, 58)
result.append(BASE58_ALPHABET[remainder])
# Handle leading zeros
for byte in data:
if byte == 0:
result.append(BASE58_ALPHABET[0])
else:
break
return "".join(reversed(result))
def base58_decode(s: str) -> bytes:
"""Decode base58 string to bytes."""
num = 0
for char in s:
num = num * 58 + BASE58_ALPHABET.index(char)
# Calculate byte length
result = []
while num > 0:
num, remainder = divmod(num, 256)
result.append(remainder)
# Handle leading ones (zeros in decoded)
for char in s:
if char == BASE58_ALPHABET[0]:
result.append(0)
else:
break
return bytes(reversed(result))
class DidMethod(Enum):
"""DID method identifier."""
LUX = "lux"
KEY = "key"
WEB = "web"
class VerificationMethodType(Enum):
"""Verification method type."""
JSON_WEB_KEY_2020 = "JsonWebKey2020"
MULTIKEY = "Multikey"
ML_DSA_65_VERIFICATION_KEY_2024 = "MlDsa65VerificationKey2024"
class ServiceType(Enum):
"""Service type."""
ZAP_AGENT = "ZapAgent"
DID_COMM_MESSAGING = "DIDCommMessaging"
LINKED_DOMAINS = "LinkedDomains"
CREDENTIAL_REGISTRY = "CredentialRegistry"
@dataclass
class ServiceEndpoint:
"""Service endpoint configuration."""
uri: str
accept: Optional[List[str]] = None
routing_keys: Optional[List[str]] = None
def to_dict(self) -> Union[str, Dict[str, Any]]:
"""Convert to JSON-serializable format."""
if self.accept is None and self.routing_keys is None:
return self.uri
result: Dict[str, Any] = {"uri": self.uri}
if self.accept:
result["accept"] = self.accept
if self.routing_keys:
result["routingKeys"] = self.routing_keys
return result
@dataclass
class VerificationMethod:
"""Verification method (public key) in DID Document."""
id: str
type: VerificationMethodType
controller: str
public_key_multibase: Optional[str] = None
public_key_jwk: Optional[Dict[str, Any]] = None
blockchain_account_id: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to JSON-serializable format."""
result = {
"id": self.id,
"type": self.type.value,
"controller": self.controller,
}
if self.public_key_multibase:
result["publicKeyMultibase"] = self.public_key_multibase
if self.public_key_jwk:
result["publicKeyJwk"] = self.public_key_jwk
if self.blockchain_account_id:
result["blockchainAccountId"] = self.blockchain_account_id
return result
@dataclass
class Service:
"""Service endpoint in DID Document."""
id: str
type: ServiceType
service_endpoint: ServiceEndpoint
def to_dict(self) -> Dict[str, Any]:
"""Convert to JSON-serializable format."""
return {
"id": self.id,
"type": self.type.value,
"serviceEndpoint": self.service_endpoint.to_dict(),
}
@dataclass
class DidDocument:
"""W3C DID Document."""
id: str
context: List[str] = field(default_factory=lambda: [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1",
])
controller: Optional[str] = None
verification_method: List[VerificationMethod] = field(default_factory=list)
authentication: List[str] = field(default_factory=list)
assertion_method: List[str] = field(default_factory=list)
key_agreement: List[str] = field(default_factory=list)
capability_invocation: List[str] = field(default_factory=list)
capability_delegation: List[str] = field(default_factory=list)
service: List[Service] = field(default_factory=list)
def primary_verification_method(self) -> Optional[VerificationMethod]:
"""Get the primary verification method."""
return self.verification_method[0] if self.verification_method else None
def get_verification_method(self, id: str) -> Optional[VerificationMethod]:
"""Get a verification method by ID."""
for vm in self.verification_method:
if vm.id == id:
return vm
return None
def get_service(self, id: str) -> Optional[Service]:
"""Get a service by ID."""
for svc in self.service:
if svc.id == id:
return svc
return None
def to_dict(self) -> Dict[str, Any]:
"""Convert to JSON-serializable format."""
result: Dict[str, Any] = {
"@context": self.context,
"id": self.id,
}
if self.controller:
result["controller"] = self.controller
if self.verification_method:
result["verificationMethod"] = [vm.to_dict() for vm in self.verification_method]
if self.authentication:
result["authentication"] = self.authentication
if self.assertion_method:
result["assertionMethod"] = self.assertion_method
if self.key_agreement:
result["keyAgreement"] = self.key_agreement
if self.capability_invocation:
result["capabilityInvocation"] = self.capability_invocation
if self.capability_delegation:
result["capabilityDelegation"] = self.capability_delegation
if self.service:
result["service"] = [svc.to_dict() for svc in self.service]
return result
def to_json(self, indent: int = 2) -> str:
"""Serialize to JSON string."""
return json.dumps(self.to_dict(), indent=indent)
@classmethod
def from_json(cls, json_str: str) -> "DidDocument":
"""Deserialize from JSON string."""
data = json.loads(json_str)
return cls.from_dict(data)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "DidDocument":
"""Create from dictionary."""
verification_methods = []
for vm_data in data.get("verificationMethod", []):
verification_methods.append(VerificationMethod(
id=vm_data["id"],
type=VerificationMethodType(vm_data["type"]),
controller=vm_data["controller"],
public_key_multibase=vm_data.get("publicKeyMultibase"),
public_key_jwk=vm_data.get("publicKeyJwk"),
blockchain_account_id=vm_data.get("blockchainAccountId"),
))
services = []
for svc_data in data.get("service", []):
endpoint_data = svc_data["serviceEndpoint"]
if isinstance(endpoint_data, str):
endpoint = ServiceEndpoint(uri=endpoint_data)
else:
endpoint = ServiceEndpoint(
uri=endpoint_data["uri"],
accept=endpoint_data.get("accept"),
routing_keys=endpoint_data.get("routingKeys"),
)
services.append(Service(
id=svc_data["id"],
type=ServiceType(svc_data["type"]),
service_endpoint=endpoint,
))
return cls(
id=data["id"],
context=data.get("@context", []),
controller=data.get("controller"),
verification_method=verification_methods,
authentication=data.get("authentication", []),
assertion_method=data.get("assertionMethod", []),
key_agreement=data.get("keyAgreement", []),
capability_invocation=data.get("capabilityInvocation", []),
capability_delegation=data.get("capabilityDelegation", []),
service=services,
)
@dataclass
class Did:
"""W3C Decentralized Identifier (DID)."""
method: DidMethod
id: str
def uri(self) -> str:
"""Get the full DID URI string."""
return f"did:{self.method.value}:{self.id}"
def __str__(self) -> str:
return self.uri()
def __hash__(self) -> int:
return hash((self.method, self.id))
def extract_key_material(self) -> bytes:
"""Extract raw key material from did:key or did:lux identifier."""
if not self.id:
raise IdentityError("empty DID identifier")
if not self.id.startswith(MULTIBASE_BASE58BTC):
raise IdentityError(
f"unsupported multibase encoding: expected '{MULTIBASE_BASE58BTC}', "
f"got '{self.id[0]}'"
)
# Decode base58btc (skip multibase prefix)
try:
decoded = base58_decode(self.id[1:])
except Exception as e:
raise IdentityError(f"invalid base58btc encoding: {e}")
if len(decoded) < 2:
raise IdentityError("DID identifier too short")
# Skip multicodec prefix if it matches ML-DSA-65
if decoded[:2] == MULTICODEC_MLDSA65:
return decoded[2:]
return decoded
def document(self) -> DidDocument:
"""Generate a W3C DID Document for this DID."""
did_uri = self.uri()
# Create verification method based on DID type
if self.method in (DidMethod.KEY, DidMethod.LUX):
key_material = self.extract_key_material()
blockchain_account_id = None
if self.method == DidMethod.LUX:
blockchain_account_id = f"lux:{key_material[:20].hex()}"
verification_method = VerificationMethod(
id=f"{did_uri}#keys-1",
type=VerificationMethodType.JSON_WEB_KEY_2020,
controller=did_uri,
public_key_multibase=self.id,
blockchain_account_id=blockchain_account_id,
)
else:
verification_method = VerificationMethod(
id=f"{did_uri}#keys-1",
type=VerificationMethodType.JSON_WEB_KEY_2020,
controller=did_uri,
)
# Create default service endpoint for ZAP protocol
service = Service(
id=f"{did_uri}#zap-agent",
type=ServiceType.ZAP_AGENT,
service_endpoint=ServiceEndpoint(uri=f"zap://{self.id}"),
)
return DidDocument(
id=did_uri,
verification_method=[verification_method],
authentication=[f"{did_uri}#keys-1"],
assertion_method=[f"{did_uri}#keys-1"],
capability_invocation=[f"{did_uri}#keys-1"],
service=[service],
)
def parse_did(s: str) -> Did:
"""
Parse a DID from a string in the format "did:method:id".
Args:
s: DID string to parse
Returns:
Parsed Did object
Raises:
IdentityError: If the DID string is invalid
Example:
>>> did = parse_did("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
>>> did.method
<DidMethod.LUX: 'lux'>
"""
if not s.startswith("did:"):
raise IdentityError(f"invalid DID: must start with 'did:', got '{s}'")
rest = s[4:] # Skip "did:"
parts = rest.split(":", 1)
if len(parts) != 2:
raise IdentityError(f"invalid DID format: expected 'did:method:id', got '{s}'")
method_str, did_id = parts
try:
method = DidMethod(method_str)
except ValueError:
raise IdentityError(f"unknown DID method: {method_str}")
if not did_id:
raise IdentityError("DID identifier cannot be empty")
return Did(method=method, id=did_id)
def create_did_from_key(public_key: bytes, method: DidMethod = DidMethod.KEY) -> Did:
"""
Create a DID from an ML-DSA-65 public key.
Args:
public_key: ML-DSA-65 public key bytes (1952 bytes)
method: DID method to use (KEY or LUX)
Returns:
New Did object
Raises:
IdentityError: If the public key is invalid
Example:
>>> did = create_did_from_key(public_key_bytes)
>>> print(did) # did:key:z6Mk...
"""
if len(public_key) != MLDSA_PUBLIC_KEY_SIZE:
raise IdentityError(
f"invalid ML-DSA public key size: expected {MLDSA_PUBLIC_KEY_SIZE}, "
f"got {len(public_key)}"
)
# Create multicodec-prefixed key
prefixed = MULTICODEC_MLDSA65 + public_key
# Encode with multibase (base58btc)
encoded = base58_encode(prefixed)
did_id = f"{MULTIBASE_BASE58BTC}{encoded}"
return Did(method=method, id=did_id)
def create_did_from_web(domain: str, path: Optional[str] = None) -> Did:
"""
Create a web DID from a domain and optional path.
Args:
domain: Domain name (e.g., "example.com")
path: Optional path (e.g., "users/alice")
Returns:
New Did object with method=WEB
Raises:
IdentityError: If the domain is invalid
Example:
>>> did = create_did_from_web("example.com", "users/alice")
>>> print(did) # did:web:example.com:users:alice
"""
if not domain:
raise IdentityError("domain cannot be empty")
if "/" in domain or ":" in domain:
raise IdentityError(f"invalid domain for did:web: {domain}")
if path:
# Replace '/' with ':' per did:web spec
path_parts = path.replace("/", ":")
did_id = f"{domain}:{path_parts}"
else:
did_id = domain
return Did(method=DidMethod.WEB, id=did_id)
class StakeRegistry(Protocol):
"""Protocol for stake registry implementations."""
def get_stake(self, did: Did) -> int:
"""Get the staked amount for a DID."""
...
def set_stake(self, did: Did, amount: int) -> None:
"""Set the staked amount for a DID."""
...
def total_stake(self) -> int:
"""Get total staked amount across all DIDs."""
...
class InMemoryStakeRegistry:
"""In-memory stake registry for testing."""
def __init__(self) -> None:
self._stakes: Dict[str, int] = {}
def get_stake(self, did: Did) -> int:
"""Get the staked amount for a DID."""
return self._stakes.get(did.uri(), 0)
def set_stake(self, did: Did, amount: int) -> None:
"""Set the staked amount for a DID."""
self._stakes[did.uri()] = amount
def total_stake(self) -> int:
"""Get total staked amount across all DIDs."""
return sum(self._stakes.values())
def has_sufficient_stake(self, did: Did, minimum: int) -> bool:
"""Check if a DID has sufficient stake."""
return self.get_stake(did) >= minimum
def stake_weight(self, did: Did) -> float:
"""Get the stake weight (0.0-1.0) for a DID relative to total."""
stake = self.get_stake(did)
total = self.total_stake()
if total == 0:
return 0.0
return stake / total
@dataclass
class NodeIdentity:
"""
Node identity combining DID with cryptographic keypair.
Used for authenticated node participation in the ZAP network.
"""
did: Did
public_key: bytes
stake: Optional[int] = None
stake_registry: Optional[str] = None
_signer: Optional[Any] = field(default=None, repr=False)
def can_sign(self) -> bool:
"""Check if this node has signing capability."""
return self._signer is not None
def sign(self, message: bytes) -> bytes:
"""Sign a message with this node's private key."""
if self._signer is None:
raise IdentityError("no private key available for signing")
return self._signer.sign(message)
def verify(self, message: bytes, signature: bytes) -> bool:
"""Verify a signature against this node's public key."""
try:
from .crypto import PQSignature, PQ_AVAILABLE
if not PQ_AVAILABLE:
raise IdentityError("verification requires pqcrypto")
if self._signer is not None:
return self._signer.verify(message, signature)
else:
verifier = PQSignature.from_public_key(self.public_key)
return verifier.verify(message, signature)
except ImportError:
raise IdentityError("verification requires pqcrypto")
def document(self) -> DidDocument:
"""Get the DID document for this node identity."""
return self.did.document()
def with_stake(self, amount: int) -> "NodeIdentity":
"""Set the stake amount for this node."""
self.stake = amount
return self
def with_registry(self, registry: str) -> "NodeIdentity":
"""Set the stake registry reference."""
self.stake_registry = registry
return self
def generate_identity(method: DidMethod = DidMethod.LUX) -> NodeIdentity:
"""
Generate a new node identity with fresh ML-DSA-65 keypair.
Args:
method: DID method to use (default: LUX)
Returns:
New NodeIdentity with signing capability
Raises:
IdentityError: If pqcrypto is not available
Example:
>>> identity = generate_identity()
>>> print(identity.did) # did:lux:z6Mk...
>>> identity.can_sign()
True
"""
try:
from .crypto import PQSignature, PQ_AVAILABLE
if not PQ_AVAILABLE:
raise IdentityError("identity generation requires pqcrypto")
signer = PQSignature.generate()
public_key = signer.public_key
did = create_did_from_key(public_key, method=method)
return NodeIdentity(
did=did,
public_key=public_key,
_signer=signer,
)
except ImportError:
raise IdentityError("identity generation requires pqcrypto")
# Export public API
__all__ = [
"IdentityError",
"DidMethod",
"VerificationMethodType",
"ServiceType",
"ServiceEndpoint",
"VerificationMethod",
"Service",
"DidDocument",
"Did",
"parse_did",
"create_did_from_key",
"create_did_from_web",
"StakeRegistry",
"InMemoryStakeRegistry",
"NodeIdentity",
"generate_identity",
"MLDSA_PUBLIC_KEY_SIZE",
]
+20
View File
@@ -0,0 +1,20 @@
"""ZAP server implementation."""
from __future__ import annotations
from .config import Config
class Server:
"""ZAP server."""
def __init__(self, config: Config | None = None) -> None:
self.config = config or Config()
async def run(self) -> None:
"""Run the server."""
addr = f"{self.config.listen}:{self.config.port}"
print(f"ZAP server listening on {addr}")
# TODO: Start Cap'n Proto RPC server
import asyncio
await asyncio.Event().wait()
+1
View File
@@ -0,0 +1 @@
# ZAP Python Tests
+24
View File
@@ -0,0 +1,24 @@
"""Pytest configuration and fixtures for ZAP tests."""
import pytest
@pytest.fixture
def sample_did():
"""Create a sample DID for testing."""
from hanzo_zap.identity import Did
return Did(method="lux", id="z6MkTest123")
@pytest.fixture
def sample_did_key():
"""Create a sample did:key DID."""
from hanzo_zap.identity import Did
return Did(method="key", id="z6MkTestKey456")
@pytest.fixture
def sample_query(sample_did):
"""Create a sample query for testing."""
from hanzo_zap.agent_consensus import Query
return Query.create("What is 2+2?", sample_did)
@@ -0,0 +1,280 @@
"""Tests for hanzo_zap.agent_consensus module."""
import pytest
from hanzo_zap.identity import Did, DidMethod
from hanzo_zap.agent_consensus import (
Query,
Response,
ConsensusResult,
AgentConsensusVoting,
consensus_decide,
)
def make_did(name: str) -> Did:
"""Create a test DID."""
return Did(method=DidMethod.LUX, id=f"z6Mk{name}")
class TestQuery:
"""Tests for Query class."""
def test_create_query(self):
"""Test creating a query."""
submitter = make_did("Alice")
query = Query.create("What is 2+2?", submitter)
assert query.content == "What is 2+2?"
assert query.submitter == submitter
assert len(query.id) == 32 # blake2b digest size
assert query.timestamp > 0
def test_query_id_unique(self):
"""Test that different queries have different IDs."""
submitter = make_did("Alice")
q1 = Query.create("What is 2+2?", submitter)
q2 = Query.create("What is 3+3?", submitter)
assert q1.id != q2.id
def test_query_id_deterministic_content(self):
"""Test that same content from different submitters gives different IDs."""
alice = make_did("Alice")
bob = make_did("Bob")
q1 = Query.create("What is 2+2?", alice)
q2 = Query.create("What is 2+2?", bob)
assert q1.id != q2.id
class TestResponse:
"""Tests for Response class."""
def test_create_response(self):
"""Test creating a response."""
query_id = bytes(32)
responder = make_did("Bob")
response = Response.create(query_id, "4", responder)
assert response.query_id == query_id
assert response.content == "4"
assert response.responder == responder
assert len(response.id) == 32
assert response.timestamp > 0
def test_response_id_unique(self):
"""Test that different responses have different IDs."""
query_id = bytes(32)
responder = make_did("Bob")
r1 = Response.create(query_id, "4", responder)
r2 = Response.create(query_id, "5", responder)
assert r1.id != r2.id
class TestAgentConsensusVoting:
"""Tests for AgentConsensusVoting class."""
@pytest.mark.asyncio
async def test_submit_query(self):
"""Test submitting a query."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
query = Query.create("What is 2+2?", make_did("Alice"))
query_id = await consensus.submit_query(query)
assert query_id == query.id
@pytest.mark.asyncio
async def test_submit_response(self):
"""Test submitting a response."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
query = Query.create("What is 2+2?", make_did("Alice"))
await consensus.submit_query(query)
response = Response.create(query.id, "4", make_did("Bob"))
response_id = await consensus.submit_response(response)
assert response_id == response.id
@pytest.mark.asyncio
async def test_submit_response_invalid_query(self):
"""Test submitting response to non-existent query."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
response = Response.create(bytes(32), "4", make_did("Bob"))
with pytest.raises(ValueError, match="Query not found"):
await consensus.submit_response(response)
@pytest.mark.asyncio
async def test_vote(self):
"""Test voting for a response."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
query = Query.create("What is 2+2?", make_did("Alice"))
await consensus.submit_query(query)
response = Response.create(query.id, "4", make_did("Bob"))
response_id = await consensus.submit_response(response)
await consensus.vote(query.id, response_id, make_did("Voter1"))
# Should reach consensus with 1 vote at threshold 0.5
assert await consensus.is_finalized(query.id)
@pytest.mark.asyncio
async def test_vote_double_vote_prevented(self):
"""Test that double voting is prevented."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=2)
query = Query.create("Test", make_did("Alice"))
await consensus.submit_query(query)
response = Response.create(query.id, "Answer", make_did("Bob"))
response_id = await consensus.submit_response(response)
voter = make_did("Voter1")
await consensus.vote(query.id, response_id, voter)
with pytest.raises(ValueError, match="Already voted"):
await consensus.vote(query.id, response_id, voter)
@pytest.mark.asyncio
async def test_vote_invalid_query(self):
"""Test voting on non-existent query."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
with pytest.raises(ValueError, match="Query not found"):
await consensus.vote(bytes(32), bytes(32), make_did("Voter"))
@pytest.mark.asyncio
async def test_vote_invalid_response(self):
"""Test voting for non-existent response."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
query = Query.create("Test", make_did("Alice"))
await consensus.submit_query(query)
with pytest.raises(ValueError, match="Response not found"):
await consensus.vote(query.id, bytes(32), make_did("Voter"))
@pytest.mark.asyncio
async def test_consensus_threshold(self):
"""Test consensus with multiple responses and votes."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=2, min_votes=3)
query = Query.create("Best language?", make_did("Alice"))
await consensus.submit_query(query)
r1 = Response.create(query.id, "Rust", make_did("Bob"))
r1_id = await consensus.submit_response(r1)
r2 = Response.create(query.id, "Python", make_did("Carol"))
r2_id = await consensus.submit_response(r2)
# Vote: 2 for Rust, 1 for Python
await consensus.vote(query.id, r1_id, make_did("V1"))
await consensus.vote(query.id, r1_id, make_did("V2"))
await consensus.vote(query.id, r2_id, make_did("V3"))
assert await consensus.is_finalized(query.id)
result = await consensus.get_result(query.id)
assert result is not None
assert result.response.content == "Rust"
assert result.votes == 2
assert result.total_voters == 3
@pytest.mark.asyncio
async def test_no_consensus_below_threshold(self):
"""Test that consensus is not reached below threshold."""
consensus = AgentConsensusVoting(threshold=0.6, min_responses=3, min_votes=3)
query = Query.create("Test", make_did("Alice"))
await consensus.submit_query(query)
r1 = Response.create(query.id, "A", make_did("Bob"))
r1_id = await consensus.submit_response(r1)
r2 = Response.create(query.id, "B", make_did("Carol"))
r2_id = await consensus.submit_response(r2)
r3 = Response.create(query.id, "C", make_did("Dave"))
r3_id = await consensus.submit_response(r3)
# Split vote: 1-1-1 (none reaches 60%)
await consensus.vote(query.id, r1_id, make_did("V1"))
await consensus.vote(query.id, r2_id, make_did("V2"))
await consensus.vote(query.id, r3_id, make_did("V3"))
assert not await consensus.is_finalized(query.id)
@pytest.mark.asyncio
async def test_get_responses(self):
"""Test getting all responses for a query."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
query = Query.create("Test", make_did("Alice"))
await consensus.submit_query(query)
r1 = Response.create(query.id, "A", make_did("Bob"))
r2 = Response.create(query.id, "B", make_did("Carol"))
await consensus.submit_response(r1)
await consensus.submit_response(r2)
responses = await consensus.get_responses(query.id)
assert responses is not None
assert len(responses) == 2
@pytest.mark.asyncio
async def test_get_vote_counts(self):
"""Test getting vote counts."""
consensus = AgentConsensusVoting(threshold=0.5, min_responses=1, min_votes=1)
query = Query.create("Test", make_did("Alice"))
await consensus.submit_query(query)
response = Response.create(query.id, "Answer", make_did("Bob"))
response_id = await consensus.submit_response(response)
await consensus.vote(query.id, response_id, make_did("V1"))
counts = await consensus.get_vote_counts(query.id)
assert counts is not None
assert counts.get(response_id) == 1
class TestConsensusDecide:
"""Tests for convenience function consensus_decide."""
@pytest.mark.asyncio
async def test_consensus_decide_simple(self):
"""Test one-shot consensus decision."""
# Note: consensus_decide allows voting until consensus is reached
# With threshold=0.5 and 2 votes for response[0], we reach 100% > 50%
# So we only pass the votes that will be accepted before finalization
result = await consensus_decide(
query="What is 2+2?",
submitter=make_did("Alice"),
responses=[
("4", make_did("Bob")),
("5", make_did("Carol")),
],
votes=[
(0, make_did("V1")), # 100% for "4", consensus reached
],
threshold=0.5,
)
assert result is not None
assert result.response.content == "4"
assert result.votes == 1
@pytest.mark.asyncio
async def test_consensus_decide_no_consensus(self):
"""Test one-shot with no consensus reached."""
# With no votes at all, no consensus can be reached
result = await consensus_decide(
query="Test",
submitter=make_did("Alice"),
responses=[
("A", make_did("Bob")),
("B", make_did("Carol")),
],
votes=[], # No votes = no consensus
threshold=0.5,
)
assert result is None
+81
View File
@@ -0,0 +1,81 @@
"""Tests for hanzo_zap.config module."""
import pytest
from hanzo_zap.config import Config, ServerConfig, Transport
class TestConfig:
"""Tests for Config class."""
def test_default_config(self):
"""Test creating default config."""
config = Config()
assert config.listen == "0.0.0.0"
assert config.port == 9999
assert config.log_level == "info"
assert config.servers == []
def test_custom_config(self):
"""Test creating custom config."""
config = Config(listen="127.0.0.1", port=8888, log_level="debug")
assert config.listen == "127.0.0.1"
assert config.port == 8888
assert config.log_level == "debug"
def test_config_from_dict(self):
"""Test creating config from dict."""
data = {
"listen": "localhost",
"port": 9000,
"log_level": "warn",
"servers": [
{"name": "test", "url": "http://localhost:8080"}
]
}
config = Config.from_dict(data)
assert config.listen == "localhost"
assert config.port == 9000
assert config.log_level == "warn"
assert len(config.servers) == 1
assert config.servers[0].name == "test"
def test_config_default_path(self):
"""Test default config path."""
path = Config.default_path()
assert "zap" in str(path)
assert "config.toml" in str(path)
class TestServerConfig:
"""Tests for ServerConfig class."""
def test_server_config(self):
"""Test creating server config."""
config = ServerConfig(name="test", url="http://localhost:8080")
assert config.name == "test"
assert config.url == "http://localhost:8080"
assert config.transport == Transport.STDIO
assert config.timeout == 30000
def test_server_config_with_transport(self):
"""Test server config with transport."""
config = ServerConfig(
name="test",
url="ws://localhost:8080",
transport=Transport.WEBSOCKET,
timeout=60000
)
assert config.transport == Transport.WEBSOCKET
assert config.timeout == 60000
class TestTransport:
"""Tests for Transport enum."""
def test_transport_values(self):
"""Test transport enum values."""
assert Transport.STDIO.value == "stdio"
assert Transport.HTTP.value == "http"
assert Transport.WEBSOCKET.value == "websocket"
assert Transport.ZAP.value == "zap"
assert Transport.UNIX.value == "unix"
+140
View File
@@ -0,0 +1,140 @@
"""Tests for hanzo_zap.crypto module."""
import pytest
from hanzo_zap import crypto
class TestHashFunctions:
"""Tests for hash functions."""
def test_blake3_hash(self):
"""Test BLAKE3 hashing."""
if not hasattr(crypto, 'blake3_hash'):
pytest.skip("blake3_hash not implemented")
data = b"hello world"
hash1 = crypto.blake3_hash(data)
hash2 = crypto.blake3_hash(data)
assert hash1 == hash2
assert len(hash1) == 32
def test_blake3_hash_different_inputs(self):
"""Test BLAKE3 produces different hashes for different inputs."""
if not hasattr(crypto, 'blake3_hash'):
pytest.skip("blake3_hash not implemented")
hash1 = crypto.blake3_hash(b"hello")
hash2 = crypto.blake3_hash(b"world")
assert hash1 != hash2
class TestKeyGeneration:
"""Tests for key generation functions."""
def test_generate_keypair(self):
"""Test generating a keypair."""
if not hasattr(crypto, 'generate_keypair'):
pytest.skip("generate_keypair not implemented")
public_key, secret_key = crypto.generate_keypair()
assert len(public_key) == 32 # Ed25519 public key
assert len(secret_key) == 64 # Ed25519 secret key
def test_generate_keypair_unique(self):
"""Test that each keypair is unique."""
if not hasattr(crypto, 'generate_keypair'):
pytest.skip("generate_keypair not implemented")
pk1, _ = crypto.generate_keypair()
pk2, _ = crypto.generate_keypair()
assert pk1 != pk2
class TestSignatureVerification:
"""Tests for signature operations."""
def test_sign_and_verify(self):
"""Test signing and verifying a message."""
if not hasattr(crypto, 'sign') or not hasattr(crypto, 'verify'):
pytest.skip("sign/verify not implemented")
public_key, secret_key = crypto.generate_keypair()
message = b"test message"
signature = crypto.sign(message, secret_key)
assert crypto.verify(message, signature, public_key)
def test_verify_invalid_signature(self):
"""Test verification fails with invalid signature."""
if not hasattr(crypto, 'sign') or not hasattr(crypto, 'verify'):
pytest.skip("sign/verify not implemented")
public_key, secret_key = crypto.generate_keypair()
message = b"test message"
signature = crypto.sign(message, secret_key)
# Corrupt signature
bad_signature = bytes([(b + 1) % 256 for b in signature])
assert not crypto.verify(message, bad_signature, public_key)
class TestHKDF:
"""Tests for HKDF key derivation."""
def test_hkdf_derive(self):
"""Test HKDF key derivation."""
if not hasattr(crypto, 'hkdf_derive'):
pytest.skip("hkdf_derive not implemented")
ikm = b"input key material"
salt = b"salt"
info = b"info"
derived = crypto.hkdf_derive(ikm, salt, info, 32)
assert len(derived) == 32
def test_hkdf_deterministic(self):
"""Test HKDF is deterministic."""
if not hasattr(crypto, 'hkdf_derive'):
pytest.skip("hkdf_derive not implemented")
ikm = b"input key material"
salt = b"salt"
info = b"info"
derived1 = crypto.hkdf_derive(ikm, salt, info, 32)
derived2 = crypto.hkdf_derive(ikm, salt, info, 32)
assert derived1 == derived2
class TestAEAD:
"""Tests for AEAD encryption."""
def test_aead_encrypt_decrypt(self):
"""Test AEAD encrypt/decrypt roundtrip."""
if not hasattr(crypto, 'aead_encrypt') or not hasattr(crypto, 'aead_decrypt'):
pytest.skip("AEAD not implemented")
key = bytes(32) # 256-bit key
nonce = bytes(12)
plaintext = b"secret message"
aad = b"additional data"
ciphertext = crypto.aead_encrypt(key, nonce, plaintext, aad)
decrypted = crypto.aead_decrypt(key, nonce, ciphertext, aad)
assert decrypted == plaintext
class TestCryptoModule:
"""General crypto module tests."""
def test_module_has_constants(self):
"""Test crypto module has expected constants."""
# Just test the module is importable and has some content
assert hasattr(crypto, '__name__')
+32
View File
@@ -0,0 +1,32 @@
"""Tests for hanzo_zap.error module."""
import pytest
from hanzo_zap.error import ZapError
class TestZapError:
"""Tests for ZapError class."""
def test_zap_error_creation(self):
"""Test creating a ZapError."""
error = ZapError("Something went wrong")
assert str(error) == "Something went wrong"
def test_zap_error_is_exception(self):
"""Test ZapError is an Exception."""
error = ZapError("test")
assert isinstance(error, Exception)
def test_zap_error_raise_and_catch(self):
"""Test raising and catching ZapError."""
with pytest.raises(ZapError) as exc_info:
raise ZapError("test error")
assert "test error" in str(exc_info.value)
def test_zap_error_subclass_behavior(self):
"""Test ZapError can be caught as Exception."""
try:
raise ZapError("test")
except Exception as e:
assert isinstance(e, ZapError)
+158
View File
@@ -0,0 +1,158 @@
"""Tests for hanzo_zap.identity module."""
import pytest
from hanzo_zap.identity import (
Did,
DidDocument,
DidMethod,
VerificationMethod,
VerificationMethodType,
parse_did,
create_did_from_key,
create_did_from_web,
base58_encode,
base58_decode,
IdentityError,
)
class TestDid:
"""Tests for Did class."""
def test_create_lux_did(self):
"""Test creating a did:lux DID."""
did = Did(method=DidMethod.LUX, id="z6MkTest123")
assert did.method == DidMethod.LUX
assert did.id == "z6MkTest123"
def test_create_key_did(self):
"""Test creating a did:key DID."""
did = Did(method=DidMethod.KEY, id="z6MkTestKey456")
assert did.method == DidMethod.KEY
assert did.id == "z6MkTestKey456"
def test_create_web_did(self):
"""Test creating a did:web DID."""
did = Did(method=DidMethod.WEB, id="example.com:user:alice")
assert did.method == DidMethod.WEB
assert did.id == "example.com:user:alice"
def test_did_uri(self):
"""Test DID URI formatting."""
did = Did(method=DidMethod.LUX, id="z6MkTest123")
assert did.uri() == "did:lux:z6MkTest123"
def test_did_equality(self):
"""Test DID equality comparison."""
did1 = Did(method=DidMethod.LUX, id="z6MkTest123")
did2 = Did(method=DidMethod.LUX, id="z6MkTest123")
did3 = Did(method=DidMethod.LUX, id="z6MkDifferent")
assert did1.uri() == did2.uri()
assert did1.uri() != did3.uri()
class TestParseDid:
"""Tests for parse_did function."""
def test_parse_lux_did(self):
"""Test parsing did:lux DID."""
did = parse_did("did:lux:z6MkTest123")
assert did.method == DidMethod.LUX
assert did.id == "z6MkTest123"
def test_parse_key_did(self):
"""Test parsing did:key DID."""
did = parse_did("did:key:z6MkTestKey456")
assert did.method == DidMethod.KEY
assert did.id == "z6MkTestKey456"
def test_parse_web_did(self):
"""Test parsing did:web DID."""
did = parse_did("did:web:example.com:user:alice")
assert did.method == DidMethod.WEB
assert did.id == "example.com:user:alice"
def test_parse_invalid(self):
"""Test parsing invalid DID."""
with pytest.raises((ValueError, IdentityError)):
parse_did("invalid")
def test_parse_missing_method(self):
"""Test parsing DID without method."""
with pytest.raises((ValueError, IdentityError)):
parse_did("did:")
class TestDidDocument:
"""Tests for DidDocument class."""
def test_create_did_document(self):
"""Test creating a DID document."""
did = Did(method=DidMethod.LUX, id="z6MkTest123")
doc = did.document()
assert doc.id == "did:lux:z6MkTest123"
def test_did_document_to_json(self):
"""Test converting DID document to JSON."""
did = Did(method=DidMethod.LUX, id="z6MkTest123")
doc = did.document()
json_str = doc.to_json()
assert '"id": "did:lux:z6MkTest123"' in json_str or '"id":"did:lux:z6MkTest123"' in json_str
class TestBase58:
"""Tests for base58 encoding/decoding."""
def test_base58_encode(self):
"""Test base58 encoding."""
data = b"hello"
encoded = base58_encode(data)
assert len(encoded) > 0
def test_base58_decode(self):
"""Test base58 decoding."""
data = b"hello"
encoded = base58_encode(data)
decoded = base58_decode(encoded)
assert decoded == data
def test_base58_roundtrip(self):
"""Test base58 encode/decode roundtrip."""
test_cases = [
b"a",
b"hello world",
bytes(range(1, 256)), # Skip leading zeros for this test
]
for data in test_cases:
encoded = base58_encode(data)
decoded = base58_decode(encoded)
assert decoded == data
class TestCreateDidFromKey:
"""Tests for create_did_from_key function."""
def test_create_did_from_key(self):
"""Test creating did:key from bytes."""
# 1952-byte fake ML-DSA public key (exact size required)
public_key = bytes([i % 256 for i in range(1952)])
did = create_did_from_key(public_key)
assert did.method == DidMethod.KEY
assert did.id.startswith("z")
class TestCreateDidFromWeb:
"""Tests for create_did_from_web function."""
def test_create_did_from_web(self):
"""Test creating did:web from domain."""
did = create_did_from_web("example.com")
assert did.method == DidMethod.WEB
assert did.id == "example.com"
def test_create_did_from_web_with_path(self):
"""Test creating did:web with path."""
did = create_did_from_web("example.com", "users/alice")
assert did.method == DidMethod.WEB
assert "example.com" in did.id
+610
View File
@@ -0,0 +1,610 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[package]]
name = "anyio"
version = "4.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
]
[[package]]
name = "certifi"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.13.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" },
{ url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" },
{ url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" },
{ url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" },
{ url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" },
{ url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" },
{ url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" },
{ url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" },
{ url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" },
{ url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" },
{ url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" },
{ url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" },
{ url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" },
{ url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" },
{ url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" },
{ url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" },
{ url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" },
{ url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" },
{ url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" },
{ url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" },
{ url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" },
{ url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" },
{ url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" },
{ url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" },
{ url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" },
{ url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" },
{ url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" },
{ url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" },
{ url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" },
{ url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" },
{ url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" },
{ url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" },
{ url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" },
{ url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" },
{ url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" },
{ url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" },
{ url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" },
{ url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" },
{ url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" },
{ url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" },
{ url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" },
{ url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" },
{ url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" },
{ url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" },
{ url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" },
{ url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" },
{ url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" },
{ url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" },
{ url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" },
{ url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" },
{ url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" },
{ url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" },
{ url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" },
{ url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" },
{ url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" },
{ url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" },
{ url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" },
{ url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" },
{ url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" },
{ url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" },
{ url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" },
{ url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" },
{ url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" },
{ url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" },
{ url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" },
{ url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" },
{ url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" },
{ url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" },
{ url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" },
{ url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" },
{ url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" },
{ url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" },
{ url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" },
{ url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" },
{ url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "hanzo-zap"
version = "0.2.1"
source = { editable = "." }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "pycapnp" },
]
[package.optional-dependencies]
dev = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "anyio", specifier = ">=4.0" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.13" },
{ name = "pycapnp", specifier = ">=2.0.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8" },
]
provides-extras = ["dev"]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "librt"
version = "0.7.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" },
{ url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" },
{ url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" },
{ url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" },
{ url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" },
{ url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" },
{ url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" },
{ url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" },
{ url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
{ url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
{ url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
{ url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
{ url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
{ url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
{ url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
{ url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
{ url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
{ url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
{ url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
{ url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
{ url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
{ url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
{ url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
{ url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
{ url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
{ url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
{ url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
{ url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
{ url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
{ url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
{ url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
{ url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
{ url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
{ url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
{ url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
{ url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
{ url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
{ url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
{ url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
{ url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
{ url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
{ url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
{ url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
{ url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
{ url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
{ url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
{ url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
{ url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
{ url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
{ url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
{ url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
{ url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
{ url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
{ url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
]
[[package]]
name = "mypy"
version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
{ url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
{ url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
{ url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
{ url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
{ url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
{ url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
{ url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
{ url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
{ url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
{ url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
{ url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
{ url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pathspec"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pycapnp"
version = "2.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/7b/b2f356bc24220068beffc03e94062e8059a1383addb837303794398aec36/pycapnp-2.2.2.tar.gz", hash = "sha256:7f6c23c2283173a3cb6f1a5086dd0114779d508a7cd1b138d25a6357857d02b6", size = 730142, upload-time = "2026-01-21T01:22:13.73Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/e7/4ac70e2103d4c5c0c759d1ee45af862b0a98579aeebc22c8dfdc5d4b7e1c/pycapnp-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ef914139f914d709fbda4b178275d911214f8e742465d70fde2ccdccf5a7ef1", size = 1615815, upload-time = "2026-01-21T01:19:35.651Z" },
{ url = "https://files.pythonhosted.org/packages/92/f2/dc7b1c317585da05a1058c5957d7da811fcfeb5bcf305cd9d42f40db22c4/pycapnp-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c11a80978a8ecac5294bc28a5bc5778c8e9a1a20ef78e7b1a94f71c2bc2c7268", size = 1490267, upload-time = "2026-01-21T01:19:37.759Z" },
{ url = "https://files.pythonhosted.org/packages/10/3b/e87736801b50c2b29dc184bf250a3698b2c6640c56d90a3036922cf9e40d/pycapnp-2.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b85c0c31a5808070260e64262989447f33b7546e25303b7d64d56a99b653044f", size = 5108841, upload-time = "2026-01-21T01:19:39.72Z" },
{ url = "https://files.pythonhosted.org/packages/67/ee/fc1786fb6e53afa3afa6266684b02f4dcb02a12c0722cf561880114a2c1e/pycapnp-2.2.2-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:41d4efb9156a4b8230f64c2d41c96207883e68f31b37e2a799340db8f1956dd7", size = 5187231, upload-time = "2026-01-21T01:19:42.855Z" },
{ url = "https://files.pythonhosted.org/packages/38/d6/3d283d9a6dc9ce3ed540a3a6cb940bf6403e0c02cdeeb47162b4a4adaebd/pycapnp-2.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:27d933c385ff3db8702eb30c5ad67daf9f91c8ab22ee61358fa88fb875dcad7e", size = 5570545, upload-time = "2026-01-21T01:19:45.088Z" },
{ url = "https://files.pythonhosted.org/packages/a1/98/337e09fd7bcf40ed28fb9b4a09057e72a2f02f1fd61d0ff1446553feea6e/pycapnp-2.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:f6a87a1f1c42d703be8cd9fadc81db885b1abb1d0bf1893d55ceaabaf4b16939", size = 5600909, upload-time = "2026-01-21T01:19:47.249Z" },
{ url = "https://files.pythonhosted.org/packages/ea/11/a2d16797ff94179d3d03160c54b12663b1799cb82199def2c07be6efed18/pycapnp-2.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0847f2430816fdaee815c355730a53bc5c035b7a0b6aab6d0fcf33d89b675730", size = 5293548, upload-time = "2026-01-21T01:19:48.792Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d0/f6f890463dd273838fbcbb8f93e615834879e6941b0cafd556ed98f67243/pycapnp-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e3042d48244042b63f433b55f8d90ef58edc1ca1b51f751c8d43c698496ffd0d", size = 5999760, upload-time = "2026-01-21T01:19:51.11Z" },
{ url = "https://files.pythonhosted.org/packages/12/b3/154bdc94551586430aded973cc6fbaf72d9f6de750988a704773b0c5c536/pycapnp-2.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:53d2e280a0bae38b21387caf6312af857973a904dd56498d5abd6d02ec84ec76", size = 6326984, upload-time = "2026-01-21T01:19:52.88Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1f/9c49a6c8c3b03cf236d71e997d4fa136809ec228aeea17168c41b01655c8/pycapnp-2.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b6488d6e7aa1611ed4c764fc6f3628951f89b1ec36291a2a163b53a34c49bb9f", size = 6504257, upload-time = "2026-01-21T01:19:55.196Z" },
{ url = "https://files.pythonhosted.org/packages/14/4e/017ced1eb532102957cba850ea981433822dedbb5fda6201c903a7cc9c75/pycapnp-2.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:25cdb9d48193deda927e7f9572c9ff93173ef983065793381ada8b8f2b4e1fdc", size = 6524744, upload-time = "2026-01-21T01:19:57.487Z" },
{ url = "https://files.pythonhosted.org/packages/8c/10/f928661e1f61cbab6c273b489e7651329d14697b01a63da40404b870bbea/pycapnp-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3fbb2f79c71dc2d6345e334864d1760e83bbe29ce2fcba160be99d48afc9eb39", size = 6264637, upload-time = "2026-01-21T01:19:59.153Z" },
{ url = "https://files.pythonhosted.org/packages/e6/3b/6fe34a5197e6b859e56a3476e6cc41674f333c61381bf8ddc656965338cb/pycapnp-2.2.2-cp310-cp310-win32.whl", hash = "sha256:e96d31d2ab9a1ee48b5304751ff603a55a8d99c1f0aad87d20c82b73969d0581", size = 1063842, upload-time = "2026-01-21T01:20:01.229Z" },
{ url = "https://files.pythonhosted.org/packages/36/2c/c0cfc8c7d89c5345f7f4b08a906c13b9fb140d8a9080381f81e21c790f36/pycapnp-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:4d2b791abc65fa6c052d06f0d6057f6b08a712e8a1068a0b1d8c5e2ddea32dd7", size = 1215433, upload-time = "2026-01-21T01:20:03.074Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2c/65e0efb7d3d3f8bff7e55fb32ce74da008b7f5c2700b8f50e5ee69fc228d/pycapnp-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b49906285d5fdd91b352329eaa33ec54162053a0fae5467389f3762a6c27e09", size = 1614876, upload-time = "2026-01-21T01:20:04.32Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4d/87e7b0050317c9152a6ed4162f6159377d683163432acdbaa39d90e5081b/pycapnp-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a603298b27356cb1766b4c965964a90a1af98b97c7513a369ed483bb267dfd83", size = 1489709, upload-time = "2026-01-21T01:20:05.574Z" },
{ url = "https://files.pythonhosted.org/packages/30/22/09477810aee36e435717994a8788c0b11430159a0f5ada8de1cb4f4c3cda/pycapnp-2.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c858cd32f9b940bd5412e8a992f5b3983010611ad03f2a548f39b50aa5c60e76", size = 5259323, upload-time = "2026-01-21T01:20:07.533Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d5/aae7616c24c7a36e740ca8f1c7b0751cf048ff6a27b2391e0a768141451d/pycapnp-2.2.2-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:7fc395295229817b7436702cd3aaaf50a8587a9842b0616d683504f81166c3b0", size = 5318216, upload-time = "2026-01-21T01:20:09.194Z" },
{ url = "https://files.pythonhosted.org/packages/7a/ca/0e115255dbf177bb7a6a5c2388c8b5888b590149c79dd34a96c0f1eeac3c/pycapnp-2.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:3c64660d72dda255d597151024105318f931404c1f5d8e38d32bd03a5a1cbc5b", size = 5731883, upload-time = "2026-01-21T01:20:11.315Z" },
{ url = "https://files.pythonhosted.org/packages/42/00/19273a09dc4d63c3001035b449f3eb3e3f06331f3839a1e39c85cc68f080/pycapnp-2.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:0023a873567d1cb2c11de599a3d3a49810bfbbe1eacc66001ff61140a0b57040", size = 5751518, upload-time = "2026-01-21T01:20:12.913Z" },
{ url = "https://files.pythonhosted.org/packages/f2/93/6db5789b57dadfd1dc621d7db1c062c456d6a6817b19efa91af54715f845/pycapnp-2.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2905d571800c8871b14bba31effc80383d321eaced37506ce050521c5962eb61", size = 5436583, upload-time = "2026-01-21T01:20:14.961Z" },
{ url = "https://files.pythonhosted.org/packages/f2/74/02872a27f1ebc410c78a93023349d76200919db60e2397d460021f79fe6a/pycapnp-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f731680b232235038f4d7390c2e6d6f1dc03d42d0e2c12f72e99e666cefdff01", size = 6146136, upload-time = "2026-01-21T01:20:16.685Z" },
{ url = "https://files.pythonhosted.org/packages/16/35/7df523707e5080cf3f28a39e029ce7d41feaa0097fb16ba58ec229aadce2/pycapnp-2.2.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6eed184d6e7e8d2ed7ef3ffd35fc2a6616835342ca99457845945dbe088b8874", size = 6459689, upload-time = "2026-01-21T01:20:18.492Z" },
{ url = "https://files.pythonhosted.org/packages/c7/4e/26b6c9c5fa210748288338a53feaaa4e1cf466f38b0f28b0385289036179/pycapnp-2.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2e7b6b2339e394cd7aed712d01303313a5e50259d68003ed63b50bf73ba382d5", size = 6659002, upload-time = "2026-01-21T01:20:20.371Z" },
{ url = "https://files.pythonhosted.org/packages/da/33/c1f6415db40cb997d0b501ea0e9b98c8cca67d20dee75024514454714fdd/pycapnp-2.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19a58b202a306c8a5b1ceb939bd1b6521061d161cf7c19fde3fcedc4db6e20dd", size = 6673137, upload-time = "2026-01-21T01:20:22.047Z" },
{ url = "https://files.pythonhosted.org/packages/8d/37/abb9037f22df289bcf1a7c25db36fb28ad82df3a7ddbaef5140953218bc6/pycapnp-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:973d132853433054739646958719301bb925621c2649e8dbcee0d7e9aaffa926", size = 6405879, upload-time = "2026-01-21T01:20:23.676Z" },
{ url = "https://files.pythonhosted.org/packages/04/b7/e256add75663db169170257717943edc4beb3627a51d0b8ae6619a4f2f18/pycapnp-2.2.2-cp311-cp311-win32.whl", hash = "sha256:b2df0674a4edcbf89829e3537815c760b95fae2e37ec0a14af37dda05d62d062", size = 1061803, upload-time = "2026-01-21T01:20:25.134Z" },
{ url = "https://files.pythonhosted.org/packages/1c/64/f7c31fbead4a05340deabf8d4a5f541e8c3f00f63dbe79b31525bc108e40/pycapnp-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:a662c8bcc27f00ab86a78d377d01e42a61b510d0e33399267fef7639d020c71f", size = 1217362, upload-time = "2026-01-21T01:20:28.205Z" },
{ url = "https://files.pythonhosted.org/packages/8a/76/f8f81d32ddf950e934ec144facbc112e5acbef31a63ba5be0c5f34a00fd5/pycapnp-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b86cb8ea5b8011b562c4e022325a826a62f91196ceb5aa33a766c0bea0b8fd3", size = 1605194, upload-time = "2026-01-21T01:20:29.604Z" },
{ url = "https://files.pythonhosted.org/packages/50/dd/a31be782d56a8648fef899f39aeeab867cf544a6b170871e3f4cbfc58af6/pycapnp-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2353531cfa669e3eeb99be9f993573341650276abec46676d687cc12b3e6b6d9", size = 1486613, upload-time = "2026-01-21T01:20:31.415Z" },
{ url = "https://files.pythonhosted.org/packages/aa/bf/8da830dda94eb7327c6508d6c26fbd964897d742f8c1c0ec48623f0c515b/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ee27bdc78c7ccd8eaa0fe31e09f0ec4ef31deda3f475fc9373bb4b0de8083053", size = 5186701, upload-time = "2026-01-21T01:20:32.836Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a1/13d0baa2f337f4f6fe8c2142646ba437a26b9c433f5d7ce016a912bad052/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a8ded808911d1d7a9a2197626c09eea6e269e74dc1276760789538b1efcf6cd5", size = 5239464, upload-time = "2026-01-21T01:20:34.793Z" },
{ url = "https://files.pythonhosted.org/packages/82/76/0451c64b5f0132e4b75a0afe8cec957c8bf8fa981264a7c0b264cb94663e/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:59e92e1db40041d82a95eab0bd8de2676ce50c6b97c1457e2edde4d134b6d046", size = 5542887, upload-time = "2026-01-21T01:20:36.463Z" },
{ url = "https://files.pythonhosted.org/packages/04/00/d025d68d9a5330d55cbe2d018091cacfef0835c3ad422eb6778c4525041f/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:ee1e9ac2f0b80fa892b922b60e36efc925d072ecf1204ba3e59d8d9ac7c3dc83", size = 5659696, upload-time = "2026-01-21T01:20:38.069Z" },
{ url = "https://files.pythonhosted.org/packages/58/b7/28f7c539a5f4cbaa12e55ec27d081d11473464230f2e801e4714606d3453/pycapnp-2.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:53273b385be78ed8ac997ff8697f2a4c760e93c190b509822a937de5531f4861", size = 5413827, upload-time = "2026-01-21T01:20:39.781Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a7/83bc13d90675f0cee8a38d4ad8401bb2f8662c543b3a6622aeffb7b56b1e/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:812cbdd002bc542b63f969b85c6b9041dfdaf4185613635a6d4feea84c9092fa", size = 6046815, upload-time = "2026-01-21T01:20:42.172Z" },
{ url = "https://files.pythonhosted.org/packages/0d/8a/80f46baa1684bbcc4754ce22c5a44693a1209a64de6df2b256b85b8b8a97/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9c330218a44bd649b96f565dbf5326d183fdd20f9887bdedfeabd73f0366c2e1", size = 6367625, upload-time = "2026-01-21T01:20:44.004Z" },
{ url = "https://files.pythonhosted.org/packages/02/00/60e82eaf6b4e78d887157bf9f18234c852771cc575355e63d1114c4a5d79/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:796aa0ba18bcd4e6b2815471bbed059ad7ee8a815a30e81ac8a9aa030ec7818d", size = 6487265, upload-time = "2026-01-21T01:20:46.137Z" },
{ url = "https://files.pythonhosted.org/packages/57/6e/2dedd8f95dc22357c50a775ee2b8711b3d711f30344d244141e0e1962c3e/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:251a6abdd64b9b11d2a8e16fc365b922ef6ba6c968959b72a3a3d9d8ec8cc8d7", size = 6576699, upload-time = "2026-01-21T01:20:47.987Z" },
{ url = "https://files.pythonhosted.org/packages/2f/53/f7f69ed1d11ea30ea4f0f6d8319fbc18bc8781c480c118005e0a394492a7/pycapnp-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6aab811e0fcc27ae8bf5f04dedaa7e0af47e0d4db51d9c85ab0d2dad26a46bd7", size = 6344114, upload-time = "2026-01-21T01:20:50.367Z" },
{ url = "https://files.pythonhosted.org/packages/ab/78/ab78ee42797ff44c7e1fc0d1aa9396c6742cb05ff01a7cdf9c8f19e0defe/pycapnp-2.2.2-cp312-cp312-win32.whl", hash = "sha256:5061c85dd8f843b2656720ca6976d2a9b418845580c6f6d9602f7119fc2208d5", size = 1047207, upload-time = "2026-01-21T01:20:51.842Z" },
{ url = "https://files.pythonhosted.org/packages/4c/fb/6edf56d5144c476270fa8b2e6a660ef5a188fb0097193e342618fbcb0210/pycapnp-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:700eb8c77405222903af3fb5a371c0d766f86139c3d51f4bff41ccd6403b51f9", size = 1185178, upload-time = "2026-01-21T01:20:53.429Z" },
{ url = "https://files.pythonhosted.org/packages/ba/70/376c3f1be4ba453584bc96a9e6a7372486ce920cb9c0869c06066e77d626/pycapnp-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90138fceca1e85ea3eaa0de6656e33c4bef1c8da3c191db0a5b5bacc969f7889", size = 1603886, upload-time = "2026-01-21T01:20:55.383Z" },
{ url = "https://files.pythonhosted.org/packages/b6/11/2728b563f3f25d826024136cd3aab39f5d1727195de5c90a2ba3d232e897/pycapnp-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd0549036bfddb003f8e371c3c4ed3f56ac847953eb57cdd371100bb52afa64e", size = 1485731, upload-time = "2026-01-21T01:20:57.587Z" },
{ url = "https://files.pythonhosted.org/packages/77/1b/ab9bb376e7314b92dde24843fcf3d6459f10e48d95eb54d25912f973f5d7/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7c5a8f6d96017a7bb7e202fa8920fcdad119deab0d761f9aca1e6a4755376cef", size = 5209046, upload-time = "2026-01-21T01:20:59.049Z" },
{ url = "https://files.pythonhosted.org/packages/5f/62/303548df0316740caad513e4b81b18b2db1990785f3f01c36fd19889e7d4/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:bba31a8ba8ec32a04c5a14a5e469df7e1f1b85e169f49f7c2edfdfb78ec5075f", size = 5235782, upload-time = "2026-01-21T01:21:00.676Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ec/df320105d17e118207f01208af1e505d48981856c07fe10b04a8feab39d9/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2decdaeb517120e152f7d9ddede393a830087ac3f1024c5224f2ec58e1735abd", size = 5544647, upload-time = "2026-01-21T01:21:02.812Z" },
{ url = "https://files.pythonhosted.org/packages/03/47/bc3ea9b0d71cc3e0694993a5907ac56aab2c1ad803697be068fff55a0e1b/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:e879127ac9580005efcd51ec9ae903f1dd98954fb4ea88db9cf534e9a4afb379", size = 5596651, upload-time = "2026-01-21T01:21:04.504Z" },
{ url = "https://files.pythonhosted.org/packages/84/82/c500220d4eccca845a06f2f9d0747ad8043f8b893ef5278e2019cbc906cc/pycapnp-2.2.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7092c2393191221b8ce1c03ddc1343d1ff26d36129a831017d2371867e9c09bb", size = 5393131, upload-time = "2026-01-21T01:21:06.459Z" },
{ url = "https://files.pythonhosted.org/packages/ed/61/be35f0b8d81cf107a9d0329d180f1fd8f5d6d75e117fdf7fcad779443f17/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682812bf9ba4a60309b7150763cd5abed9341a31375398f3d2f60fee26143e13", size = 6071761, upload-time = "2026-01-21T01:21:08.238Z" },
{ url = "https://files.pythonhosted.org/packages/54/c0/d09da26ebb1bf0a130650a940b8e32f41219241c8d5f9c0891f15343dd9c/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6f321dbf33767bc2fd77dd66265177284697089f9e45b6dee7fd462b2b5cf918", size = 6369216, upload-time = "2026-01-21T01:21:10.338Z" },
{ url = "https://files.pythonhosted.org/packages/3d/58/a651de950437d8ef9fa91c3cce84ecd068e05011d421a6cf288f930f331c/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1d443d38f1cbeaec5b20b420117885fd5de812c5687e3bc8456d2e2c5cba371e", size = 6488273, upload-time = "2026-01-21T01:21:12.127Z" },
{ url = "https://files.pythonhosted.org/packages/4b/28/254f511272fde9fea1220807d759bcbdb47ecdff64ac860499be9aeddeaa/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:20a31fedb7ef30ec53d5d2d951a7cec8f639c954557093a3ba3d11aba5d174b4", size = 6542078, upload-time = "2026-01-21T01:21:14.581Z" },
{ url = "https://files.pythonhosted.org/packages/f9/b3/eea39216b7b59cefd21e6783e38a4d78db7e58c4f8f1f45e8351057432b7/pycapnp-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fbefb388464501899233d0b7d27256edd22c3cebb5a312da5d9f246671e9455e", size = 6333374, upload-time = "2026-01-21T01:21:16.522Z" },
{ url = "https://files.pythonhosted.org/packages/be/08/42ab52a3e7e5381d8fd7ae227ae75f54454f9e339d214ebe92763bb78115/pycapnp-2.2.2-cp313-cp313-win32.whl", hash = "sha256:a25e3b3ef40d430309acc7c024aa62b1ee6b2e7a85b341a8b7a8a6f8e29d4133", size = 1046173, upload-time = "2026-01-21T01:21:18.074Z" },
{ url = "https://files.pythonhosted.org/packages/34/d4/fec2023c4709d3631fe24e09dae2badc23d613f1288b4a6b398302945984/pycapnp-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:db65c9ec9d69bac6d19092db43b5e1f3cb2c680810418fd1e02316f7ef157fc1", size = 1184414, upload-time = "2026-01-21T01:21:19.334Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "ruff"
version = "0.14.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
{ url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
{ url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
{ url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
{ url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
{ url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
{ url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
{ url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
{ url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
{ url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
{ url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
{ url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
]
[[package]]
name = "tomli"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
+836
View File
@@ -0,0 +1,836 @@
@0xb2a3f4c5d6e7f8a9;
# ZAP - Zero-Copy App Proto
# Cap'n Proto schema for high-performance agent communication
using Go = import "/go.capnp";
$Go.package("zap");
$Go.import("github.com/zap-protocol/zap-go");
# Core types
struct Timestamp {
seconds @0 :Int64;
nanos @1 :UInt32;
}
struct Metadata {
entries @0 :List(Entry);
struct Entry {
key @0 :Text;
value @1 :Text;
}
}
# Tool definitions
struct Tool {
name @0 :Text;
description @1 :Text;
schema @2 :Data; # JSON Schema as bytes
annotations @3 :Metadata;
}
struct ToolList {
tools @0 :List(Tool);
}
struct ToolCall {
id @0 :Text;
name @1 :Text;
args @2 :Data; # JSON arguments as bytes
metadata @3 :Metadata;
}
struct ToolResult {
id @0 :Text;
content @1 :Data; # Result content as bytes
error @2 :Text;
metadata @3 :Metadata;
}
# Resource definitions
struct Resource {
uri @0 :Text;
name @1 :Text;
description @2 :Text;
mimeType @3 :Text;
annotations @4 :Metadata;
}
struct ResourceList {
resources @0 :List(Resource);
}
struct ResourceContent {
uri @0 :Text;
mimeType @1 :Text;
content :union {
text @2 :Text;
blob @3 :Data;
}
}
# Prompt definitions
struct Prompt {
name @0 :Text;
description @1 :Text;
arguments @2 :List(Argument);
struct Argument {
name @0 :Text;
description @1 :Text;
required @2 :Bool;
}
}
struct PromptList {
prompts @0 :List(Prompt);
}
struct PromptMessage {
role @0 :Role;
content @1 :Content;
enum Role {
user @0;
assistant @1;
system @2;
}
struct Content {
union {
text @0 :Text;
image @1 :ImageContent;
resource @2 :ResourceContent;
}
}
struct ImageContent {
data @0 :Data;
mimeType @1 :Text;
}
}
# Server info
struct ServerInfo {
name @0 :Text;
version @1 :Text;
capabilities @2 :Capabilities;
struct Capabilities {
tools @0 :Bool;
resources @1 :Bool;
prompts @2 :Bool;
logging @3 :Bool;
}
}
# Main ZAP interface
interface Zap {
# Initialize connection
init @0 (client :ClientInfo) -> (server :ServerInfo);
# Tool operations
listTools @1 () -> (tools :ToolList);
callTool @2 (call :ToolCall) -> (result :ToolResult);
# Resource operations
listResources @3 () -> (resources :ResourceList);
readResource @4 (uri :Text) -> (content :ResourceContent);
subscribe @5 (uri :Text) -> (stream :ResourceStream);
# Prompt operations
listPrompts @6 () -> (prompts :PromptList);
getPrompt @7 (name :Text, args :Metadata) -> (messages :List(PromptMessage));
# Logging
log @8 (level :LogLevel, message :Text, data :Data);
enum LogLevel {
debug @0;
info @1;
warn @2;
error @3;
}
}
struct ClientInfo {
name @0 :Text;
version @1 :Text;
}
interface ResourceStream {
next @0 () -> (content :ResourceContent, done :Bool);
cancel @1 () -> ();
}
# Gateway interface for MCP bridging
interface Gateway extends(Zap) {
# Add MCP server
addServer @0 (name :Text, url :Text, config :ServerConfig) -> (id :Text);
# Remove MCP server
removeServer @1 (id :Text) -> ();
# List connected servers
listServers @2 () -> (servers :List(ConnectedServer));
# Get server status
serverStatus @3 (id :Text) -> (status :ServerStatus);
struct ServerConfig {
transport @0 :Transport;
auth @1 :Auth;
timeout @2 :UInt32; # milliseconds
enum Transport {
stdio @0;
http @1;
websocket @2;
zap @3;
unix @4;
}
struct Auth {
union {
none @0 :Void;
bearer @1 :Text;
basic @2 :BasicAuth;
}
struct BasicAuth {
username @0 :Text;
password @1 :Text;
}
}
}
struct ConnectedServer {
id @0 :Text;
name @1 :Text;
url @2 :Text;
status @3 :ServerStatus;
tools @4 :UInt32;
resources @5 :UInt32;
}
enum ServerStatus {
connecting @0;
connected @1;
disconnected @2;
error @3;
}
}
# Coordination interface for distributed agents
interface Coordinator {
# Register agent
register @0 (agent :AgentInfo) -> (id :Text, gateway :Gateway);
# Heartbeat
heartbeat @1 (id :Text) -> (ok :Bool);
# Discover agents
discover @2 (filter :AgentFilter) -> (agents :List(AgentInfo));
struct AgentInfo {
id @0 :Text;
name @1 :Text;
capabilities @2 :List(Text);
metadata @3 :Metadata;
}
struct AgentFilter {
capabilities @0 :List(Text);
metadata @1 :Metadata;
}
}
# Post-Quantum Cryptography Types
# Implements NIST FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA)
struct MLKEMPublicKey {
# ML-KEM-768 public key (1184 bytes)
data @0 :Data;
}
struct MLKEMCiphertext {
# ML-KEM-768 ciphertext (1088 bytes)
data @0 :Data;
}
struct MLDSAPublicKey {
# ML-DSA-65 public key (1952 bytes)
data @0 :Data;
}
struct MLDSASignature {
# ML-DSA-65 detached signature (3293 bytes)
data @0 :Data;
}
struct X25519PublicKey {
# X25519 public key (32 bytes)
data @0 :Data;
}
# Hybrid X25519 + ML-KEM handshake for post-quantum key exchange
struct PQHandshake {
# Initiator's X25519 ephemeral public key
x25519PublicKey @0 :Data;
# Initiator's ML-KEM-768 public key
mlkemPublicKey @1 :MLKEMPublicKey;
# Responder's ML-KEM ciphertext (encapsulated shared secret)
mlkemCiphertext @2 :MLKEMCiphertext;
# Optional: signature over handshake transcript for authentication
signature @3 :MLDSASignature;
}
# Handshake message for initiating a PQ-secure connection
struct PQHandshakeInit {
# Client's X25519 ephemeral public key (32 bytes)
x25519PublicKey @0 :Data;
# Client's ML-KEM-768 public key for key encapsulation
mlkemPublicKey @1 :MLKEMPublicKey;
# Optional: Client's ML-DSA identity public key for authentication
identityKey @2 :MLDSAPublicKey;
# Optional: signature over (x25519PublicKey || mlkemPublicKey)
identitySignature @3 :MLDSASignature;
# Random nonce to prevent replay attacks
nonce @4 :Data;
# Protocol version
version @5 :UInt16;
}
# Handshake response from server
struct PQHandshakeResponse {
# Server's X25519 ephemeral public key (32 bytes)
x25519PublicKey @0 :Data;
# ML-KEM ciphertext containing encapsulated shared secret
mlkemCiphertext @1 :MLKEMCiphertext;
# Optional: Server's ML-DSA identity public key
identityKey @2 :MLDSAPublicKey;
# Optional: signature over (clientNonce || x25519PublicKey || mlkemCiphertext)
identitySignature @3 :MLDSASignature;
# Echo client nonce to bind response to request
clientNonce @4 :Data;
# Server nonce for session binding
serverNonce @5 :Data;
}
# Authenticated channel after PQ handshake
struct PQChannelMessage {
# Sequence number for replay protection
sequence @0 :UInt64;
# AEAD-encrypted payload (using derived session key)
ciphertext @1 :Data;
# AEAD authentication tag
tag @2 :Data;
# Associated data (plaintext, authenticated but not encrypted)
associatedData @3 :Data;
}
# Key rotation message for long-lived sessions
struct PQKeyRotation {
# New ML-KEM public key for next epoch
newMlkemPublicKey @0 :MLKEMPublicKey;
# ML-KEM ciphertext for the new key
mlkemCiphertext @1 :MLKEMCiphertext;
# Signature over (epoch || newMlkemPublicKey) with identity key
signature @2 :MLDSASignature;
# Epoch number (monotonically increasing)
epoch @3 :UInt64;
}
# =============================================================================
# Ringtail Consensus Types
# Threshold lattice-based signing protocol for post-quantum security
# =============================================================================
# Ring polynomial coefficients mod Q
struct Poly {
# Coefficients in Z_Q[X]/(X^phi + 1), phi=256
coeffs @0 :List(UInt64);
}
struct PolyVector {
polys @0 :List(Poly);
}
struct PolyMatrix {
rows @0 :List(PolyVector);
}
# Ringtail protocol parameters
struct RingtailParams {
m @0 :UInt16 = 8; # Matrix rows
n @1 :UInt16 = 7; # Matrix columns
dbar @2 :UInt16 = 48; # Commitment dimension
kappa @3 :UInt16 = 23; # Challenge weight (Hamming weight)
phi @4 :UInt16 = 256; # Ring dimension (2^8)
keySize @5 :UInt16 = 32; # Key size in bytes (256 bits)
q @6 :UInt64 = 0x1000000004A01; # 48-bit NTT-friendly prime modulus
xi @7 :UInt32 = 30; # Rounding parameter
nu @8 :UInt32 = 29; # Rounding parameter
}
# Party information for threshold signing
struct RingtailPartyInfo {
partyId @0 :UInt32; # This party's ID (0-indexed)
totalParties @1 :UInt32; # Total number of parties (K)
threshold @2 :UInt32; # Signing threshold (t-of-K)
address @3 :Text; # Network address
publicKey @4 :Data; # Party's public key share
}
# MAC for authenticating round 1 commitments
struct RingtailMac {
senderId @0 :UInt32;
recipientId @1 :UInt32;
value @2 :Data; # 32-byte BLAKE3 MAC
}
# Round 1 output: commitment D_i and MACs
struct RingtailRound1Output {
partyId @0 :UInt32;
sessionId @1 :UInt64;
dMatrix @2 :PolyMatrix; # M x (Dbar+1) commitment matrix D_i
macs @3 :List(RingtailMac); # MACs for other participating parties
timestamp @4 :Timestamp;
}
# Round 2 output: response share z_i
struct RingtailRound2Output {
partyId @0 :UInt32;
sessionId @1 :UInt64;
zShare @2 :PolyVector; # N-dimensional response vector z_i
timestamp @3 :Timestamp;
}
# Final Ringtail threshold signature (c, z, Delta)
struct RingtailSignature {
c @0 :Poly; # Challenge polynomial (sparse, Hamming weight kappa)
z @1 :PolyVector; # Aggregated response vector (N-dimensional)
delta @2 :PolyVector; # Correction term (M-dimensional)
signers @3 :List(UInt32); # IDs of parties that contributed
sessionId @4 :UInt64; # Signing session identifier
}
# Request to initiate threshold signing
struct RingtailSignRequest {
message @0 :Data; # Message to sign
sessionId @1 :UInt64; # Unique session identifier
participants @2 :List(UInt32); # Party IDs participating in this signing
timeoutMs @3 :UInt32; # Timeout in milliseconds
}
# Response from signing operation
struct RingtailSignResponse {
union {
signature @0 :RingtailSignature; # Completed signature
error @1 :Text; # Error message
progress @2 :RingtailSignProgress; # Still in progress
}
}
# Progress update during threshold signing
struct RingtailSignProgress {
sessionId @0 :UInt64;
currentRound @1 :UInt8; # 1 or 2
completedParties @2 :List(UInt32);
pendingParties @3 :List(UInt32);
estimatedTimeMs @4 :UInt32;
}
# Request to verify a Ringtail signature
struct RingtailVerifyRequest {
message @0 :Data;
signature @1 :RingtailSignature;
publicKey @2 :PolyVector; # Public key b_tilde
publicMatrix @3 :PolyMatrix; # Public matrix A
}
# Response from verification
struct RingtailVerifyResponse {
valid @0 :Bool;
error @1 :Text;
}
# Peer-to-peer message for distributed signing
struct RingtailPeerMessage {
from @0 :UInt32; # Sender party ID
to @1 :UInt32; # Recipient (0 = broadcast)
sessionId @2 :UInt64;
timestamp @3 :Timestamp;
union {
round1 @4 :RingtailRound1Output;
round2 @5 :RingtailRound2Output;
signRequest @6 :RingtailSignRequest;
signResponse @7 :RingtailSignResponse;
heartbeat @8 :RingtailHeartbeat;
abort @9 :RingtailAbort;
}
}
# Heartbeat for liveness checking
struct RingtailHeartbeat {
partyId @0 :UInt32;
status @1 :RingtailPartyStatus;
currentSession @2 :UInt64;
load @3 :Float32; # Current load (0.0-1.0)
}
enum RingtailPartyStatus {
idle @0;
signingRound1 @1;
signingRound2 @2;
combining @3;
busy @4;
offline @5;
}
# Abort message to cancel a signing session
struct RingtailAbort {
sessionId @0 :UInt64;
reason @1 :Text;
partyId @2 :UInt32;
}
# =============================================================================
# Agent Consensus Types
# Simplified voting-based consensus for AI agent response aggregation
# =============================================================================
# Query identifier (32-byte hash)
struct AgentQueryId {
hash @0 :Data; # BLAKE3 hash of query content
}
# State of a consensus query
struct AgentQueryState {
queryId @0 :AgentQueryId;
query @1 :Text; # Original query content
responses @2 :List(AgentResponseEntry);
votes @3 :List(AgentResponseVote);
finalized @4 :Bool;
result @5 :Text; # Final consensus result (if finalized)
createdAt @6 :Timestamp;
}
# Response from an agent
struct AgentResponseEntry {
agentId @0 :Text;
response @1 :Text;
timestamp @2 :Timestamp;
signature @3 :Data; # Optional signature over response
confidence @4 :Float32; # Agent's confidence (0.0-1.0)
}
# Vote tally for a response
struct AgentResponseVote {
responseHash @0 :Data; # BLAKE3 hash of response content
voteCount @1 :UInt32;
voters @2 :List(Text); # Agent IDs that voted for this response
}
# Configuration for agent consensus
struct AgentConsensusConfig {
threshold @0 :Float64; # Required vote fraction (0.0-1.0)
minResponses @1 :UInt32; # Minimum responses before consensus check
timeoutSecs @2 :UInt32; # Query timeout in seconds
requireSignatures @3 :Bool; # Whether agent signatures are required
}
# Request to submit a new query
struct AgentSubmitQueryRequest {
query @0 :Text;
config @1 :AgentConsensusConfig;
}
# Response from submitting a query
struct AgentSubmitQueryResponse {
queryId @0 :AgentQueryId;
}
# Request to submit an agent's response
struct AgentSubmitResponseRequest {
queryId @0 :AgentQueryId;
agentId @1 :Text;
response @2 :Text;
signature @3 :Data; # Optional signature
confidence @4 :Float32;
}
# Result of submitting a response
struct AgentSubmitResponseResult {
union {
success @0 :Void;
error @1 :Text;
duplicate @2 :Void; # Agent already submitted
}
}
# Request to check for consensus
struct AgentTryConsensusRequest {
queryId @0 :AgentQueryId;
}
# Result of consensus check
struct AgentTryConsensusResponse {
union {
result @0 :Text; # Consensus reached - final answer
pending @1 :AgentQueryState; # Still collecting responses
noConsensus @2 :AgentQueryState; # Min responses met but no consensus
error @3 :Text;
}
}
# =============================================================================
# RPC Interfaces for Consensus
# =============================================================================
# Threshold signing party interface
interface RingtailParty {
# Get party information
getInfo @0 () -> (info :RingtailPartyInfo);
# Execute Round 1 of signing protocol
signRound1 @1 (request :RingtailSignRequest) -> (output :RingtailRound1Output);
# Execute Round 2 of signing protocol
signRound2 @2 (
request :RingtailSignRequest,
round1Outputs :List(RingtailRound1Output)
) -> (output :RingtailRound2Output);
# Finalize signature (combiner only)
finalize @3 (
request :RingtailSignRequest,
round2Outputs :List(RingtailRound2Output)
) -> (response :RingtailSignResponse);
# Verify a signature
verify @4 (request :RingtailVerifyRequest) -> (response :RingtailVerifyResponse);
}
# Agent consensus voting service
interface AgentConsensusService {
# Submit a new query for consensus
submitQuery @0 (request :AgentSubmitQueryRequest) -> (response :AgentSubmitQueryResponse);
# Submit an agent's response to a query
submitResponse @1 (request :AgentSubmitResponseRequest) -> (result :AgentSubmitResponseResult);
# Try to reach consensus on a query
tryConsensus @2 (request :AgentTryConsensusRequest) -> (response :AgentTryConsensusResponse);
# Get current query state
getQuery @3 (queryId :AgentQueryId) -> (state :AgentQueryState);
# Clean up expired queries
cleanup @4 () -> ();
# Get number of active queries
activeQueries @5 () -> (count :UInt32);
}
# Coordinator for distributed signing sessions
interface RingtailCoordinator {
# Initialize a new signing session
initSession @0 (
message :Data,
participants :List(UInt32)
) -> (sessionId :UInt64);
# Collect Round 1 outputs from all parties
collectRound1 @1 (sessionId :UInt64) -> (outputs :List(RingtailRound1Output));
# Collect Round 2 outputs from all parties
collectRound2 @2 (sessionId :UInt64) -> (outputs :List(RingtailRound2Output));
# Get final signature for a session
getSignature @3 (sessionId :UInt64) -> (response :RingtailSignResponse);
# Cancel a signing session
cancelSession @4 (sessionId :UInt64) -> ();
# List active sessions
listSessions @5 () -> (sessions :List(UInt64));
}
# =============================================================================
# W3C Decentralized Identifier (DID) Types
# Implements W3C DID Core 1.0 specification
# =============================================================================
# DID Method enum
enum DidMethod {
lux @0; # Lux blockchain-anchored DID
key @1; # Self-certifying DID from cryptographic key
web @2; # DNS-based DID
}
# Decentralized Identifier
struct Did {
method @0 :DidMethod;
id @1 :Text; # Method-specific identifier (e.g., z6Mk...)
}
# Verification method type
enum VerificationMethodType {
jsonWebKey2020 @0;
multikey @1;
mlDsa65VerificationKey2024 @2;
}
# Verification method (public key) in DID Document
struct VerificationMethod {
id @0 :Text; # e.g., "did:lux:z6Mk...#keys-1"
type @1 :VerificationMethodType;
controller @2 :Text; # Controller DID
publicKeyMultibase @3 :Text; # Multibase-encoded public key
publicKeyJwk @4 :Data; # JSON Web Key (optional, as JSON bytes)
blockchainAccountId @5 :Text; # Blockchain account ID (for Lux DIDs)
}
# Service type
enum ServiceType {
zapAgent @0; # ZAP Agent service
didCommMessaging @1; # DID Communication
linkedDomains @2; # Linked Domains
credentialRegistry @3; # Credential Registry
}
# Service endpoint
struct ServiceEndpoint {
union {
uri @0 :Text; # Single URI endpoint
uris @1 :List(Text); # Multiple URI endpoints
structured @2 :StructuredEndpoint;
}
struct StructuredEndpoint {
uri @0 :Text;
accept @1 :List(Text);
routingKeys @2 :List(Text);
}
}
# Service in DID Document
struct Service {
id @0 :Text; # e.g., "did:lux:z6Mk...#zap-agent"
type @1 :ServiceType;
serviceEndpoint @2 :ServiceEndpoint;
}
# W3C DID Document
struct DidDocument {
context @0 :List(Text); # JSON-LD context
id @1 :Text; # DID subject
controller @2 :Text; # Optional controller DID
# Verification methods (public keys)
verificationMethod @3 :List(VerificationMethod);
# Verification relationships (references to verification method IDs)
authentication @4 :List(Text);
assertionMethod @5 :List(Text);
keyAgreement @6 :List(Text);
capabilityInvocation @7 :List(Text);
capabilityDelegation @8 :List(Text);
# Service endpoints
service @9 :List(Service);
}
# Node identity for ZAP network participation
struct NodeIdentity {
did @0 :Did;
publicKey @1 :Data; # ML-DSA-65 public key (1952 bytes)
stake @2 :UInt64; # Staked amount (optional, 0 if none)
stakeRegistry @3 :Text; # Stake registry reference
}
# Stake registry entry
struct StakeEntry {
did @0 :Did;
amount @1 :UInt64;
timestamp @2 :Timestamp;
}
# Request to resolve a DID to its document
struct DidResolveRequest {
did @0 :Text; # Full DID URI string
}
# Response from DID resolution
struct DidResolveResponse {
union {
document @0 :DidDocument;
error @1 :Text;
notFound @2 :Void;
}
}
# Request to create/register a new DID
struct DidCreateRequest {
method @0 :DidMethod;
publicKey @1 :Data; # ML-DSA-65 public key
services @2 :List(Service); # Optional initial services
}
# Response from DID creation
struct DidCreateResponse {
union {
did @0 :Did;
error @1 :Text;
}
}
# DID Registry interface for resolution and management
interface DidRegistry {
# Resolve a DID to its document
resolve @0 (request :DidResolveRequest) -> (response :DidResolveResponse);
# Create/register a new DID
create @1 (request :DidCreateRequest) -> (response :DidCreateResponse);
# Update a DID document (requires authentication)
update @2 (did :Text, document :DidDocument, signature :Data) -> (success :Bool, error :Text);
# Deactivate a DID (requires authentication)
deactivate @3 (did :Text, signature :Data) -> (success :Bool, error :Text);
# Get stake for a DID
getStake @4 (did :Text) -> (amount :UInt64);
# Set stake for a DID (requires authentication)
setStake @5 (did :Text, amount :UInt64, signature :Data) -> (success :Bool, error :Text);
}
+431
View File
@@ -0,0 +1,431 @@
//! Agentic consensus for response voting
//!
//! Agents vote on responses to queries. No trust needed - majority wins.
//! As long as majority are honest, you get correct results.
use crate::error::Error;
use crate::identity::Did;
use blake3::Hasher;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
/// Query ID (32-byte hash)
pub type QueryId = [u8; 32];
/// Response ID (32-byte hash)
pub type ResponseId = [u8; 32];
/// A query submitted to the agent network
#[derive(Debug, Clone)]
pub struct Query {
pub id: QueryId,
pub content: String,
pub submitter: Did,
pub timestamp: u64,
}
impl Query {
/// Create a new query
pub fn new(content: String, submitter: Did) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut hasher = Hasher::new();
hasher.update(content.as_bytes());
hasher.update(submitter.to_string().as_bytes());
hasher.update(&timestamp.to_le_bytes());
let hash: [u8; 32] = *hasher.finalize().as_bytes();
Self {
id: hash,
content,
submitter,
timestamp,
}
}
}
/// A response to a query
#[derive(Debug, Clone)]
pub struct Response {
pub id: ResponseId,
pub query_id: QueryId,
pub content: String,
pub responder: Did,
pub timestamp: u64,
}
impl Response {
/// Create a new response
pub fn new(query_id: QueryId, content: String, responder: Did) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut hasher = Hasher::new();
hasher.update(&query_id);
hasher.update(content.as_bytes());
hasher.update(responder.to_string().as_bytes());
hasher.update(&timestamp.to_le_bytes());
let hash: [u8; 32] = *hasher.finalize().as_bytes();
Self {
id: hash,
query_id,
content,
responder,
timestamp,
}
}
}
/// Result of consensus voting
#[derive(Debug, Clone)]
pub struct ConsensusResult {
/// The winning response
pub response: Response,
/// Number of votes for this response
pub votes: usize,
/// Total number of voters
pub total_voters: usize,
/// Confidence (votes / total)
pub confidence: f64,
}
/// Internal state for a query
struct QueryState {
query: Query,
responses: HashMap<ResponseId, Response>,
votes: HashMap<ResponseId, Vec<Did>>,
finalized: Option<ResponseId>,
}
/// Agentic consensus for response voting
///
/// Agents submit responses and vote. Majority wins.
pub struct AgentConsensusVoting {
queries: Arc<RwLock<HashMap<QueryId, QueryState>>>,
/// Threshold for consensus (e.g., 0.5 for simple majority)
threshold: f64,
/// Minimum responses before consensus can be reached
min_responses: usize,
/// Minimum votes before consensus can be reached
min_votes: usize,
}
impl AgentConsensusVoting {
/// Create new consensus instance
///
/// # Arguments
/// * `threshold` - Fraction of votes needed (0.5 = majority)
/// * `min_responses` - Minimum responses before checking consensus
/// * `min_votes` - Minimum votes before checking consensus
pub fn new(threshold: f64, min_responses: usize, min_votes: usize) -> Self {
Self {
queries: Arc::new(RwLock::new(HashMap::new())),
threshold: threshold.clamp(0.0, 1.0),
min_responses,
min_votes,
}
}
/// Submit a new query
pub async fn submit_query(&self, query: Query) -> QueryId {
let mut queries = self.queries.write().await;
let id = query.id;
queries.insert(
id,
QueryState {
query,
responses: HashMap::new(),
votes: HashMap::new(),
finalized: None,
},
);
id
}
/// Submit a response to a query
pub async fn submit_response(&self, response: Response) -> Result<ResponseId, Error> {
let mut queries = self.queries.write().await;
let state = queries
.get_mut(&response.query_id)
.ok_or_else(|| Error::Consensus("Query not found".into()))?;
if state.finalized.is_some() {
return Err(Error::Consensus("Query already finalized".into()));
}
let id = response.id;
state.responses.insert(id, response);
state.votes.insert(id, Vec::new());
Ok(id)
}
/// Vote for a response
///
/// Each agent can only vote once per query (across all responses)
pub async fn vote(
&self,
query_id: QueryId,
response_id: ResponseId,
voter: Did,
) -> Result<(), Error> {
let mut queries = self.queries.write().await;
let state = queries
.get_mut(&query_id)
.ok_or_else(|| Error::Consensus("Query not found".into()))?;
if state.finalized.is_some() {
return Err(Error::Consensus("Query already finalized".into()));
}
if !state.responses.contains_key(&response_id) {
return Err(Error::Consensus("Response not found".into()));
}
// Check if voter already voted
for votes in state.votes.values() {
if votes.iter().any(|v| v == &voter) {
return Err(Error::Consensus("Already voted on this query".into()));
}
}
state.votes.get_mut(&response_id).unwrap().push(voter);
// Check if consensus reached
self.check_consensus(state);
Ok(())
}
/// Check if consensus has been reached
fn check_consensus(&self, state: &mut QueryState) {
if state.finalized.is_some() {
return;
}
// Need minimum responses
if state.responses.len() < self.min_responses {
return;
}
// Count total votes
let total_votes: usize = state.votes.values().map(|v| v.len()).sum();
if total_votes < self.min_votes {
return;
}
// Find response with most votes that meets threshold
let mut best: Option<(ResponseId, usize)> = None;
for (response_id, voters) in &state.votes {
let vote_count = voters.len();
let confidence = vote_count as f64 / total_votes as f64;
if confidence >= self.threshold {
match best {
None => best = Some((*response_id, vote_count)),
Some((_, best_count)) if vote_count > best_count => {
best = Some((*response_id, vote_count))
}
_ => {}
}
}
}
if let Some((response_id, _)) = best {
state.finalized = Some(response_id);
}
}
/// Get the consensus result for a query
pub async fn get_result(&self, query_id: QueryId) -> Option<ConsensusResult> {
let queries = self.queries.read().await;
let state = queries.get(&query_id)?;
let winning_id = state.finalized?;
let response = state.responses.get(&winning_id)?.clone();
let votes = state.votes.get(&winning_id)?.len();
let total_voters: usize = state.votes.values().map(|v| v.len()).sum();
Some(ConsensusResult {
response,
votes,
total_voters,
confidence: if total_voters > 0 {
votes as f64 / total_voters as f64
} else {
0.0
},
})
}
/// Check if a query has reached consensus
pub async fn is_finalized(&self, query_id: QueryId) -> bool {
let queries = self.queries.read().await;
queries
.get(&query_id)
.map(|s| s.finalized.is_some())
.unwrap_or(false)
}
/// Get all responses for a query
pub async fn get_responses(&self, query_id: QueryId) -> Option<Vec<Response>> {
let queries = self.queries.read().await;
let state = queries.get(&query_id)?;
Some(state.responses.values().cloned().collect())
}
/// Get vote counts for a query
pub async fn get_vote_counts(&self, query_id: QueryId) -> Option<HashMap<ResponseId, usize>> {
let queries = self.queries.read().await;
let state = queries.get(&query_id)?;
Some(
state
.votes
.iter()
.map(|(id, voters)| (*id, voters.len()))
.collect(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::DidMethod;
fn make_did(name: &str) -> Did {
Did {
method: DidMethod::Lux,
id: format!("z6Mk{}", name),
}
}
#[tokio::test]
async fn test_submit_query() {
let consensus = AgentConsensusVoting::new(0.5, 1, 1);
let query = Query::new("What is 2+2?".into(), make_did("Alice"));
let id = consensus.submit_query(query.clone()).await;
assert_eq!(id, query.id);
}
#[tokio::test]
async fn test_submit_response() {
let consensus = AgentConsensusVoting::new(0.5, 1, 1);
let query = Query::new("What is 2+2?".into(), make_did("Alice"));
let query_id = consensus.submit_query(query).await;
let response = Response::new(query_id, "4".into(), make_did("Bob"));
let result = consensus.submit_response(response.clone()).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_vote_and_consensus() {
let consensus = AgentConsensusVoting::new(0.5, 1, 2);
let query = Query::new("What is 2+2?".into(), make_did("Alice"));
let query_id = consensus.submit_query(query).await;
let response = Response::new(query_id, "4".into(), make_did("Bob"));
let response_id = consensus.submit_response(response).await.unwrap();
// First vote - not enough yet
consensus
.vote(query_id, response_id, make_did("Voter1"))
.await
.unwrap();
assert!(!consensus.is_finalized(query_id).await);
// Second vote - should reach consensus
consensus
.vote(query_id, response_id, make_did("Voter2"))
.await
.unwrap();
assert!(consensus.is_finalized(query_id).await);
let result = consensus.get_result(query_id).await.unwrap();
assert_eq!(result.response.content, "4");
assert_eq!(result.votes, 2);
assert_eq!(result.confidence, 1.0);
}
#[tokio::test]
async fn test_double_vote_prevented() {
let consensus = AgentConsensusVoting::new(0.5, 1, 1);
let query = Query::new("Test".into(), make_did("Alice"));
let query_id = consensus.submit_query(query).await;
let response = Response::new(query_id, "Answer".into(), make_did("Bob"));
let response_id = consensus.submit_response(response).await.unwrap();
let voter = make_did("Voter1");
consensus.vote(query_id, response_id, voter.clone()).await.unwrap();
// Second vote from same voter should fail
let result = consensus.vote(query_id, response_id, voter).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_majority_wins() {
let consensus = AgentConsensusVoting::new(0.5, 2, 3);
let query = Query::new("Best language?".into(), make_did("Alice"));
let query_id = consensus.submit_query(query).await;
let r1 = Response::new(query_id, "Rust".into(), make_did("Bob"));
let r1_id = consensus.submit_response(r1).await.unwrap();
let r2 = Response::new(query_id, "Python".into(), make_did("Carol"));
let r2_id = consensus.submit_response(r2).await.unwrap();
// Vote: 2 for Rust, 1 for Python
consensus.vote(query_id, r1_id, make_did("V1")).await.unwrap();
consensus.vote(query_id, r1_id, make_did("V2")).await.unwrap();
consensus.vote(query_id, r2_id, make_did("V3")).await.unwrap();
assert!(consensus.is_finalized(query_id).await);
let result = consensus.get_result(query_id).await.unwrap();
assert_eq!(result.response.content, "Rust");
assert_eq!(result.votes, 2);
assert_eq!(result.total_voters, 3);
}
#[tokio::test]
async fn test_no_consensus_below_threshold() {
let consensus = AgentConsensusVoting::new(0.6, 2, 3);
let query = Query::new("Test".into(), make_did("Alice"));
let query_id = consensus.submit_query(query).await;
let r1 = Response::new(query_id, "A".into(), make_did("Bob"));
let r1_id = consensus.submit_response(r1).await.unwrap();
let r2 = Response::new(query_id, "B".into(), make_did("Carol"));
let r2_id = consensus.submit_response(r2).await.unwrap();
// Split vote: 1-1-1 (none reaches 60%)
let r3 = Response::new(query_id, "C".into(), make_did("Dave"));
let r3_id = consensus.submit_response(r3).await.unwrap();
consensus.vote(query_id, r1_id, make_did("V1")).await.unwrap();
consensus.vote(query_id, r2_id, make_did("V2")).await.unwrap();
consensus.vote(query_id, r3_id, make_did("V3")).await.unwrap();
// No consensus - 33% each, threshold is 60%
assert!(!consensus.is_finalized(query_id).await);
}
#[tokio::test]
async fn test_get_vote_counts() {
let consensus = AgentConsensusVoting::new(0.5, 1, 1);
let query = Query::new("Test".into(), make_did("Alice"));
let query_id = consensus.submit_query(query).await;
let r1 = Response::new(query_id, "A".into(), make_did("Bob"));
let r1_id = consensus.submit_response(r1).await.unwrap();
consensus.vote(query_id, r1_id, make_did("V1")).await.unwrap();
let counts = consensus.get_vote_counts(query_id).await.unwrap();
assert_eq!(counts.get(&r1_id), Some(&1));
}
}
+109
View File
@@ -0,0 +1,109 @@
//! ZAP CLI tool
use clap::{Parser, Subcommand};
use zap::{Client, Config, Result};
#[derive(Parser)]
#[command(name = "zap")]
#[command(about = "ZAP - Zero-Copy App Proto CLI")]
#[command(version)]
struct Cli {
/// Config file path
#[arg(short, long, global = true)]
config: Option<std::path::PathBuf>,
/// Gateway URL
#[arg(short = 'u', long, global = true, default_value = "zap://localhost:9999")]
url: String,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// List available tools
Tools,
/// Call a tool
Call {
/// Tool name
name: String,
/// JSON arguments
#[arg(default_value = "{}")]
args: String,
},
/// List available resources
Resources,
/// Read a resource
Read {
/// Resource URI
uri: String,
},
/// List available prompts
Prompts,
/// Get a prompt
Prompt {
/// Prompt name
name: String,
/// JSON arguments
#[arg(default_value = "{}")]
args: String,
},
/// Show gateway status
Status,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
let client = Client::connect(&cli.url).await?;
match cli.command {
Commands::Tools => {
let tools = client.list_tools().await?;
for tool in tools {
println!("{}: {}", tool.name, tool.description);
}
}
Commands::Call { name, args } => {
let args: serde_json::Value = serde_json::from_str(&args)?;
let result = client.call_tool(&name, args).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
}
Commands::Resources => {
let resources = client.list_resources().await?;
for resource in resources {
println!("{}: {}", resource.uri, resource.name);
}
}
Commands::Read { uri } => {
let content = client.read_resource(&uri).await?;
match content.content {
zap::client::Content::Text(text) => println!("{}", text),
zap::client::Content::Blob(blob) => {
use std::io::Write;
std::io::stdout().write_all(&blob)?;
}
}
}
Commands::Prompts => {
println!("Prompts listing not yet implemented");
}
Commands::Prompt { name, args } => {
println!("Prompt {} with args {}", name, args);
}
Commands::Status => {
println!("Connected to {}", cli.url);
}
}
Ok(())
}
+59
View File
@@ -0,0 +1,59 @@
//! ZAP daemon (gateway)
use clap::Parser;
use zap::{Config, Gateway, Result};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "zapd")]
#[command(about = "ZAP daemon - Gateway for MCP and ZAP servers")]
#[command(version)]
struct Cli {
/// Config file path
#[arg(short, long)]
config: Option<PathBuf>,
/// Listen address
#[arg(short, long, default_value = "0.0.0.0")]
listen: String,
/// Port
#[arg(short, long, default_value = "9999")]
port: u16,
/// Log level
#[arg(long, default_value = "info")]
log_level: String,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(&cli.log_level)
.init();
let config = if let Some(path) = &cli.config {
Config::load(path)?
} else {
Config {
listen: cli.listen,
port: cli.port,
log_level: cli.log_level,
..Default::default()
}
};
tracing::info!(
"Starting ZAP gateway v{} on {}:{}",
zap::VERSION,
config.listen,
config.port
);
let mut gateway = Gateway::new(config);
gateway.run().await?;
Ok(())
}
+79
View File
@@ -0,0 +1,79 @@
//! ZAP client implementation
use crate::{Error, Result};
use serde_json::Value;
/// ZAP client for connecting to ZAP gateways
pub struct Client {
url: String,
// Connection state would be here
}
impl Client {
/// Connect to a ZAP gateway
pub async fn connect(url: &str) -> Result<Self> {
let url = url.to_string();
// TODO: Establish Cap'n Proto RPC connection
Ok(Self { url })
}
/// List available tools
pub async fn list_tools(&self) -> Result<Vec<Tool>> {
// TODO: Implement RPC call
Ok(Vec::new())
}
/// Call a tool
pub async fn call_tool(&self, name: &str, args: Value) -> Result<Value> {
// TODO: Implement RPC call
Ok(Value::Null)
}
/// List available resources
pub async fn list_resources(&self) -> Result<Vec<Resource>> {
// TODO: Implement RPC call
Ok(Vec::new())
}
/// Read a resource
pub async fn read_resource(&self, uri: &str) -> Result<ResourceContent> {
// TODO: Implement RPC call
Ok(ResourceContent {
uri: uri.to_string(),
mime_type: "text/plain".to_string(),
content: Content::Text(String::new()),
})
}
}
/// Tool definition
#[derive(Debug, Clone)]
pub struct Tool {
pub name: String,
pub description: String,
pub schema: Value,
}
/// Resource definition
#[derive(Debug, Clone)]
pub struct Resource {
pub uri: String,
pub name: String,
pub description: String,
pub mime_type: String,
}
/// Resource content
#[derive(Debug, Clone)]
pub struct ResourceContent {
pub uri: String,
pub mime_type: String,
pub content: Content,
}
/// Content types
#[derive(Debug, Clone)]
pub enum Content {
Text(String),
Blob(Vec<u8>),
}
+118
View File
@@ -0,0 +1,118 @@
//! Configuration for ZAP
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// ZAP configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Listen address for the gateway
#[serde(default = "default_listen")]
pub listen: String,
/// Port number
#[serde(default = "default_port")]
pub port: u16,
/// Connected servers
#[serde(default)]
pub servers: Vec<ServerConfig>,
/// Logging level
#[serde(default = "default_log_level")]
pub log_level: String,
}
/// Server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
/// Server name
pub name: String,
/// Server URL (stdio://, http://, ws://, zap://, unix://)
pub url: String,
/// Transport type
#[serde(default)]
pub transport: Transport,
/// Connection timeout in milliseconds
#[serde(default = "default_timeout")]
pub timeout: u32,
/// Authentication
#[serde(default)]
pub auth: Option<Auth>,
}
/// Transport type
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Transport {
#[default]
Stdio,
Http,
WebSocket,
Zap,
Unix,
}
/// Authentication
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Auth {
Bearer { token: String },
Basic { username: String, password: String },
}
fn default_listen() -> String {
"0.0.0.0".to_string()
}
fn default_port() -> u16 {
9999
}
fn default_timeout() -> u32 {
30000
}
fn default_log_level() -> String {
"info".to_string()
}
impl Default for Config {
fn default() -> Self {
Self {
listen: default_listen(),
port: default_port(),
servers: Vec::new(),
log_level: default_log_level(),
}
}
}
impl Config {
/// Load config from file
pub fn load(path: &PathBuf) -> crate::Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)
.map_err(|e| crate::Error::Config(e.to_string()))?;
Ok(config)
}
/// Save config to file
pub fn save(&self, path: &PathBuf) -> crate::Result<()> {
let content = toml::to_string_pretty(self)
.map_err(|e| crate::Error::Config(e.to_string()))?;
std::fs::write(path, content)?;
Ok(())
}
/// Get default config path
pub fn default_path() -> PathBuf {
directories::ProjectDirs::from("ai", "hanzo", "zap")
.map(|dirs| dirs.config_dir().join("config.toml"))
.unwrap_or_else(|| PathBuf::from("zap.toml"))
}
}
File diff suppressed because it is too large Load Diff
+587
View File
@@ -0,0 +1,587 @@
//! Post-Quantum Cryptography Module for ZAP
//!
//! Provides ML-KEM-768 key exchange, ML-DSA-65 signatures, and hybrid X25519+ML-KEM handshake.
//!
//! # Security
//!
//! This module implements NIST FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) standards
//! for post-quantum cryptographic protection. The hybrid handshake combines
//! classical X25519 with ML-KEM-768 for defense-in-depth.
//!
//! # Example
//!
//! ```rust,ignore
//! use zap::crypto::{PQKeyExchange, PQSignature, HybridHandshake};
//!
//! // Key exchange
//! let alice = PQKeyExchange::generate()?;
//! let (ciphertext, shared_alice) = alice.encapsulate(&bob_pk)?;
//! let shared_bob = bob.decapsulate(&ciphertext)?;
//! assert_eq!(shared_alice, shared_bob);
//!
//! // Signatures
//! let signer = PQSignature::generate()?;
//! let sig = signer.sign(b"message")?;
//! signer.verify(b"message", &sig)?;
//!
//! // Hybrid handshake
//! let initiator = HybridHandshake::initiate()?;
//! let (responder, response) = HybridHandshake::respond(&initiator.public_data())?;
//! let shared = initiator.finalize(&response)?;
//! ```
use crate::error::{Error, Result};
// Constants for key/ciphertext sizes
// ML-KEM-768 sizes
/// ML-KEM-768 public key size in bytes
pub const MLKEM_PUBLIC_KEY_SIZE: usize = 1184;
/// ML-KEM-768 ciphertext size in bytes
pub const MLKEM_CIPHERTEXT_SIZE: usize = 1088;
/// ML-KEM-768 shared secret size in bytes
pub const MLKEM_SHARED_SECRET_SIZE: usize = 32;
// ML-DSA-65 (Dilithium3) sizes
/// ML-DSA-65 public key size in bytes
pub const MLDSA_PUBLIC_KEY_SIZE: usize = 1952;
/// ML-DSA-65 signature size in bytes
pub const MLDSA_SIGNATURE_SIZE: usize = 3309;
/// ML-DSA-65 secret key size in bytes
pub const MLDSA_SECRET_KEY_SIZE: usize = 4000;
// X25519 sizes
/// X25519 public key size in bytes
pub const X25519_PUBLIC_KEY_SIZE: usize = 32;
/// Hybrid shared secret size after HKDF
pub const HYBRID_SHARED_SECRET_SIZE: usize = 32;
#[cfg(feature = "pq")]
mod pq_impl {
use super::*;
use hkdf::Hkdf;
use pqcrypto_dilithium::dilithium3;
use pqcrypto_mlkem::mlkem768;
use pqcrypto_traits::kem::{Ciphertext, PublicKey as KemPublicKey, SharedSecret};
use pqcrypto_traits::sign::{
DetachedSignature as DetachedSignatureTrait, PublicKey as SignPublicKey,
SecretKey as SignSecretKey,
};
use rand::rngs::OsRng;
use sha2::Sha256;
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
use zeroize::Zeroize;
/// ML-KEM-768 Key Encapsulation Mechanism
///
/// Implements NIST FIPS 203 ML-KEM-768 for post-quantum key exchange.
/// Security level: NIST Level 3 (~AES-192 equivalent).
pub struct PQKeyExchange {
public_key: mlkem768::PublicKey,
secret_key: mlkem768::SecretKey,
}
impl PQKeyExchange {
/// Generate a new ML-KEM-768 keypair
pub fn generate() -> Result<Self> {
let (pk, sk) = mlkem768::keypair();
Ok(Self {
public_key: pk,
secret_key: sk,
})
}
/// Get the public key bytes
pub fn public_key_bytes(&self) -> Vec<u8> {
self.public_key.as_bytes().to_vec()
}
/// Create from existing public key bytes (for encapsulation only)
pub fn from_public_key(bytes: &[u8]) -> Result<Self> {
if bytes.len() != MLKEM_PUBLIC_KEY_SIZE {
return Err(Error::Crypto(format!(
"invalid ML-KEM public key size: expected {}, got {}",
MLKEM_PUBLIC_KEY_SIZE,
bytes.len()
)));
}
let pk = mlkem768::PublicKey::from_bytes(bytes)
.map_err(|e| Error::Crypto(format!("invalid ML-KEM public key: {e:?}")))?;
// Create dummy secret key - this instance can only encapsulate
let (_, dummy_sk) = mlkem768::keypair();
Ok(Self {
public_key: pk,
secret_key: dummy_sk,
})
}
/// Encapsulate: generate ciphertext and shared secret for a recipient's public key
pub fn encapsulate(&self, recipient_pk: &[u8]) -> Result<(Vec<u8>, [u8; 32])> {
let pk = mlkem768::PublicKey::from_bytes(recipient_pk)
.map_err(|e| Error::Crypto(format!("invalid recipient public key: {e:?}")))?;
let (ss, ct) = mlkem768::encapsulate(&pk);
let mut shared = [0u8; 32];
shared.copy_from_slice(ss.as_bytes());
Ok((ct.as_bytes().to_vec(), shared))
}
/// Decapsulate: recover shared secret from ciphertext
pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<[u8; 32]> {
if ciphertext.len() != MLKEM_CIPHERTEXT_SIZE {
return Err(Error::Crypto(format!(
"invalid ML-KEM ciphertext size: expected {}, got {}",
MLKEM_CIPHERTEXT_SIZE,
ciphertext.len()
)));
}
let ct = mlkem768::Ciphertext::from_bytes(ciphertext)
.map_err(|e| Error::Crypto(format!("invalid ciphertext: {e:?}")))?;
let ss = mlkem768::decapsulate(&ct, &self.secret_key);
let mut shared = [0u8; 32];
shared.copy_from_slice(ss.as_bytes());
Ok(shared)
}
}
/// ML-DSA-65 Digital Signature Algorithm
///
/// Implements NIST FIPS 204 ML-DSA-65 (Dilithium3) for post-quantum signatures.
/// Security level: NIST Level 3 (~AES-192 equivalent).
pub struct PQSignature {
public_key: dilithium3::PublicKey,
secret_key: Option<dilithium3::SecretKey>,
}
impl std::fmt::Debug for PQSignature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PQSignature")
.field("public_key", &"<public_key>")
.field("secret_key", &self.secret_key.as_ref().map(|_| "<secret_key>"))
.finish()
}
}
impl Clone for PQSignature {
fn clone(&self) -> Self {
// Clone by re-parsing the bytes
let pk_bytes = self.public_key.as_bytes().to_vec();
let public_key = dilithium3::PublicKey::from_bytes(&pk_bytes).unwrap();
let secret_key = self.secret_key.as_ref().map(|sk| {
let sk_bytes = sk.as_bytes().to_vec();
dilithium3::SecretKey::from_bytes(&sk_bytes).unwrap()
});
Self { public_key, secret_key }
}
}
impl PQSignature {
/// Generate a new ML-DSA-65 keypair
pub fn generate() -> Result<Self> {
let (pk, sk) = dilithium3::keypair();
Ok(Self {
public_key: pk,
secret_key: Some(sk),
})
}
/// Get the public key bytes
pub fn public_key_bytes(&self) -> Vec<u8> {
self.public_key.as_bytes().to_vec()
}
/// Create from existing public key bytes (for verification only)
pub fn from_public_key(bytes: &[u8]) -> Result<Self> {
if bytes.len() != MLDSA_PUBLIC_KEY_SIZE {
return Err(Error::Crypto(format!(
"invalid ML-DSA public key size: expected {}, got {}",
MLDSA_PUBLIC_KEY_SIZE,
bytes.len()
)));
}
let pk = dilithium3::PublicKey::from_bytes(bytes)
.map_err(|e| Error::Crypto(format!("invalid ML-DSA public key: {e:?}")))?;
Ok(Self {
public_key: pk,
secret_key: None,
})
}
/// Sign a message
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
let sk = self
.secret_key
.as_ref()
.ok_or_else(|| Error::Crypto("no secret key available for signing".into()))?;
let sig = dilithium3::detached_sign(message, sk);
Ok(sig.as_bytes().to_vec())
}
/// Verify a signature
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<()> {
if signature.len() != MLDSA_SIGNATURE_SIZE {
return Err(Error::Crypto(format!(
"invalid ML-DSA signature size: expected {}, got {}",
MLDSA_SIGNATURE_SIZE,
signature.len()
)));
}
let sig = dilithium3::DetachedSignature::from_bytes(signature)
.map_err(|e| Error::Crypto(format!("invalid signature format: {e:?}")))?;
dilithium3::verify_detached_signature(&sig, message, &self.public_key)
.map_err(|_| Error::Crypto("signature verification failed".into()))
}
}
/// Public data from the initiator for the responder
#[derive(Debug, Clone)]
pub struct HybridInitiatorData {
pub x25519_public_key: [u8; 32],
pub mlkem_public_key: Vec<u8>,
}
/// Response data from the responder for the initiator
#[derive(Debug, Clone)]
pub struct HybridResponderData {
pub x25519_public_key: [u8; 32],
pub mlkem_ciphertext: Vec<u8>,
}
/// Completed hybrid handshake result
#[derive(Clone)]
pub struct HybridSharedSecret {
secret: [u8; HYBRID_SHARED_SECRET_SIZE],
}
impl HybridSharedSecret {
/// Get the shared secret bytes
pub fn as_bytes(&self) -> &[u8; HYBRID_SHARED_SECRET_SIZE] {
&self.secret
}
/// Consume and return the shared secret
pub fn into_bytes(self) -> [u8; HYBRID_SHARED_SECRET_SIZE] {
self.secret
}
}
impl Drop for HybridSharedSecret {
fn drop(&mut self) {
self.secret.zeroize();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HandshakeRole {
Initiator,
Responder,
}
/// Hybrid X25519 + ML-KEM-768 Handshake
///
/// Combines classical elliptic curve Diffie-Hellman (X25519) with post-quantum
/// ML-KEM-768 for defense-in-depth. Even if one algorithm is broken, the other
/// provides protection.
///
/// The final shared secret is derived using HKDF-SHA256 over both shared secrets.
pub struct HybridHandshake {
x25519_secret: Option<EphemeralSecret>,
x25519_public: X25519PublicKey,
mlkem: PQKeyExchange,
role: HandshakeRole,
}
impl HybridHandshake {
/// Initiate a hybrid handshake (client side)
pub fn initiate() -> Result<Self> {
let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
let x25519_public = X25519PublicKey::from(&x25519_secret);
let mlkem = PQKeyExchange::generate()?;
Ok(Self {
x25519_secret: Some(x25519_secret),
x25519_public,
mlkem,
role: HandshakeRole::Initiator,
})
}
/// Get the public data to send to the responder
pub fn public_data(&self) -> HybridInitiatorData {
HybridInitiatorData {
x25519_public_key: self.x25519_public.to_bytes(),
mlkem_public_key: self.mlkem.public_key_bytes(),
}
}
/// Respond to a hybrid handshake (server side)
pub fn respond(
initiator_data: &HybridInitiatorData,
) -> Result<(Self, HybridResponderData)> {
// Validate input sizes
if initiator_data.mlkem_public_key.len() != MLKEM_PUBLIC_KEY_SIZE {
return Err(Error::Crypto(format!(
"invalid initiator ML-KEM public key size: expected {}, got {}",
MLKEM_PUBLIC_KEY_SIZE,
initiator_data.mlkem_public_key.len()
)));
}
// Generate responder's X25519 keypair
let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
let x25519_public = X25519PublicKey::from(&x25519_secret);
// Generate ML-KEM keypair and encapsulate to initiator
let mlkem = PQKeyExchange::generate()?;
let (mlkem_ciphertext, _) = mlkem.encapsulate(&initiator_data.mlkem_public_key)?;
let response = HybridResponderData {
x25519_public_key: x25519_public.to_bytes(),
mlkem_ciphertext,
};
let handshake = Self {
x25519_secret: Some(x25519_secret),
x25519_public,
mlkem,
role: HandshakeRole::Responder,
};
Ok((handshake, response))
}
/// Finalize the handshake and derive the shared secret (initiator side)
pub fn finalize(mut self, responder_data: &HybridResponderData) -> Result<HybridSharedSecret> {
if self.role != HandshakeRole::Initiator {
return Err(Error::Crypto(
"finalize() can only be called by initiator".into(),
));
}
// X25519 key exchange
let x25519_secret = self
.x25519_secret
.take()
.ok_or_else(|| Error::Crypto("X25519 secret already consumed".into()))?;
let peer_x25519_public = X25519PublicKey::from(responder_data.x25519_public_key);
let x25519_shared = x25519_secret.diffie_hellman(&peer_x25519_public);
// ML-KEM decapsulation
let mlkem_shared = self.mlkem.decapsulate(&responder_data.mlkem_ciphertext)?;
// Combine shared secrets with HKDF
Self::derive_hybrid_secret(x25519_shared.as_bytes(), &mlkem_shared)
}
/// Complete the handshake and derive the shared secret (responder side)
pub fn complete(
mut self,
initiator_data: &HybridInitiatorData,
mlkem_shared: &[u8; 32],
) -> Result<HybridSharedSecret> {
if self.role != HandshakeRole::Responder {
return Err(Error::Crypto(
"complete() can only be called by responder".into(),
));
}
// X25519 key exchange
let x25519_secret = self
.x25519_secret
.take()
.ok_or_else(|| Error::Crypto("X25519 secret already consumed".into()))?;
let peer_x25519_public = X25519PublicKey::from(initiator_data.x25519_public_key);
let x25519_shared = x25519_secret.diffie_hellman(&peer_x25519_public);
// Combine shared secrets with HKDF
Self::derive_hybrid_secret(x25519_shared.as_bytes(), mlkem_shared)
}
/// Derive hybrid shared secret using HKDF-SHA256
fn derive_hybrid_secret(
x25519_shared: &[u8],
mlkem_shared: &[u8; 32],
) -> Result<HybridSharedSecret> {
// Concatenate both shared secrets
let mut ikm = Vec::with_capacity(x25519_shared.len() + mlkem_shared.len());
ikm.extend_from_slice(x25519_shared);
ikm.extend_from_slice(mlkem_shared);
// HKDF extract and expand
let hkdf = Hkdf::<Sha256>::new(Some(b"ZAP-HYBRID-HANDSHAKE-v1"), &ikm);
let mut secret = [0u8; HYBRID_SHARED_SECRET_SIZE];
hkdf.expand(b"shared-secret", &mut secret)
.map_err(|_| Error::Crypto("HKDF expansion failed".into()))?;
// Zeroize intermediate material
ikm.zeroize();
Ok(HybridSharedSecret { secret })
}
}
/// Perform a complete hybrid handshake between two parties
///
/// This is a convenience function for testing and simple use cases.
pub fn hybrid_handshake() -> Result<(
[u8; HYBRID_SHARED_SECRET_SIZE],
[u8; HYBRID_SHARED_SECRET_SIZE],
)> {
// Initiator starts
let initiator = HybridHandshake::initiate()?;
let init_data = initiator.public_data();
// Responder receives and responds
let (responder, resp_data) = HybridHandshake::respond(&init_data)?;
// Responder also needs to encapsulate to get their copy of the ML-KEM shared secret
let mlkem_for_responder = PQKeyExchange::generate()?;
let (_, mlkem_shared_responder) =
mlkem_for_responder.encapsulate(&init_data.mlkem_public_key)?;
// Initiator finalizes
let initiator_secret = initiator.finalize(&resp_data)?;
// Responder completes
let responder_secret = responder.complete(&init_data, &mlkem_shared_responder)?;
Ok((initiator_secret.into_bytes(), responder_secret.into_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mlkem_key_exchange() {
let alice = PQKeyExchange::generate().unwrap();
let bob = PQKeyExchange::generate().unwrap();
// Alice encapsulates to Bob's public key
let (ciphertext, alice_shared) = alice.encapsulate(&bob.public_key_bytes()).unwrap();
// Bob decapsulates
let bob_shared = bob.decapsulate(&ciphertext).unwrap();
assert_eq!(alice_shared, bob_shared);
}
#[test]
fn test_mlkem_invalid_public_key() {
let alice = PQKeyExchange::generate().unwrap();
let bad_pk = vec![0u8; 100]; // Wrong size
assert!(alice.encapsulate(&bad_pk).is_err());
}
#[test]
fn test_mldsa_signature() {
let signer = PQSignature::generate().unwrap();
let message = b"The quick brown fox jumps over the lazy dog";
let signature = signer.sign(message).unwrap();
// Verify with same key
signer.verify(message, &signature).unwrap();
// Verify with public key only
let verifier = PQSignature::from_public_key(&signer.public_key_bytes()).unwrap();
verifier.verify(message, &signature).unwrap();
}
#[test]
fn test_mldsa_invalid_signature() {
let signer = PQSignature::generate().unwrap();
let message = b"Hello, World!";
let signature = signer.sign(message).unwrap();
// Wrong message
assert!(signer.verify(b"Wrong message", &signature).is_err());
// Corrupted signature
let mut bad_sig = signature.clone();
bad_sig[0] ^= 0xFF;
assert!(signer.verify(message, &bad_sig).is_err());
}
#[test]
fn test_mldsa_verify_only() {
let verifier = PQSignature::from_public_key(
&PQSignature::generate().unwrap().public_key_bytes(),
)
.unwrap();
assert!(verifier.sign(b"test").is_err());
}
#[test]
fn test_hybrid_handshake_basic() {
// Initiator starts
let initiator = HybridHandshake::initiate().unwrap();
let init_data = initiator.public_data();
// Responder receives init_data and creates response
let responder_mlkem = PQKeyExchange::generate().unwrap();
let (mlkem_ct, _mlkem_shared_responder) = responder_mlkem
.encapsulate(&init_data.mlkem_public_key)
.unwrap();
let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
let x25519_public = X25519PublicKey::from(&x25519_secret);
let resp_data = HybridResponderData {
x25519_public_key: x25519_public.to_bytes(),
mlkem_ciphertext: mlkem_ct,
};
// Initiator finalizes
let _initiator_secret = initiator.finalize(&resp_data).unwrap();
// Note: In real use, both parties derive the same secret
// This test just verifies the API works
}
#[test]
fn test_hybrid_handshake_sizes() {
let initiator = HybridHandshake::initiate().unwrap();
let init_data = initiator.public_data();
assert_eq!(init_data.x25519_public_key.len(), X25519_PUBLIC_KEY_SIZE);
assert_eq!(init_data.mlkem_public_key.len(), MLKEM_PUBLIC_KEY_SIZE);
}
}
}
// Re-export PQ types when feature is enabled
#[cfg(feature = "pq")]
pub use pq_impl::{
hybrid_handshake, HybridHandshake, HybridInitiatorData, HybridResponderData,
HybridSharedSecret, PQKeyExchange, PQSignature,
};
// Stub implementations when pq feature is not enabled
#[cfg(not(feature = "pq"))]
pub struct PQKeyExchange;
#[cfg(not(feature = "pq"))]
impl PQKeyExchange {
pub fn generate() -> Result<Self> {
Err(Error::Crypto("PQ crypto requires 'pq' feature".into()))
}
}
#[cfg(not(feature = "pq"))]
pub struct PQSignature;
#[cfg(not(feature = "pq"))]
impl PQSignature {
pub fn generate() -> Result<Self> {
Err(Error::Crypto("PQ crypto requires 'pq' feature".into()))
}
}
#[cfg(not(feature = "pq"))]
pub struct HybridHandshake;
#[cfg(not(feature = "pq"))]
impl HybridHandshake {
pub fn initiate() -> Result<Self> {
Err(Error::Crypto("PQ crypto requires 'pq' feature".into()))
}
}
+55
View File
@@ -0,0 +1,55 @@
//! Error types for ZAP
use thiserror::Error;
/// ZAP error type
#[derive(Error, Debug)]
pub enum Error {
#[error("connection failed: {0}")]
Connection(String),
#[error("transport error: {0}")]
Transport(String),
#[error("protocol error: {0}")]
Protocol(String),
#[error("tool not found: {0}")]
ToolNotFound(String),
#[error("tool call failed: {0}")]
ToolCallFailed(String),
#[error("resource not found: {0}")]
ResourceNotFound(String),
#[error("server error: {0}")]
Server(String),
#[error("config error: {0}")]
Config(String),
#[error("crypto error: {0}")]
Crypto(String),
#[error("identity error: {0}")]
Identity(String),
#[error("consensus error: {0}")]
Consensus(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("capnp error: {0}")]
Capnp(#[from] capnp::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("url error: {0}")]
Url(#[from] url::ParseError),
}
/// Result type for ZAP operations
pub type Result<T> = std::result::Result<T, Error>;
+105
View File
@@ -0,0 +1,105 @@
//! ZAP gateway for MCP bridging
use crate::{Config, Result, config::ServerConfig};
use std::collections::HashMap;
/// ZAP gateway that bridges MCP servers
pub struct Gateway {
config: Config,
servers: HashMap<String, ConnectedServer>,
}
/// Connected server state
struct ConnectedServer {
id: String,
config: ServerConfig,
status: ServerStatus,
}
/// Server connection status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerStatus {
Connecting,
Connected,
Disconnected,
Error,
}
impl Gateway {
/// Create a new gateway
pub fn new(config: Config) -> Self {
Self {
config,
servers: HashMap::new(),
}
}
/// Add an MCP server
pub async fn add_server(&mut self, name: &str, url: &str, config: ServerConfig) -> Result<String> {
let id = uuid();
let server = ConnectedServer {
id: id.clone(),
config,
status: ServerStatus::Connecting,
};
self.servers.insert(id.clone(), server);
// TODO: Connect to MCP server
Ok(id)
}
/// Remove a server
pub fn remove_server(&mut self, id: &str) -> Result<()> {
self.servers.remove(id);
Ok(())
}
/// List connected servers
pub fn list_servers(&self) -> Vec<ServerInfo> {
self.servers
.values()
.map(|s| ServerInfo {
id: s.id.clone(),
name: s.config.name.clone(),
url: s.config.url.clone(),
status: s.status,
})
.collect()
}
/// Run the gateway
pub async fn run(&mut self) -> Result<()> {
let addr = format!("{}:{}", self.config.listen, self.config.port);
tracing::info!("ZAP gateway listening on {}", addr);
// Connect to configured servers
let servers: Vec<_> = self.config.servers.clone();
for server_config in servers {
let name = server_config.name.clone();
let url = server_config.url.clone();
let _ = self.add_server(&name, &url, server_config).await;
}
// TODO: Start Cap'n Proto RPC server
tokio::signal::ctrl_c().await?;
Ok(())
}
}
/// Server info
#[derive(Debug, Clone)]
pub struct ServerInfo {
pub id: String,
pub name: String,
pub url: String,
pub status: ServerStatus,
}
fn uuid() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{:x}", now)
}
+836
View File
@@ -0,0 +1,836 @@
//! W3C Decentralized Identifier (DID) Implementation
//!
//! Implements W3C DID Core 1.0 specification with support for:
//! - did:lux - Lux blockchain-anchored DIDs
//! - did:key - Self-certifying DIDs from cryptographic keys
//! - did:web - DNS-based DIDs
//!
//! # Example
//!
//! ```rust,ignore
//! use zap::identity::{Did, DidMethod, NodeIdentity};
//!
//! // Parse existing DID
//! let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")?;
//!
//! // Create from ML-DSA public key
//! let did = Did::from_mldsa_key(&public_key_bytes)?;
//!
//! // Generate DID Document
//! let doc = did.document()?;
//!
//! // Create node identity
//! let identity = NodeIdentity::generate()?;
//! ```
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "pq")]
use crate::crypto::PQSignature;
/// Multibase prefix for base58btc encoding
const MULTIBASE_BASE58BTC: char = 'z';
/// Multicodec prefix for ML-DSA-65 public key (0xED = Ed25519, 0x1309 = ML-DSA-65)
/// Using 0x1309 as provisional multicodec for ML-DSA-65
const MULTICODEC_MLDSA65: [u8; 2] = [0x13, 0x09];
/// DID method identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DidMethod {
/// Lux blockchain-anchored DID
Lux,
/// Self-certifying DID from cryptographic key
Key,
/// DNS-based DID
Web,
}
impl DidMethod {
/// Get the method string for DID URI
pub fn as_str(&self) -> &'static str {
match self {
DidMethod::Lux => "lux",
DidMethod::Key => "key",
DidMethod::Web => "web",
}
}
/// Parse method from string
pub fn from_str(s: &str) -> Result<Self> {
match s {
"lux" => Ok(DidMethod::Lux),
"key" => Ok(DidMethod::Key),
"web" => Ok(DidMethod::Web),
_ => Err(Error::Identity(format!("unknown DID method: {}", s))),
}
}
}
impl fmt::Display for DidMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// W3C Decentralized Identifier (DID)
///
/// A DID is a globally unique identifier that enables verifiable,
/// decentralized digital identity.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Did {
/// The DID method (lux, key, web)
pub method: DidMethod,
/// The method-specific identifier
pub id: String,
}
impl Did {
/// Create a new DID with the specified method and ID
pub fn new(method: DidMethod, id: String) -> Self {
Self { method, id }
}
/// Parse a DID from a string in the format "did:method:id"
///
/// # Example
///
/// ```rust,ignore
/// let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")?;
/// assert_eq!(did.method, DidMethod::Lux);
/// ```
pub fn parse(s: &str) -> Result<Self> {
// DID syntax: did:method:method-specific-id
if !s.starts_with("did:") {
return Err(Error::Identity(format!(
"invalid DID: must start with 'did:', got '{}'",
s
)));
}
let rest = &s[4..]; // Skip "did:"
let parts: Vec<&str> = rest.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(Error::Identity(format!(
"invalid DID format: expected 'did:method:id', got '{}'",
s
)));
}
let method = DidMethod::from_str(parts[0])?;
let id = parts[1].to_string();
if id.is_empty() {
return Err(Error::Identity("DID identifier cannot be empty".to_string()));
}
Ok(Self { method, id })
}
/// Create a DID from an ML-DSA-65 public key
///
/// The resulting DID uses the did:key method with multibase-encoded
/// multicodec-prefixed public key.
///
/// # Example
///
/// ```rust,ignore
/// let signer = PQSignature::generate()?;
/// let did = Did::from_mldsa_key(&signer.public_key_bytes())?;
/// println!("DID: {}", did); // did:key:z6Mk...
/// ```
pub fn from_mldsa_key(public_key: &[u8]) -> Result<Self> {
// Expected ML-DSA-65 public key size
const MLDSA_PUBLIC_KEY_SIZE: usize = 1952;
if public_key.len() != MLDSA_PUBLIC_KEY_SIZE {
return Err(Error::Identity(format!(
"invalid ML-DSA public key size: expected {}, got {}",
MLDSA_PUBLIC_KEY_SIZE,
public_key.len()
)));
}
// Create multicodec-prefixed key
let mut prefixed = Vec::with_capacity(MULTICODEC_MLDSA65.len() + public_key.len());
prefixed.extend_from_slice(&MULTICODEC_MLDSA65);
prefixed.extend_from_slice(public_key);
// Encode with multibase (base58btc)
let encoded = bs58::encode(&prefixed).into_string();
let id = format!("{}{}", MULTIBASE_BASE58BTC, encoded);
Ok(Self {
method: DidMethod::Key,
id,
})
}
/// Create a Lux blockchain-anchored DID from an ML-DSA public key
pub fn from_mldsa_key_lux(public_key: &[u8]) -> Result<Self> {
let key_did = Self::from_mldsa_key(public_key)?;
Ok(Self {
method: DidMethod::Lux,
id: key_did.id,
})
}
/// Create a web DID from a domain and optional path
///
/// # Example
///
/// ```rust,ignore
/// let did = Did::from_web("example.com", Some("users/alice"))?;
/// assert_eq!(did.to_string(), "did:web:example.com:users:alice");
/// ```
pub fn from_web(domain: &str, path: Option<&str>) -> Result<Self> {
if domain.is_empty() {
return Err(Error::Identity("domain cannot be empty".to_string()));
}
// Validate domain (basic check)
if domain.contains('/') || domain.contains(':') {
return Err(Error::Identity(format!(
"invalid domain for did:web: {}",
domain
)));
}
let id = match path {
Some(p) if !p.is_empty() => {
// Replace '/' with ':' per did:web spec
let path_parts = p.replace('/', ":");
format!("{}:{}", domain, path_parts)
}
_ => domain.to_string(),
};
Ok(Self {
method: DidMethod::Web,
id,
})
}
/// Get the full DID URI string
pub fn uri(&self) -> String {
format!("did:{}:{}", self.method, self.id)
}
/// Generate a W3C DID Document for this DID
///
/// The document includes verification methods and service endpoints.
pub fn document(&self) -> Result<DidDocument> {
let did_uri = self.uri();
// Create verification method based on DID type
let verification_method = match self.method {
DidMethod::Key | DidMethod::Lux => {
// Extract public key from multibase-encoded identifier
let key_material = self.extract_key_material()?;
VerificationMethod {
id: format!("{}#keys-1", did_uri),
type_: VerificationMethodType::JsonWebKey2020,
controller: did_uri.clone(),
public_key_multibase: Some(self.id.clone()),
public_key_jwk: None,
blockchain_account_id: if self.method == DidMethod::Lux {
Some(format!("lux:{}", hex::encode(&key_material[..20])))
} else {
None
},
}
}
DidMethod::Web => VerificationMethod {
id: format!("{}#keys-1", did_uri),
type_: VerificationMethodType::JsonWebKey2020,
controller: did_uri.clone(),
public_key_multibase: None,
public_key_jwk: None,
blockchain_account_id: None,
},
};
// Create default service endpoint for ZAP protocol
let service = Service {
id: format!("{}#zap-agent", did_uri),
type_: ServiceType::ZapAgent,
service_endpoint: ServiceEndpoint::Uri(format!("zap://{}", self.id)),
};
Ok(DidDocument {
context: vec![
"https://www.w3.org/ns/did/v1".to_string(),
"https://w3id.org/security/suites/jws-2020/v1".to_string(),
],
id: did_uri.clone(),
controller: None,
verification_method: vec![verification_method],
authentication: vec![format!("{}#keys-1", did_uri)],
assertion_method: vec![format!("{}#keys-1", did_uri)],
key_agreement: vec![],
capability_invocation: vec![format!("{}#keys-1", did_uri)],
capability_delegation: vec![],
service: vec![service],
})
}
/// Extract the raw key material from a did:key or did:lux identifier
fn extract_key_material(&self) -> Result<Vec<u8>> {
if self.id.is_empty() {
return Err(Error::Identity("empty DID identifier".to_string()));
}
// Check multibase prefix
let first_char = self.id.chars().next().unwrap();
if first_char != MULTIBASE_BASE58BTC {
return Err(Error::Identity(format!(
"unsupported multibase encoding: expected '{}', got '{}'",
MULTIBASE_BASE58BTC, first_char
)));
}
// Decode base58btc (skip the multibase prefix)
let decoded = bs58::decode(&self.id[1..])
.into_vec()
.map_err(|e| Error::Identity(format!("invalid base58btc encoding: {}", e)))?;
// Skip multicodec prefix (2 bytes for ML-DSA-65)
if decoded.len() < 2 {
return Err(Error::Identity("DID identifier too short".to_string()));
}
// Verify multicodec prefix matches ML-DSA-65
if decoded[0..2] != MULTICODEC_MLDSA65 {
// Allow other key types, just return raw material
return Ok(decoded);
}
Ok(decoded[2..].to_vec())
}
}
impl fmt::Display for Did {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.uri())
}
}
impl std::str::FromStr for Did {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Did::parse(s)
}
}
/// W3C DID Document
///
/// A DID Document contains information associated with a DID,
/// including verification methods and service endpoints.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DidDocument {
/// JSON-LD context
#[serde(rename = "@context")]
pub context: Vec<String>,
/// The DID subject
pub id: String,
/// Optional controller DID
#[serde(skip_serializing_if = "Option::is_none")]
pub controller: Option<String>,
/// Verification methods (public keys)
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub verification_method: Vec<VerificationMethod>,
/// Authentication verification methods
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authentication: Vec<String>,
/// Assertion/credential issuance verification methods
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assertion_method: Vec<String>,
/// Key agreement verification methods
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub key_agreement: Vec<String>,
/// Capability invocation verification methods
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capability_invocation: Vec<String>,
/// Capability delegation verification methods
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capability_delegation: Vec<String>,
/// Service endpoints
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub service: Vec<Service>,
}
impl DidDocument {
/// Get the primary verification method
pub fn primary_verification_method(&self) -> Option<&VerificationMethod> {
self.verification_method.first()
}
/// Get a verification method by ID
pub fn get_verification_method(&self, id: &str) -> Option<&VerificationMethod> {
self.verification_method.iter().find(|vm| vm.id == id)
}
/// Get a service by ID
pub fn get_service(&self, id: &str) -> Option<&Service> {
self.service.iter().find(|s| s.id == id)
}
/// Serialize to JSON
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).map_err(|e| Error::Identity(format!("JSON serialization failed: {}", e)))
}
/// Deserialize from JSON
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| Error::Identity(format!("JSON deserialization failed: {}", e)))
}
}
/// Verification method type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VerificationMethodType {
/// JSON Web Key 2020
JsonWebKey2020,
/// Multikey (new W3C standard)
Multikey,
/// ML-DSA-65 Verification Key 2024 (post-quantum)
#[serde(rename = "MlDsa65VerificationKey2024")]
MlDsa65VerificationKey2024,
}
/// Verification method (public key) in DID Document
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationMethod {
/// Verification method ID (e.g., "did:example:123#keys-1")
pub id: String,
/// Verification method type
#[serde(rename = "type")]
pub type_: VerificationMethodType,
/// Controller DID
pub controller: String,
/// Public key in multibase encoding
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_multibase: Option<String>,
/// Public key as JWK
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_jwk: Option<serde_json::Value>,
/// Blockchain account ID (for Lux DIDs)
#[serde(skip_serializing_if = "Option::is_none")]
pub blockchain_account_id: Option<String>,
}
/// Service type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ServiceType {
/// ZAP Agent service
ZapAgent,
/// DID Communication
#[serde(rename = "DIDCommMessaging")]
DidCommMessaging,
/// Linked Domains
LinkedDomains,
/// Credential Registry
CredentialRegistry,
}
/// Service endpoint
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ServiceEndpoint {
/// Single URI endpoint
Uri(String),
/// Multiple URI endpoints
Uris(Vec<String>),
/// Structured endpoint with additional properties
Structured {
uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
accept: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
routing_keys: Option<Vec<String>>,
},
}
/// Service endpoint in DID Document
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Service {
/// Service ID
pub id: String,
/// Service type
#[serde(rename = "type")]
pub type_: ServiceType,
/// Service endpoint URI or structured endpoint
pub service_endpoint: ServiceEndpoint,
}
/// Node identity combining DID with cryptographic keypair
///
/// Used for authenticated node participation in the ZAP network.
#[derive(Debug, Clone)]
pub struct NodeIdentity {
/// The node's DID
pub did: Did,
/// ML-DSA-65 public key bytes
pub public_key: Vec<u8>,
/// Optional staked amount (in smallest unit)
pub stake: Option<u64>,
/// Optional stake registry reference
pub stake_registry: Option<String>,
#[cfg(feature = "pq")]
/// ML-DSA-65 signer (private key)
signer: Option<PQSignature>,
#[cfg(not(feature = "pq"))]
/// Placeholder for when pq feature is disabled
_signer: std::marker::PhantomData<()>,
}
impl NodeIdentity {
/// Create a new node identity from an existing DID and public key
pub fn new(did: Did, public_key: Vec<u8>) -> Self {
Self {
did,
public_key,
stake: None,
stake_registry: None,
#[cfg(feature = "pq")]
signer: None,
#[cfg(not(feature = "pq"))]
_signer: std::marker::PhantomData,
}
}
/// Generate a new node identity with fresh ML-DSA-65 keypair
#[cfg(feature = "pq")]
pub fn generate() -> Result<Self> {
let signer = PQSignature::generate()?;
let public_key = signer.public_key_bytes();
let did = Did::from_mldsa_key_lux(&public_key)?;
Ok(Self {
did,
public_key,
stake: None,
stake_registry: None,
signer: Some(signer),
})
}
/// Generate a new node identity (stub when pq feature is disabled)
#[cfg(not(feature = "pq"))]
pub fn generate() -> Result<Self> {
Err(Error::Identity(
"node identity generation requires 'pq' feature".to_string(),
))
}
/// Sign a message with this node's private key
#[cfg(feature = "pq")]
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
let signer = self
.signer
.as_ref()
.ok_or_else(|| Error::Identity("no private key available for signing".to_string()))?;
signer.sign(message)
}
/// Sign a message (stub when pq feature is disabled)
#[cfg(not(feature = "pq"))]
pub fn sign(&self, _message: &[u8]) -> Result<Vec<u8>> {
Err(Error::Identity(
"signing requires 'pq' feature".to_string(),
))
}
/// Verify a signature against this node's public key
#[cfg(feature = "pq")]
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<()> {
let verifier = match &self.signer {
Some(s) => s.verify(message, signature)?,
None => {
let v = PQSignature::from_public_key(&self.public_key)?;
v.verify(message, signature)?;
}
};
Ok(verifier)
}
/// Verify a signature (stub when pq feature is disabled)
#[cfg(not(feature = "pq"))]
pub fn verify(&self, _message: &[u8], _signature: &[u8]) -> Result<()> {
Err(Error::Identity(
"verification requires 'pq' feature".to_string(),
))
}
/// Set the stake amount for this node
pub fn with_stake(mut self, amount: u64) -> Self {
self.stake = Some(amount);
self
}
/// Set the stake registry reference
pub fn with_registry(mut self, registry: String) -> Self {
self.stake_registry = Some(registry);
self
}
/// Get the DID document for this node identity
pub fn document(&self) -> Result<DidDocument> {
self.did.document()
}
/// Check if this node has signing capability
pub fn can_sign(&self) -> bool {
#[cfg(feature = "pq")]
{
self.signer.is_some()
}
#[cfg(not(feature = "pq"))]
{
false
}
}
}
/// Trait for stake registry implementations
///
/// A stake registry tracks staked amounts for DIDs and can be used
/// for weighted consensus and reputation systems.
pub trait StakeRegistry: Send + Sync {
/// Get the staked amount for a DID
fn get_stake(&self, did: &Did) -> Result<u64>;
/// Set the staked amount for a DID
fn set_stake(&mut self, did: &Did, amount: u64) -> Result<()>;
/// Check if a DID has sufficient stake
fn has_sufficient_stake(&self, did: &Did, minimum: u64) -> Result<bool> {
Ok(self.get_stake(did)? >= minimum)
}
/// Get total staked amount across all DIDs
fn total_stake(&self) -> Result<u64>;
/// Get the stake weight (0.0-1.0) for a DID relative to total
fn stake_weight(&self, did: &Did) -> Result<f64> {
let stake = self.get_stake(did)?;
let total = self.total_stake()?;
if total == 0 {
return Ok(0.0);
}
Ok(stake as f64 / total as f64)
}
}
/// In-memory stake registry for testing
#[derive(Debug, Default)]
pub struct InMemoryStakeRegistry {
stakes: std::collections::HashMap<String, u64>,
}
impl InMemoryStakeRegistry {
/// Create a new empty stake registry
pub fn new() -> Self {
Self::default()
}
}
impl StakeRegistry for InMemoryStakeRegistry {
fn get_stake(&self, did: &Did) -> Result<u64> {
Ok(*self.stakes.get(&did.uri()).unwrap_or(&0))
}
fn set_stake(&mut self, did: &Did, amount: u64) -> Result<()> {
self.stakes.insert(did.uri(), amount);
Ok(())
}
fn total_stake(&self) -> Result<u64> {
Ok(self.stakes.values().sum())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_did_parse_lux() {
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
assert_eq!(did.method, DidMethod::Lux);
assert_eq!(did.id, "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
}
#[test]
fn test_did_parse_key() {
let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
assert_eq!(did.method, DidMethod::Key);
}
#[test]
fn test_did_parse_web() {
let did = Did::parse("did:web:example.com:users:alice").unwrap();
assert_eq!(did.method, DidMethod::Web);
assert_eq!(did.id, "example.com:users:alice");
}
#[test]
fn test_did_parse_invalid() {
assert!(Did::parse("not-a-did").is_err());
assert!(Did::parse("did:unknown:abc").is_err());
assert!(Did::parse("did:lux:").is_err());
}
#[test]
fn test_did_from_web() {
let did = Did::from_web("example.com", Some("users/alice")).unwrap();
assert_eq!(did.uri(), "did:web:example.com:users:alice");
let did2 = Did::from_web("example.com", None).unwrap();
assert_eq!(did2.uri(), "did:web:example.com");
}
#[test]
fn test_did_method_display() {
assert_eq!(DidMethod::Lux.to_string(), "lux");
assert_eq!(DidMethod::Key.to_string(), "key");
assert_eq!(DidMethod::Web.to_string(), "web");
}
#[test]
fn test_did_document_generation() {
let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let doc = did.document().unwrap();
assert_eq!(doc.id, "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
assert!(!doc.verification_method.is_empty());
assert!(!doc.authentication.is_empty());
assert!(!doc.service.is_empty());
}
#[test]
fn test_did_document_json_roundtrip() {
let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let doc = did.document().unwrap();
let json = doc.to_json().unwrap();
let parsed = DidDocument::from_json(&json).unwrap();
assert_eq!(doc.id, parsed.id);
assert_eq!(doc.verification_method.len(), parsed.verification_method.len());
}
#[test]
fn test_stake_registry() {
let mut registry = InMemoryStakeRegistry::new();
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
assert_eq!(registry.get_stake(&did).unwrap(), 0);
registry.set_stake(&did, 1000).unwrap();
assert_eq!(registry.get_stake(&did).unwrap(), 1000);
assert!(registry.has_sufficient_stake(&did, 500).unwrap());
assert!(!registry.has_sufficient_stake(&did, 2000).unwrap());
assert_eq!(registry.total_stake().unwrap(), 1000);
}
#[test]
fn test_stake_weight() {
let mut registry = InMemoryStakeRegistry::new();
let did1 = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let did2 = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doL").unwrap();
registry.set_stake(&did1, 750).unwrap();
registry.set_stake(&did2, 250).unwrap();
let weight1 = registry.stake_weight(&did1).unwrap();
let weight2 = registry.stake_weight(&did2).unwrap();
assert!((weight1 - 0.75).abs() < 0.001);
assert!((weight2 - 0.25).abs() < 0.001);
}
#[test]
fn test_node_identity_new() {
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let identity = NodeIdentity::new(did.clone(), vec![0u8; 1952]);
assert_eq!(identity.did, did);
assert_eq!(identity.stake, None);
assert!(!identity.can_sign());
}
#[test]
fn test_node_identity_with_stake() {
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let identity = NodeIdentity::new(did, vec![0u8; 1952])
.with_stake(5000)
.with_registry("lux:mainnet".to_string());
assert_eq!(identity.stake, Some(5000));
assert_eq!(identity.stake_registry, Some("lux:mainnet".to_string()));
}
#[cfg(feature = "pq")]
#[test]
fn test_node_identity_generate() {
let identity = NodeIdentity::generate().unwrap();
assert!(identity.can_sign());
assert_eq!(identity.did.method, DidMethod::Lux);
assert!(!identity.public_key.is_empty());
// Test signing
let message = b"test message";
let signature = identity.sign(message).unwrap();
identity.verify(message, &signature).unwrap();
}
#[cfg(feature = "pq")]
#[test]
fn test_did_from_mldsa_key() {
use crate::crypto::PQSignature;
let signer = PQSignature::generate().unwrap();
let public_key = signer.public_key_bytes();
let did = Did::from_mldsa_key(&public_key).unwrap();
assert_eq!(did.method, DidMethod::Key);
assert!(did.id.starts_with('z'));
// Verify we can generate document
let doc = did.document().unwrap();
assert!(!doc.verification_method.is_empty());
}
}
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
+67
View File
@@ -0,0 +1,67 @@
//! Transport implementations for ZAP
use crate::Result;
use std::pin::Pin;
use std::future::Future;
use url::Url;
/// Transport trait for ZAP connections
pub trait Transport: Send + Sync {
/// Send a message
fn send(&mut self, data: &[u8]) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
/// Receive a message
fn recv(&mut self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + '_>>;
/// Close the transport
fn close(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
}
/// Stub transport for placeholder implementations
pub struct StubTransport;
impl Transport for StubTransport {
fn send(&mut self, _data: &[u8]) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
Box::pin(async { Err(crate::Error::Transport("not implemented".into())) })
}
fn recv(&mut self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send + '_>> {
Box::pin(async { Err(crate::Error::Transport("not implemented".into())) })
}
fn close(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
}
/// Create a transport from a URL
pub async fn connect(url: &str) -> Result<Box<dyn Transport>> {
let parsed = Url::parse(url)?;
match parsed.scheme() {
"zap" | "zap+tcp" => {
// TCP transport - placeholder
Ok(Box::new(StubTransport))
}
"zap+unix" | "unix" => {
// Unix socket transport - placeholder
Ok(Box::new(StubTransport))
}
"stdio" => {
// Stdio transport for subprocess MCP servers - placeholder
Ok(Box::new(StubTransport))
}
"http" | "https" => {
// HTTP transport (SSE) - placeholder
Ok(Box::new(StubTransport))
}
"ws" | "wss" => {
// WebSocket transport - placeholder
Ok(Box::new(StubTransport))
}
_ => Err(crate::Error::Transport(format!(
"unsupported scheme: {}",
parsed.scheme()
))),
}
}
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
{
"name": "@hanzo/zap",
"version": "0.2.1",
"description": "ZAP - Zero-Copy App Proto for TypeScript",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint src",
"format": "prettier --write src"
},
"keywords": [
"capnproto",
"rpc",
"agents",
"mcp",
"ai",
"hanzo"
],
"author": "Hanzo AI <dev@hanzo.ai>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/hanzoai/zap"
},
"homepage": "https://hanzo.ai/zap",
"bugs": {
"url": "https://github.com/hanzoai/zap/issues"
},
"dependencies": {
"capnp-ts": "^0.7.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^2.0.0",
"typescript": "^5.7.0",
"vitest": "^2.0.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0"
},
"engines": {
"node": ">=18"
},
"publishConfig": {
"access": "public"
}
}
+1935
View File
File diff suppressed because it is too large Load Diff
+316
View File
@@ -0,0 +1,316 @@
/**
* Agentic consensus for response voting.
*
* Agents vote on responses to queries. No trust needed - majority wins.
* As long as majority are honest, you get correct results.
*/
import { Did } from './identity.js';
/** Query ID (32-byte hash as hex string) */
export type QueryId = string;
/** Response ID (32-byte hash as hex string) */
export type ResponseId = string;
/** A query submitted to the agent network */
export interface Query {
id: QueryId;
content: string;
submitter: Did;
timestamp: number;
}
/** A response to a query */
export interface Response {
id: ResponseId;
queryId: QueryId;
content: string;
responder: Did;
timestamp: number;
}
/** Result of consensus voting */
export interface ConsensusResult {
response: Response;
votes: number;
totalVoters: number;
confidence: number;
}
/** Internal state for a query */
interface QueryState {
query: Query;
responses: Map<ResponseId, Response>;
votes: Map<ResponseId, Did[]>;
finalized: ResponseId | null;
}
/**
* Create a query with auto-generated ID.
*/
export async function createQuery(content: string, submitter: Did): Promise<Query> {
const timestamp = Math.floor(Date.now() / 1000);
const encoder = new TextEncoder();
const data = new Uint8Array([
...encoder.encode(content),
...encoder.encode(didUri(submitter)),
...numberToBytes(timestamp),
]);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const id = bufferToHex(hashBuffer);
return { id, content, submitter, timestamp };
}
/**
* Create a response with auto-generated ID.
*/
export async function createResponse(
queryId: QueryId,
content: string,
responder: Did,
): Promise<Response> {
const timestamp = Math.floor(Date.now() / 1000);
const encoder = new TextEncoder();
const data = new Uint8Array([
...hexToBytes(queryId),
...encoder.encode(content),
...encoder.encode(didUri(responder)),
...numberToBytes(timestamp),
]);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const id = bufferToHex(hashBuffer);
return { id, queryId, content, responder, timestamp };
}
/**
* Agentic consensus for response voting.
*
* Agents submit responses and vote. Majority wins.
*/
export class AgentConsensusVoting {
private queries = new Map<QueryId, QueryState>();
private threshold: number;
private minResponses: number;
private minVotes: number;
/**
* Create a new consensus instance.
*
* @param threshold - Fraction of votes needed (0.5 = majority)
* @param minResponses - Minimum responses before checking consensus
* @param minVotes - Minimum votes before checking consensus
*/
constructor(
threshold: number = 0.5,
minResponses: number = 1,
minVotes: number = 1,
) {
this.threshold = Math.max(0, Math.min(1, threshold));
this.minResponses = minResponses;
this.minVotes = minVotes;
}
/** Submit a new query */
async submitQuery(query: Query): Promise<QueryId> {
this.queries.set(query.id, {
query,
responses: new Map(),
votes: new Map(),
finalized: null,
});
return query.id;
}
/** Submit a response to a query */
async submitResponse(response: Response): Promise<ResponseId> {
const state = this.queries.get(response.queryId);
if (!state) {
throw new Error('Query not found');
}
if (state.finalized !== null) {
throw new Error('Query already finalized');
}
state.responses.set(response.id, response);
state.votes.set(response.id, []);
return response.id;
}
/**
* Vote for a response.
*
* Each agent can only vote once per query (across all responses).
*/
async vote(queryId: QueryId, responseId: ResponseId, voter: Did): Promise<void> {
const state = this.queries.get(queryId);
if (!state) {
throw new Error('Query not found');
}
if (state.finalized !== null) {
throw new Error('Query already finalized');
}
if (!state.responses.has(responseId)) {
throw new Error('Response not found');
}
// Check if voter already voted
const voterUri = didUri(voter);
for (const voters of state.votes.values()) {
if (voters.some((v) => didUri(v) === voterUri)) {
throw new Error('Already voted on this query');
}
}
state.votes.get(responseId)!.push(voter);
this.checkConsensus(state);
}
private checkConsensus(state: QueryState): void {
if (state.finalized !== null) {
return;
}
if (state.responses.size < this.minResponses) {
return;
}
let totalVotes = 0;
for (const voters of state.votes.values()) {
totalVotes += voters.length;
}
if (totalVotes < this.minVotes) {
return;
}
// Find response with most votes that meets threshold
let best: { id: ResponseId; count: number } | null = null;
for (const [responseId, voters] of state.votes) {
const voteCount = voters.length;
const confidence = totalVotes > 0 ? voteCount / totalVotes : 0;
if (confidence >= this.threshold) {
if (best === null || voteCount > best.count) {
best = { id: responseId, count: voteCount };
}
}
}
if (best !== null) {
state.finalized = best.id;
}
}
/** Get the consensus result for a query */
async getResult(queryId: QueryId): Promise<ConsensusResult | null> {
const state = this.queries.get(queryId);
if (!state || state.finalized === null) {
return null;
}
const response = state.responses.get(state.finalized)!;
const votes = state.votes.get(state.finalized)!.length;
let totalVoters = 0;
for (const voters of state.votes.values()) {
totalVoters += voters.length;
}
return {
response,
votes,
totalVoters,
confidence: totalVoters > 0 ? votes / totalVoters : 0,
};
}
/** Check if a query has reached consensus */
async isFinalized(queryId: QueryId): Promise<boolean> {
const state = this.queries.get(queryId);
return state !== null && state?.finalized !== null;
}
/** Get all responses for a query */
async getResponses(queryId: QueryId): Promise<Response[] | null> {
const state = this.queries.get(queryId);
if (!state) {
return null;
}
return Array.from(state.responses.values());
}
/** Get vote counts for a query */
async getVoteCounts(queryId: QueryId): Promise<Map<ResponseId, number> | null> {
const state = this.queries.get(queryId);
if (!state) {
return null;
}
const counts = new Map<ResponseId, number>();
for (const [id, voters] of state.votes) {
counts.set(id, voters.length);
}
return counts;
}
}
// Helper functions
function didUri(did: Did): string {
return `did:${did.method}:${did.id}`;
}
function numberToBytes(n: number): Uint8Array {
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setBigUint64(0, BigInt(n), true); // little-endian
return new Uint8Array(buffer);
}
function bufferToHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
/**
* One-shot consensus decision.
*/
export async function consensusDecide(
queryContent: string,
submitter: Did,
responses: Array<{ content: string; responder: Did }>,
votes: Array<{ responseIndex: number; voter: Did }>,
threshold: number = 0.5,
): Promise<ConsensusResult | null> {
const consensus = new AgentConsensusVoting(threshold, 1, 1);
const query = await createQuery(queryContent, submitter);
await consensus.submitQuery(query);
const responseIds: ResponseId[] = [];
for (const { content, responder } of responses) {
const response = await createResponse(query.id, content, responder);
await consensus.submitResponse(response);
responseIds.push(response.id);
}
for (const { responseIndex, voter } of votes) {
const responseId = responseIds[responseIndex];
if (responseId === undefined) {
throw new Error(`Invalid response index: ${responseIndex}`);
}
await consensus.vote(query.id, responseId, voter);
}
return consensus.getResult(query.id);
}
+64
View File
@@ -0,0 +1,64 @@
/**
* ZAP client implementation
*/
import type { Tool, Resource, ResourceContent } from './types.js';
import { ZapError } from './error.js';
/** ZAP client for connecting to ZAP gateways */
export class Client {
private connected = false;
private constructor(_url: string) {
// URL stored for future RPC connection
}
/** Connect to a ZAP gateway */
static async connect(url: string): Promise<Client> {
const client = new Client(url);
// TODO: Establish Cap'n Proto RPC connection
client.connected = true;
return client;
}
/** List available tools */
async listTools(): Promise<Tool[]> {
if (!this.connected) {
throw new ZapError('Not connected');
}
// TODO: Implement RPC call
return [];
}
/** Call a tool */
async callTool(_name: string, _args: Record<string, unknown>): Promise<unknown> {
if (!this.connected) {
throw new ZapError('Not connected');
}
// TODO: Implement RPC call
return null;
}
/** List available resources */
async listResources(): Promise<Resource[]> {
if (!this.connected) {
throw new ZapError('Not connected');
}
// TODO: Implement RPC call
return [];
}
/** Read a resource */
async readResource(uri: string): Promise<ResourceContent> {
if (!this.connected) {
throw new ZapError('Not connected');
}
// TODO: Implement RPC call
return { uri, mimeType: 'text/plain', content: '' };
}
/** Close the connection */
async close(): Promise<void> {
this.connected = false;
}
}
+94
View File
@@ -0,0 +1,94 @@
/**
* ZAP configuration types
*/
import type { Transport, LogLevel } from './types.js';
/** Server configuration */
export interface ServerConfig {
/** Server name */
name: string;
/** Server URL */
url: string;
/** Transport type */
transport: Transport;
/** Command for stdio transport */
command?: string;
/** Arguments for stdio transport */
args?: string[];
/** Environment variables */
env?: Record<string, string>;
}
/** Gateway configuration */
export interface Config {
/** Listen address */
listen: string;
/** Listen port */
port: number;
/** Configured servers */
servers: ServerConfig[];
/** Log level */
logLevel: LogLevel;
/** TLS certificate path */
tlsCert?: string;
/** TLS key path */
tlsKey?: string;
/** Maximum connections */
maxConnections?: number;
/** Connection timeout in milliseconds */
connectionTimeout?: number;
/** Request timeout in milliseconds */
requestTimeout?: number;
}
/** Default configuration values */
export const DEFAULT_CONFIG: Config = {
listen: '0.0.0.0',
port: 9999,
servers: [],
logLevel: 'info',
maxConnections: 1000,
connectionTimeout: 30000,
requestTimeout: 60000,
};
/** Load configuration from environment */
export function loadConfigFromEnv(): Partial<Config> {
const config: Partial<Config> = {};
const env = process.env;
if (env['ZAP_LISTEN']) {
config.listen = env['ZAP_LISTEN'];
}
if (env['ZAP_PORT']) {
config.port = parseInt(env['ZAP_PORT'], 10);
}
if (env['ZAP_LOG_LEVEL']) {
config.logLevel = env['ZAP_LOG_LEVEL'] as LogLevel;
}
if (env['ZAP_TLS_CERT']) {
config.tlsCert = env['ZAP_TLS_CERT'];
}
if (env['ZAP_TLS_KEY']) {
config.tlsKey = env['ZAP_TLS_KEY'];
}
if (env['ZAP_MAX_CONNECTIONS']) {
config.maxConnections = parseInt(env['ZAP_MAX_CONNECTIONS'], 10);
}
return config;
}
/** Merge configurations with defaults */
export function mergeConfig(...configs: Partial<Config>[]): Config {
return configs.reduce(
(merged, config) => ({ ...merged, ...config }),
{ ...DEFAULT_CONFIG }
) as Config;
}
+578
View File
@@ -0,0 +1,578 @@
/**
* Post-Quantum Cryptography Module for ZAP
*
* Provides ML-KEM-768 key exchange, ML-DSA-65 signatures, and hybrid X25519+ML-KEM handshake.
*
* Security:
* This module implements NIST FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) standards
* for post-quantum cryptographic protection. The hybrid handshake combines
* classical X25519 with ML-KEM-768 for defense-in-depth.
*
* Note:
* Full PQ crypto requires liboqs-node or similar native bindings.
* This module provides the interface and stubs - implement with actual
* PQ library when available in production.
*
* @example
* ```typescript
* import { PQKeyExchange, PQSignature, HybridHandshake } from '@hanzo/zap/crypto';
*
* // Key exchange
* const alice = await PQKeyExchange.generate();
* const bob = await PQKeyExchange.generate();
* const [ciphertext, sharedAlice] = await alice.encapsulate(bob.publicKey);
* const sharedBob = await bob.decapsulate(ciphertext);
* // sharedAlice === sharedBob
*
* // Signatures
* const signer = await PQSignature.generate();
* const sig = await signer.sign(new TextEncoder().encode('message'));
* await signer.verify(new TextEncoder().encode('message'), sig);
*
* // Hybrid handshake
* const initiator = await HybridHandshake.initiate();
* const [responder, response] = await HybridHandshake.respond(initiator.publicData);
* const sharedInit = await initiator.finalize(response);
* ```
*/
// Constants
export const MLKEM_PUBLIC_KEY_SIZE = 1184;
export const MLKEM_CIPHERTEXT_SIZE = 1088;
export const MLKEM_SHARED_SECRET_SIZE = 32;
export const MLDSA_PUBLIC_KEY_SIZE = 1952;
export const MLDSA_SIGNATURE_SIZE = 3293;
export const X25519_PUBLIC_KEY_SIZE = 32;
export const HYBRID_SHARED_SECRET_SIZE = 32;
/**
* Cryptographic operation error.
*/
export class CryptoError extends Error {
constructor(message: string) {
super(message);
this.name = 'CryptoError';
}
}
/**
* Check if Web Crypto API is available.
*/
export function isWebCryptoAvailable(): boolean {
return typeof globalThis.crypto !== 'undefined' &&
typeof globalThis.crypto.subtle !== 'undefined';
}
/**
* Check if PQ crypto is available.
* Note: Full implementation requires liboqs-node or similar.
*/
export function isPQAvailable(): boolean {
// TODO: Check for actual PQ library availability
return false;
}
/**
* Public data from the initiator for the responder.
*/
export interface HybridInitiatorData {
x25519PublicKey: Uint8Array;
mlkemPublicKey: Uint8Array;
}
/**
* Response data from the responder for the initiator.
*/
export interface HybridResponderData {
x25519PublicKey: Uint8Array;
mlkemCiphertext: Uint8Array;
}
/**
* ML-KEM-768 Key Encapsulation Mechanism.
*
* Implements NIST FIPS 203 ML-KEM-768 for post-quantum key exchange.
* Security level: NIST Level 3 (~AES-192 equivalent).
*
* Note: This is a stub implementation. For production use, integrate with
* liboqs-node or another PQ crypto library.
*/
export class PQKeyExchange {
private readonly _publicKey: Uint8Array;
private readonly _secretKey: Uint8Array | null;
private constructor(publicKey: Uint8Array, secretKey: Uint8Array | null) {
this._publicKey = publicKey;
this._secretKey = secretKey;
}
/**
* Generate a new ML-KEM-768 keypair.
*
* @throws {CryptoError} If PQ crypto is not available.
*/
static async generate(): Promise<PQKeyExchange> {
if (!isPQAvailable()) {
throw new CryptoError(
'PQ crypto not available - requires liboqs-node or similar library'
);
}
// TODO: Implement with actual PQ library
// const { publicKey, secretKey } = await mlkem768.keypair();
throw new CryptoError('PQ crypto not implemented');
}
/**
* Create instance from public key (for encapsulation only).
*/
static fromPublicKey(publicKey: Uint8Array): PQKeyExchange {
if (publicKey.length !== MLKEM_PUBLIC_KEY_SIZE) {
throw new CryptoError(
`Invalid ML-KEM public key size: expected ${MLKEM_PUBLIC_KEY_SIZE}, got ${publicKey.length}`
);
}
return new PQKeyExchange(publicKey, null);
}
/**
* Get the public key bytes.
*/
get publicKey(): Uint8Array {
return this._publicKey;
}
/**
* Encapsulate: generate ciphertext and shared secret for a recipient's public key.
*
* @param recipientPk - The recipient's ML-KEM public key.
* @returns Tuple of [ciphertext, sharedSecret].
*/
async encapsulate(recipientPk: Uint8Array): Promise<[Uint8Array, Uint8Array]> {
if (!isPQAvailable()) {
throw new CryptoError('PQ crypto not available');
}
if (recipientPk.length !== MLKEM_PUBLIC_KEY_SIZE) {
throw new CryptoError(
`Invalid recipient public key size: expected ${MLKEM_PUBLIC_KEY_SIZE}, got ${recipientPk.length}`
);
}
// TODO: Implement with actual PQ library
// const { ciphertext, sharedSecret } = await mlkem768.encapsulate(recipientPk);
throw new CryptoError('PQ crypto not implemented');
}
/**
* Decapsulate: recover shared secret from ciphertext.
*
* @param ciphertext - The ML-KEM ciphertext.
* @returns The shared secret bytes.
*/
async decapsulate(ciphertext: Uint8Array): Promise<Uint8Array> {
if (!isPQAvailable()) {
throw new CryptoError('PQ crypto not available');
}
if (this._secretKey === null) {
throw new CryptoError('No secret key available for decapsulation');
}
if (ciphertext.length !== MLKEM_CIPHERTEXT_SIZE) {
throw new CryptoError(
`Invalid ML-KEM ciphertext size: expected ${MLKEM_CIPHERTEXT_SIZE}, got ${ciphertext.length}`
);
}
// TODO: Implement with actual PQ library
// const sharedSecret = await mlkem768.decapsulate(ciphertext, this._secretKey);
throw new CryptoError('PQ crypto not implemented');
}
}
/**
* ML-DSA-65 Digital Signature Algorithm.
*
* Implements NIST FIPS 204 ML-DSA-65 (Dilithium3) for post-quantum signatures.
* Security level: NIST Level 3 (~AES-192 equivalent).
*
* Note: This is a stub implementation. For production use, integrate with
* liboqs-node or another PQ crypto library.
*/
export class PQSignature {
private readonly _publicKey: Uint8Array;
private readonly _secretKey: Uint8Array | null;
private constructor(publicKey: Uint8Array, secretKey: Uint8Array | null) {
this._publicKey = publicKey;
this._secretKey = secretKey;
}
/**
* Generate a new ML-DSA-65 keypair.
*
* @throws {CryptoError} If PQ crypto is not available.
*/
static async generate(): Promise<PQSignature> {
if (!isPQAvailable()) {
throw new CryptoError(
'PQ crypto not available - requires liboqs-node or similar library'
);
}
// TODO: Implement with actual PQ library
// const { publicKey, secretKey } = await dilithium3.keypair();
throw new CryptoError('PQ crypto not implemented');
}
/**
* Create instance from public key (for verification only).
*/
static fromPublicKey(publicKey: Uint8Array): PQSignature {
if (publicKey.length !== MLDSA_PUBLIC_KEY_SIZE) {
throw new CryptoError(
`Invalid ML-DSA public key size: expected ${MLDSA_PUBLIC_KEY_SIZE}, got ${publicKey.length}`
);
}
return new PQSignature(publicKey, null);
}
/**
* Get the public key bytes.
*/
get publicKey(): Uint8Array {
return this._publicKey;
}
/**
* Sign a message.
*
* @param _message - The message bytes to sign.
* @returns The signature bytes.
*/
async sign(_message: Uint8Array): Promise<Uint8Array> {
if (!isPQAvailable()) {
throw new CryptoError('PQ crypto not available');
}
if (this._secretKey === null) {
throw new CryptoError('No secret key available for signing');
}
// TODO: Implement with actual PQ library
// const signature = await dilithium3.sign(_message, this._secretKey);
throw new CryptoError('PQ crypto not implemented');
}
/**
* Verify a signature.
*
* @param _message - The original message bytes.
* @param signature - The signature bytes.
* @returns True if valid.
* @throws {CryptoError} If verification fails.
*/
async verify(_message: Uint8Array, signature: Uint8Array): Promise<boolean> {
if (!isPQAvailable()) {
throw new CryptoError('PQ crypto not available');
}
if (signature.length !== MLDSA_SIGNATURE_SIZE) {
throw new CryptoError(
`Invalid ML-DSA signature size: expected ${MLDSA_SIGNATURE_SIZE}, got ${signature.length}`
);
}
// TODO: Implement with actual PQ library
// const valid = await dilithium3.verify(_message, signature, this._publicKey);
throw new CryptoError('PQ crypto not implemented');
}
}
/**
* Handshake role.
*/
export type HandshakeRole = 'initiator' | 'responder';
/**
* Hybrid X25519 + ML-KEM-768 Handshake.
*
* Combines classical elliptic curve Diffie-Hellman (X25519) with post-quantum
* ML-KEM-768 for defense-in-depth. Even if one algorithm is broken, the other
* provides protection.
*
* The final shared secret is derived using HKDF-SHA256 over both shared secrets.
*
* Note: X25519 is available via Web Crypto API, but ML-KEM requires liboqs-node.
*/
// Web Crypto types for Node.js (available in Node 18+)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type WebCryptoKey = any;
export class HybridHandshake {
private _x25519Private: WebCryptoKey | null;
private readonly _x25519Public: Uint8Array;
private readonly _mlkem: PQKeyExchange;
private readonly _role: HandshakeRole;
private constructor(
x25519Private: WebCryptoKey | null,
x25519Public: Uint8Array,
mlkem: PQKeyExchange,
role: HandshakeRole
) {
this._x25519Private = x25519Private;
this._x25519Public = x25519Public;
this._mlkem = mlkem;
this._role = role;
}
/**
* Initiate a hybrid handshake (client side).
*
* @throws {CryptoError} If crypto is not available.
*/
static async initiate(): Promise<HybridHandshake> {
if (!isWebCryptoAvailable()) {
throw new CryptoError('Web Crypto API not available');
}
if (!isPQAvailable()) {
throw new CryptoError('PQ crypto not available');
}
// Generate X25519 keypair using Web Crypto
const x25519KeyPair = (await crypto.subtle.generateKey(
{ name: 'X25519' },
true,
['deriveBits']
)) as { publicKey: WebCryptoKey; privateKey: WebCryptoKey };
const x25519PublicRaw = await crypto.subtle.exportKey(
'raw',
x25519KeyPair.publicKey
);
// Generate ML-KEM keypair
const mlkem = await PQKeyExchange.generate();
return new HybridHandshake(
x25519KeyPair.privateKey,
new Uint8Array(x25519PublicRaw),
mlkem,
'initiator'
);
}
/**
* Get the public data to send to the responder.
*/
get publicData(): HybridInitiatorData {
return {
x25519PublicKey: this._x25519Public,
mlkemPublicKey: this._mlkem.publicKey,
};
}
/**
* Respond to a hybrid handshake (server side).
*
* @param initiatorData - Public data from the initiator.
* @returns Tuple of [HybridHandshake, HybridResponderData].
*/
static async respond(
initiatorData: HybridInitiatorData
): Promise<[HybridHandshake, HybridResponderData]> {
if (!isWebCryptoAvailable()) {
throw new CryptoError('Web Crypto API not available');
}
if (!isPQAvailable()) {
throw new CryptoError('PQ crypto not available');
}
// Validate input
if (initiatorData.x25519PublicKey.length !== X25519_PUBLIC_KEY_SIZE) {
throw new CryptoError(
`Invalid X25519 public key size: expected ${X25519_PUBLIC_KEY_SIZE}, got ${initiatorData.x25519PublicKey.length}`
);
}
if (initiatorData.mlkemPublicKey.length !== MLKEM_PUBLIC_KEY_SIZE) {
throw new CryptoError(
`Invalid ML-KEM public key size: expected ${MLKEM_PUBLIC_KEY_SIZE}, got ${initiatorData.mlkemPublicKey.length}`
);
}
// Generate responder's X25519 keypair
const x25519KeyPair = (await crypto.subtle.generateKey(
{ name: 'X25519' },
true,
['deriveBits']
)) as { publicKey: WebCryptoKey; privateKey: WebCryptoKey };
const x25519PublicRaw = await crypto.subtle.exportKey(
'raw',
x25519KeyPair.publicKey
);
// Generate ML-KEM keypair and encapsulate to initiator
const mlkem = await PQKeyExchange.generate();
const [mlkemCiphertext] = await mlkem.encapsulate(initiatorData.mlkemPublicKey);
const response: HybridResponderData = {
x25519PublicKey: new Uint8Array(x25519PublicRaw),
mlkemCiphertext,
};
const handshake = new HybridHandshake(
x25519KeyPair.privateKey,
new Uint8Array(x25519PublicRaw),
mlkem,
'responder'
);
return [handshake, response];
}
/**
* Finalize the handshake and derive the shared secret (initiator side).
*
* @param responderData - Response data from the responder.
* @returns The derived shared secret (32 bytes).
*/
async finalize(responderData: HybridResponderData): Promise<Uint8Array> {
if (this._role !== 'initiator') {
throw new CryptoError('finalize() can only be called by initiator');
}
if (this._x25519Private === null) {
throw new CryptoError('X25519 private key not available');
}
// Import peer's X25519 public key
const peerX25519Public = await crypto.subtle.importKey(
'raw',
responderData.x25519PublicKey,
{ name: 'X25519' },
false,
[]
);
// X25519 key exchange
const x25519Shared = await crypto.subtle.deriveBits(
{ name: 'X25519', public: peerX25519Public },
this._x25519Private,
256
);
// ML-KEM decapsulation
const mlkemShared = await this._mlkem.decapsulate(responderData.mlkemCiphertext);
// Clear private key reference
this._x25519Private = null;
// Combine shared secrets with HKDF
return this.deriveHybridSecret(new Uint8Array(x25519Shared), mlkemShared);
}
/**
* Complete the handshake and derive the shared secret (responder side).
*
* @param initiatorData - Public data from the initiator.
* @param mlkemShared - Optional pre-computed ML-KEM shared secret.
* @returns The derived shared secret (32 bytes).
*/
async complete(
initiatorData: HybridInitiatorData,
mlkemShared?: Uint8Array
): Promise<Uint8Array> {
if (this._role !== 'responder') {
throw new CryptoError('complete() can only be called by responder');
}
if (this._x25519Private === null) {
throw new CryptoError('X25519 private key not available');
}
// Import peer's X25519 public key
const peerX25519Public = await crypto.subtle.importKey(
'raw',
initiatorData.x25519PublicKey,
{ name: 'X25519' },
false,
[]
);
// X25519 key exchange
const x25519Shared = await crypto.subtle.deriveBits(
{ name: 'X25519', public: peerX25519Public },
this._x25519Private,
256
);
// Use provided ML-KEM shared secret or compute it
let finalMlkemShared = mlkemShared;
if (!finalMlkemShared) {
[, finalMlkemShared] = await this._mlkem.encapsulate(initiatorData.mlkemPublicKey);
}
// Clear private key reference
this._x25519Private = null;
// Combine shared secrets with HKDF
return this.deriveHybridSecret(new Uint8Array(x25519Shared), finalMlkemShared);
}
/**
* Derive hybrid shared secret using HKDF-SHA256.
*/
private async deriveHybridSecret(
x25519Shared: Uint8Array,
mlkemShared: Uint8Array
): Promise<Uint8Array> {
// Concatenate both shared secrets
const ikm = new Uint8Array(x25519Shared.length + mlkemShared.length);
ikm.set(x25519Shared);
ikm.set(mlkemShared, x25519Shared.length);
// Import IKM as raw key material
const ikmKey = await crypto.subtle.importKey(
'raw',
ikm,
{ name: 'HKDF' },
false,
['deriveBits']
);
// HKDF extract and expand
const salt = new TextEncoder().encode('ZAP-HYBRID-HANDSHAKE-v1');
const info = new TextEncoder().encode('shared-secret');
const derived = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt,
info,
},
ikmKey,
HYBRID_SHARED_SECRET_SIZE * 8
);
return new Uint8Array(derived);
}
}
/**
* Perform a complete hybrid handshake between two parties.
*
* This is a convenience function for testing and simple use cases.
*
* @returns Tuple of [initiatorSecret, responderSecret] - both should be equal.
*/
export async function hybridHandshake(): Promise<[Uint8Array, Uint8Array]> {
// Initiator starts
const initiator = await HybridHandshake.initiate();
const initData = initiator.publicData;
// Responder receives and responds
const [responder, respData] = await HybridHandshake.respond(initData);
// Responder also encapsulates to get shared secret
const mlkem = await PQKeyExchange.generate();
const [, mlkemShared] = await mlkem.encapsulate(initData.mlkemPublicKey);
// Initiator finalizes
const initiatorSecret = await initiator.finalize(respData);
// Responder completes
const responderSecret = await responder.complete(initData, mlkemShared);
return [initiatorSecret, responderSecret];
}
+96
View File
@@ -0,0 +1,96 @@
/**
* ZAP error types
*/
/** Base ZAP error */
export class ZapError extends Error {
readonly code: string;
readonly details: Record<string, unknown> | undefined;
constructor(
message: string,
code = 'ZAP_ERROR',
details?: Record<string, unknown>
) {
super(message);
this.name = 'ZapError';
this.code = code;
this.details = details;
Object.setPrototypeOf(this, ZapError.prototype);
}
}
/** Connection error */
export class ConnectionError extends ZapError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, 'CONNECTION_ERROR', details);
this.name = 'ConnectionError';
Object.setPrototypeOf(this, ConnectionError.prototype);
}
}
/** Transport error */
export class TransportError extends ZapError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, 'TRANSPORT_ERROR', details);
this.name = 'TransportError';
Object.setPrototypeOf(this, TransportError.prototype);
}
}
/** Protocol error */
export class ProtocolError extends ZapError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, 'PROTOCOL_ERROR', details);
this.name = 'ProtocolError';
Object.setPrototypeOf(this, ProtocolError.prototype);
}
}
/** Timeout error */
export class TimeoutError extends ZapError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, 'TIMEOUT_ERROR', details);
this.name = 'TimeoutError';
Object.setPrototypeOf(this, TimeoutError.prototype);
}
}
/** Server error */
export class ServerError extends ZapError {
constructor(message: string, details?: Record<string, unknown>) {
super(message, 'SERVER_ERROR', details);
this.name = 'ServerError';
Object.setPrototypeOf(this, ServerError.prototype);
}
}
/** Tool not found error */
export class ToolNotFoundError extends ZapError {
constructor(toolName: string) {
super(`Tool not found: ${toolName}`, 'TOOL_NOT_FOUND', { toolName });
this.name = 'ToolNotFoundError';
Object.setPrototypeOf(this, ToolNotFoundError.prototype);
}
}
/** Resource not found error */
export class ResourceNotFoundError extends ZapError {
constructor(uri: string) {
super(`Resource not found: ${uri}`, 'RESOURCE_NOT_FOUND', { uri });
this.name = 'ResourceNotFoundError';
Object.setPrototypeOf(this, ResourceNotFoundError.prototype);
}
}
/** Invalid argument error */
export class InvalidArgumentError extends ZapError {
constructor(argument: string, reason: string) {
super(`Invalid argument '${argument}': ${reason}`, 'INVALID_ARGUMENT', {
argument,
reason,
});
this.name = 'InvalidArgumentError';
Object.setPrototypeOf(this, InvalidArgumentError.prototype);
}
}
+179
View File
@@ -0,0 +1,179 @@
/**
* ZAP gateway implementation
*/
import type { Config, ServerConfig } from './config.js';
import type { Tool, Resource, ConnectedServer } from './types.js';
import { ZapError } from './error.js';
/** Gateway for aggregating multiple MCP servers */
export class Gateway {
private config: Config;
private servers: Map<string, ConnectedServer> = new Map();
private running = false;
constructor(config?: Partial<Config>) {
this.config = {
listen: '0.0.0.0',
port: 9999,
servers: [],
logLevel: 'info',
...config,
};
}
/** Start the gateway */
async start(): Promise<void> {
if (this.running) {
throw new ZapError('Gateway already running');
}
this.running = true;
const addr = `${this.config.listen}:${this.config.port}`;
console.log(`ZAP gateway starting on ${addr}`);
// Connect to configured servers
for (const serverConfig of this.config.servers) {
await this.connectServer(serverConfig);
}
// TODO: Start Cap'n Proto RPC server
console.log(`ZAP gateway ready with ${this.servers.size} servers`);
}
/** Stop the gateway */
async stop(): Promise<void> {
if (!this.running) {
return;
}
// Disconnect all servers
for (const [id] of this.servers) {
await this.disconnectServer(id);
}
this.running = false;
console.log('ZAP gateway stopped');
}
/** Connect to an MCP server */
async connectServer(config: ServerConfig): Promise<string> {
const id = crypto.randomUUID();
const server: ConnectedServer = {
id,
name: config.name,
url: config.url,
status: 'connecting',
tools: 0,
resources: 0,
};
this.servers.set(id, server);
try {
// TODO: Establish connection based on transport
server.status = 'connected';
console.log(`Connected to server: ${config.name}`);
} catch (error) {
server.status = 'error';
throw new ZapError(`Failed to connect to ${config.name}: ${error}`);
}
return id;
}
/** Disconnect from a server */
async disconnectServer(id: string): Promise<void> {
const server = this.servers.get(id);
if (!server) {
throw new ZapError(`Server not found: ${id}`);
}
server.status = 'disconnected';
this.servers.delete(id);
console.log(`Disconnected from server: ${server.name}`);
}
/** List all connected servers */
listServers(): ConnectedServer[] {
return Array.from(this.servers.values());
}
/** Get server by ID */
getServer(id: string): ConnectedServer | undefined {
return this.servers.get(id);
}
/** List all tools from all servers */
async listTools(): Promise<Tool[]> {
const tools: Tool[] = [];
for (const [, server] of this.servers) {
if (server.status !== 'connected') {
continue;
}
// TODO: Fetch tools from server via RPC
}
return tools;
}
/** Call a tool on a specific server */
async callTool(
serverId: string,
_name: string,
_args: Record<string, unknown>
): Promise<unknown> {
const server = this.servers.get(serverId);
if (!server) {
throw new ZapError(`Server not found: ${serverId}`);
}
if (server.status !== 'connected') {
throw new ZapError(`Server not connected: ${server.name}`);
}
// TODO: Implement RPC call
return null;
}
/** List all resources from all servers */
async listResources(): Promise<Resource[]> {
const resources: Resource[] = [];
for (const [, server] of this.servers) {
if (server.status !== 'connected') {
continue;
}
// TODO: Fetch resources from server via RPC
}
return resources;
}
/** Read a resource from a specific server */
async readResource(serverId: string, _uri: string): Promise<unknown> {
const server = this.servers.get(serverId);
if (!server) {
throw new ZapError(`Server not found: ${serverId}`);
}
if (server.status !== 'connected') {
throw new ZapError(`Server not connected: ${server.name}`);
}
// TODO: Implement RPC call
return null;
}
/** Check if gateway is running */
isRunning(): boolean {
return this.running;
}
/** Get gateway configuration */
getConfig(): Config {
return { ...this.config };
}
}
+584
View File
@@ -0,0 +1,584 @@
/**
* W3C Decentralized Identifier (DID) Implementation
*
* Implements W3C DID Core 1.0 specification with support for:
* - did:lux - Lux blockchain-anchored DIDs
* - did:key - Self-certifying DIDs from cryptographic keys
* - did:web - DNS-based DIDs
*
* @example
* ```typescript
* import { Did, DidMethod, NodeIdentity, parseDid, createDidFromKey } from './identity';
*
* // Parse existing DID
* const did = parseDid("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
*
* // Create from ML-DSA public key
* const did = createDidFromKey(publicKeyBytes);
*
* // Generate DID Document
* const doc = did.document();
*
* // Generate node identity
* const identity = await generateIdentity();
* ```
*/
// Base58 alphabet (Bitcoin style)
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
// Multibase prefix for base58btc
const MULTIBASE_BASE58BTC = 'z';
// Multicodec prefix for ML-DSA-65 public key (provisional)
const MULTICODEC_MLDSA65 = new Uint8Array([0x13, 0x09]);
// Expected ML-DSA-65 public key size
export const MLDSA_PUBLIC_KEY_SIZE = 1952;
/**
* Identity-related error
*/
export class IdentityError extends Error {
constructor(message: string) {
super(message);
this.name = 'IdentityError';
}
}
/**
* Encode bytes to base58 (Bitcoin alphabet)
*/
function base58Encode(data: Uint8Array): string {
let num = BigInt(0);
for (let i = 0; i < data.length; i++) {
num = num * BigInt(256) + BigInt(data[i]!);
}
const result: string[] = [];
while (num > BigInt(0)) {
const remainder = Number(num % BigInt(58));
num = num / BigInt(58);
result.push(BASE58_ALPHABET[remainder]!);
}
// Handle leading zeros
for (let i = 0; i < data.length; i++) {
if (data[i] === 0) {
result.push(BASE58_ALPHABET[0]!);
} else {
break;
}
}
return result.reverse().join('');
}
/**
* Decode base58 string to bytes
*/
function base58Decode(s: string): Uint8Array {
let num = BigInt(0);
for (const char of s) {
const index = BASE58_ALPHABET.indexOf(char);
if (index === -1) {
throw new IdentityError(`invalid base58 character: ${char}`);
}
num = num * BigInt(58) + BigInt(index);
}
// Convert to bytes
const bytes: number[] = [];
while (num > BigInt(0)) {
bytes.push(Number(num % BigInt(256)));
num = num / BigInt(256);
}
// Handle leading ones (zeros in decoded)
for (const char of s) {
if (char === BASE58_ALPHABET[0]) {
bytes.push(0);
} else {
break;
}
}
return new Uint8Array(bytes.reverse());
}
/**
* DID method identifier
*/
export enum DidMethod {
Lux = 'lux',
Key = 'key',
Web = 'web',
}
/**
* Verification method type
*/
export enum VerificationMethodType {
JsonWebKey2020 = 'JsonWebKey2020',
Multikey = 'Multikey',
MlDsa65VerificationKey2024 = 'MlDsa65VerificationKey2024',
}
/**
* Service type
*/
export enum ServiceType {
ZapAgent = 'ZapAgent',
DIDCommMessaging = 'DIDCommMessaging',
LinkedDomains = 'LinkedDomains',
CredentialRegistry = 'CredentialRegistry',
}
/**
* Service endpoint configuration
*/
export interface ServiceEndpoint {
uri: string;
accept?: string[];
routingKeys?: string[];
}
/**
* Verification method (public key) in DID Document
*/
export interface VerificationMethod {
id: string;
type: VerificationMethodType;
controller: string;
publicKeyMultibase?: string;
publicKeyJwk?: Record<string, unknown>;
blockchainAccountId?: string;
}
/**
* Service endpoint in DID Document
*/
export interface Service {
id: string;
type: ServiceType;
serviceEndpoint: string | ServiceEndpoint;
}
/**
* W3C DID Document
*/
export interface DidDocument {
'@context': string[];
id: string;
controller?: string;
verificationMethod?: VerificationMethod[];
authentication?: string[];
assertionMethod?: string[];
keyAgreement?: string[];
capabilityInvocation?: string[];
capabilityDelegation?: string[];
service?: Service[];
}
/**
* W3C Decentralized Identifier (DID)
*/
export interface Did {
method: DidMethod;
id: string;
}
/**
* Create a DID URI string
*/
export function didUri(did: Did): string {
return `did:${did.method}:${did.id}`;
}
/**
* Extract raw key material from did:key or did:lux identifier
*/
export function extractKeyMaterial(did: Did): Uint8Array {
if (!did.id) {
throw new IdentityError('empty DID identifier');
}
if (!did.id.startsWith(MULTIBASE_BASE58BTC)) {
throw new IdentityError(
`unsupported multibase encoding: expected '${MULTIBASE_BASE58BTC}', got '${did.id[0]}'`
);
}
// Decode base58btc (skip multibase prefix)
const decoded = base58Decode(did.id.slice(1));
if (decoded.length < 2) {
throw new IdentityError('DID identifier too short');
}
// Skip multicodec prefix if it matches ML-DSA-65
if (decoded[0] === MULTICODEC_MLDSA65[0] && decoded[1] === MULTICODEC_MLDSA65[1]) {
return decoded.slice(2);
}
return decoded;
}
/**
* Generate a W3C DID Document for a DID
*/
export function generateDocument(did: Did): DidDocument {
const uri = didUri(did);
let verificationMethod: VerificationMethod;
if (did.method === DidMethod.Key || did.method === DidMethod.Lux) {
const keyMaterial = extractKeyMaterial(did);
if (did.method === DidMethod.Lux) {
// Create blockchain account ID from first 20 bytes
const accountBytes = keyMaterial.slice(0, 20);
const blockchainAccountId = `lux:${Array.from(accountBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')}`;
verificationMethod = {
id: `${uri}#keys-1`,
type: VerificationMethodType.JsonWebKey2020,
controller: uri,
publicKeyMultibase: did.id,
blockchainAccountId,
};
} else {
verificationMethod = {
id: `${uri}#keys-1`,
type: VerificationMethodType.JsonWebKey2020,
controller: uri,
publicKeyMultibase: did.id,
};
}
} else {
verificationMethod = {
id: `${uri}#keys-1`,
type: VerificationMethodType.JsonWebKey2020,
controller: uri,
};
}
const service: Service = {
id: `${uri}#zap-agent`,
type: ServiceType.ZapAgent,
serviceEndpoint: `zap://${did.id}`,
};
return {
'@context': [
'https://www.w3.org/ns/did/v1',
'https://w3id.org/security/suites/jws-2020/v1',
],
id: uri,
verificationMethod: [verificationMethod],
authentication: [`${uri}#keys-1`],
assertionMethod: [`${uri}#keys-1`],
capabilityInvocation: [`${uri}#keys-1`],
service: [service],
};
}
/**
* Parse a DID from a string in the format "did:method:id"
*
* @param s - DID string to parse
* @returns Parsed Did object
* @throws IdentityError if the DID string is invalid
*
* @example
* ```typescript
* const did = parseDid("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
* console.log(did.method); // DidMethod.Lux
* ```
*/
export function parseDid(s: string): Did {
if (!s.startsWith('did:')) {
throw new IdentityError(`invalid DID: must start with 'did:', got '${s}'`);
}
const rest = s.slice(4); // Skip "did:"
const colonIndex = rest.indexOf(':');
if (colonIndex === -1) {
throw new IdentityError(`invalid DID format: expected 'did:method:id', got '${s}'`);
}
const methodStr = rest.slice(0, colonIndex);
const didId = rest.slice(colonIndex + 1);
let method: DidMethod;
switch (methodStr) {
case 'lux':
method = DidMethod.Lux;
break;
case 'key':
method = DidMethod.Key;
break;
case 'web':
method = DidMethod.Web;
break;
default:
throw new IdentityError(`unknown DID method: ${methodStr}`);
}
if (!didId) {
throw new IdentityError('DID identifier cannot be empty');
}
return { method, id: didId };
}
/**
* Create a DID from an ML-DSA-65 public key
*
* @param publicKey - ML-DSA-65 public key bytes (1952 bytes)
* @param method - DID method to use (KEY or LUX)
* @returns New Did object
* @throws IdentityError if the public key is invalid
*
* @example
* ```typescript
* const did = createDidFromKey(publicKeyBytes);
* console.log(didUri(did)); // did:key:z6Mk...
* ```
*/
export function createDidFromKey(
publicKey: Uint8Array,
method: DidMethod = DidMethod.Key
): Did {
if (publicKey.length !== MLDSA_PUBLIC_KEY_SIZE) {
throw new IdentityError(
`invalid ML-DSA public key size: expected ${MLDSA_PUBLIC_KEY_SIZE}, got ${publicKey.length}`
);
}
// Create multicodec-prefixed key
const prefixed = new Uint8Array(MULTICODEC_MLDSA65.length + publicKey.length);
prefixed.set(MULTICODEC_MLDSA65, 0);
prefixed.set(publicKey, MULTICODEC_MLDSA65.length);
// Encode with multibase (base58btc)
const encoded = base58Encode(prefixed);
const id = `${MULTIBASE_BASE58BTC}${encoded}`;
return { method, id };
}
/**
* Create a web DID from a domain and optional path
*
* @param domain - Domain name (e.g., "example.com")
* @param path - Optional path (e.g., "users/alice")
* @returns New Did object with method=WEB
* @throws IdentityError if the domain is invalid
*
* @example
* ```typescript
* const did = createDidFromWeb("example.com", "users/alice");
* console.log(didUri(did)); // did:web:example.com:users:alice
* ```
*/
export function createDidFromWeb(domain: string, path?: string): Did {
if (!domain) {
throw new IdentityError('domain cannot be empty');
}
if (domain.includes('/') || domain.includes(':')) {
throw new IdentityError(`invalid domain for did:web: ${domain}`);
}
let id: string;
if (path) {
// Replace '/' with ':' per did:web spec
const pathParts = path.replace(/\//g, ':');
id = `${domain}:${pathParts}`;
} else {
id = domain;
}
return { method: DidMethod.Web, id };
}
/**
* Stake registry interface
*/
export interface StakeRegistry {
getStake(did: Did): Promise<bigint>;
setStake(did: Did, amount: bigint): Promise<void>;
totalStake(): Promise<bigint>;
hasSufficientStake(did: Did, minimum: bigint): Promise<boolean>;
stakeWeight(did: Did): Promise<number>;
}
/**
* In-memory stake registry for testing
*/
export class InMemoryStakeRegistry implements StakeRegistry {
private stakes: Map<string, bigint> = new Map();
async getStake(did: Did): Promise<bigint> {
return this.stakes.get(didUri(did)) ?? BigInt(0);
}
async setStake(did: Did, amount: bigint): Promise<void> {
this.stakes.set(didUri(did), amount);
}
async totalStake(): Promise<bigint> {
let total = BigInt(0);
this.stakes.forEach((stake) => {
total += stake;
});
return total;
}
async hasSufficientStake(did: Did, minimum: bigint): Promise<boolean> {
const stake = await this.getStake(did);
return stake >= minimum;
}
async stakeWeight(did: Did): Promise<number> {
const stake = await this.getStake(did);
const total = await this.totalStake();
if (total === BigInt(0)) {
return 0.0;
}
return Number(stake) / Number(total);
}
}
/**
* Signer interface for cryptographic operations
*/
export interface Signer {
sign(message: Uint8Array): Promise<Uint8Array>;
verify(message: Uint8Array, signature: Uint8Array): Promise<boolean>;
publicKey: Uint8Array;
}
/**
* Node identity combining DID with cryptographic keypair
*
* Used for authenticated node participation in the ZAP network.
*/
export class NodeIdentity {
public readonly did: Did;
public readonly publicKey: Uint8Array;
public stake: bigint | undefined;
public stakeRegistry: string | undefined;
private signer: Signer | undefined;
constructor(did: Did, publicKey: Uint8Array, signer?: Signer) {
this.did = did;
this.publicKey = publicKey;
this.signer = signer;
}
/**
* Check if this node has signing capability
*/
canSign(): boolean {
return this.signer !== undefined;
}
/**
* Sign a message with this node's private key
*/
async sign(message: Uint8Array): Promise<Uint8Array> {
if (!this.signer) {
throw new IdentityError('no private key available for signing');
}
return this.signer.sign(message);
}
/**
* Verify a signature against this node's public key
*/
async verify(message: Uint8Array, signature: Uint8Array): Promise<boolean> {
if (this.signer) {
return this.signer.verify(message, signature);
}
throw new IdentityError('verification requires a signer implementation');
}
/**
* Get the DID document for this node identity
*/
document(): DidDocument {
return generateDocument(this.did);
}
/**
* Get the DID URI string
*/
uri(): string {
return didUri(this.did);
}
/**
* Set the stake amount for this node
*/
withStake(amount: bigint): this {
this.stake = amount;
return this;
}
/**
* Set the stake registry reference
*/
withRegistry(registry: string): this {
this.stakeRegistry = registry;
return this;
}
}
/**
* Generate a new node identity with fresh ML-DSA-65 keypair
*
* Note: This requires a Signer implementation to be provided.
* In production, use a proper cryptographic library.
*
* @param signer - Signer implementation with ML-DSA-65 keypair
* @param method - DID method to use (default: LUX)
* @returns New NodeIdentity with signing capability
*
* @example
* ```typescript
* // With a signer implementation
* const identity = generateIdentity(signer);
* console.log(identity.uri()); // did:lux:z6Mk...
* identity.canSign(); // true
* ```
*/
export function generateIdentity(
signer: Signer,
method: DidMethod = DidMethod.Lux
): NodeIdentity {
const publicKey = signer.publicKey;
if (publicKey.length !== MLDSA_PUBLIC_KEY_SIZE) {
throw new IdentityError(
`invalid ML-DSA public key size: expected ${MLDSA_PUBLIC_KEY_SIZE}, got ${publicKey.length}`
);
}
const did = createDidFromKey(publicKey, method);
return new NodeIdentity(did, publicKey, signer);
}
/**
* Create a NodeIdentity from an existing DID and public key (verification only)
*
* @param did - Existing DID
* @param publicKey - Public key bytes
* @returns NodeIdentity without signing capability
*/
export function createNodeIdentity(did: Did, publicKey: Uint8Array): NodeIdentity {
return new NodeIdentity(did, publicKey);
}
+43
View File
@@ -0,0 +1,43 @@
/**
* ZAP - Zero-Copy App Proto
*
* High-performance Cap'n Proto RPC for AI agent communication.
*
* @example
* ```typescript
* import { Client } from '@hanzo/zap';
*
* const client = await Client.connect('zap://localhost:9999');
* const tools = await client.listTools();
* const result = await client.callTool('search', { query: 'hello' });
* ```
*
* @packageDocumentation
*/
export { Client } from './client.js';
export { Server } from './server.js';
export { Gateway } from './gateway.js';
export type { Config, ServerConfig } from './config.js';
export { DEFAULT_CONFIG, loadConfigFromEnv, mergeConfig } from './config.js';
export {
ZapError,
ConnectionError,
TransportError,
ProtocolError,
TimeoutError,
ServerError,
ToolNotFoundError,
ResourceNotFoundError,
InvalidArgumentError,
} from './error.js';
export * from './types.js';
export * from './identity.js';
export * from './agent_consensus.js';
export * from './lux_consensus.js';
/** ZAP protocol version */
export const VERSION = '0.2.1';
/** Default port for ZAP connections */
export const DEFAULT_PORT = 9999;
+365
View File
@@ -0,0 +1,365 @@
/**
* Lux Consensus Bridge
*
* TypeScript types and interfaces for integrating ZAP with Lux's Quasar consensus.
* This module provides the client-side interface for communicating with the
* Lux node's ZAP consensus bridge.
*/
import { Did, didUri } from './identity.js';
import type { Query, Response, ConsensusResult } from './agent_consensus.js';
/**
* Quasar signature types matching Lux consensus/quasar/types.go
*/
export enum SignatureType {
BLS = 0,
Ringtail = 1,
Quasar = 2, // Hybrid BLS + Ringtail
MLDSA = 3,
}
/**
* Post-quantum signature types
*/
export enum PQSignatureType {
MLDSA65 = 0,
Ringtail = 1,
Hybrid = 2,
}
/**
* Bridge configuration options
*/
export interface BridgeConfig {
/** Fraction of votes needed for consensus (default: 0.5) */
consensusThreshold: number;
/** Minimum responses before checking consensus (default: 1) */
minResponses: number;
/** Minimum votes before checking consensus (default: 3) */
minVotes: number;
/** Enable post-quantum signatures (default: true) */
enablePQCrypto: boolean;
}
/**
* Default bridge configuration
*/
export const DEFAULT_BRIDGE_CONFIG: BridgeConfig = {
consensusThreshold: 0.5,
minResponses: 1,
minVotes: 3,
enablePQCrypto: true,
};
/**
* Bridge statistics
*/
export interface BridgeStats {
registeredValidators: number;
activeQueries: number;
finalizedQueries: number;
quasarInitialized: boolean;
ringtailStats?: RingtailStats;
}
/**
* Ringtail coordinator statistics
*/
export interface RingtailStats {
numParties: number;
threshold: number;
initialized: boolean;
}
/**
* Validator information
*/
export interface ValidatorInfo {
nodeId: string;
did: Did;
weight: bigint;
stake: bigint;
active: boolean;
}
/**
* Finality proof for a query
*/
export interface FinalityProof {
queryId: string;
responseId: string;
votes: number;
totalVoters: number;
confidence: number;
timestamp: number;
signature: Uint8Array; // Quasar hybrid signature
}
/**
* Quasar signature (hybrid BLS + Ringtail)
*/
export interface QuasarSignature {
type: SignatureType;
signature: Uint8Array;
signers: string[]; // NodeIDs of signers
}
/**
* Ringtail signature (post-quantum threshold)
*/
export interface RingtailSignature {
signature: Uint8Array;
signers: string[];
}
/**
* BLS signature (classical aggregate)
*/
export interface BLSSignature {
signature: Uint8Array;
signers: string[];
}
// RPC response types
interface RPCResponse<T> {
jsonrpc: string;
id: number;
result?: T;
error?: {
code: number;
message: string;
};
}
interface QueryIdResult {
queryId: string;
}
interface ResponseIdResult {
responseId: string;
}
interface FinalizedResult {
finalized: boolean;
}
interface ConsensusResultRPC {
response: Response | null;
votes: number;
totalVoters: number;
confidence: number;
}
interface FinalityProofRPC {
proof: {
queryId: string;
responseId: string;
votes: number;
totalVoters: number;
confidence: number;
timestamp: number;
signature: string;
} | null;
}
interface SignatureResult {
signature: string;
signers: string[];
}
interface VerifyResult {
valid: boolean;
}
/**
* Lux consensus bridge client
*
* Provides methods for interacting with the Lux node's ZAP consensus bridge
* via JSON-RPC or gRPC.
*/
export class LuxConsensusBridge {
private endpoint: string;
private _config: BridgeConfig;
constructor(endpoint: string, config: Partial<BridgeConfig> = {}) {
this.endpoint = endpoint;
this._config = { ...DEFAULT_BRIDGE_CONFIG, ...config };
}
/** Get the current configuration */
get config(): BridgeConfig {
return this._config;
}
/**
* Register a validator's DID with the bridge
*/
async registerValidator(nodeId: string, did: Did): Promise<void> {
await this.rpc<void>('zap_registerValidator', {
nodeId,
did: didUri(did),
});
}
/**
* Submit a query for agentic consensus
*/
async submitQuery(query: Query): Promise<string> {
const result = await this.rpc<QueryIdResult>('zap_submitQuery', {
id: query.id,
content: query.content,
submitter: didUri(query.submitter),
timestamp: query.timestamp,
});
return result.queryId;
}
/**
* Submit a response to a query
*/
async submitResponse(response: Response): Promise<string> {
const result = await this.rpc<ResponseIdResult>('zap_submitResponse', {
id: response.id,
queryId: response.queryId,
content: response.content,
responder: didUri(response.responder),
timestamp: response.timestamp,
});
return result.responseId;
}
/**
* Cast a vote for a response
*/
async vote(queryId: string, responseId: string, voter: Did): Promise<void> {
await this.rpc<void>('zap_vote', {
queryId,
responseId,
voter: didUri(voter),
});
}
/**
* Check if a query has reached consensus
*/
async isFinalized(queryId: string): Promise<boolean> {
const result = await this.rpc<FinalizedResult>('zap_isFinalized', { queryId });
return result.finalized;
}
/**
* Get the consensus result for a query
*/
async getResult(queryId: string): Promise<ConsensusResult | null> {
const result = await this.rpc<ConsensusResultRPC>('zap_getResult', { queryId });
if (!result.response) {
return null;
}
return {
response: result.response,
votes: result.votes,
totalVoters: result.totalVoters,
confidence: result.confidence,
};
}
/**
* Get finality proof for a finalized query
*/
async getFinalityProof(queryId: string): Promise<FinalityProof | null> {
const result = await this.rpc<FinalityProofRPC>('zap_getFinalityProof', { queryId });
if (!result.proof) {
return null;
}
return {
queryId: result.proof.queryId,
responseId: result.proof.responseId,
votes: result.proof.votes,
totalVoters: result.proof.totalVoters,
confidence: result.proof.confidence,
timestamp: result.proof.timestamp,
signature: hexToBytes(result.proof.signature),
};
}
/**
* Get bridge statistics
*/
async stats(): Promise<BridgeStats> {
return await this.rpc<BridgeStats>('zap_stats', {});
}
/**
* Sign a message using Quasar hybrid signatures
*/
async signWithQuasar(message: Uint8Array): Promise<QuasarSignature> {
const result = await this.rpc<SignatureResult>('zap_signQuasar', {
message: bytesToHex(message),
});
return {
type: SignatureType.Quasar,
signature: hexToBytes(result.signature),
signers: result.signers,
};
}
/**
* Verify a Quasar signature
*/
async verifyQuasar(message: Uint8Array, signature: QuasarSignature): Promise<boolean> {
const result = await this.rpc<VerifyResult>('zap_verifyQuasar', {
message: bytesToHex(message),
signature: bytesToHex(signature.signature),
});
return result.valid;
}
private async rpc<T>(method: string, params: Record<string, unknown>): Promise<T> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method,
params,
}),
});
if (!response.ok) {
throw new Error(`RPC request failed: ${response.status} ${response.statusText}`);
}
const json = (await response.json()) as RPCResponse<T>;
if (json.error) {
throw new Error(`RPC error: ${json.error.message}`);
}
return json.result as T;
}
}
// Helper functions
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
/**
* Create a bridge client connected to a Lux node
*/
export function createBridge(endpoint: string, config?: Partial<BridgeConfig>): LuxConsensusBridge {
return new LuxConsensusBridge(endpoint, config);
}
+28
View File
@@ -0,0 +1,28 @@
/**
* ZAP server implementation
*/
import type { Config } from './config.js';
/** ZAP server */
export class Server {
private config: Config;
constructor(config?: Partial<Config>) {
this.config = {
listen: '0.0.0.0',
port: 9999,
servers: [],
logLevel: 'info',
...config,
};
}
/** Run the server */
async run(): Promise<void> {
const addr = `${this.config.listen}:${this.config.port}`;
console.log(`ZAP server listening on ${addr}`);
// TODO: Start Cap'n Proto RPC server
await new Promise(() => {}); // Wait forever
}
}
+92
View File
@@ -0,0 +1,92 @@
/**
* ZAP type definitions
*/
/** Tool definition */
export interface Tool {
name: string;
description: string;
schema: Record<string, unknown>;
}
/** Resource definition */
export interface Resource {
uri: string;
name: string;
description: string;
mimeType: string;
}
/** Resource content */
export interface ResourceContent {
uri: string;
mimeType: string;
content: string | Uint8Array;
}
/** Prompt definition */
export interface Prompt {
name: string;
description: string;
arguments: PromptArgument[];
}
/** Prompt argument */
export interface PromptArgument {
name: string;
description: string;
required: boolean;
}
/** Prompt message */
export interface PromptMessage {
role: 'user' | 'assistant' | 'system';
content: TextContent | ImageContent | ResourceContent;
}
/** Text content */
export interface TextContent {
type: 'text';
text: string;
}
/** Image content */
export interface ImageContent {
type: 'image';
data: Uint8Array;
mimeType: string;
}
/** Server info */
export interface ServerInfo {
name: string;
version: string;
capabilities: ServerCapabilities;
}
/** Server capabilities */
export interface ServerCapabilities {
tools: boolean;
resources: boolean;
prompts: boolean;
logging: boolean;
}
/** Connected server info */
export interface ConnectedServer {
id: string;
name: string;
url: string;
status: ServerStatus;
tools: number;
resources: number;
}
/** Server status */
export type ServerStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
/** Transport type */
export type Transport = 'stdio' | 'http' | 'websocket' | 'zap' | 'unix';
/** Log level */
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
@@ -0,0 +1,257 @@
import { describe, it, expect } from 'vitest';
import {
createQuery,
createResponse,
AgentConsensusVoting,
consensusDecide,
} from '../src/agent_consensus.js';
import { Did, DidMethod } from '../src/identity.js';
function makeDid(name: string): Did {
return { method: DidMethod.Lux, id: `z6Mk${name}` };
}
describe('Query', () => {
it('should create a query with auto-generated ID', async () => {
const submitter = makeDid('Alice');
const query = await createQuery('What is 2+2?', submitter);
expect(query.content).toBe('What is 2+2?');
expect(query.submitter).toEqual(submitter);
expect(query.id).toHaveLength(64); // SHA-256 hex string
expect(query.timestamp).toBeGreaterThan(0);
});
it('should generate unique IDs for different queries', async () => {
const submitter = makeDid('Alice');
const q1 = await createQuery('What is 2+2?', submitter);
const q2 = await createQuery('What is 3+3?', submitter);
expect(q1.id).not.toBe(q2.id);
});
});
describe('Response', () => {
it('should create a response with auto-generated ID', async () => {
const queryId = '0'.repeat(64);
const responder = makeDid('Bob');
const response = await createResponse(queryId, '4', responder);
expect(response.queryId).toBe(queryId);
expect(response.content).toBe('4');
expect(response.responder).toEqual(responder);
expect(response.id).toHaveLength(64);
expect(response.timestamp).toBeGreaterThan(0);
});
it('should generate unique IDs for different responses', async () => {
const queryId = '0'.repeat(64);
const responder = makeDid('Bob');
const r1 = await createResponse(queryId, '4', responder);
const r2 = await createResponse(queryId, '5', responder);
expect(r1.id).not.toBe(r2.id);
});
});
describe('AgentConsensusVoting', () => {
it('should submit a query', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
const query = await createQuery('What is 2+2?', makeDid('Alice'));
const queryId = await consensus.submitQuery(query);
expect(queryId).toBe(query.id);
});
it('should submit a response', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
const query = await createQuery('What is 2+2?', makeDid('Alice'));
await consensus.submitQuery(query);
const response = await createResponse(query.id, '4', makeDid('Bob'));
const responseId = await consensus.submitResponse(response);
expect(responseId).toBe(response.id);
});
it('should throw when submitting response to non-existent query', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
const response = await createResponse('0'.repeat(64), '4', makeDid('Bob'));
await expect(consensus.submitResponse(response)).rejects.toThrow('Query not found');
});
it('should vote for a response', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
const query = await createQuery('What is 2+2?', makeDid('Alice'));
await consensus.submitQuery(query);
const response = await createResponse(query.id, '4', makeDid('Bob'));
const responseId = await consensus.submitResponse(response);
await consensus.vote(query.id, responseId, makeDid('Voter1'));
const finalized = await consensus.isFinalized(query.id);
expect(finalized).toBe(true);
});
it('should prevent double voting', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 2);
const query = await createQuery('Test', makeDid('Alice'));
await consensus.submitQuery(query);
const response = await createResponse(query.id, 'Answer', makeDid('Bob'));
const responseId = await consensus.submitResponse(response);
const voter = makeDid('Voter1');
await consensus.vote(query.id, responseId, voter);
await expect(consensus.vote(query.id, responseId, voter)).rejects.toThrow(
'Already voted',
);
});
it('should throw when voting on non-existent query', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
await expect(
consensus.vote('0'.repeat(64), '0'.repeat(64), makeDid('Voter')),
).rejects.toThrow('Query not found');
});
it('should throw when voting for non-existent response', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
const query = await createQuery('Test', makeDid('Alice'));
await consensus.submitQuery(query);
await expect(
consensus.vote(query.id, '0'.repeat(64), makeDid('Voter')),
).rejects.toThrow('Response not found');
});
it('should reach consensus with threshold', async () => {
const consensus = new AgentConsensusVoting(0.5, 2, 3);
const query = await createQuery('Best language?', makeDid('Alice'));
await consensus.submitQuery(query);
const r1 = await createResponse(query.id, 'Rust', makeDid('Bob'));
const r1Id = await consensus.submitResponse(r1);
const r2 = await createResponse(query.id, 'Python', makeDid('Carol'));
const r2Id = await consensus.submitResponse(r2);
// Vote: 2 for Rust, 1 for Python
await consensus.vote(query.id, r1Id, makeDid('V1'));
await consensus.vote(query.id, r1Id, makeDid('V2'));
await consensus.vote(query.id, r2Id, makeDid('V3'));
expect(await consensus.isFinalized(query.id)).toBe(true);
const result = await consensus.getResult(query.id);
expect(result).not.toBeNull();
expect(result!.response.content).toBe('Rust');
expect(result!.votes).toBe(2);
expect(result!.totalVoters).toBe(3);
});
it('should not reach consensus below threshold', async () => {
const consensus = new AgentConsensusVoting(0.6, 3, 3);
const query = await createQuery('Test', makeDid('Alice'));
await consensus.submitQuery(query);
const r1 = await createResponse(query.id, 'A', makeDid('Bob'));
const r1Id = await consensus.submitResponse(r1);
const r2 = await createResponse(query.id, 'B', makeDid('Carol'));
const r2Id = await consensus.submitResponse(r2);
const r3 = await createResponse(query.id, 'C', makeDid('Dave'));
const r3Id = await consensus.submitResponse(r3);
// Split vote: 1-1-1 (none reaches 60%)
await consensus.vote(query.id, r1Id, makeDid('V1'));
await consensus.vote(query.id, r2Id, makeDid('V2'));
await consensus.vote(query.id, r3Id, makeDid('V3'));
expect(await consensus.isFinalized(query.id)).toBe(false);
});
it('should get all responses for a query', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
const query = await createQuery('Test', makeDid('Alice'));
await consensus.submitQuery(query);
const r1 = await createResponse(query.id, 'A', makeDid('Bob'));
const r2 = await createResponse(query.id, 'B', makeDid('Carol'));
await consensus.submitResponse(r1);
await consensus.submitResponse(r2);
const responses = await consensus.getResponses(query.id);
expect(responses).not.toBeNull();
expect(responses).toHaveLength(2);
});
it('should get vote counts', async () => {
const consensus = new AgentConsensusVoting(0.5, 1, 1);
const query = await createQuery('Test', makeDid('Alice'));
await consensus.submitQuery(query);
const response = await createResponse(query.id, 'Answer', makeDid('Bob'));
const responseId = await consensus.submitResponse(response);
await consensus.vote(query.id, responseId, makeDid('V1'));
const counts = await consensus.getVoteCounts(query.id);
expect(counts).not.toBeNull();
expect(counts!.get(responseId)).toBe(1);
});
});
describe('consensusDecide', () => {
it('should perform one-shot consensus decision', async () => {
// With threshold=0.5, a single vote (100% > 50%) reaches consensus
const result = await consensusDecide(
'What is 2+2?',
makeDid('Alice'),
[
{ content: '4', responder: makeDid('Bob') },
{ content: '5', responder: makeDid('Carol') },
],
[
{ responseIndex: 0, voter: makeDid('V1') }, // 100% for "4", consensus reached
],
0.5,
);
expect(result).not.toBeNull();
expect(result!.response.content).toBe('4');
expect(result!.votes).toBe(1);
});
it('should return null when no consensus reached', async () => {
// With no votes, no consensus can be reached
const result = await consensusDecide(
'Test',
makeDid('Alice'),
[
{ content: 'A', responder: makeDid('Bob') },
{ content: 'B', responder: makeDid('Carol') },
],
[], // No votes = no consensus
0.5,
);
expect(result).toBeNull();
});
it('should throw on invalid response index', async () => {
await expect(
consensusDecide(
'Test',
makeDid('Alice'),
[{ content: 'A', responder: makeDid('Bob') }],
[{ responseIndex: 99, voter: makeDid('V1') }],
),
).rejects.toThrow('Invalid response index');
});
});
+65
View File
@@ -0,0 +1,65 @@
import { describe, it, expect } from 'vitest';
import {
DEFAULT_CONFIG,
loadConfigFromEnv,
mergeConfig,
type Config,
type ServerConfig,
} from '../src/config.js';
describe('DEFAULT_CONFIG', () => {
it('should have default listen and port', () => {
expect(DEFAULT_CONFIG.listen).toBe('0.0.0.0');
expect(DEFAULT_CONFIG.port).toBe(9999);
});
it('should have default log level', () => {
expect(DEFAULT_CONFIG.logLevel).toBe('info');
});
it('should have empty servers array', () => {
expect(DEFAULT_CONFIG.servers).toEqual([]);
});
});
describe('mergeConfig', () => {
it('should merge partial config with defaults', () => {
const partial: Partial<Config> = { listen: '127.0.0.1' };
const merged = mergeConfig(partial);
expect(merged.listen).toBe('127.0.0.1');
expect(merged.port).toBe(DEFAULT_CONFIG.port);
});
it('should override all fields when provided', () => {
const full: Partial<Config> = { listen: 'custom.com', port: 8888 };
const merged = mergeConfig(full);
expect(merged.listen).toBe('custom.com');
expect(merged.port).toBe(8888);
});
it('should return defaults when no config provided', () => {
const merged = mergeConfig({});
expect(merged.listen).toBe(DEFAULT_CONFIG.listen);
expect(merged.port).toBe(DEFAULT_CONFIG.port);
});
it('should merge multiple configs in order', () => {
const config1: Partial<Config> = { listen: 'first.com' };
const config2: Partial<Config> = { listen: 'second.com', port: 8000 };
const merged = mergeConfig(config1, config2);
expect(merged.listen).toBe('second.com');
expect(merged.port).toBe(8000);
});
});
describe('loadConfigFromEnv', () => {
it('should return partial config object', () => {
const config = loadConfigFromEnv();
// Should return an object (might be empty if no env vars set)
expect(typeof config).toBe('object');
});
});
+164
View File
@@ -0,0 +1,164 @@
import { describe, it, expect } from 'vitest';
import {
ZapError,
ConnectionError,
TransportError,
ProtocolError,
TimeoutError,
ServerError,
ToolNotFoundError,
ResourceNotFoundError,
InvalidArgumentError,
} from '../src/error.js';
describe('ZapError', () => {
it('should create a ZapError', () => {
const error = new ZapError('Something went wrong');
expect(error.message).toBe('Something went wrong');
expect(error.name).toBe('ZapError');
});
it('should be instanceof Error', () => {
const error = new ZapError('test');
expect(error).toBeInstanceOf(Error);
});
});
describe('ConnectionError', () => {
it('should create a ConnectionError', () => {
const error = new ConnectionError('Failed to connect');
expect(error.message).toBe('Failed to connect');
expect(error.name).toBe('ConnectionError');
});
it('should be instanceof ZapError', () => {
const error = new ConnectionError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('TransportError', () => {
it('should create a TransportError', () => {
const error = new TransportError('Transport failed');
expect(error.message).toBe('Transport failed');
expect(error.name).toBe('TransportError');
});
it('should be instanceof ZapError', () => {
const error = new TransportError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('ProtocolError', () => {
it('should create a ProtocolError', () => {
const error = new ProtocolError('Invalid protocol');
expect(error.message).toBe('Invalid protocol');
expect(error.name).toBe('ProtocolError');
});
it('should be instanceof ZapError', () => {
const error = new ProtocolError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('TimeoutError', () => {
it('should create a TimeoutError', () => {
const error = new TimeoutError('Request timed out');
expect(error.message).toBe('Request timed out');
expect(error.name).toBe('TimeoutError');
});
it('should be instanceof ZapError', () => {
const error = new TimeoutError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('ServerError', () => {
it('should create a ServerError', () => {
const error = new ServerError('Internal server error');
expect(error.message).toBe('Internal server error');
expect(error.name).toBe('ServerError');
});
it('should be instanceof ZapError', () => {
const error = new ServerError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('ToolNotFoundError', () => {
it('should create a ToolNotFoundError', () => {
const error = new ToolNotFoundError('search');
// Message includes tool name
expect(error.message).toContain('search');
expect(error.name).toBe('ToolNotFoundError');
});
it('should be instanceof ZapError', () => {
const error = new ToolNotFoundError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('ResourceNotFoundError', () => {
it('should create a ResourceNotFoundError', () => {
const error = new ResourceNotFoundError('config.json');
// Message includes resource name
expect(error.message).toContain('config.json');
expect(error.name).toBe('ResourceNotFoundError');
});
it('should be instanceof ZapError', () => {
const error = new ResourceNotFoundError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('InvalidArgumentError', () => {
it('should create an InvalidArgumentError', () => {
const error = new InvalidArgumentError('count');
// Message includes argument name
expect(error.message).toContain('count');
expect(error.name).toBe('InvalidArgumentError');
});
it('should be instanceof ZapError', () => {
const error = new InvalidArgumentError('test');
expect(error).toBeInstanceOf(ZapError);
});
});
describe('Error inheritance chain', () => {
it('should catch specific error types', () => {
try {
throw new ConnectionError('test');
} catch (e) {
if (e instanceof ConnectionError) {
expect(true).toBe(true);
} else {
expect.fail('Should catch ConnectionError');
}
}
});
it('should catch ZapError for all error types', () => {
const errors = [
new ConnectionError('test'),
new TransportError('test'),
new ProtocolError('test'),
new TimeoutError('test'),
new ServerError('test'),
new ToolNotFoundError('test'),
new ResourceNotFoundError('test'),
new InvalidArgumentError('test'),
];
for (const error of errors) {
expect(error).toBeInstanceOf(ZapError);
expect(error).toBeInstanceOf(Error);
}
});
});
+118
View File
@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest';
import {
Did,
DidDocument,
DidMethod,
VerificationMethod,
VerificationMethodType,
didUri,
parseDid,
createDidFromKey,
createDidFromWeb,
generateDocument,
MLDSA_PUBLIC_KEY_SIZE,
} from '../src/identity.js';
describe('Did', () => {
it('should create a did:lux DID', () => {
const did: Did = { method: DidMethod.Lux, id: 'z6MkTest123' };
expect(did.method).toBe(DidMethod.Lux);
expect(did.id).toBe('z6MkTest123');
});
it('should create a did:key DID', () => {
const did: Did = { method: DidMethod.Key, id: 'z6MkTestKey456' };
expect(did.method).toBe(DidMethod.Key);
expect(did.id).toBe('z6MkTestKey456');
});
it('should create a did:web DID', () => {
const did: Did = { method: DidMethod.Web, id: 'example.com:user:alice' };
expect(did.method).toBe(DidMethod.Web);
expect(did.id).toBe('example.com:user:alice');
});
});
describe('didUri', () => {
it('should format DID URI', () => {
const did: Did = { method: DidMethod.Lux, id: 'z6MkTest123' };
expect(didUri(did)).toBe('did:lux:z6MkTest123');
});
});
describe('parseDid', () => {
it('should parse did:lux DID', () => {
const did = parseDid('did:lux:z6MkTest123');
expect(did.method).toBe(DidMethod.Lux);
expect(did.id).toBe('z6MkTest123');
});
it('should parse did:key DID', () => {
const did = parseDid('did:key:z6MkTestKey456');
expect(did.method).toBe(DidMethod.Key);
expect(did.id).toBe('z6MkTestKey456');
});
it('should parse did:web DID', () => {
const did = parseDid('did:web:example.com:user:alice');
expect(did.method).toBe(DidMethod.Web);
expect(did.id).toBe('example.com:user:alice');
});
it('should throw on invalid DID', () => {
expect(() => parseDid('invalid')).toThrow();
});
});
describe('generateDocument', () => {
it('should create a DID document', () => {
const did: Did = { method: DidMethod.Lux, id: 'z6MkTest123' };
const doc = generateDocument(did);
expect(doc.id).toBe('did:lux:z6MkTest123');
});
it('should include context', () => {
const did: Did = { method: DidMethod.Lux, id: 'z6MkTest123' };
const doc = generateDocument(did);
// Context can be string or array
const context = doc['@context'];
if (Array.isArray(context)) {
expect(context).toContain('https://www.w3.org/ns/did/v1');
} else {
expect(context).toBe('https://www.w3.org/ns/did/v1');
}
});
});
describe('createDidFromKey', () => {
it('should create did:key from ML-DSA public key', () => {
// 1952-byte fake public key
const publicKey = new Uint8Array(MLDSA_PUBLIC_KEY_SIZE);
for (let i = 0; i < publicKey.length; i++) {
publicKey[i] = i % 256;
}
const did = createDidFromKey(publicKey);
expect(did.method).toBe(DidMethod.Key);
expect(did.id.startsWith('z')).toBe(true);
});
it('should throw on invalid key length', () => {
const shortKey = new Uint8Array(16);
expect(() => createDidFromKey(shortKey)).toThrow();
});
});
describe('createDidFromWeb', () => {
it('should create did:web from domain', () => {
const did = createDidFromWeb('example.com');
expect(did.method).toBe(DidMethod.Web);
expect(did.id).toBe('example.com');
});
it('should create did:web with path', () => {
const did = createDidFromWeb('example.com', 'users:alice');
expect(did.method).toBe(DidMethod.Web);
expect(did.id).toContain('example.com');
});
});
+37
View File
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+26
View File
@@ -0,0 +1,26 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
reportsDirectory: './coverage',
exclude: [
'node_modules/**',
'dist/**',
'**/*.test.ts',
'vitest.config.ts',
],
thresholds: {
lines: 80,
functions: 80,
branches: 70,
statements: 80,
},
},
},
});