chore(security): enhance environment configuration, CI workflows, and wallet daemon with security improvements
- Restructure .env.example with security-focused documentation, service-specific environment file references, and AWS Secrets Manager integration - Update CLI tests workflow to single Python 3.13 version, add pytest-mock dependency, and consolidate test execution with coverage - Add comprehensive security validation to package publishing workflow with manual approval gates, secret scanning, and release
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
"""
|
||||
AITBC Guardian Contract - Spending Limit Protection for Agent Wallets
|
||||
|
||||
This contract implements a spending limit guardian that protects autonomous agent
|
||||
wallets from unlimited spending in case of compromise. It provides:
|
||||
- Per-transaction spending limits
|
||||
- Per-period (daily/hourly) spending caps
|
||||
- Time-lock for large withdrawals
|
||||
- Emergency pause functionality
|
||||
- Multi-signature recovery for critical operations
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from eth_account import Account
|
||||
from eth_utils import to_checksum_address, keccak
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpendingLimit:
|
||||
"""Spending limit configuration"""
|
||||
per_transaction: int # Maximum per transaction
|
||||
per_hour: int # Maximum per hour
|
||||
per_day: int # Maximum per day
|
||||
per_week: int # Maximum per week
|
||||
|
||||
@dataclass
|
||||
class TimeLockConfig:
|
||||
"""Time lock configuration for large withdrawals"""
|
||||
threshold: int # Amount that triggers time lock
|
||||
delay_hours: int # Delay period in hours
|
||||
max_delay_hours: int # Maximum delay period
|
||||
|
||||
|
||||
@dataclass
|
||||
class GuardianConfig:
|
||||
"""Complete guardian configuration"""
|
||||
limits: SpendingLimit
|
||||
time_lock: TimeLockConfig
|
||||
guardians: List[str] # Guardian addresses for recovery
|
||||
pause_enabled: bool = True
|
||||
emergency_mode: bool = False
|
||||
|
||||
|
||||
class GuardianContract:
|
||||
"""
|
||||
Guardian contract implementation for agent wallet protection
|
||||
"""
|
||||
|
||||
def __init__(self, agent_address: str, config: GuardianConfig):
|
||||
self.agent_address = to_checksum_address(agent_address)
|
||||
self.config = config
|
||||
self.spending_history: List[Dict] = []
|
||||
self.pending_operations: Dict[str, Dict] = {}
|
||||
self.paused = False
|
||||
self.emergency_mode = False
|
||||
|
||||
# Contract state
|
||||
self.nonce = 0
|
||||
self.guardian_approvals: Dict[str, bool] = {}
|
||||
|
||||
def _get_period_key(self, timestamp: datetime, period: str) -> str:
|
||||
"""Generate period key for spending tracking"""
|
||||
if period == "hour":
|
||||
return timestamp.strftime("%Y-%m-%d-%H")
|
||||
elif period == "day":
|
||||
return timestamp.strftime("%Y-%m-%d")
|
||||
elif period == "week":
|
||||
# Get week number (Monday as first day)
|
||||
week_num = timestamp.isocalendar()[1]
|
||||
return f"{timestamp.year}-W{week_num:02d}"
|
||||
else:
|
||||
raise ValueError(f"Invalid period: {period}")
|
||||
|
||||
def _get_spent_in_period(self, period: str, timestamp: datetime = None) -> int:
|
||||
"""Calculate total spent in given period"""
|
||||
if timestamp is None:
|
||||
timestamp = datetime.utcnow()
|
||||
|
||||
period_key = self._get_period_key(timestamp, period)
|
||||
|
||||
total = 0
|
||||
for record in self.spending_history:
|
||||
record_time = datetime.fromisoformat(record["timestamp"])
|
||||
record_period = self._get_period_key(record_time, period)
|
||||
|
||||
if record_period == period_key and record["status"] == "completed":
|
||||
total += record["amount"]
|
||||
|
||||
return total
|
||||
|
||||
def _check_spending_limits(self, amount: int, timestamp: datetime = None) -> Tuple[bool, str]:
|
||||
"""Check if amount exceeds spending limits"""
|
||||
if timestamp is None:
|
||||
timestamp = datetime.utcnow()
|
||||
|
||||
# Check per-transaction limit
|
||||
if amount > self.config.limits.per_transaction:
|
||||
return False, f"Amount {amount} exceeds per-transaction limit {self.config.limits.per_transaction}"
|
||||
|
||||
# Check per-hour limit
|
||||
spent_hour = self._get_spent_in_period("hour", timestamp)
|
||||
if spent_hour + amount > self.config.limits.per_hour:
|
||||
return False, f"Hourly spending {spent_hour + amount} would exceed limit {self.config.limits.per_hour}"
|
||||
|
||||
# Check per-day limit
|
||||
spent_day = self._get_spent_in_period("day", timestamp)
|
||||
if spent_day + amount > self.config.limits.per_day:
|
||||
return False, f"Daily spending {spent_day + amount} would exceed limit {self.config.limits.per_day}"
|
||||
|
||||
# Check per-week limit
|
||||
spent_week = self._get_spent_in_period("week", timestamp)
|
||||
if spent_week + amount > self.config.limits.per_week:
|
||||
return False, f"Weekly spending {spent_week + amount} would exceed limit {self.config.limits.per_week}"
|
||||
|
||||
return True, "Spending limits check passed"
|
||||
|
||||
def _requires_time_lock(self, amount: int) -> bool:
|
||||
"""Check if amount requires time lock"""
|
||||
return amount >= self.config.time_lock.threshold
|
||||
|
||||
def _create_operation_hash(self, operation: Dict) -> str:
|
||||
"""Create hash for operation identification"""
|
||||
operation_str = json.dumps(operation, sort_keys=True)
|
||||
return keccak(operation_str.encode()).hex()
|
||||
|
||||
def initiate_transaction(self, to_address: str, amount: int, data: str = "") -> Dict:
|
||||
"""
|
||||
Initiate a transaction with guardian protection
|
||||
|
||||
Args:
|
||||
to_address: Recipient address
|
||||
amount: Amount to transfer
|
||||
data: Transaction data (optional)
|
||||
|
||||
Returns:
|
||||
Operation result with status and details
|
||||
"""
|
||||
# Check if paused
|
||||
if self.paused:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"reason": "Guardian contract is paused",
|
||||
"operation_id": None
|
||||
}
|
||||
|
||||
# Check emergency mode
|
||||
if self.emergency_mode:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"reason": "Emergency mode activated",
|
||||
"operation_id": None
|
||||
}
|
||||
|
||||
# Validate address
|
||||
try:
|
||||
to_address = to_checksum_address(to_address)
|
||||
except:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"reason": "Invalid recipient address",
|
||||
"operation_id": None
|
||||
}
|
||||
|
||||
# Check spending limits
|
||||
limits_ok, limits_reason = self._check_spending_limits(amount)
|
||||
if not limits_ok:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"reason": limits_reason,
|
||||
"operation_id": None
|
||||
}
|
||||
|
||||
# Create operation
|
||||
operation = {
|
||||
"type": "transaction",
|
||||
"to": to_address,
|
||||
"amount": amount,
|
||||
"data": data,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"nonce": self.nonce,
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
operation_id = self._create_operation_hash(operation)
|
||||
operation["operation_id"] = operation_id
|
||||
|
||||
# Check if time lock is required
|
||||
if self._requires_time_lock(amount):
|
||||
unlock_time = datetime.utcnow() + timedelta(hours=self.config.time_lock.delay_hours)
|
||||
operation["unlock_time"] = unlock_time.isoformat()
|
||||
operation["status"] = "time_locked"
|
||||
|
||||
# Store for later execution
|
||||
self.pending_operations[operation_id] = operation
|
||||
|
||||
return {
|
||||
"status": "time_locked",
|
||||
"operation_id": operation_id,
|
||||
"unlock_time": unlock_time.isoformat(),
|
||||
"delay_hours": self.config.time_lock.delay_hours,
|
||||
"message": f"Transaction requires {self.config.time_lock.delay_hours}h time lock"
|
||||
}
|
||||
|
||||
# Immediate execution for smaller amounts
|
||||
self.pending_operations[operation_id] = operation
|
||||
|
||||
return {
|
||||
"status": "approved",
|
||||
"operation_id": operation_id,
|
||||
"message": "Transaction approved for execution"
|
||||
}
|
||||
|
||||
def execute_transaction(self, operation_id: str, signature: str) -> Dict:
|
||||
"""
|
||||
Execute a previously approved transaction
|
||||
|
||||
Args:
|
||||
operation_id: Operation ID from initiate_transaction
|
||||
signature: Transaction signature from agent
|
||||
|
||||
Returns:
|
||||
Execution result
|
||||
"""
|
||||
if operation_id not in self.pending_operations:
|
||||
return {
|
||||
"status": "error",
|
||||
"reason": "Operation not found"
|
||||
}
|
||||
|
||||
operation = self.pending_operations[operation_id]
|
||||
|
||||
# Check if operation is time locked
|
||||
if operation["status"] == "time_locked":
|
||||
unlock_time = datetime.fromisoformat(operation["unlock_time"])
|
||||
if datetime.utcnow() < unlock_time:
|
||||
return {
|
||||
"status": "error",
|
||||
"reason": f"Operation locked until {unlock_time.isoformat()}"
|
||||
}
|
||||
|
||||
operation["status"] = "ready"
|
||||
|
||||
# Verify signature (simplified - in production, use proper verification)
|
||||
try:
|
||||
# In production, verify the signature matches the agent address
|
||||
# For now, we'll assume signature is valid
|
||||
pass
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"reason": f"Invalid signature: {str(e)}"
|
||||
}
|
||||
|
||||
# Record the transaction
|
||||
record = {
|
||||
"operation_id": operation_id,
|
||||
"to": operation["to"],
|
||||
"amount": operation["amount"],
|
||||
"data": operation.get("data", ""),
|
||||
"timestamp": operation["timestamp"],
|
||||
"executed_at": datetime.utcnow().isoformat(),
|
||||
"status": "completed",
|
||||
"nonce": operation["nonce"]
|
||||
}
|
||||
|
||||
self.spending_history.append(record)
|
||||
self.nonce += 1
|
||||
|
||||
# Remove from pending
|
||||
del self.pending_operations[operation_id]
|
||||
|
||||
return {
|
||||
"status": "executed",
|
||||
"operation_id": operation_id,
|
||||
"transaction_hash": f"0x{keccak(f'{operation_id}{signature}'.encode()).hex()}",
|
||||
"executed_at": record["executed_at"]
|
||||
}
|
||||
|
||||
def emergency_pause(self, guardian_address: str) -> Dict:
|
||||
"""
|
||||
Emergency pause function (guardian only)
|
||||
|
||||
Args:
|
||||
guardian_address: Address of guardian initiating pause
|
||||
|
||||
Returns:
|
||||
Pause result
|
||||
"""
|
||||
if guardian_address not in self.config.guardians:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"reason": "Not authorized: guardian address not recognized"
|
||||
}
|
||||
|
||||
self.paused = True
|
||||
self.emergency_mode = True
|
||||
|
||||
return {
|
||||
"status": "paused",
|
||||
"paused_at": datetime.utcnow().isoformat(),
|
||||
"guardian": guardian_address,
|
||||
"message": "Emergency pause activated - all operations halted"
|
||||
}
|
||||
|
||||
def emergency_unpause(self, guardian_signatures: List[str]) -> Dict:
|
||||
"""
|
||||
Emergency unpause function (requires multiple guardian signatures)
|
||||
|
||||
Args:
|
||||
guardian_signatures: Signatures from required guardians
|
||||
|
||||
Returns:
|
||||
Unpause result
|
||||
"""
|
||||
# In production, verify all guardian signatures
|
||||
required_signatures = len(self.config.guardians)
|
||||
if len(guardian_signatures) < required_signatures:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"reason": f"Requires {required_signatures} guardian signatures, got {len(guardian_signatures)}"
|
||||
}
|
||||
|
||||
# Verify signatures (simplified)
|
||||
# In production, verify each signature matches a guardian address
|
||||
|
||||
self.paused = False
|
||||
self.emergency_mode = False
|
||||
|
||||
return {
|
||||
"status": "unpaused",
|
||||
"unpaused_at": datetime.utcnow().isoformat(),
|
||||
"message": "Emergency pause lifted - operations resumed"
|
||||
}
|
||||
|
||||
def update_limits(self, new_limits: SpendingLimit, guardian_address: str) -> Dict:
|
||||
"""
|
||||
Update spending limits (guardian only)
|
||||
|
||||
Args:
|
||||
new_limits: New spending limits
|
||||
guardian_address: Address of guardian making the change
|
||||
|
||||
Returns:
|
||||
Update result
|
||||
"""
|
||||
if guardian_address not in self.config.guardians:
|
||||
return {
|
||||
"status": "rejected",
|
||||
"reason": "Not authorized: guardian address not recognized"
|
||||
}
|
||||
|
||||
old_limits = self.config.limits
|
||||
self.config.limits = new_limits
|
||||
|
||||
return {
|
||||
"status": "updated",
|
||||
"old_limits": old_limits,
|
||||
"new_limits": new_limits,
|
||||
"updated_at": datetime.utcnow().isoformat(),
|
||||
"guardian": guardian_address
|
||||
}
|
||||
|
||||
def get_spending_status(self) -> Dict:
|
||||
"""Get current spending status and limits"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
return {
|
||||
"agent_address": self.agent_address,
|
||||
"current_limits": self.config.limits,
|
||||
"spent": {
|
||||
"current_hour": self._get_spent_in_period("hour", now),
|
||||
"current_day": self._get_spent_in_period("day", now),
|
||||
"current_week": self._get_spent_in_period("week", now)
|
||||
},
|
||||
"remaining": {
|
||||
"current_hour": self.config.limits.per_hour - self._get_spent_in_period("hour", now),
|
||||
"current_day": self.config.limits.per_day - self._get_spent_in_period("day", now),
|
||||
"current_week": self.config.limits.per_week - self._get_spent_in_period("week", now)
|
||||
},
|
||||
"pending_operations": len(self.pending_operations),
|
||||
"paused": self.paused,
|
||||
"emergency_mode": self.emergency_mode,
|
||||
"nonce": self.nonce
|
||||
}
|
||||
|
||||
def get_operation_history(self, limit: int = 50) -> List[Dict]:
|
||||
"""Get operation history"""
|
||||
return sorted(self.spending_history, key=lambda x: x["timestamp"], reverse=True)[:limit]
|
||||
|
||||
def get_pending_operations(self) -> List[Dict]:
|
||||
"""Get all pending operations"""
|
||||
return list(self.pending_operations.values())
|
||||
|
||||
|
||||
# Factory function for creating guardian contracts
|
||||
def create_guardian_contract(
|
||||
agent_address: str,
|
||||
per_transaction: int = 1000,
|
||||
per_hour: int = 5000,
|
||||
per_day: int = 20000,
|
||||
per_week: int = 100000,
|
||||
time_lock_threshold: int = 10000,
|
||||
time_lock_delay: int = 24,
|
||||
guardians: List[str] = None
|
||||
) -> GuardianContract:
|
||||
"""
|
||||
Create a guardian contract with default security parameters
|
||||
|
||||
Args:
|
||||
agent_address: The agent wallet address to protect
|
||||
per_transaction: Maximum amount per transaction
|
||||
per_hour: Maximum amount per hour
|
||||
per_day: Maximum amount per day
|
||||
per_week: Maximum amount per week
|
||||
time_lock_threshold: Amount that triggers time lock
|
||||
time_lock_delay: Time lock delay in hours
|
||||
guardians: List of guardian addresses
|
||||
|
||||
Returns:
|
||||
Configured GuardianContract instance
|
||||
"""
|
||||
if guardians is None:
|
||||
# Default to using the agent address as guardian (should be overridden)
|
||||
guardians = [agent_address]
|
||||
|
||||
limits = SpendingLimit(
|
||||
per_transaction=per_transaction,
|
||||
per_hour=per_hour,
|
||||
per_day=per_day,
|
||||
per_week=per_week
|
||||
)
|
||||
|
||||
time_lock = TimeLockConfig(
|
||||
threshold=time_lock_threshold,
|
||||
delay_hours=time_lock_delay,
|
||||
max_delay_hours=168 # 1 week max
|
||||
)
|
||||
|
||||
config = GuardianConfig(
|
||||
limits=limits,
|
||||
time_lock=time_lock,
|
||||
guardians=[to_checksum_address(g) for g in guardians]
|
||||
)
|
||||
|
||||
return GuardianContract(agent_address, config)
|
||||
|
||||
|
||||
# Example usage and security configurations
|
||||
CONSERVATIVE_CONFIG = {
|
||||
"per_transaction": 100, # $100 per transaction
|
||||
"per_hour": 500, # $500 per hour
|
||||
"per_day": 2000, # $2,000 per day
|
||||
"per_week": 10000, # $10,000 per week
|
||||
"time_lock_threshold": 1000, # Time lock over $1,000
|
||||
"time_lock_delay": 24 # 24 hour delay
|
||||
}
|
||||
|
||||
AGGRESSIVE_CONFIG = {
|
||||
"per_transaction": 1000, # $1,000 per transaction
|
||||
"per_hour": 5000, # $5,000 per hour
|
||||
"per_day": 20000, # $20,000 per day
|
||||
"per_week": 100000, # $100,000 per week
|
||||
"time_lock_threshold": 10000, # Time lock over $10,000
|
||||
"time_lock_delay": 12 # 12 hour delay
|
||||
}
|
||||
|
||||
HIGH_SECURITY_CONFIG = {
|
||||
"per_transaction": 50, # $50 per transaction
|
||||
"per_hour": 200, # $200 per hour
|
||||
"per_day": 1000, # $1,000 per day
|
||||
"per_week": 5000, # $5,000 per week
|
||||
"time_lock_threshold": 500, # Time lock over $500
|
||||
"time_lock_delay": 48 # 48 hour delay
|
||||
}
|
||||
Reference in New Issue
Block a user