chore: consolidate local working changes
This commit is contained in:
Vendored
+153
@@ -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)"
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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',
|
||||
]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
# ZAP Python Tests
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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__')
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Generated
+610
@@ -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" },
|
||||
]
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(×tamp.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(×tamp.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));
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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>),
|
||||
}
|
||||
@@ -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
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -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()
|
||||
))),
|
||||
}
|
||||
}
|
||||
Generated
+2520
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+1935
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user