From a87d6a6c2b4de1cb265af341d914a253248453c9 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 27 May 2026 12:09:32 +0100 Subject: [PATCH 1/6] Harden buyer payment control semantics --- docs/OmniClaw_Whitepaper_v1.0.pdf | Bin 34315 -> 0 bytes docs/OmniClaw_Whitepaper_v2.md | 421 ----------------------------- src/omniclaw/audit.py | 112 ++++++++ src/omniclaw/client.py | 242 ++++++++++++++++- src/omniclaw/core/authorization.py | 86 ++++++ src/omniclaw/core/exceptions.py | 22 ++ src/omniclaw/core/state_machine.py | 14 + src/omniclaw/core/types.py | 2 + src/omniclaw/intents/service.py | 10 + src/omniclaw/ledger/ledger.py | 1 + src/omniclaw/resilience/retry.py | 4 + tests/test_research_hardening.py | 124 +++++++++ 12 files changed, 612 insertions(+), 426 deletions(-) delete mode 100644 docs/OmniClaw_Whitepaper_v1.0.pdf delete mode 100644 docs/OmniClaw_Whitepaper_v2.md create mode 100644 src/omniclaw/audit.py create mode 100644 src/omniclaw/core/authorization.py create mode 100644 tests/test_research_hardening.py diff --git a/docs/OmniClaw_Whitepaper_v1.0.pdf b/docs/OmniClaw_Whitepaper_v1.0.pdf deleted file mode 100644 index c9376504461f4ecce57f5d0fc1ec58c341844ef9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34315 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 KAbm^! PolicyEvaluating -> AwaitingApproval or ApprovedForExecution -> Executing -> Finalized / FailedTerminal / ReconciliationRequired - -ReconciliationRequired may transition only to Finalized or FailedTerminal after the actual settlement outcome is resolved. - -The critical invariants are: - -1. at most one live execution attempt per immutable intent -2. terminal states do not transition backward into pre-execution states -3. uncertain outcomes are modeled explicitly, not guessed away - -The current artifact already supports intent lifecycle management and rejects illegal transitions in tests such as `tests/test_intent_transitions.py`. That artifact evidence is important because it shows OmniClaw is not merely describing a state machine abstractly; it is already enforcing transition semantics in code. - ---- - -## 8. Retry Safety, Idempotency, And Reconciliation - -Retry logic is one of the most financially dangerous surfaces in autonomous systems. - -OmniClaw’s model is that retries must be anchored to immutable business identity, not to transient RPC attempts. The implementation already derives idempotency keys in normalized form, demonstrated in `tests/test_idempotency.py`, and the product docs require caller-provided idempotency keys for job-based payments. - -The stronger control-plane formulation is: - -1. derive a stable settlement identity from immutable payment parameters -2. persist an execution-attempt record before calling the provider -3. submit provider-side idempotency keyed to the same identity -4. if a provider call may have happened and the outcome is uncertain, enter reconciliation rather than replay -5. replay only after authoritative proof that the earlier attempt did not settle - -This rule is more precise than generic “idempotency support.” It means timeout is not failure. A provider call that may have happened moves the system into a different control path. - ---- - -## 9. Policy Races And Atomic Reservation - -A safe autonomous payments system cannot rely on point-in-time checks alone. - -The OmniClaw artifact already includes reservation services, fund locks, and documentation stating that reservations hold spend capacity while fund locks serialize wallet execution. Tests such as `tests/test_payment_concurrency.py`, `tests/test_reservation_integrity.py`, and `tests/test_sdk_integration_extended.py` show that concurrency and reservation logic are treated as first-class implementation concerns. - -The control-plane semantics are: - -1. evaluate an intent against a specific policy version or snapshot -2. persist that version with the intent -3. reserve relevant spending capacity atomically when an intent becomes executable -4. keep the reservation while the intent is in flight -5. release the reservation only when the intent finalizes, fails terminally, or is explicitly revoked -6. allow emergency revalidation before execution under revocation, freeze, or emergency-stop conditions - -This avoids two distinct failure classes: - -- stale-approved execution after a policy change -- aggregate overspend under concurrent workers - -This is one of the most publication-worthy parts of OmniClaw because it turns budget enforcement into a distributed systems correctness problem rather than a generic “payment limit” feature. - ---- - -## 10. Counterparty-Type-Aware Policy - -Not all recipients create the same risk. - -The task-derived architecture, which is consistent with OmniClaw’s broader control model, makes this explicit: - -- Human-operated service - Standard recipient allowlist, ordinary approval thresholds, and contractual accountability. - -- Internal service - Potentially looser thresholds, but only when workload identity, service registry entry, destination account, and transaction class match internal control records. - -- Autonomous agent - Lower auto-approval ceilings, narrower transaction classes, dedicated allowlists, and escalation for novel destinations or unusual amounts. - -This is not a cosmetic policy choice. It is a recognition that counterparty accountability, operational trust, and machine-speed request generation differ materially by counterparty class. - -A worthwhile research claim here is that amount-only financial policy is insufficient for autonomous systems; counterparty class must be part of the policy decision. - ---- - -## 11. Finality-Aware Policy - -Payment rails are not uniform. Some allow intervention before finality, others do not. - -That difference should change the control path. - -- Reversible-before-finality rails can support a pending window, automated rechecks, cancellation or clawback, and somewhat looser approval thresholds where post-authorization intervention remains possible. - -- Irreversible rails require stricter pre-execution controls: tighter limits, lower auto-approval thresholds, stronger destination verification, and no replay until reconciliation proves non-settlement. - -This is another strong research contribution because it formalizes a dimension that generic payment APIs usually leave implicit: finality is a policy input, not just a rail detail. - ---- - -## 12. Adversarial Counterparties And Bounded Blast Radius - -OmniClaw addresses two related but distinct safety questions. - -### 12.1 What can a compromised agent do? - -The answer should be bounded externally by policy: - -- single-payment limits -- rolling limits -- recipient controls -- transaction-class restrictions -- approval thresholds - -This means an agent compromise becomes policy-bounded risk rather than direct wallet risk. - -### 12.2 What can an adversarial counterparty do? - -The architecture should limit: - -- destination redirection through allowlists -- pull-style draining through push-only execution -- category abuse through transaction-class restrictions -- false outcome claims through independent settlement verification -- pre-finality abuse through reversible intervention windows where available - -This distinction matters because “agent compromise” and “counterparty manipulation” are not the same threat, even though they often get bundled together in product discussion. - ---- - -## 13. Auditability And Accountability - -OmniClaw’s audit story is not just observability. It is an accountability chain. - -For each material event, the system should be able to answer: - -- which agent requested the payment -- which policy version allowed or blocked it -- whether approval was required -- which execution attempt ran -- what settlement rail was used -- how the final state was reached - -The compliance architecture documentation in the repo is already strong on this point. For a research audience, the main refinement is to make clear that auditability is not a side effect of logs; it is a consequence of explicit authorization and state semantics. - ---- - -## 14. Implementation Evidence - -The credibility of OmniClaw as a research system comes from the fact that these ideas are not merely proposed. They are reflected in the artifact surface: - -- intent lifecycle services in `src/omniclaw/intents/service.py` -- fund reservations in `src/omniclaw/intents/reservation.py` -- trust-layer types and verdicts in `src/omniclaw/identity/types.py` -- guard, reservation, and fund-lock documentation in `docs/FEATURES.md` -- compliance framing in `docs/compliance-architecture.md` -- product surfaces across buyer SDK, policy engine, and CLI workflows - -There is also substantial test evidence: - -- `tests/test_idempotency.py` -- `tests/test_intent_transitions.py` -- `tests/test_payment_concurrency.py` -- `tests/test_reservation_integrity.py` -- `tests/test_payment_failures.py` -- `tests/test_trust_gate.py` -- `tests/test_x402_idempotency.py` - -That matters because it shows OmniClaw is not a speculative architecture. It is an implemented system with explicit correctness concerns. - ---- - -## 15. Evaluation Agenda - -To turn OmniClaw into a publishable systems/security paper, the next step is to convert its existing artifact surface into a structured evaluation. - -### 15.1 Concurrency Safety - -Measure whether atomic reservation prevents overspend compared with naive point-in-time approval under concurrent workers. - -### 15.2 Retry Safety - -Measure whether intent-bound idempotency plus reconciliation prevents duplicate settlement under crash and timeout scenarios. - -### 15.3 Policy-Race Safety - -Measure stale-approved execution under policy changes with and without versioned revalidation. - -### 15.4 Finality-Aware Control - -Compare approval and replay behavior on reversible versus irreversible rails. - -### 15.5 Operational Overhead - -Measure the latency and throughput cost of policy evaluation, reservation, and reconciliation relative to direct execution. - -### 15.6 Trust And Counterparty Policy - -Evaluate whether counterparty-type-aware policy reduces unsafe auto-approval compared with uniform thresholding. - ---- - -## 16. Comparison Baselines - -The natural baselines are: - -1. Direct-wallet agent execution -2. Approval gateway without execution binding -3. Naive dedupe without explicit uncertain-outcome semantics - -OmniClaw should be evaluated against these models, not just against “no system at all.” - ---- - -## 17. Limitations - -A rigorous paper should state limitations plainly. - -- OmniClaw reduces but does not eliminate financial risk. -- It assumes trustworthy separation between control and execution domains. -- It depends on storage and locking behavior for some guarantees. -- Trust gating quality depends on identity and reputation signal quality. -- Regulatory alignment is not the same as legal compliance. - -These are not weaknesses to hide. They are what make the paper credible. - ---- - -## 18. Conclusion - -Autonomous payments need more than settlement rails. They need a control architecture that answers who authorized this transaction, under which rules, with which state semantics, under which failure conditions, and with what recourse when the outcome is uncertain. - -OmniClaw’s central contribution is to treat financial execution for autonomous systems as a control-plane problem rather than a wallet problem. By separating intent from settlement, binding execution to approved parameters, reserving capacity under concurrency, modeling uncertain outcomes explicitly, branching policy by counterparty type and finality, and preserving a tamper-evident authorization trail, the system provides a stronger authority model for agentic commerce than raw wallet delegation or settlement-only adapters. - -The remaining work is not to invent the architecture. It is to evaluate and present it with the same precision with which it is already being built. - ---- - -## Appendix A. Candidate Claims For External Use - -These are safe, strong statements for a whitepaper, preprint, or outreach memo. - -- OmniClaw is a control layer for autonomous payments, not just a settlement adapter. -- OmniClaw separates agent intent from settlement authority. -- OmniClaw enforces policy before funds move. -- OmniClaw uses reservation and locking semantics to reduce overspend risk under concurrency. -- OmniClaw treats uncertain outcomes as explicit reconciliation cases rather than ordinary failures. -- OmniClaw supports policy branching by counterparty type and rail finality. -- OmniClaw is backed by a working artifact with tests, demos, and operator controls. - -## Appendix B. Candidate Next Documents - -- Whitepaper v2 polished PDF -- Evidence matrix mapping claims to tests and code -- Technical article for engineers -- Short research-lab memo diff --git a/src/omniclaw/audit.py b/src/omniclaw/audit.py new file mode 100644 index 0000000..e78bbd6 --- /dev/null +++ b/src/omniclaw/audit.py @@ -0,0 +1,112 @@ +"""Buyer-side audit reconstruction records.""" + +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from omniclaw.storage.base import StorageBackend + + +@dataclass +class AuditEvent: + """Single buyer-side authorization/audit event.""" + + event_type: str + wallet_id: str + id: str = field(default_factory=lambda: str(uuid.uuid4())) + timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + intent_id: str | None = None + ledger_entry_id: str | None = None + agent_id: str | None = None + correlation_id: str | None = None + payload: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return { + "id": self.id, + "event_type": self.event_type, + "timestamp": self.timestamp.isoformat(), + "wallet_id": self.wallet_id, + "intent_id": self.intent_id, + "ledger_entry_id": self.ledger_entry_id, + "agent_id": self.agent_id, + "correlation_id": self.correlation_id, + "payload": self.payload, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AuditEvent: + return cls( + id=data.get("id", str(uuid.uuid4())), + event_type=data["event_type"], + timestamp=datetime.fromisoformat(data["timestamp"]), + wallet_id=data["wallet_id"], + intent_id=data.get("intent_id"), + ledger_entry_id=data.get("ledger_entry_id"), + agent_id=data.get("agent_id"), + correlation_id=data.get("correlation_id"), + payload=data.get("payload", {}), + ) + + +class BuyerAuditLog: + """Append-style audit log for reconstructing buyer-side authorization chains.""" + + COLLECTION = "buyer_audit_events" + + def __init__(self, storage: StorageBackend) -> None: + self._storage = storage + + async def record( + self, + event_type: str, + *, + wallet_id: str, + intent_id: str | None = None, + ledger_entry_id: str | None = None, + agent_id: str | None = None, + correlation_id: str | None = None, + payload: dict[str, Any] | None = None, + ) -> str: + event = AuditEvent( + event_type=event_type, + wallet_id=wallet_id, + intent_id=intent_id, + ledger_entry_id=ledger_entry_id, + agent_id=agent_id, + correlation_id=correlation_id, + payload=payload or {}, + ) + await self._storage.save(self.COLLECTION, event.id, event.to_dict()) + return event.id + + async def trace( + self, + *, + wallet_id: str | None = None, + intent_id: str | None = None, + ledger_entry_id: str | None = None, + correlation_id: str | None = None, + ) -> list[AuditEvent]: + filters: dict[str, Any] = {} + if wallet_id: + filters["wallet_id"] = wallet_id + if intent_id: + filters["intent_id"] = intent_id + if ledger_entry_id: + filters["ledger_entry_id"] = ledger_entry_id + if correlation_id: + filters["correlation_id"] = correlation_id + + raw_events = await self._storage.query(self.COLLECTION, filters=filters or None, limit=None) + events = [AuditEvent.from_dict(event) for event in raw_events] + events.sort(key=lambda event: event.timestamp) + return events + + +__all__ = ["AuditEvent", "BuyerAuditLog"] + diff --git a/src/omniclaw/client.py b/src/omniclaw/client.py index aa7907d..b2baf01 100644 --- a/src/omniclaw/client.py +++ b/src/omniclaw/client.py @@ -3,7 +3,9 @@ from __future__ import annotations import asyncio +import hashlib import ipaddress +import json import os import re import uuid @@ -17,13 +19,21 @@ if TYPE_CHECKING: from omniclaw.protocols.nanopayments.client import NanopaymentClient +from omniclaw.audit import BuyerAuditLog from omniclaw.core.config import Config from omniclaw.core.exceptions import ( ConfigurationError, InsufficientBalanceError, PaymentError, + PaymentOutcomeUnknownError, + TransactionTimeoutError, ValidationError, ) +from omniclaw.core.authorization import ( + bind_authorization, + build_authorization_snapshot, + verify_authorization_binding, +) from omniclaw.core.idempotency import derive_idempotency_key from omniclaw.core.state_machine import is_irreversible_success_status from omniclaw.core.types import ( @@ -174,6 +184,7 @@ def __init__( os.environ.get("OMNICLAW_REQUIRE_TRUST_GATE", "false").lower() == "true" ) self._ledger = Ledger(self._storage) + self._audit = BuyerAuditLog(self._storage) self._fund_lock = FundLockService(self._storage) self._guard_manager = GuardManager(self._storage) self._wallet_service = WalletService( @@ -297,6 +308,50 @@ def _route_uses_gateway_balance(cls, detected_route: Any, preferred_url_route: A return False return cls._route_value(detected_route) == PaymentMethod.NANOPAYMENT.value + async def _policy_snapshot_hash( + self, + wallet_id: str, + wallet_set_id: str | None = None, + ) -> str: + """Hash the stored buyer-side guard policy visible to a payment request.""" + snapshot: dict[str, Any] = {"wallet": None, "wallet_set": None} + wallet_policy = await self._storage.get("guard_registrations", f"wallet:{wallet_id}") + snapshot["wallet"] = wallet_policy or {"guards": []} + if wallet_set_id: + set_policy = await self._storage.get( + "guard_registrations", f"wallet_set:{wallet_set_id}" + ) + snapshot["wallet_set"] = set_policy or {"guards": []} + + canonical = json.dumps(snapshot, sort_keys=True, separators=(",", ":"), default=str) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + def _intent_authorization_snapshot(self, intent: PaymentIntent) -> dict[str, Any]: + metadata = intent.metadata or {} + return build_authorization_snapshot( + wallet_id=intent.wallet_id, + recipient=intent.recipient, + amount=intent.amount, + currency=intent.currency, + purpose=intent.purpose, + expires_at=intent.expires_at, + route=metadata.get("simulated_route"), + idempotency_key=metadata.get("idempotency_key"), + policy_snapshot_hash=metadata.get("policy_snapshot_hash"), + ) + + def _verify_intent_authorization(self, intent: PaymentIntent) -> None: + metadata = intent.metadata or {} + current_snapshot = self._intent_authorization_snapshot(intent) + if not verify_authorization_binding( + stored_snapshot=metadata.get("authorization_snapshot"), + stored_digest=metadata.get("authorization_digest"), + current_snapshot=current_snapshot, + ): + raise ValidationError( + "Payment intent authorization binding mismatch; refusing execution." + ) + def _init_nanopayments(self) -> None: """Initialize nanopayments components (direct private key only).""" if not self._config.nanopayments_enabled: @@ -374,6 +429,11 @@ def ledger(self) -> Ledger: """Get the transaction ledger.""" return self._ledger + @property + def audit(self) -> BuyerAuditLog: + """Get the buyer-side audit reconstruction log.""" + return self._audit + @property def webhooks(self) -> WebhookParser: """Get webhook parser for verifying and parsing events.""" @@ -982,6 +1042,20 @@ async def pay( metadata=meta, ) await self._ledger.record(ledger_entry) + await self._audit.record( + "payment.requested", + wallet_id=wallet_id, + intent_id=consume_intent_id, + ledger_entry_id=ledger_entry.id, + correlation_id=idempotency_key, + payload={ + "recipient": recipient, + "amount": str(amount_decimal), + "purpose": purpose, + "idempotency_key": idempotency_key, + "trust": trust_result.to_dict() if trust_result else None, + }, + ) guards_chain = None reservation_tokens = [] @@ -1023,6 +1097,17 @@ async def pay( # Reserve budget/limits first (atomic counters) reservation_tokens = await guards_chain.reserve(context) guards_passed = [g.name for g in guards_chain] + await self._audit.record( + "policy.reserved", + wallet_id=wallet_id, + intent_id=consume_intent_id, + ledger_entry_id=ledger_entry.id, + correlation_id=idempotency_key, + payload={ + "guards": guards_passed, + "reservation_token_count": len(reservation_tokens), + }, + ) except Exception as e: from omniclaw.guards.confirm import ConfirmRequiredError @@ -1049,6 +1134,14 @@ async def pay( }, ) if isinstance(e, ValueError): + await self._audit.record( + "policy.blocked", + wallet_id=wallet_id, + intent_id=consume_intent_id, + ledger_entry_id=ledger_entry.id, + correlation_id=idempotency_key, + payload={"reason": str(e)}, + ) return PaymentResult( success=False, transaction_id=None, @@ -1163,6 +1256,18 @@ async def pay( async with circuit: if lock_lost_event.is_set(): raise PaymentError("Wallet lock lease was lost before payment execution.") + await self._audit.record( + "execution.attempted", + wallet_id=wallet_id, + intent_id=consume_intent_id, + ledger_entry_id=ledger_entry.id, + correlation_id=idempotency_key, + payload={ + "recipient": recipient, + "amount": str(amount_decimal), + "route": self._route_value(detected_route), + }, + ) if strategy == PaymentStrategy.RETRY_THEN_FAIL: result = await execute_with_retry( self._router.pay, @@ -1198,7 +1303,19 @@ async def pay( raise PaymentError("Wallet lock lease was lost during payment execution.") # 3. Success Handling - if result.success or ( + if result.status == PaymentStatus.OUTCOME_UNKNOWN: + if consume_intent_id: + await self._reservation.reserve(wallet_id, amount_decimal, consume_intent_id) + await self._ledger.update_status( + ledger_entry.id, + LedgerEntryStatus.OUTCOME_UNKNOWN, + result.blockchain_tx, + metadata_updates={ + "transaction_id": result.transaction_id, + "outcome_unknown": True, + }, + ) + elif result.success or ( result.status in ( PaymentStatus.AUTHORIZED, @@ -1252,9 +1369,67 @@ async def pay( if guards_chain: await guards_chain.release(reservation_tokens) + await self._audit.record( + "payment.outcome_recorded", + wallet_id=wallet_id, + intent_id=consume_intent_id, + ledger_entry_id=ledger_entry.id, + correlation_id=idempotency_key, + payload={ + "success": result.success, + "status": result.status.value, + "transaction_id": result.transaction_id, + "blockchain_tx": result.blockchain_tx, + }, + ) return result except Exception as e: + if isinstance(e, (PaymentOutcomeUnknownError, TransactionTimeoutError)): + tx_id = getattr(e, "transaction_id", None) + tx_hash = getattr(e, "blockchain_tx", None) + if consume_intent_id: + await self._reservation.reserve(wallet_id, amount_decimal, consume_intent_id) + await self._ledger.update_status( + ledger_entry.id, + LedgerEntryStatus.OUTCOME_UNKNOWN, + tx_hash=tx_hash, + metadata_updates={ + "error": str(e), + "outcome_unknown": True, + "transaction_id": tx_id, + "idempotency_key": idempotency_key, + }, + ) + await self._audit.record( + "payment.outcome_unknown", + wallet_id=wallet_id, + intent_id=consume_intent_id, + ledger_entry_id=ledger_entry.id, + correlation_id=idempotency_key, + payload={ + "error": str(e), + "transaction_id": tx_id, + "blockchain_tx": tx_hash, + }, + ) + return PaymentResult( + success=False, + transaction_id=tx_id, + blockchain_tx=tx_hash, + amount=amount_decimal, + recipient=recipient, + method=detected_route, + status=PaymentStatus.OUTCOME_UNKNOWN, + error=str(e), + guards_passed=guards_passed, + metadata={ + "outcome_unknown": True, + "ledger_entry_id": ledger_entry.id, + "idempotency_key": idempotency_key, + }, + ) + # 4. Failure Handling & Queueing if strategy == PaymentStrategy.QUEUE_BACKGROUND: if execution_result and ( @@ -1646,9 +1821,14 @@ async def create_payment_intent( return existing_intent metadata = kwargs.copy() + policy_snapshot_hash = await self._policy_snapshot_hash( + wallet_id, + wallet_set_id=kwargs.get("wallet_set_id"), + ) metadata.update( { "idempotency_key": idempotency_key, + "policy_snapshot_hash": policy_snapshot_hash, "simulated_route": getattr(sim_result.route, "value", str(sim_result.route)), } ) @@ -1671,6 +1851,23 @@ async def create_payment_intent( metadata=metadata, ) + auth_snapshot = self._intent_authorization_snapshot(intent) + intent = await self._intent_service.update_metadata( + intent.id, + bind_authorization(auth_snapshot), + ) + await self._audit.record( + "intent.authorized", + wallet_id=wallet_id, + intent_id=intent.id, + correlation_id=idempotency_key, + payload={ + "authorization_digest": intent.metadata.get("authorization_digest"), + "policy_snapshot_hash": policy_snapshot_hash, + "route": metadata.get("simulated_route"), + }, + ) + # If the transaction requires manual review, map it to the correct Intent State if is_trust_held: from omniclaw.core.types import PaymentIntentStatus @@ -1720,6 +1917,8 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: if not approved: raise ValidationError("Intent requires manual trust approval before confirmation.") + self._verify_intent_authorization(intent) + # Check expiry if intent.expires_at: from datetime import datetime, timezone @@ -1736,13 +1935,27 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: try: # Update to Processing await self._intent_service.update_status(intent.id, PaymentIntentStatus.PROCESSING) + await self._audit.record( + "intent.execution_started", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=(intent.metadata or {}).get("idempotency_key"), + payload={ + "authorization_digest": (intent.metadata or {}).get( + "authorization_digest" + ) + }, + ) # Prepare exec args from intent + metadata exec_kwargs = intent.metadata.copy() # Remove internal metadata keys that aren't for routing - purpose = exec_kwargs.pop("purpose", None) + purpose = exec_kwargs.pop("purpose", intent.purpose) idempotency_key = exec_kwargs.pop("idempotency_key", None) + exec_kwargs.pop("authorization_snapshot", None) + exec_kwargs.pop("authorization_digest", None) + exec_kwargs.pop("policy_snapshot_hash", None) exec_kwargs.pop("simulated_route", None) exec_kwargs.pop("trust_status", None) exec_kwargs.pop("trust_reason", None) @@ -1762,6 +1975,10 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: if result.success: await self._intent_service.update_status(intent.id, PaymentIntentStatus.SUCCEEDED) + elif result.status == PaymentStatus.OUTCOME_UNKNOWN: + await self._intent_service.update_status( + intent.id, PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK + ) else: await self._reservation.release(intent.id) await self._intent_service.update_status(intent.id, PaymentIntentStatus.FAILED) @@ -1769,6 +1986,11 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: return result except Exception as e: + if isinstance(e, (PaymentOutcomeUnknownError, TransactionTimeoutError)): + await self._intent_service.update_status( + intent.id, PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK + ) + raise e # Mark failed on exception await self._reservation.release(intent.id) await self._intent_service.update_status(intent.id, PaymentIntentStatus.FAILED) @@ -1883,11 +2105,19 @@ async def list_pending_settlements( limit: int = 100, ) -> list[LedgerEntry]: """List ledger entries awaiting settlement finalization.""" - return await self._ledger.query( + pending = await self._ledger.query( wallet_id=wallet_id, status=LedgerEntryStatus.PENDING, limit=limit, ) + unknown = await self._ledger.query( + wallet_id=wallet_id, + status=LedgerEntryStatus.OUTCOME_UNKNOWN, + limit=limit, + ) + entries = [*pending, *unknown] + entries.sort(key=lambda entry: entry.timestamp, reverse=True) + return entries[:limit] async def finalize_pending_settlement( self, @@ -1907,8 +2137,10 @@ async def finalize_pending_settlement( entry = await self._ledger.get(entry_id) if not entry: raise ValidationError(f"Ledger entry not found: {entry_id}") - if entry.status != LedgerEntryStatus.PENDING: - raise ValidationError(f"Ledger entry is not pending settlement: {entry.status.value}") + if entry.status not in (LedgerEntryStatus.PENDING, LedgerEntryStatus.OUTCOME_UNKNOWN): + raise ValidationError( + f"Ledger entry is not pending or outcome-unknown: {entry.status.value}" + ) final_status = LedgerEntryStatus.COMPLETED if settled else LedgerEntryStatus.FAILED merged_updates = { diff --git a/src/omniclaw/core/authorization.py b/src/omniclaw/core/authorization.py new file mode 100644 index 0000000..c548519 --- /dev/null +++ b/src/omniclaw/core/authorization.py @@ -0,0 +1,86 @@ +"""Authorization binding helpers for payment intents.""" + +from __future__ import annotations + +import hashlib +import json +from datetime import datetime +from decimal import Decimal +from typing import Any + + +def _normalize_decimal(value: Decimal | str | int | float) -> str: + decimal_value = value if isinstance(value, Decimal) else Decimal(str(value)) + normalized = format(decimal_value.normalize(), "f") + if "." in normalized: + normalized = normalized.rstrip("0").rstrip(".") + return normalized or "0" + + +def _normalize_datetime(value: datetime | None) -> str | None: + if value is None: + return None + return value.isoformat() + + +def build_authorization_snapshot( + *, + wallet_id: str, + recipient: str, + amount: Decimal, + currency: str, + purpose: str | None, + expires_at: datetime | None, + route: str | None, + idempotency_key: str | None, + policy_snapshot_hash: str | None, +) -> dict[str, Any]: + """Build the canonical buyer-side authorization snapshot for an intent.""" + return { + "amount": _normalize_decimal(amount), + "currency": currency, + "expires_at": _normalize_datetime(expires_at), + "idempotency_key": idempotency_key, + "policy_snapshot_hash": policy_snapshot_hash, + "purpose": purpose, + "recipient": recipient, + "route": route, + "wallet_id": wallet_id, + } + + +def derive_authorization_digest(snapshot: dict[str, Any]) -> str: + """Derive a deterministic digest for an authorization snapshot.""" + canonical = json.dumps(snapshot, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def bind_authorization(snapshot: dict[str, Any]) -> dict[str, Any]: + """Return stored binding metadata for a canonical authorization snapshot.""" + return { + "authorization_snapshot": snapshot, + "authorization_digest": derive_authorization_digest(snapshot), + } + + +def verify_authorization_binding( + *, + stored_snapshot: dict[str, Any] | None, + stored_digest: str | None, + current_snapshot: dict[str, Any], +) -> bool: + """Return True when the current snapshot matches the stored binding.""" + if not stored_snapshot or not stored_digest: + return False + if stored_snapshot != current_snapshot: + return False + return derive_authorization_digest(current_snapshot) == stored_digest + + +__all__ = [ + "bind_authorization", + "build_authorization_snapshot", + "derive_authorization_digest", + "verify_authorization_binding", +] + diff --git a/src/omniclaw/core/exceptions.py b/src/omniclaw/core/exceptions.py index 9b61a4d..b1ff0ef 100644 --- a/src/omniclaw/core/exceptions.py +++ b/src/omniclaw/core/exceptions.py @@ -298,6 +298,28 @@ def __init__( self.timeout_seconds = timeout_seconds +class PaymentOutcomeUnknownError(PaymentError): + """ + Payment attempt outcome is unknown and must not be blindly retried. + + Raised when a payment call may have reached the settlement rail, but the + caller cannot yet prove success or non-settlement. + """ + + def __init__( + self, + message: str, + transaction_id: str | None = None, + blockchain_tx: str | None = None, + recipient: str | None = None, + amount: Decimal | None = None, + details: dict[str, Any] | None = None, + ) -> None: + super().__init__(message, recipient=recipient, amount=amount, details=details) + self.transaction_id = transaction_id + self.blockchain_tx = blockchain_tx + + class IdempotencyError(PaymentError): """ Idempotency key conflict. diff --git a/src/omniclaw/core/state_machine.py b/src/omniclaw/core/state_machine.py index 59fc2af..5bafe4e 100644 --- a/src/omniclaw/core/state_machine.py +++ b/src/omniclaw/core/state_machine.py @@ -7,6 +7,7 @@ _PAYMENT_TRANSITIONS: dict[PaymentStatus, set[PaymentStatus]] = { PaymentStatus.AUTHORIZED: { + PaymentStatus.OUTCOME_UNKNOWN, PaymentStatus.PENDING_SETTLEMENT, PaymentStatus.PROCESSING, PaymentStatus.SETTLED, @@ -15,6 +16,7 @@ }, PaymentStatus.PENDING: { PaymentStatus.AUTHORIZED, + PaymentStatus.OUTCOME_UNKNOWN, PaymentStatus.PROCESSING, PaymentStatus.PENDING_SETTLEMENT, PaymentStatus.SETTLED, @@ -24,6 +26,7 @@ PaymentStatus.BLOCKED, }, PaymentStatus.PROCESSING: { + PaymentStatus.OUTCOME_UNKNOWN, PaymentStatus.PENDING_SETTLEMENT, PaymentStatus.SETTLED, PaymentStatus.FAILED, @@ -31,10 +34,16 @@ PaymentStatus.CANCELLED, }, PaymentStatus.PENDING_SETTLEMENT: { + PaymentStatus.OUTCOME_UNKNOWN, PaymentStatus.SETTLED, PaymentStatus.FAILED, PaymentStatus.FAILED_FINAL, }, + PaymentStatus.OUTCOME_UNKNOWN: { + PaymentStatus.PENDING_SETTLEMENT, + PaymentStatus.SETTLED, + PaymentStatus.FAILED_FINAL, + }, PaymentStatus.COMPLETED: set(), PaymentStatus.SETTLED: set(), PaymentStatus.FAILED: set(), @@ -57,6 +66,11 @@ PaymentIntentStatus.FAILED, }, PaymentIntentStatus.PROCESSING: { + PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK, + PaymentIntentStatus.SUCCEEDED, + PaymentIntentStatus.FAILED, + }, + PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK: { PaymentIntentStatus.SUCCEEDED, PaymentIntentStatus.FAILED, }, diff --git a/src/omniclaw/core/types.py b/src/omniclaw/core/types.py index fa2a49d..c8df82b 100644 --- a/src/omniclaw/core/types.py +++ b/src/omniclaw/core/types.py @@ -215,6 +215,7 @@ class PaymentStatus(str, Enum): """Payment transaction status.""" AUTHORIZED = "authorized" # Authorization created but settlement not started + OUTCOME_UNKNOWN = "outcome_unknown" # Attempt may have reached rail; final outcome unknown PENDING_SETTLEMENT = "pending_settlement" # Submitted and awaiting final settlement SETTLED = "settled" # Irreversible settlement confirmed FAILED_FINAL = "failed_final" # Irreversible terminal failure @@ -232,6 +233,7 @@ class PaymentIntentStatus(str, Enum): REQUIRES_CONFIRMATION = "requires_confirmation" # Created, ready to confirm REQUIRES_REVIEW = "requires_review" # Created, but TrustGate HELD PROCESSING = "processing" # Executive in progress + REQUIRES_SETTLEMENT_CHECK = "requires_settlement_check" # Outcome unknown; do not replay SUCCEEDED = "succeeded" # Completed successfully CANCELED = "canceled" # Canceled by user/agent FAILED = "failed" # Execution failed diff --git a/src/omniclaw/intents/service.py b/src/omniclaw/intents/service.py index 0f78735..96bb480 100644 --- a/src/omniclaw/intents/service.py +++ b/src/omniclaw/intents/service.py @@ -114,6 +114,16 @@ async def update_status(self, intent_id: str, status: PaymentIntentStatus) -> Pa await self._save(intent) return intent + async def update_metadata(self, intent_id: str, metadata: dict[str, Any]) -> PaymentIntent: + """Merge metadata into a stored payment intent.""" + intent = await self.get(intent_id) + if not intent: + raise ValidationError(f"Intent not found: {intent_id}") + + intent.metadata.update(metadata) + await self._save(intent) + return intent + async def cancel(self, intent_id: str, reason: str | None = None) -> PaymentIntent: """ Cancel a payment intent. diff --git a/src/omniclaw/ledger/ledger.py b/src/omniclaw/ledger/ledger.py index 5219b0e..5e56dbe 100644 --- a/src/omniclaw/ledger/ledger.py +++ b/src/omniclaw/ledger/ledger.py @@ -31,6 +31,7 @@ class LedgerEntryStatus(str, Enum): """Status of ledger entries.""" PENDING = "pending" + OUTCOME_UNKNOWN = "outcome_unknown" COMPLETED = "completed" FAILED = "failed" CANCELLED = "cancelled" diff --git a/src/omniclaw/resilience/retry.py b/src/omniclaw/resilience/retry.py index a5edd64..21f2033 100644 --- a/src/omniclaw/resilience/retry.py +++ b/src/omniclaw/resilience/retry.py @@ -10,6 +10,8 @@ from collections.abc import Callable from typing import Any +from omniclaw.core.exceptions import PaymentOutcomeUnknownError, TransactionTimeoutError + try: from tenacity import ( AsyncRetrying, @@ -34,6 +36,8 @@ def decorator(f): def is_transient_error(exception: Exception) -> bool: """Check if exception is a transient network/infrastructure error.""" + if isinstance(exception, (PaymentOutcomeUnknownError, TransactionTimeoutError)): + return False msg = str(exception).lower() return any( x in msg diff --git a/tests/test_research_hardening.py b/tests/test_research_hardening.py new file mode 100644 index 0000000..c2c3829 --- /dev/null +++ b/tests/test_research_hardening.py @@ -0,0 +1,124 @@ +from decimal import Decimal +from unittest.mock import MagicMock + +import pytest + +from omniclaw.client import OmniClaw +from omniclaw.core.exceptions import PaymentOutcomeUnknownError, ValidationError +from omniclaw.core.types import ( + Network, + PaymentIntentStatus, + PaymentMethod, + PaymentResult, + PaymentStatus, + SimulationResult, +) + + +@pytest.fixture +def client() -> OmniClaw: + c = OmniClaw( + network=Network.ARC_TESTNET, + circle_api_key="mock_key", + entity_secret="mock_secret", + ) + c._wallet_service = MagicMock() + c._wallet_service.get_usdc_balance_amount.return_value = Decimal("500.00") + c._wallet_service.get_wallet.return_value = MagicMock(blockchain="ETH-SEPOLIA") + + async def mock_simulate(*args, **kwargs): + return SimulationResult(would_succeed=True, route=PaymentMethod.TRANSFER) + + async def mock_pay(*args, **kwargs): + return PaymentResult( + success=True, + transaction_id="tx-ok", + blockchain_tx="0xok", + amount=kwargs["amount"], + recipient=kwargs["recipient"], + method=PaymentMethod.TRANSFER, + status=PaymentStatus.COMPLETED, + ) + + c._router.simulate = mock_simulate + c._router.pay = mock_pay + return c + + +@pytest.mark.asyncio +async def test_intent_authorization_binding_rejects_amount_tampering(client): + intent = await client.intent.create( + wallet_id="wallet-bind", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="25.00", + purpose="bound execution", + ) + + assert intent.metadata["authorization_digest"] + assert intent.metadata["authorization_snapshot"]["amount"] == "25" + + await client._storage.update( + client._intent_service.COLLECTION, + f"intent:{intent.id}", + {"amount": "26.00"}, + ) + + with pytest.raises(ValidationError, match="authorization binding mismatch"): + await client.intent.confirm(intent.id) + + +@pytest.mark.asyncio +async def test_outcome_unknown_marks_intent_for_settlement_check_and_blocks_replay(client): + async def unknown_pay(*args, **kwargs): + raise PaymentOutcomeUnknownError( + "payment submitted but final outcome is unknown", + transaction_id="tx-unknown", + blockchain_tx="0xmaybe", + recipient=kwargs["recipient"], + amount=kwargs["amount"], + ) + + client._router.pay = unknown_pay + + intent = await client.intent.create( + wallet_id="wallet-unknown", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="10.00", + ) + + result = await client.intent.confirm(intent.id) + + assert result.status == PaymentStatus.OUTCOME_UNKNOWN + updated = await client.intent.get(intent.id) + assert updated.status == PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK + + reserved = await client._reservation.get_reserved_total("wallet-unknown") + assert reserved == Decimal("10.00") + + with pytest.raises(ValidationError, match="cannot be confirmed"): + await client.intent.confirm(intent.id) + + +@pytest.mark.asyncio +async def test_buyer_audit_trace_reconstructs_intent_authorization_chain(client): + intent = await client.intent.create( + wallet_id="wallet-audit", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="7.50", + purpose="audit reconstruction", + idempotency_key="audit-job-1", + ) + + await client.intent.confirm(intent.id) + + trace = await client.audit.trace(intent_id=intent.id) + event_types = [event.event_type for event in trace] + + assert "intent.authorized" in event_types + assert "intent.execution_started" in event_types + assert "payment.requested" in event_types + assert "execution.attempted" in event_types + assert "payment.outcome_recorded" in event_types + + authorized = next(event for event in trace if event.event_type == "intent.authorized") + assert authorized.payload["authorization_digest"] == intent.metadata["authorization_digest"] From 60cab9a70f1c0a7e6584b7ee5419f4752ff739d4 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 27 May 2026 12:16:21 +0100 Subject: [PATCH 2/6] Fix lint import ordering --- src/omniclaw/client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/omniclaw/client.py b/src/omniclaw/client.py index b2baf01..fe4426a 100644 --- a/src/omniclaw/client.py +++ b/src/omniclaw/client.py @@ -20,6 +20,11 @@ from omniclaw.protocols.nanopayments.client import NanopaymentClient from omniclaw.audit import BuyerAuditLog +from omniclaw.core.authorization import ( + bind_authorization, + build_authorization_snapshot, + verify_authorization_binding, +) from omniclaw.core.config import Config from omniclaw.core.exceptions import ( ConfigurationError, @@ -29,11 +34,6 @@ TransactionTimeoutError, ValidationError, ) -from omniclaw.core.authorization import ( - bind_authorization, - build_authorization_snapshot, - verify_authorization_binding, -) from omniclaw.core.idempotency import derive_idempotency_key from omniclaw.core.state_machine import is_irreversible_success_status from omniclaw.core.types import ( From f552587898bbd63aaa41e3a281fbb49b64ccff40 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 27 May 2026 12:19:06 +0100 Subject: [PATCH 3/6] Format research hardening files --- src/omniclaw/audit.py | 1 - src/omniclaw/client.py | 4 +--- src/omniclaw/core/authorization.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/omniclaw/audit.py b/src/omniclaw/audit.py index e78bbd6..53cb0dd 100644 --- a/src/omniclaw/audit.py +++ b/src/omniclaw/audit.py @@ -109,4 +109,3 @@ async def trace( __all__ = ["AuditEvent", "BuyerAuditLog"] - diff --git a/src/omniclaw/client.py b/src/omniclaw/client.py index fe4426a..bb6090d 100644 --- a/src/omniclaw/client.py +++ b/src/omniclaw/client.py @@ -1941,9 +1941,7 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: intent_id=intent.id, correlation_id=(intent.metadata or {}).get("idempotency_key"), payload={ - "authorization_digest": (intent.metadata or {}).get( - "authorization_digest" - ) + "authorization_digest": (intent.metadata or {}).get("authorization_digest") }, ) diff --git a/src/omniclaw/core/authorization.py b/src/omniclaw/core/authorization.py index c548519..a07d1c3 100644 --- a/src/omniclaw/core/authorization.py +++ b/src/omniclaw/core/authorization.py @@ -83,4 +83,3 @@ def verify_authorization_binding( "derive_authorization_digest", "verify_authorization_binding", ] - From 19b463351f4f8e4c3b470db702e4d25a2292d935 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 27 May 2026 13:05:46 +0100 Subject: [PATCH 4/6] Harden buyer payment control paths --- src/omniclaw/audit.py | 11 +- src/omniclaw/client.py | 161 ++++++++++++++---- src/omniclaw/core/authorization.py | 37 +++- src/omniclaw/intents/intent_facade.py | 1 - src/omniclaw/resilience/retry.py | 30 ++-- src/omniclaw/wallet/service.py | 36 +++- ...ening.py => test_buyer_payment_control.py} | 96 +++++++++++ tests/test_wallet_service.py | 41 ++++- 8 files changed, 355 insertions(+), 58 deletions(-) rename tests/{test_research_hardening.py => test_buyer_payment_control.py} (55%) diff --git a/src/omniclaw/audit.py b/src/omniclaw/audit.py index 53cb0dd..93fa09b 100644 --- a/src/omniclaw/audit.py +++ b/src/omniclaw/audit.py @@ -91,6 +91,8 @@ async def trace( intent_id: str | None = None, ledger_entry_id: str | None = None, correlation_id: str | None = None, + limit: int = 100, + allow_unfiltered: bool = False, ) -> list[AuditEvent]: filters: dict[str, Any] = {} if wallet_id: @@ -102,7 +104,14 @@ async def trace( if correlation_id: filters["correlation_id"] = correlation_id - raw_events = await self._storage.query(self.COLLECTION, filters=filters or None, limit=None) + if not filters and not allow_unfiltered: + raise ValueError("At least one audit trace selector is required.") + + raw_events = await self._storage.query( + self.COLLECTION, + filters=filters or None, + limit=limit, + ) events = [AuditEvent.from_dict(event) for event in raw_events] events.sort(key=lambda event: event.timestamp) return events diff --git a/src/omniclaw/client.py b/src/omniclaw/client.py index bb6090d..f8c6c33 100644 --- a/src/omniclaw/client.py +++ b/src/omniclaw/client.py @@ -326,9 +326,22 @@ async def _policy_snapshot_hash( canonical = json.dumps(snapshot, sort_keys=True, separators=(",", ":"), default=str) return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + def _authorization_secret(self) -> str: + """Return the local secret used to seal payment-intent authorization bindings.""" + secret = ( + self._config.entity_secret + or self._config.nanopayments_private_key + or self._config.circle_api_key + ) + if not secret: + raise ConfigurationError("A local authorization secret is required.") + return secret + def _intent_authorization_snapshot(self, intent: PaymentIntent) -> dict[str, Any]: metadata = intent.metadata or {} return build_authorization_snapshot( + intent_id=intent.id, + created_at=intent.created_at, wallet_id=intent.wallet_id, recipient=intent.recipient, amount=intent.amount, @@ -338,15 +351,25 @@ def _intent_authorization_snapshot(self, intent: PaymentIntent) -> dict[str, Any route=metadata.get("simulated_route"), idempotency_key=metadata.get("idempotency_key"), policy_snapshot_hash=metadata.get("policy_snapshot_hash"), + execution_params=metadata.get("execution_params") or {}, ) - def _verify_intent_authorization(self, intent: PaymentIntent) -> None: + async def _verify_intent_authorization(self, intent: PaymentIntent) -> None: metadata = intent.metadata or {} + execution_params = metadata.get("execution_params") or {} + current_policy_hash = await self._policy_snapshot_hash( + intent.wallet_id, + wallet_set_id=execution_params.get("wallet_set_id"), + ) + if metadata.get("policy_snapshot_hash") != current_policy_hash: + raise ValidationError("Payment intent policy snapshot changed; refusing execution.") + current_snapshot = self._intent_authorization_snapshot(intent) if not verify_authorization_binding( stored_snapshot=metadata.get("authorization_snapshot"), stored_digest=metadata.get("authorization_digest"), current_snapshot=current_snapshot, + secret=self._authorization_secret(), ): raise ValidationError( "Payment intent authorization binding mismatch; refusing execution." @@ -928,6 +951,9 @@ async def pay( f"Invalid fee_level {fee_level!r}. Use LOW, MEDIUM, or HIGH." ) from exc + expected_route = kwargs.pop("expected_route", None) + expected_route_value = self._route_value(expected_route) if expected_route else None + if self._config.auto_reconcile_pending_settlements: try: await self.reconcile_pending_settlements(wallet_id=wallet_id, limit=20) @@ -975,6 +1001,8 @@ async def pay( meta = metadata or {} meta["idempotency_key"] = idempotency_key meta["strategy"] = strategy.value + if consume_intent_id: + meta["intent_id"] = consume_intent_id if self._require_trust_gate and self._trust_gate and not self._trust_gate.is_configured: raise ConfigurationError( @@ -1089,6 +1117,12 @@ async def pay( except Exception: detected_route = PaymentMethod.TRANSFER + if expected_route_value and self._route_value(detected_route) != expected_route_value: + raise ValidationError( + "Payment route changed after authorization: " + f"expected {expected_route_value}, got {self._route_value(detected_route)}" + ) + if not skip_guards: guards_chain = await self._guard_manager.get_guard_chain( wallet_id=wallet_id, wallet_set_id=wallet_set_id @@ -1301,19 +1335,39 @@ async def pay( execution_result = result if lock_lost_event.is_set(): raise PaymentError("Wallet lock lease was lost during payment execution.") + if ( + expected_route_value + and self._route_value(result.method) != expected_route_value + ): + raise PaymentOutcomeUnknownError( + "Executed payment route did not match authorization.", + transaction_id=result.transaction_id, + blockchain_tx=result.blockchain_tx, + recipient=recipient, + amount=amount_decimal, + ) # 3. Success Handling if result.status == PaymentStatus.OUTCOME_UNKNOWN: if consume_intent_id: await self._reservation.reserve(wallet_id, amount_decimal, consume_intent_id) + unknown_updates = { + "transaction_id": result.transaction_id, + "outcome_unknown": True, + "intent_id": consume_intent_id, + } + if guards_chain: + unknown_updates.update( + { + "guard_resolution_pending": True, + "guard_reservation_tokens": reservation_tokens, + } + ) await self._ledger.update_status( ledger_entry.id, LedgerEntryStatus.OUTCOME_UNKNOWN, result.blockchain_tx, - metadata_updates={ - "transaction_id": result.transaction_id, - "outcome_unknown": True, - }, + metadata_updates=unknown_updates, ) elif result.success or ( result.status @@ -1385,21 +1439,30 @@ async def pay( return result except Exception as e: - if isinstance(e, (PaymentOutcomeUnknownError, TransactionTimeoutError)): + if isinstance(e, PaymentOutcomeUnknownError | TransactionTimeoutError): tx_id = getattr(e, "transaction_id", None) tx_hash = getattr(e, "blockchain_tx", None) if consume_intent_id: await self._reservation.reserve(wallet_id, amount_decimal, consume_intent_id) + unknown_updates = { + "error": str(e), + "outcome_unknown": True, + "transaction_id": tx_id, + "idempotency_key": idempotency_key, + "intent_id": consume_intent_id, + } + if guards_chain: + unknown_updates.update( + { + "guard_resolution_pending": True, + "guard_reservation_tokens": reservation_tokens, + } + ) await self._ledger.update_status( ledger_entry.id, LedgerEntryStatus.OUTCOME_UNKNOWN, tx_hash=tx_hash, - metadata_updates={ - "error": str(e), - "outcome_unknown": True, - "transaction_id": tx_id, - "idempotency_key": idempotency_key, - }, + metadata_updates=unknown_updates, ) await self._audit.record( "payment.outcome_unknown", @@ -1820,10 +1883,13 @@ async def create_payment_intent( if existing_intent is not None: return existing_intent - metadata = kwargs.copy() + execution_params = kwargs.copy() + metadata = { + "execution_params": execution_params, + } policy_snapshot_hash = await self._policy_snapshot_hash( wallet_id, - wallet_set_id=kwargs.get("wallet_set_id"), + wallet_set_id=execution_params.get("wallet_set_id"), ) metadata.update( { @@ -1854,7 +1920,7 @@ async def create_payment_intent( auth_snapshot = self._intent_authorization_snapshot(intent) intent = await self._intent_service.update_metadata( intent.id, - bind_authorization(auth_snapshot), + bind_authorization(auth_snapshot, self._authorization_secret()), ) await self._audit.record( "intent.authorized", @@ -1863,6 +1929,7 @@ async def create_payment_intent( correlation_id=idempotency_key, payload={ "authorization_digest": intent.metadata.get("authorization_digest"), + "authorization_snapshot": auth_snapshot, "policy_snapshot_hash": policy_snapshot_hash, "route": metadata.get("simulated_route"), }, @@ -1917,7 +1984,7 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: if not approved: raise ValidationError("Intent requires manual trust approval before confirmation.") - self._verify_intent_authorization(intent) + await self._verify_intent_authorization(intent) # Check expiry if intent.expires_at: @@ -1945,18 +2012,11 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: }, ) - # Prepare exec args from intent + metadata - exec_kwargs = intent.metadata.copy() - - # Remove internal metadata keys that aren't for routing - purpose = exec_kwargs.pop("purpose", intent.purpose) - idempotency_key = exec_kwargs.pop("idempotency_key", None) - exec_kwargs.pop("authorization_snapshot", None) - exec_kwargs.pop("authorization_digest", None) - exec_kwargs.pop("policy_snapshot_hash", None) - exec_kwargs.pop("simulated_route", None) - exec_kwargs.pop("trust_status", None) - exec_kwargs.pop("trust_reason", None) + metadata = intent.metadata or {} + exec_kwargs = dict(metadata.get("execution_params") or {}) + purpose = intent.purpose + idempotency_key = metadata.get("idempotency_key") + authorized_route = metadata.get("simulated_route") # Execute Pay result = await self.pay( @@ -1967,7 +2027,7 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: idempotency_key=idempotency_key, check_trust=False, consume_intent_id=intent.id, # Key part: releases reservation inside the lock - validate_recipient=False, # Intent already validated recipient at creation + expected_route=authorized_route, **exec_kwargs, ) @@ -1984,7 +2044,7 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: return result except Exception as e: - if isinstance(e, (PaymentOutcomeUnknownError, TransactionTimeoutError)): + if isinstance(e, PaymentOutcomeUnknownError | TransactionTimeoutError): await self._intent_service.update_status( intent.id, PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK ) @@ -2096,6 +2156,41 @@ async def sync_transaction(self, entry_id: str) -> LedgerEntry: updated = await self._ledger.get(entry.id) return updated # type: ignore + async def _resolve_linked_intent_and_guards( + self, + entry: LedgerEntry, + *, + settled: bool, + ) -> None: + """Resolve buyer-side holds linked to a finalized settlement entry.""" + metadata = entry.metadata or {} + + if metadata.get("guard_resolution_pending"): + guard_tokens = metadata.get("guard_reservation_tokens") or [] + guard_chain = await self._guard_manager.get_guard_chain( + wallet_id=entry.wallet_id, + wallet_set_id=entry.wallet_set_id, + ) + if settled: + await guard_chain.commit(guard_tokens) + else: + await guard_chain.release(guard_tokens) + + intent_id = metadata.get("intent_id") + if not intent_id: + return + + intent = await self._intent_service.get(intent_id) + if not intent: + return + + await self._reservation.release(intent_id) + target_status = PaymentIntentStatus.SUCCEEDED if settled else PaymentIntentStatus.FAILED + if intent.status == target_status: + return + if intent.status == PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK: + await self._intent_service.update_status(intent_id, target_status) + async def list_pending_settlements( self, *, @@ -2150,6 +2245,8 @@ async def finalize_pending_settlement( if metadata_updates: merged_updates.update(metadata_updates) + await self._resolve_linked_intent_and_guards(entry, settled=settled) + await self._ledger.update_status( entry_id, final_status, @@ -2195,12 +2292,14 @@ async def reconcile_pending_settlements( LedgerEntryStatus.FAILED, LedgerEntryStatus.CANCELLED, ): + settled = updated.status == LedgerEntryStatus.COMPLETED + await self._resolve_linked_intent_and_guards(updated, settled=settled) await self._ledger.update_status( updated.id, updated.status, tx_hash=updated.tx_hash, metadata_updates={ - "settlement_final": updated.status == LedgerEntryStatus.COMPLETED, + "settlement_final": settled, "settlement_reconciled_at": datetime.now(timezone.utc).isoformat(), }, ) diff --git a/src/omniclaw/core/authorization.py b/src/omniclaw/core/authorization.py index a07d1c3..5d52716 100644 --- a/src/omniclaw/core/authorization.py +++ b/src/omniclaw/core/authorization.py @@ -3,9 +3,11 @@ from __future__ import annotations import hashlib +import hmac import json from datetime import datetime from decimal import Decimal +from enum import Enum from typing import Any @@ -23,8 +25,25 @@ def _normalize_datetime(value: datetime | None) -> str | None: return value.isoformat() +def _normalize_value(value: Any) -> Any: + """Normalize execution parameters into deterministic JSON-safe values.""" + if isinstance(value, Decimal): + return _normalize_decimal(value) + if isinstance(value, datetime): + return _normalize_datetime(value) + if isinstance(value, Enum): + return value.value + if isinstance(value, dict): + return {str(k): _normalize_value(v) for k, v in sorted(value.items())} + if isinstance(value, list | tuple): + return [_normalize_value(v) for v in value] + return value + + def build_authorization_snapshot( *, + intent_id: str, + created_at: datetime, wallet_id: str, recipient: str, amount: Decimal, @@ -34,13 +53,17 @@ def build_authorization_snapshot( route: str | None, idempotency_key: str | None, policy_snapshot_hash: str | None, + execution_params: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build the canonical buyer-side authorization snapshot for an intent.""" return { "amount": _normalize_decimal(amount), + "created_at": _normalize_datetime(created_at), "currency": currency, "expires_at": _normalize_datetime(expires_at), + "execution_params": _normalize_value(execution_params or {}), "idempotency_key": idempotency_key, + "intent_id": intent_id, "policy_snapshot_hash": policy_snapshot_hash, "purpose": purpose, "recipient": recipient, @@ -49,17 +72,17 @@ def build_authorization_snapshot( } -def derive_authorization_digest(snapshot: dict[str, Any]) -> str: - """Derive a deterministic digest for an authorization snapshot.""" +def derive_authorization_digest(snapshot: dict[str, Any], secret: str) -> str: + """Derive a keyed deterministic digest for an authorization snapshot.""" canonical = json.dumps(snapshot, sort_keys=True, separators=(",", ":"), ensure_ascii=True) - return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + return hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest() -def bind_authorization(snapshot: dict[str, Any]) -> dict[str, Any]: +def bind_authorization(snapshot: dict[str, Any], secret: str) -> dict[str, Any]: """Return stored binding metadata for a canonical authorization snapshot.""" return { "authorization_snapshot": snapshot, - "authorization_digest": derive_authorization_digest(snapshot), + "authorization_digest": derive_authorization_digest(snapshot, secret), } @@ -68,13 +91,15 @@ def verify_authorization_binding( stored_snapshot: dict[str, Any] | None, stored_digest: str | None, current_snapshot: dict[str, Any], + secret: str, ) -> bool: """Return True when the current snapshot matches the stored binding.""" if not stored_snapshot or not stored_digest: return False if stored_snapshot != current_snapshot: return False - return derive_authorization_digest(current_snapshot) == stored_digest + expected = derive_authorization_digest(current_snapshot, secret) + return hmac.compare_digest(expected, stored_digest) __all__ = [ diff --git a/src/omniclaw/intents/intent_facade.py b/src/omniclaw/intents/intent_facade.py index 3518291..0b3cebd 100644 --- a/src/omniclaw/intents/intent_facade.py +++ b/src/omniclaw/intents/intent_facade.py @@ -81,7 +81,6 @@ async def create( destination_chain=destination_chain, skip_guards=skip_guards, check_trust=check_trust, - validate_recipient=False, **kwargs, ) diff --git a/src/omniclaw/resilience/retry.py b/src/omniclaw/resilience/retry.py index 21f2033..b600764 100644 --- a/src/omniclaw/resilience/retry.py +++ b/src/omniclaw/resilience/retry.py @@ -10,7 +10,11 @@ from collections.abc import Callable from typing import Any -from omniclaw.core.exceptions import PaymentOutcomeUnknownError, TransactionTimeoutError +from omniclaw.core.exceptions import ( + PaymentError, + PaymentOutcomeUnknownError, + TransactionTimeoutError, +) try: from tenacity import ( @@ -35,23 +39,15 @@ def decorator(f): def is_transient_error(exception: Exception) -> bool: - """Check if exception is a transient network/infrastructure error.""" - if isinstance(exception, (PaymentOutcomeUnknownError, TransactionTimeoutError)): + """Check whether an exception is explicitly safe to retry. + + Payment execution retries must not infer safety from generic timeout strings. Callers + can mark pre-submission operations with ``retry_safe=True`` when a repeated attempt + cannot create another economic action. + """ + if isinstance(exception, PaymentOutcomeUnknownError | TransactionTimeoutError | PaymentError): return False - msg = str(exception).lower() - return any( - x in msg - for x in [ - "timeout", - "connection refused", - "500", - "502", - "503", - "504", - "network error", - "rate limit", # Sometimes retryable with backoff - ] - ) + return bool(getattr(exception, "retry_safe", False)) # Standard Retry Policy diff --git a/src/omniclaw/wallet/service.py b/src/omniclaw/wallet/service.py index 0be6afa..3a0fabd 100644 --- a/src/omniclaw/wallet/service.py +++ b/src/omniclaw/wallet/service.py @@ -16,6 +16,8 @@ from omniclaw.core.config import Config from omniclaw.core.exceptions import ( InsufficientBalanceError, + PaymentOutcomeUnknownError, + TransactionTimeoutError, WalletError, ) from omniclaw.core.types import ( @@ -475,6 +477,13 @@ async def transfer( idempotency_key=idempotency_key, ) except Exception as e: + if self._is_ambiguous_submission_error(e): + raise PaymentOutcomeUnknownError( + "Transfer submission outcome is unknown.", + recipient=destination_address, + amount=amount_decimal, + details={"idempotency_key": idempotency_key}, + ) from e return TransferResult( success=False, error=str(e), @@ -518,12 +527,37 @@ async def _wait_for_transaction( elapsed = time.time() - start_time if elapsed >= timeout_seconds: - return tx + state = tx.state.value if hasattr(tx.state, "value") else str(tx.state) + raise TransactionTimeoutError( + "Transaction did not reach a terminal state before timeout.", + transaction_id=transaction_id, + last_state=state, + timeout_seconds=timeout_seconds, + ) await asyncio.sleep(poll_interval) # ==================== Utility Methods ==================== + @staticmethod + def _is_ambiguous_submission_error(error: Exception) -> bool: + """Return true when a transfer request may have reached the provider.""" + message = str(error).lower() + return any( + token in message + for token in ( + "timeout", + "connection", + "network", + "temporarily unavailable", + "rate limit", + "500", + "502", + "503", + "504", + ) + ) + def get_or_create_default_wallet_set( self, name: str = "OmniClaw Default", diff --git a/tests/test_research_hardening.py b/tests/test_buyer_payment_control.py similarity index 55% rename from tests/test_research_hardening.py rename to tests/test_buyer_payment_control.py index c2c3829..2293abd 100644 --- a/tests/test_research_hardening.py +++ b/tests/test_buyer_payment_control.py @@ -13,6 +13,7 @@ PaymentStatus, SimulationResult, ) +from omniclaw.ledger.ledger import LedgerEntryStatus @pytest.fixture @@ -67,6 +68,47 @@ async def test_intent_authorization_binding_rejects_amount_tampering(client): await client.intent.confirm(intent.id) +@pytest.mark.asyncio +async def test_intent_authorization_binding_rejects_execution_parameter_tampering(client): + intent = await client.intent.create( + wallet_id="wallet-param-bind", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="12.00", + purpose="bound route", + preferred_url_route="transfer", + ) + + tampered_metadata = dict(intent.metadata) + tampered_metadata["execution_params"] = {"preferred_url_route": "x402"} + await client._storage.update( + client._intent_service.COLLECTION, + f"intent:{intent.id}", + {"metadata": tampered_metadata}, + ) + + with pytest.raises(ValidationError, match="authorization binding mismatch"): + await client.intent.confirm(intent.id) + + +@pytest.mark.asyncio +async def test_intent_confirmation_rejects_changed_policy_snapshot(client): + intent = await client.intent.create( + wallet_id="wallet-policy-bind", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="12.00", + purpose="policy bound", + ) + + await client._storage.save( + "guard_registrations", + "wallet:wallet-policy-bind", + {"guards": [{"name": "daily_budget", "limit": "25.00"}]}, + ) + + with pytest.raises(ValidationError, match="policy snapshot changed"): + await client.intent.confirm(intent.id) + + @pytest.mark.asyncio async def test_outcome_unknown_marks_intent_for_settlement_check_and_blocks_replay(client): async def unknown_pay(*args, **kwargs): @@ -99,6 +141,45 @@ async def unknown_pay(*args, **kwargs): await client.intent.confirm(intent.id) +@pytest.mark.asyncio +async def test_finalize_pending_settlement_resolves_linked_intent_and_reservation(client): + async def unknown_pay(*args, **kwargs): + raise PaymentOutcomeUnknownError( + "payment submitted but final outcome is unknown", + transaction_id="tx-finalize", + blockchain_tx="0xmaybe", + recipient=kwargs["recipient"], + amount=kwargs["amount"], + ) + + client._router.pay = unknown_pay + + intent = await client.intent.create( + wallet_id="wallet-finalize", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="10.00", + ) + + result = await client.intent.confirm(intent.id) + assert result.status == PaymentStatus.OUTCOME_UNKNOWN + + [pending_entry] = await client.list_pending_settlements(wallet_id="wallet-finalize") + finalized = await client.finalize_pending_settlement( + pending_entry.id, + settled=True, + settlement_tx_hash="0xsettled", + ) + + assert finalized.status == LedgerEntryStatus.COMPLETED + assert finalized.metadata["settlement_final"] is True + + updated = await client.intent.get(intent.id) + assert updated.status == PaymentIntentStatus.SUCCEEDED + + reserved = await client._reservation.get_reserved_total("wallet-finalize") + assert reserved == Decimal("0") + + @pytest.mark.asyncio async def test_buyer_audit_trace_reconstructs_intent_authorization_chain(client): intent = await client.intent.create( @@ -122,3 +203,18 @@ async def test_buyer_audit_trace_reconstructs_intent_authorization_chain(client) authorized = next(event for event in trace if event.event_type == "intent.authorized") assert authorized.payload["authorization_digest"] == intent.metadata["authorization_digest"] + + +@pytest.mark.asyncio +async def test_buyer_audit_trace_requires_selector_by_default(client): + await client.intent.create( + wallet_id="wallet-audit-guard", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="2.00", + purpose="audit guard", + ) + + with pytest.raises(ValueError, match="selector is required"): + await client.audit.trace() + + assert await client.audit.trace(allow_unfiltered=True) diff --git a/tests/test_wallet_service.py b/tests/test_wallet_service.py index 666dd7d..ca59a36 100644 --- a/tests/test_wallet_service.py +++ b/tests/test_wallet_service.py @@ -6,7 +6,12 @@ import pytest from omniclaw.core.config import Config -from omniclaw.core.exceptions import InsufficientBalanceError, WalletError +from omniclaw.core.exceptions import ( + InsufficientBalanceError, + PaymentOutcomeUnknownError, + TransactionTimeoutError, + WalletError, +) from omniclaw.core.types import ( AccountType, Balance, @@ -458,6 +463,40 @@ async def test_transfer_skip_balance_check( assert result.success is True + async def test_transfer_raises_outcome_unknown_for_ambiguous_submission_error( + self, + wallet_service: WalletService, + mock_circle_client: MagicMock, + ) -> None: + """Test ambiguous provider submission failures are not treated as final failures.""" + mock_circle_client.find_usdc_token_id.return_value = "usdc-token-id" + mock_circle_client.create_transfer.side_effect = TimeoutError("request timeout") + + with pytest.raises(PaymentOutcomeUnknownError): + await wallet_service.transfer( + wallet_id="wallet-123", + destination_address="0xdest...", + amount=Decimal("10.00"), + check_balance=False, + idempotency_key="transfer-1", + ) + + async def test_wait_for_transaction_timeout_raises_non_terminal_status( + self, + wallet_service: WalletService, + mock_circle_client: MagicMock, + ) -> None: + """Test polling timeout reports an unknown final settlement outcome.""" + mock_circle_client.get_transaction.return_value = TransactionInfo( + id="tx-timeout", + state=TransactionState.INITIATED, + ) + + with pytest.raises(TransactionTimeoutError) as exc_info: + await wallet_service._wait_for_transaction("tx-timeout", timeout_seconds=0) + + assert exc_info.value.transaction_id == "tx-timeout" + class TestUtilityMethods: """Tests for utility methods.""" From bac990c8506e33223d83ea06dd37730b5e1c11d8 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 27 May 2026 13:27:31 +0100 Subject: [PATCH 5/6] Add buyer audit chain coverage --- src/omniclaw/audit.py | 70 +++++++++++++++++++++++++++-- src/omniclaw/client.py | 67 ++++++++++++++++++++++++++- tests/test_buyer_payment_control.py | 50 +++++++++++++++++++++ 3 files changed, 183 insertions(+), 4 deletions(-) diff --git a/src/omniclaw/audit.py b/src/omniclaw/audit.py index 93fa09b..331d29d 100644 --- a/src/omniclaw/audit.py +++ b/src/omniclaw/audit.py @@ -2,6 +2,8 @@ from __future__ import annotations +import hashlib +import json import uuid from dataclasses import dataclass, field from datetime import datetime, timezone @@ -24,6 +26,8 @@ class AuditEvent: agent_id: str | None = None correlation_id: str | None = None payload: dict[str, Any] = field(default_factory=dict) + previous_hash: str | None = None + event_hash: str | None = None def to_dict(self) -> dict[str, Any]: return { @@ -36,6 +40,8 @@ def to_dict(self) -> dict[str, Any]: "agent_id": self.agent_id, "correlation_id": self.correlation_id, "payload": self.payload, + "previous_hash": self.previous_hash, + "event_hash": self.event_hash, } @classmethod @@ -50,17 +56,31 @@ def from_dict(cls, data: dict[str, Any]) -> AuditEvent: agent_id=data.get("agent_id"), correlation_id=data.get("correlation_id"), payload=data.get("payload", {}), + previous_hash=data.get("previous_hash"), + event_hash=data.get("event_hash"), ) + def hash_payload(self) -> dict[str, Any]: + """Return the event fields covered by the audit hash.""" + data = self.to_dict() + data.pop("event_hash", None) + return data + class BuyerAuditLog: """Append-style audit log for reconstructing buyer-side authorization chains.""" COLLECTION = "buyer_audit_events" + CHAIN_COLLECTION = "buyer_audit_chains" def __init__(self, storage: StorageBackend) -> None: self._storage = storage + @staticmethod + def _derive_hash(payload: dict[str, Any]) -> str: + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + async def record( self, event_type: str, @@ -72,6 +92,12 @@ async def record( correlation_id: str | None = None, payload: dict[str, Any] | None = None, ) -> str: + chain_key = f"wallet:{wallet_id}" + lock_key = f"lock:audit:{chain_key}" + lock_token = await self._storage.acquire_lock(lock_key, ttl=10) + if not lock_token: + raise RuntimeError(f"Audit chain is locked: {wallet_id}") + event = AuditEvent( event_type=event_type, wallet_id=wallet_id, @@ -81,8 +107,23 @@ async def record( correlation_id=correlation_id, payload=payload or {}, ) - await self._storage.save(self.COLLECTION, event.id, event.to_dict()) - return event.id + try: + chain_state = await self._storage.get(self.CHAIN_COLLECTION, chain_key) or {} + event.previous_hash = chain_state.get("head_hash") + event.event_hash = self._derive_hash(event.hash_payload()) + await self._storage.save(self.COLLECTION, event.id, event.to_dict()) + await self._storage.save( + self.CHAIN_COLLECTION, + chain_key, + { + "head_hash": event.event_hash, + "last_event_id": event.id, + "updated_at": event.timestamp.isoformat(), + }, + ) + return event.id + finally: + await self._storage.release_lock(lock_key, lock_token) async def trace( self, @@ -91,7 +132,7 @@ async def trace( intent_id: str | None = None, ledger_entry_id: str | None = None, correlation_id: str | None = None, - limit: int = 100, + limit: int | None = 100, allow_unfiltered: bool = False, ) -> list[AuditEvent]: filters: dict[str, Any] = {} @@ -116,5 +157,28 @@ async def trace( events.sort(key=lambda event: event.timestamp) return events + @classmethod + def verify_events(cls, events: list[AuditEvent]) -> bool: + """Verify each event hash.""" + for event in events: + if not event.event_hash: + return False + expected_hash = cls._derive_hash(event.hash_payload()) + if expected_hash != event.event_hash: + return False + return True + + async def verify_wallet_chain(self, wallet_id: str) -> bool: + """Verify the full hash-chain order for one wallet's audit events.""" + events = await self.trace(wallet_id=wallet_id, limit=None) + if not self.verify_events(events): + return False + previous_event: AuditEvent | None = None + for event in events: + if previous_event and event.previous_hash != previous_event.event_hash: + return False + previous_event = event + return True + __all__ = ["AuditEvent", "BuyerAuditLog"] diff --git a/src/omniclaw/client.py b/src/omniclaw/client.py index f8c6c33..611f69f 100644 --- a/src/omniclaw/client.py +++ b/src/omniclaw/client.py @@ -1997,6 +1997,13 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: # Auto-cancel expired intent and release reservation await self._reservation.release(intent.id) await self._intent_service.cancel(intent.id, reason="Expired") + await self._audit.record( + "intent.expired", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=(intent.metadata or {}).get("idempotency_key"), + payload={"expires_at": intent.expires_at.isoformat()}, + ) raise ValidationError(f"Intent expired at {intent.expires_at}") try: @@ -2033,13 +2040,34 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: if result.success: await self._intent_service.update_status(intent.id, PaymentIntentStatus.SUCCEEDED) + await self._audit.record( + "intent.succeeded", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=idempotency_key, + payload={"status": result.status.value}, + ) elif result.status == PaymentStatus.OUTCOME_UNKNOWN: await self._intent_service.update_status( intent.id, PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK ) + await self._audit.record( + "intent.settlement_check_required", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=idempotency_key, + payload={"status": result.status.value}, + ) else: await self._reservation.release(intent.id) await self._intent_service.update_status(intent.id, PaymentIntentStatus.FAILED) + await self._audit.record( + "intent.failed", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=idempotency_key, + payload={"status": result.status.value, "error": result.error}, + ) return result @@ -2048,10 +2076,24 @@ async def confirm_payment_intent(self, intent_id: str) -> PaymentResult: await self._intent_service.update_status( intent.id, PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK ) + await self._audit.record( + "intent.settlement_check_required", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=(intent.metadata or {}).get("idempotency_key"), + payload={"error": str(e)}, + ) raise e # Mark failed on exception await self._reservation.release(intent.id) await self._intent_service.update_status(intent.id, PaymentIntentStatus.FAILED) + await self._audit.record( + "intent.failed", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=(intent.metadata or {}).get("idempotency_key"), + payload={"error": str(e)}, + ) raise e async def get_payment_intent(self, intent_id: str) -> PaymentIntent | None: @@ -2075,7 +2117,15 @@ async def cancel_payment_intent( # Layer 2: Release reserved funds await self._reservation.release(intent.id) - return await self._intent_service.cancel(intent.id, reason=reason) + cancelled = await self._intent_service.cancel(intent.id, reason=reason) + await self._audit.record( + "intent.cancelled", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=(intent.metadata or {}).get("idempotency_key"), + payload={"reason": reason}, + ) + return cancelled async def approve_payment_intent_review( self, @@ -2097,6 +2147,13 @@ async def approve_payment_intent_review( metadata["trust_review_approved_at"] = datetime.now(timezone.utc).isoformat() intent.metadata = metadata await self._intent_service._save(intent) + await self._audit.record( + "intent.review_approved", + wallet_id=intent.wallet_id, + intent_id=intent.id, + correlation_id=metadata.get("idempotency_key"), + payload={"approved_by": approved_by, "reason": reason or ""}, + ) return intent async def batch_pay( @@ -2190,6 +2247,14 @@ async def _resolve_linked_intent_and_guards( return if intent.status == PaymentIntentStatus.REQUIRES_SETTLEMENT_CHECK: await self._intent_service.update_status(intent_id, target_status) + await self._audit.record( + "intent.settlement_resolved", + wallet_id=entry.wallet_id, + intent_id=intent_id, + ledger_entry_id=entry.id, + correlation_id=metadata.get("idempotency_key"), + payload={"settled": settled, "target_status": target_status.value}, + ) async def list_pending_settlements( self, diff --git a/tests/test_buyer_payment_control.py b/tests/test_buyer_payment_control.py index 2293abd..679c5d4 100644 --- a/tests/test_buyer_payment_control.py +++ b/tests/test_buyer_payment_control.py @@ -200,9 +200,20 @@ async def test_buyer_audit_trace_reconstructs_intent_authorization_chain(client) assert "payment.requested" in event_types assert "execution.attempted" in event_types assert "payment.outcome_recorded" in event_types + assert "intent.succeeded" in event_types authorized = next(event for event in trace if event.event_type == "intent.authorized") assert authorized.payload["authorization_digest"] == intent.metadata["authorization_digest"] + assert all(event.event_hash for event in trace) + assert await client.audit.verify_wallet_chain("wallet-audit") + + await client._storage.update( + client.audit.COLLECTION, + trace[0].id, + {"payload": {"tampered": True}}, + ) + tampered_trace = await client.audit.trace(wallet_id="wallet-audit") + assert not client.audit.verify_events(tampered_trace) @pytest.mark.asyncio @@ -218,3 +229,42 @@ async def test_buyer_audit_trace_requires_selector_by_default(client): await client.audit.trace() assert await client.audit.trace(allow_unfiltered=True) + + +@pytest.mark.asyncio +async def test_intent_cancel_and_review_approval_are_audited(client): + cancel_intent = await client.intent.create( + wallet_id="wallet-audit-cancel", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="2.00", + purpose="cancel audit", + ) + + await client.intent.cancel(cancel_intent.id, reason="operator stopped payment") + + cancel_trace = await client.audit.trace(intent_id=cancel_intent.id) + assert "intent.cancelled" in [event.event_type for event in cancel_trace] + + async def held_simulate(*args, **kwargs): + return SimulationResult( + would_succeed=False, + route=PaymentMethod.TRANSFER, + reason="Trust Gate: HELD for manual approval", + ) + + client._router.simulate = held_simulate + review_intent = await client.intent.create( + wallet_id="wallet-audit-review", + recipient="0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", + amount="3.00", + purpose="review audit", + ) + + await client.approve_payment_intent_review( + review_intent.id, + approved_by="operator-1", + reason="known counterparty", + ) + + review_trace = await client.audit.trace(intent_id=review_intent.id) + assert "intent.review_approved" in [event.event_type for event in review_trace] From c5cf0c4391cfa9ec251a28a3685a134ec5fa69d9 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 27 May 2026 13:37:50 +0100 Subject: [PATCH 6/6] Move research planning docs out of product repo --- docs/RESEARCH_EVIDENCE_MATRIX.md | 219 --------------- docs/RESEARCH_THESIS_AND_OUTLINE.md | 400 ---------------------------- 2 files changed, 619 deletions(-) delete mode 100644 docs/RESEARCH_EVIDENCE_MATRIX.md delete mode 100644 docs/RESEARCH_THESIS_AND_OUTLINE.md diff --git a/docs/RESEARCH_EVIDENCE_MATRIX.md b/docs/RESEARCH_EVIDENCE_MATRIX.md deleted file mode 100644 index c061e7a..0000000 --- a/docs/RESEARCH_EVIDENCE_MATRIX.md +++ /dev/null @@ -1,219 +0,0 @@ -# OmniClaw Research Evidence Matrix - -Purpose: map research claims to current implementation evidence so future papers, whitepapers, and outreach materials stay anchored to the artifact. - -## Claim 1: Separation Of Intent From Settlement Authority - -Claim: -- Agents create payment intents, but the execution layer alone settles approved payments. - -Code / Docs: -- `README.md` -- `docs/compliance-architecture.md` -- `src/omniclaw/intents/intent_facade.py` -- `src/omniclaw/agent/routes.py` - -Evidence Type: -- architecture documentation -- implemented control flow - -What to verify next: -- exact execution-binding path in runtime code and authorization boundary documentation - -## Claim 2: Intent-Bound Execution Authorization - -Claim: -- Settlement is bound to exact approved parameters rather than generic approval. - -Code / Docs: -- task-derived reference answer and architecture logic -- `docs/compliance-architecture.md` -- execution-layer request handling in agent/server routes - -Evidence Type: -- architectural semantics -- runtime control-path inspection - -What to strengthen: -- add a dedicated implementation note or test showing rejection of mismatched execution parameters - -## Claim 3: Explicit Lifecycle And Transition Semantics - -Claim: -- Payment intents follow explicit states and reject illegal transitions. - -Code / Docs: -- `src/omniclaw/intents/service.py` -- `src/omniclaw/core/types.py` -- `tests/test_intent_transitions.py` - -Evidence Type: -- code -- transition tests - -Notes: -- current runtime uses `PaymentIntentStatus` values such as `REQUIRES_CONFIRMATION`, `REQUIRES_REVIEW`, `PROCESSING`, `SUCCEEDED`, `CANCELED`, and `FAILED` -- paper narrative can present a stricter generalized control-state model while mapping back to artifact states - -## Claim 4: Idempotent Retry Safety - -Claim: -- Equivalent payment requests derive the same idempotency key, reducing duplicate execution risk. - -Code / Docs: -- `tests/test_idempotency.py` -- `README.md` -- CLI and SDK docs requiring idempotency keys - -Evidence Type: -- normalization test -- product and operator docs - -What to strengthen: -- add or reference an end-to-end duplicate-settlement prevention test under retry or crash scenarios - -## Claim 5: Concurrency-Safe Reservation And Locking - -Claim: -- Reservation plus locking reduces overspend risk under concurrent workers. - -Code / Docs: -- `src/omniclaw/intents/reservation.py` -- `docs/FEATURES.md` -- `tests/test_payment_concurrency.py` -- `tests/test_reservation_integrity.py` -- `tests/test_sdk_integration_extended.py` -- `SECURITY.md` - -Evidence Type: -- code -- concurrency tests -- design documentation - -Notes: -- this is one of the strongest artifact-backed claims in the system - -## Claim 6: Policy Versioning And Revalidation - -Claim: -- Policy evaluation is tied to explicit policy state, and emergency changes can force revalidation. - -Code / Docs: -- current architecture and control-plane design -- `docs/compliance-architecture.md` -- policy docs in `docs/POLICY_REFERENCE.md` - -Evidence Type: -- design semantics -- policy documentation - -What to strengthen: -- add or document a specific code path or test for policy version attachment and emergency revalidation - -## Claim 7: Counterparty-Type-Aware Policy - -Claim: -- Policy should differ for human-operated services, internal services, and autonomous-agent counterparties. - -Code / Docs: -- task-derived architecture -- `docs/compliance-architecture.md` -- future policy schema extensions - -Evidence Type: -- design contribution - -What to strengthen: -- introduce explicit runtime policy fields or examples reflecting counterparty class -- add a policy-matrix example to docs - -## Claim 8: Finality-Aware Policy Branching - -Claim: -- Reversible and irreversible rails should follow different approval, replay, and intervention rules. - -Code / Docs: -- `docs/PRODUCTION_HARDENING.md` notes on strict settlement -- task-derived architecture - -Evidence Type: -- architectural semantics - -What to strengthen: -- add explicit docs and tests for reversible-window versus irreversible-rail behavior - -## Claim 9: Trust Gate And Counterparty Evaluation - -Claim: -- Counterparty trust can be evaluated with explicit verdicts such as approve, hold, or block. - -Code / Docs: -- `src/omniclaw/identity/types.py` -- `docs/compliance-architecture.md` -- `docs/FEATURES.md` -- `tests/test_trust_gate.py` - -Evidence Type: -- code -- tests -- design docs - -## Claim 10: Auditability And Traceability - -Claim: -- The system records who requested a payment, which policy allowed it, and how execution proceeded. - -Code / Docs: -- `docs/compliance-architecture.md` -- audit descriptions in `README.md` and `docs/FEATURES.md` -- event-emission points in intent services - -Evidence Type: -- docs -- code hooks - -What to strengthen: -- add an explicit audit-event schema doc - -## Claim 11: x402 Replay Resistance - -Claim: -- Consumed-proof handling can reduce x402 replay acceptance. - -Code / Docs: -- whitepaper v1 discussion -- x402 replay handling design notes - -Evidence Type: -- implementation hook - -What to strengthen: -- add a dedicated replay-resistance test reference into the docs and paper - -## Strongest Claims Today - -These are the claims most clearly supported by current artifact evidence: - -1. Separation of intent and settlement authority -2. Reservation and locking for concurrency safety -3. Intent lifecycle and transition enforcement -4. Trust Gate verdict model -5. Idempotency normalization and product-wide idempotency discipline - -## Claims That Need Stronger Explicit Evidence Before Formal Publication - -These are still good claims, but should be tightened with more direct code-path documentation or tests: - -1. Exact execution-parameter binding rejection path -2. Policy version attachment and emergency revalidation tests -3. Counterparty-type-aware runtime policy examples -4. Reversible-versus-irreversible runtime control-path evidence -5. Formal replay-resistance evaluation - -## Recommended Next Actions - -1. Create an artifact appendix that links each major claim to code and test files -2. Add a dedicated audit-event schema document -3. Add explicit docs or examples for counterparty-type and finality-aware policy branching -4. Add one or two tests that directly exercise the strongest paper-specific claims not yet obvious from current test names -5. Use this matrix to keep the whitepaper and future preprint honest diff --git a/docs/RESEARCH_THESIS_AND_OUTLINE.md b/docs/RESEARCH_THESIS_AND_OUTLINE.md deleted file mode 100644 index 77d4a6e..0000000 --- a/docs/RESEARCH_THESIS_AND_OUTLINE.md +++ /dev/null @@ -1,400 +0,0 @@ -# OmniClaw Research Thesis And Outline - -Purpose: extract the research-grade systems contribution from OmniClaw without changing the product-facing README. This document is the starting point for a whitepaper v2, preprint, or research-lab outreach packet. - -## Working Title Options - -1. OmniClaw: Policy-Constrained Financial Execution for Autonomous Agents -2. Separating Intent From Settlement: A Control Plane for Autonomous Payments -3. Safe Economic Execution for Agentic Systems -4. Policy-Bound Autonomous Payments Under Concurrency and Uncertain Settlement - -## One-Sentence Thesis - -OmniClaw is a control-plane architecture for autonomous financial execution that separates agent intent from settlement authority and enforces policy, concurrency safety, idempotency, and failure-aware settlement semantics before money moves. - -## Short Abstract - -Autonomous agents increasingly need to purchase compute, APIs, data, and machine services without human intervention, but existing payment rails expose execution primitives rather than safe authority models. OmniClaw addresses this gap with a policy-constrained control plane that sits between agent intent and settlement execution. The system binds execution to validated intents, enforces operator-defined policy before funds move, prevents budget overcommitment under concurrent workers through atomic reservation, treats uncertain settlement outcomes as first-class reconciliation states rather than retryable failures, and supports differentiated policy by counterparty type and settlement finality. The result is a financial execution architecture that preserves agent autonomy without giving agents unrestricted payment authority. - -## Problem Statement - -Modern payment infrastructure gives agents a way to move money but not a trustworthy way to decide when money should move. If an agent holds direct wallet authority, then hallucinations, prompt injection, compromised toolchains, stale policy, concurrent oversubscription, retry storms, or adversarial counterparties can turn ordinary automation bugs into treasury-loss events. - -The core problem is therefore not settlement alone. It is authority. - -The research question is: - -How can an autonomous system perform economic actions at machine speed while preserving bounded authority, concurrency safety, failure-aware recovery, and post-hoc auditability across heterogeneous payment rails? - -## Core Contributions - -### C1. Separation Of Intent From Settlement - -OmniClaw separates the component that decides to request payment from the component that is allowed to settle payment. Agents produce intents; the control plane evaluates policy; the execution layer settles only approved intents. - -Why this matters: -- converts agent compromise from direct-funds risk into bounded policy risk -- supports operator accountability -- creates a clean locus for audit and policy enforcement - -### C2. Intent-Bound Execution Authorization - -Settlement is bound to a validated intent through a signed authorization containing the exact amount, destination, policy version, and expiry. The execution layer rejects mismatches. - -Why this matters: -- prevents parameter tampering between approval and execution -- turns “policy approved something like this” into “execution may perform only this exact payment” - -### C3. Explicit Financial State Semantics - -OmniClaw models payment execution through explicit ledger states rather than implicit flags. Uncertain outcomes are represented as a distinct reconciliation state rather than treated as ordinary failures. - -Why this matters: -- prevents blind retries after timeouts -- makes legal transitions auditable -- supports reasoning about terminality and replay safety - -### C4. Concurrency-Safe Budget Enforcement - -OmniClaw uses atomic reservation of spend capacity when intents become executable, rather than relying on independent point-in-time balance checks by concurrent workers. - -Why this matters: -- prevents aggregate overspend under parallel agent execution -- turns budget enforcement into a shared-state correctness property - -### C5. Policy Branching By Counterparty Type And Settlement Finality - -Policy decisions vary depending on whether the counterparty is human-operated, an internal service, or another autonomous agent, and depending on whether the payment rail is reversible before finality or irreversible. - -Why this matters: -- aligns approval and limit posture to real counterparty risk -- aligns pre-execution checks to finality risk -- makes policy closer to economic reality than uniform thresholding - -### C6. Failure-Aware Idempotent Settlement - -OmniClaw ties retries to immutable intent identity and requires reconciliation against authoritative evidence before replay when an outcome is uncertain. - -Why this matters: -- prevents duplicate settlement -- distinguishes timeout from known failure -- provides crash recovery semantics suitable for real payment systems - -### C7. Auditable Financial Control Plane - -OmniClaw records control decisions, policy versioning, intent state transitions, and execution attempts in a tamper-evident audit trail. - -Why this matters: -- makes authorization provable -- supports governance, operations, and compliance review -- creates a forensic trail across automated economic actions - -## System Model - -### Actors - -- Agent Runtime: proposes payment intents -- Operator: defines policy and accepts accountability for policy scope -- Control Service: evaluates policy, creates execution authorizations, manages reservations -- Policy Store: holds versioned rules and emergency controls -- Payment-Intent Ledger: durable source of truth for intent state and attempt state -- Execution Service: performs settlement using approved, bound authorizations only -- Settlement Provider / Rail: external payment or settlement mechanism -- Counterparty: external service, internal service, or autonomous agent receiving payment -- Audit Layer: append-only record of decisions and state transitions - -### Trust Boundaries - -- Agents are outside settlement authority -- Control plane is outside raw key custody -- Execution holds settlement capability but not policy authorship -- Policy store is authoritative for rules but cannot execute settlement directly - -### Assumptions - -- persistent storage is available -- network failures and retries are normal -- exactly-once message delivery is not assumed -- clocks may be imperfectly synchronized -- settlement providers may return success, known failure, or uncertain outcomes - -## Threat Model - -### In Scope - -- compromised or prompt-injected agent runtime -- concurrent agents sharing a wallet or policy budget -- timeout after partial provider interaction -- stale approval under changed policy -- malicious or low-trust counterparty -- replay of payment proofs or settlement attempts -- parameter tampering between approval and execution -- ambiguous downstream settlement state - -### Out Of Scope Or Assumed - -- total compromise of every trust domain simultaneously -- cryptographic breaks in signature schemes or HSM/KMS systems -- perfect prevention of all external fraud -- legal identity verification guarantees outside the configured trust sources - -## Safety And Correctness Invariants - -These should become the formal backbone of a paper or whitepaper v2. - -### I1. No Direct Agent Settlement - -An agent can request payment but cannot directly trigger settlement or use settlement signing material. - -### I2. Execution Is Bound To Approved Intent - -The execution layer may settle only the exact amount, destination, intent ID, and policy version contained in the signed authorization. - -### I3. At Most One Live Execution Attempt Per Immutable Intent - -An immutable payment intent cannot have two concurrently live execution attempts. - -### I4. No Blind Retry After Uncertain Outcome - -If a provider call may have happened and the outcome is uncertain, the system must reconcile first and may replay only after authoritative proof of non-settlement. - -### I5. No Budget Overcommitment Under Concurrent Approval - -Concurrent workers sharing a budget cannot authorize aggregate spend above the available policy-controlled capacity if atomic reservation is correct. - -### I6. No Backward Transition After Terminal State - -Once an intent reaches a terminal state, it cannot legally transition back into a pre-execution state. - -### I7. Policy Evaluation Is Explainable - -Every execution-eligible intent is associated with an explicit policy version or snapshot, so a later reviewer can determine which rule set authorized it. - -### I8. Counterparty And Finality Affect Policy Path - -Policy outcomes are not solely amount-based. Counterparty type and rail finality materially alter threshold, approval, and execution behavior. - -## Hypotheses For Evaluation - -### H1. Reservation Integrity - -Under concurrent payment requests, atomic reservation prevents budget overcommitment relative to naive per-request balance checks. - -### H2. Retry Safety - -Intent-bound idempotency and reconciliation-first replay eliminate duplicate settlement under crash and timeout scenarios that produce duplicates in naive retry models. - -### H3. Policy-Race Safety - -Versioned policy evaluation plus emergency revalidation prevents stale-approved intents from settling under revoked destinations or emergency freezes. - -### H4. Salience Of Counterparty And Finality Branching - -Counterparty-type-aware and finality-aware policy branching reduces unsafe auto-approval compared with uniform threshold models. - -### H5. Overhead Acceptability - -The control-plane overhead is acceptable relative to the financial risk reduction it provides. - -## Evaluation Plan - -### 1. Functional Correctness - -Demonstrate: -- legal state transitions -- terminal-state enforcement -- reconciliation-first handling of uncertain outcomes -- execution binding to exact approved parameters - -Likely evidence: -- existing lifecycle, transition, failure, and idempotency tests - -### 2. Concurrency Evaluation - -Measure: -- overspend rate under naive approval -- overspend rate under atomic reservation -- reservation contention overhead - -Likely evidence: -- payment concurrency tests -- reservation integrity tests - -### 3. Retry / Crash / Timeout Evaluation - -Measure: -- duplicate settlement rate under naive retries -- duplicate settlement rate with intent-bound idempotency and reconciliation -- mean time to recover ambiguous outcomes - -Likely evidence: -- idempotency tests -- payment failure tests -- execution-attempt recovery tests - -### 4. Policy Race Evaluation - -Measure: -- stale-approved execution rate without revalidation -- stale-approved execution rate with persisted policy version plus emergency revalidation - -### 5. Counterparty / Finality Policy Evaluation - -Show: -- example policy matrix by counterparty class -- example policy matrix by reversible versus irreversible rail -- how approval thresholds and required checks differ - -### 6. Operational Overhead - -Measure: -- added latency from policy evaluation -- added latency from reservation and reconciliation logic -- throughput effects - -## Comparison Baselines - -At minimum compare against: - -### B1. Direct-Wallet Agent - -Agent holds settlement authority directly with local limits or heuristics only. - -### B2. Approval Gateway Without Execution Binding - -Central approval exists, but execution is not cryptographically bound to the exact approved parameters. - -### B3. Naive Dedupe Without Reconciliation Semantics - -Retries use a weak dedupe rule but do not model uncertain outcomes explicitly. - -## What The Paper Should Claim Carefully - -Use precise language. Avoid overstating. - -Safe strong claims: -- OmniClaw enforces a control-plane architecture that separates intent from settlement. -- OmniClaw prevents a class of overspend and duplicate-settlement failures under stated assumptions. -- OmniClaw provides explicit policy and state semantics for autonomous financial execution. - -Claims to calibrate carefully: -- formal proof claims should be made only where the assumptions and proof standard are explicit -- broad regulatory claims should be framed as alignment support, not legal compliance guarantees - -## Candidate Paper Structure - -### 1. Introduction - -- Why agentic systems need economic authority -- Why direct-wallet models are unsafe -- What problem existing payment rails fail to solve -- Summary of OmniClaw contributions - -### 2. Background - -- payment rails and adapters -- wallet execution versus authorization -- autonomous counterparties and trust signals - -### 3. Problem Formulation - -- failure classes -- threat model -- safety requirements - -### 4. System Architecture - -- components -- trust boundaries -- control flow -- state model - -### 5. Policy And Execution Semantics - -- policy evaluation -- execution binding -- counterparty branching -- finality branching -- reservation semantics - -### 6. Retry And Reconciliation Semantics - -- idempotency -- uncertain outcomes -- reconciliation-first replay - -### 7. Security And Correctness Properties - -- invariants -- threat discussion -- assumptions - -### 8. Implementation - -- artifact overview -- policy engine -- ledger and audit components -- payment rail integrations - -### 9. Evaluation - -- concurrency -- retry safety -- race handling -- overhead -- baseline comparison - -### 10. Limitations And Future Work - -- trust-source quality -- control-plane compromise -- settlement-provider assumptions -- broader policy language - -## Best Publication Sequence - -### Stage 1. Whitepaper v2 - -Goal: -- clean system narrative -- no hype -- precise contributions -- clear invariants - -### Stage 2. Technical Article - -Audience: -- engineers -- product builders -- infra teams - -Possible title: -- How To Give Autonomous Agents Economic Authority Without Giving Them Custody - -### Stage 3. Research Preprint - -Audience: -- systems labs -- security labs -- AI infrastructure researchers - -### Stage 4. Artifact-Centered Outreach - -Package: -- paper or whitepaper -- architecture diagram -- test-backed claims -- short implementation summary - -## Immediate Next Steps - -1. Turn this outline into a 2-3 page thesis memo -2. Extract explicit invariants from the code and tests -3. Build an evaluation matrix mapping tests and demos to each claim -4. Draft Whitepaper v2 using this structure -5. Prepare a short lab-outreach version with contributions, artifact link, and evaluation summary - -## Bottom Line - -OmniClaw already looks like more than a product. It looks like a real control-plane architecture for autonomous payments with enough implementation substance to support a serious systems or security publication. The work now is not inventing the idea. The work is packaging the idea with the right degree of formalism, evidence, and precision.