Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile.agent
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*

COPY pyproject.toml README.md uv.lock* ./
COPY .git ./.git

# Sync dependencies using uv
RUN uv sync --frozen --no-install-project || uv sync --no-install-project
Expand Down
21 changes: 18 additions & 3 deletions docker-compose.agent.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
services:
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --appendfsync everysec
volumes:
- redis-data:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 5s
timeout: 3s
retries: 5

omniclaw-agent:
build:
context: .
dockerfile: Dockerfile.agent
command: uv run omniclaw server --host 0.0.0.0 --port 8080
env_file: .env
environment:
- OMNICLAW_REDIS_URL=redis://host.docker.internal:6379/0
- OMNICLAW_REDIS_URL=redis://redis:6379/0
- OMNICLAW_AGENT_POLICY_PATH=/config/policy.json
- OMNICLAW_AGENT_TOKEN=payment-agent-token
- OMNICLAW_LOG_LEVEL=INFO
volumes:
- ./examples/agent/policy.json:/config/policy.json:ro
ports:
- "8080:8080"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
redis:
condition: service_healthy

volumes:
redis-data:
10 changes: 5 additions & 5 deletions examples/agent/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"version": "1.0",
"tokens": {
"payment-agent-token": {
"wallet_alias": "payment-agent",
"wallet_alias": "omni-bot-v4",
"active": true,
"label": "Main Payment Agent"
"label": "Main Omni Bot"
},
"api-agent-token": {
"wallet_alias": "api-agent",
Expand All @@ -18,9 +18,9 @@
}
},
"wallets": {
"payment-agent": {
"name": "Main Payment Agent",
"description": "Primary agent for general payments",
"omni-bot-v4": {
"name": "Omni Bot V1",
"description": "Upgraded autonomous bot",
"limits": {
"daily_max": "100.00",
"hourly_max": "50.00",
Expand Down
9 changes: 8 additions & 1 deletion src/omniclaw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
... )
"""

import warnings

# Suppress noisy deprecation warnings from downstream dependencies (e.g. web3, circle-sdk)
# We do this at the very top of the package to ensure it catches warnings during imports.
warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
warnings.filterwarnings("ignore", category=DeprecationWarning, module="pkg_resources")

from omniclaw.client import OmniClaw
from omniclaw.core.config import Config
from omniclaw.core.exceptions import (
Expand Down Expand Up @@ -140,7 +147,7 @@
)
from omniclaw.trust.gate import TrustGate

__version__ = "0.0.1"
__version__ = "0.1.0"
__all__ = [
# Main Client
"OmniClaw",
Expand Down
9 changes: 3 additions & 6 deletions src/omniclaw/agent/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,12 @@ async def authenticate(
self,
credentials: HTTPAuthorizationCredentials,
) -> AuthenticatedAgent:
"""Authenticate request using token."""
"""Authenticate request using token against the policy mapping."""
token = credentials.credentials

if not self._agent_token or token != self._agent_token:
raise HTTPException(status_code=401, detail="Invalid token")

wallet_id = self._policy.get_wallet_id()
wallet_id = self._policy.get_wallet_id_for_token(token)
if not wallet_id:
raise HTTPException(status_code=400, detail="Wallet not initialized")
raise HTTPException(status_code=401, detail="Invalid or unauthorized token")

return AuthenticatedAgent(
token=token,
Expand Down
19 changes: 19 additions & 0 deletions src/omniclaw/agent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,22 @@ class HealthResponse(BaseModel):

status: str
version: str = "1.0.0"


class X402PayRequest(BaseModel):
"""X402 Payment request."""

url: str = Field(..., description="x402 Service URL")
method: str = Field("GET", description="HTTP method")
body: str | None = Field(None, description="Request body")
headers: dict[str, str] | None = Field(None, description="Request headers")
idempotency_key: str | None = Field(None, description="Idempotency key")


class X402VerifyRequest(BaseModel):
"""X402 Verification request."""

signature: str = Field(..., description="Payment signature/proof")
amount: str = Field(..., description="Amount paid")
sender: str = Field(..., description="Sender address")
resource: str = Field(..., description="Resource URL")
175 changes: 96 additions & 79 deletions src/omniclaw/agent/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
import json
import os
from dataclasses import dataclass, field
Expand Down Expand Up @@ -70,34 +71,31 @@ def from_dict(cls, data: dict | None) -> RecipientConfig:

@dataclass
class Policy:
"""Main policy configuration for the single agent wallet."""
version: str = "2.0"
limits: WalletLimits = field(default_factory=WalletLimits)
rate_limits: RateLimits = field(default_factory=RateLimits)
recipients: RecipientConfig = field(default_factory=RecipientConfig)
confirm_threshold: Decimal | None = None
"""Main policy configuration for the agent economy."""
version: str = "0.0.2"
tokens: dict[str, dict[str, Any]] = field(default_factory=dict)
wallets: dict[str, dict[str, Any]] = field(default_factory=dict)

@classmethod
def from_dict(cls, data: dict | None) -> Policy:
if not data:
return cls()
return cls(
version=data.get("version", "2.0"),
limits=WalletLimits.from_dict(data.get("limits")),
rate_limits=RateLimits.from_dict(data.get("rate_limits")),
recipients=RecipientConfig.from_dict(data.get("recipients")),
confirm_threshold=Decimal(data.get("confirm_threshold", "0")) if data.get("confirm_threshold") else None,
tokens=data.get("tokens", {}),
wallets=data.get("wallets", {}),
)


class PolicyManager:
"""Manages policy loading, validation, and wallet operations."""
"""Manages policy loading, validation, and multi-agent token mapping."""
def __init__(self, policy_path: str | None = None):
self._policy_path = policy_path or os.environ.get(
"OMNICLAW_AGENT_POLICY_PATH", "/config/policy.json"
)
self._policy: Policy | None = None
self._wallet_id: str | None = None
self._token_to_wallet_id: dict[str, str] = {}
self._wallet_id_to_config: dict[str, dict[str, Any]] = {}
self._logger = logger

async def load(self) -> Policy:
Expand All @@ -112,104 +110,123 @@ async def load(self) -> Policy:
data = json.load(f)

self._policy = Policy.from_dict(data)
self._logger.info("Loaded agent policy configuration.")
self._logger.info("Loaded agent economy policy configuration.")
return self._policy

def get_policy(self) -> Policy:
"""Get current policy."""
if not self._policy:
raise RuntimeError("Policy not loaded")
return self._policy
def get_token_map(self) -> dict[str, dict[str, Any]]:
return self._policy.tokens if self._policy else {}

def get_wallet_id(self) -> str | None:
return self._wallet_id
def get_wallet_map(self) -> dict[str, dict[str, Any]]:
return self._policy.wallets if self._policy else {}

def set_wallet_id(self, wallet_id: str) -> None:
"""Set wallet ID after creation."""
self._wallet_id = wallet_id
self._logger.info(f"Set primary agent wallet ID to '{wallet_id}'")
def set_mapping(self, token: str, wallet_id: str, config: dict[str, Any]) -> None:
self._token_to_wallet_id[token] = wallet_id
self._wallet_id_to_config[wallet_id] = config

def is_valid_recipient(self, recipient: str) -> bool:
"""Check if recipient is allowed for wallet."""
if not self._policy:
return True # No policy means allow all
def get_wallet_id_for_token(self, token: str) -> str | None:
return self._token_to_wallet_id.get(token)

recipients = self._policy.recipients
if not recipients.addresses and not recipients.domains:
return True # No restrictions
def is_valid_recipient(self, recipient: str, wallet_id: str) -> bool:
"""Check if recipient is allowed for a specific wallet."""
config = self._wallet_id_to_config.get(wallet_id, {})
recipient_cfg = RecipientConfig.from_dict(config.get("recipients"))

if not recipient_cfg.addresses and not recipient_cfg.domains:
return True

if recipient in recipients.addresses:
return recipients.mode == "whitelist"
if recipient in recipient_cfg.addresses:
return recipient_cfg.mode == "whitelist"

if recipient.startswith("http"):
for domain in recipients.domains:
for domain in recipient_cfg.domains:
if domain in recipient:
return recipients.mode == "whitelist"

return recipients.mode != "whitelist"
return recipient_cfg.mode == "whitelist"

def check_limits(self, amount: Decimal) -> tuple[bool, str | None]:
"""Check if amount is within limits."""
if not self._policy:
return True, None
return recipient_cfg.mode != "whitelist"

limits = self._policy.limits
def check_limits(self, amount: Decimal, wallet_id: str) -> tuple[bool, str | None]:
config = self._wallet_id_to_config.get(wallet_id, {})
limits = WalletLimits.from_dict(config.get("limits"))

if limits.per_tx_max and amount > limits.per_tx_max:
return False, f"Amount {amount} exceeds per_tx_max {limits.per_tx_max}"

if limits.per_tx_min and amount < limits.per_tx_min:
return False, f"Amount {amount} below per_tx_min {limits.per_tx_min}"

return True, None

def requires_confirmation(self, amount: Decimal) -> bool:
"""Check if payment requires confirmation."""
if not self._policy:
return False
threshold = self._policy.confirm_threshold
if not threshold:
return False
return amount >= threshold
def requires_confirmation(self, amount: Decimal, wallet_id: str) -> bool:
config = self._wallet_id_to_config.get(wallet_id, {})
threshold = Decimal(config.get("confirm_threshold", "0"))
return threshold > 0 and amount >= threshold


class WalletManager:
"""Manages wallet creation based on policy."""
"""Manages secure wallet mapping from policy tokens."""
def __init__(self, policy_manager: PolicyManager, omniclaw_client: Any):
self._policy = policy_manager
self._client = omniclaw_client
self._logger = logger

async def initialize_wallets(self) -> dict[str, str]:
"""Ensure the single agent wallet exists."""
try:
wallet_id = os.environ.get("OMNICLAW_AGENT_WALLET_ID")
if wallet_id:
wallet = await self._client.get_wallet(wallet_id)
else:
wallet_set, wallet = await self._client.create_agent_wallet(
agent_name="omniclaw-primary-agent",
"""Initialize all wallets defined in the policy mapping (Parallel)."""
token_map = self._policy.get_token_map()
wallet_map = self._policy.get_wallet_map()
results = {}

# PHASE 1: Pre-populate token map with alias so Auth works immediately (stateless)
for token, config in token_map.items():
alias = config.get("wallet_alias", "primary")
# We don't have the real wallet_id yet, but we map it to a placeholder
# so the Agent isn't rejected with "Unauthorized"
self._policy.set_mapping(token, f"pending-{alias}", wallet_map.get(alias, {}))

# PHASE 2: Perform the intensive SDK/Network calls in PARALLEL
async def init_one(token: str, config: dict[str, Any]):
alias = config.get("wallet_alias", "primary")
wallet_cfg = wallet_map.get(alias, {})
try:
# 10/10 RESILIENCE: Explicitly handle async/sync transitions to prevent unpacking errors
coro = self._client.create_agent_wallet(
agent_name=f"omniclaw-{alias}",
apply_default_guards=False,
)

self._policy.set_wallet_id(wallet.id)
self._logger.info(f"Wallet successfully initialized: {wallet.id}")
return {"status": "success", "wallet_id": wallet.id}
except Exception as e:
self._logger.error(f"Failed to initialize agent wallet: {e}")
return {"status": "error", "message": str(e)}

async def get_wallet_address(self) -> str | None:

# Check for double-wrapping or mismatched SDK versions
if asyncio.iscoroutine(coro):
res = await coro
else:
res = coro # Should not happen if SDK is correctly async

wallet_set, wallet = res
# Success! Overwrite the placeholder with the real wallet ID
self._policy.set_mapping(token, wallet.id, wallet_cfg)
self._logger.info(f"Successfully initialized wallet '{wallet.id}' for agent '{alias}'")
return token, wallet.id
except Exception as e:
self._logger.error(f"Failed to initialize wallet for '{alias}': {e}")
return token, None

# Gather all parallel tasks
tasks = [init_one(token, config) for token, config in token_map.items()]
batch_results = await asyncio.gather(*tasks)

# Collect results
for token, wallet_id in batch_results:
if wallet_id:
results[token] = wallet_id

return results

async def get_wallet_address(self, wallet_id: str) -> str | None:
"""Get wallet address."""
wallet_id = self._policy.get_wallet_id()
if not wallet_id:
try:
wallet = await self._client.get_wallet(wallet_id)
return wallet.address if wallet else None
except Exception:
return None
wallet = await self._client.get_wallet(wallet_id)
return wallet.address if wallet else None

async def get_wallet_balance(self) -> Decimal | None:
async def get_wallet_balance(self, wallet_id: str) -> Decimal | None:
"""Get wallet balance."""
wallet_id = self._policy.get_wallet_id()
if not wallet_id:
try:
return await self._client.get_balance(wallet_id)
except Exception:
return None
return await self._client.get_balance(wallet_id)
Loading
Loading