From 93b8dac56891d52064e801a752d553ea8d5feca9 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 22 Apr 2026 15:28:34 +0100 Subject: [PATCH 1/3] research paper --- docs/OmniClaw_Whitepaper_v2.md | 421 ++++++++++++++++++++++++++++ docs/RESEARCH_EVIDENCE_MATRIX.md | 219 +++++++++++++++ docs/RESEARCH_THESIS_AND_OUTLINE.md | 400 ++++++++++++++++++++++++++ 3 files changed, 1040 insertions(+) create mode 100644 docs/OmniClaw_Whitepaper_v2.md create mode 100644 docs/RESEARCH_EVIDENCE_MATRIX.md create mode 100644 docs/RESEARCH_THESIS_AND_OUTLINE.md diff --git a/docs/OmniClaw_Whitepaper_v2.md b/docs/OmniClaw_Whitepaper_v2.md new file mode 100644 index 0000000..9a3e716 --- /dev/null +++ b/docs/OmniClaw_Whitepaper_v2.md @@ -0,0 +1,421 @@ +# OmniClaw Whitepaper v2 Draft + +## OmniClaw: Policy-Constrained Financial Execution for Autonomous Agents + +Version: v2 draft +Status: research and publication draft +Prepared from the implemented OmniClaw artifact and supporting documentation + +--- + +## Abstract + +Autonomous agents increasingly need to buy compute, data, API access, and machine services without human intervention, but existing payment infrastructure exposes execution primitives rather than safe authority models. In most current designs, a wallet, key, or provider credential is placed close to the agent itself, which turns prompt injection, tool misuse, concurrency bugs, stale policy, and timeout ambiguity into direct financial risk. OmniClaw addresses this problem with a policy-constrained financial execution control plane that separates agent intent from settlement authority. The system enforces operator-defined policy before funds move, binds execution to an approved intent, uses durable intent and reservation state to avoid duplicate settlement and budget overcommitment, treats uncertain settlement outcomes as explicit reconciliation cases rather than retryable failures, and supports differentiated policy by counterparty type and settlement finality. Unlike wallet-only or settlement-only approaches, OmniClaw provides an authority model suitable for autonomous payments across heterogeneous rails while preserving auditability and bounded operational risk. + +--- + +## 1. Introduction + +Autonomous software agents are no longer limited to search, retrieval, and workflow orchestration. They increasingly need to perform economic actions: purchasing model inference, paying for proprietary APIs, settling service fees, compensating downstream tools, or collecting revenue for machine-exposed endpoints. This shift creates a structural mismatch between what modern payment rails provide and what autonomous systems need. + +Most payment infrastructure answers the question of how to move money. It does not answer the harder question of when autonomous software should be allowed to move money. + +If an agent holds direct wallet authority or raw settlement credentials, then ordinary software failures become treasury-risk events. A hallucinated tool call can authorize a payment. A prompt injection can redirect a transfer. Concurrent workers can overcommit a shared budget. A timeout can produce an unknown outcome, yet naive retry logic may replay the payment. A stale approval can remain valid after an emergency policy update. An external autonomous counterparty can generate requests at machine speed with little human accountability. In short, the problem is not only settlement. It is authority, policy, and failure semantics. + +OmniClaw is designed as a control-plane answer to that problem. It places an explicit policy and stateful decision layer between agent intent and settlement execution. Agents may request economic actions. Operators define policy envelopes. The control layer evaluates whether a specific payment is permitted. The execution layer settles only an approved, bound intent. This separation preserves agent autonomy while avoiding the direct-wallet model that collapses authority and execution into the same compromise surface. + +The central claim of this paper is that autonomous financial execution requires more than keys and settlement adapters. It requires a control architecture with explicit trust boundaries, policy semantics, state semantics, idempotency, concurrency control, and auditability. + +--- + +## 2. Problem Statement + +The core research question is: + +How can an autonomous system perform economic actions at machine speed while preserving bounded authority, concurrency safety, failure-aware settlement semantics, and post-hoc auditability across heterogeneous payment rails? + +This question breaks into a set of concrete systems requirements: + +1. Agents must be able to request payments without directly controlling settlement credentials. +2. Policy must be evaluated before funds move, not after. +3. Approved execution must be bound to exact validated parameters. +4. Concurrent workers must not overspend a shared budget. +5. Timeouts and partial execution must not be treated as known failures. +6. Policy updates must not silently invalidate or weaken in-flight approvals. +7. Counterparty type and settlement finality must alter the control path. +8. All decisions and state changes must be reconstructible after the fact. + +Traditional wallet or settlement APIs satisfy only a subset of these requirements. They answer how to sign or submit a payment, but not how to govern an autonomous actor that wants to pay. + +--- + +## 3. Threat And Failure Model + +OmniClaw is built for the following failure and threat classes. + +### 3.1 Compromised Agent Runtime + +An agent may be compromised through prompt injection, tool misuse, dependency compromise, or operator error. The critical question is whether that compromise becomes direct settlement authority or remains bounded by external controls. + +### 3.2 Retry And Timeout Ambiguity + +A provider call can time out after settlement may already have started. In such a case, the system does not know whether replay would duplicate a valid payment or recover from a failed one. + +### 3.3 Budget Overcommitment Under Concurrency + +Multiple workers sharing a budget or wallet can independently observe available capacity and each authorize spending, even though the aggregate spend exceeds the true remaining budget. + +### 3.4 Policy Races + +A payment intent can be evaluated under one policy version and later executed after a revocation, freeze, destination change, or emergency operator action. + +### 3.5 Adversarial Counterparties + +External recipients may attempt redirection, replay, false success claims, or exploit the system’s inability to distinguish trusted from high-variance counterparties. + +### 3.6 Parameter Tampering Between Approval And Execution + +If approval says “something like this is allowed” but execution can vary amount, destination, or context, then policy can be bypassed after the fact. + +The threat model does not assume perfect network reliability, perfectly synchronized clocks, or exactly-once message delivery. It assumes durable storage and enforceable component separation, but not a perfect environment. + +--- + +## 4. Design Goals + +OmniClaw is built around five design goals. + +### G1. Separate Intent From Settlement + +Agents should be able to request economic action without holding the authority to directly settle it. + +### G2. Make Policy First-Class + +Policy should be explicit, versioned, and evaluated against every payment request before execution. + +### G3. Treat Uncertain Outcomes As A Distinct State + +Timeouts and ambiguous provider results should not be collapsed into failure or success. They should enter reconciliation. + +### G4. Preserve Correctness Under Concurrency + +Shared budgets and wallet capacity must be enforced against aggregate in-flight usage, not only local read-time observations. + +### G5. Make Authorization Auditable + +A later reviewer should be able to reconstruct which agent requested a payment, which policy version authorized it, which execution attempt ran, and why the system took the path it did. + +--- + +## 5. Architecture Overview + +OmniClaw decomposes the payment authority problem into explicit components. + +### 5.1 Agent Runtime + +The agent runtime creates attributable payment intents. It does not hold settlement keys or direct settlement authority. + +### 5.2 Payment Control Service + +The control service evaluates intents against policy, decides whether to allow, block, or escalate, and creates bound execution authorizations for approved intents. + +### 5.3 Versioned Policy Store + +The policy store contains operator-defined rules, limits, recipient controls, confirmation thresholds, and trust-related policy inputs. Each executable intent must be associated with the policy version or snapshot used during evaluation. + +### 5.4 Approval Service + +High-risk intents are routed to a higher-trust approval path rather than executed automatically. + +### 5.5 Payment-Intent Ledger + +The ledger stores durable intent state and attempt state. It is the source of truth for lifecycle and reconciliation. + +### 5.6 Execution Service + +The execution service is the only component permitted to trigger settlement. It uses provider integrations, facilitators, or rail-specific adapters, but only for already-approved intents. + +### 5.7 Audit Layer + +Every control decision and state transition is logged with enough metadata to reconstruct the authorization chain. The log must be append-only and tamper-evident. + +--- + +## 6. Trust Boundary And Execution Binding + +The core trust decision in OmniClaw is that the authority to request payment is not the same as the authority to settle payment. + +Agents authenticate to the control layer through workload identity, mTLS, or another non-exportable service credential. Approval does not itself move funds. Instead, the control service mints a short-lived execution authorization that binds the exact intent ID, amount, destination, policy version, and expiry. The execution layer must reject any settlement request that lacks a valid authorization or that does not match the bound parameters. + +This design produces a stronger invariant than “the execution layer is separate.” It produces: + +Only the exact approved intent may be settled, and only by the execution layer. + +Settlement signing material should be accessible only to the execution layer through non-exportable custody such as HSM or KMS-backed keys. Network isolation should also prevent agent runtimes from directly reaching settlement endpoints. + +--- + +## 7. Payment-Intent State Semantics + +OmniClaw requires explicit financial state rather than vague “pending” flags. + +The current implementation exposes payment-intent lifecycle support and tests legal transition behavior, while the task-derived architecture sharpens the desired semantics into a stricter operational model: + +- Submitted +- PolicyEvaluating +- AwaitingApproval +- ApprovedForExecution +- Executing +- ReconciliationRequired +- FailedTerminal +- Finalized + +The legal progression is intentionally narrow: + +Submitted -> 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 vendor + Standard vendor 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, seller, facilitator, 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/docs/RESEARCH_EVIDENCE_MATRIX.md b/docs/RESEARCH_EVIDENCE_MATRIX.md new file mode 100644 index 0000000..6f57197 --- /dev/null +++ b/docs/RESEARCH_EVIDENCE_MATRIX.md @@ -0,0 +1,219 @@ +# 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 vendors, 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: Seller-Side Replay Resistance + +Claim: +- Seller or facilitator-side consumed-proof handling can reduce replay acceptance. + +Code / Docs: +- whitepaper v1 discussion +- seller-side nonce handling in `src/omniclaw/seller/seller.py` + +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 new file mode 100644 index 0000000..d8c27a8 --- /dev/null +++ b/docs/RESEARCH_THESIS_AND_OUTLINE.md @@ -0,0 +1,400 @@ +# 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: vendor, 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 facilitators +- 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 +- facilitator 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. From d133cc5b7cda06b55a9e678be9e3db0256664644 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 22 Apr 2026 16:55:35 +0100 Subject: [PATCH 2/3] Harden x402 payments and CLI skill metadata --- docs/PRODUCTION_HARDENING.md | 2 + docs/agent-getting-started.md | 8 +- docs/agent-skills.md | 7 +- docs/cli-reference.md | 6 +- docs/omniclaw-cli-skill/SKILL.md | 43 +++++++- docs/omniclaw-cli-skill/agents/openai.yaml | 6 + .../references/cli-reference.md | 6 +- .../scripts/generate_cli_reference.py | 6 +- docs/operator-cli.md | 4 +- src/omniclaw/agent/routes.py | 10 +- src/omniclaw/core/config.py | 4 +- src/omniclaw/protocols/gateway.py | 2 +- .../protocols/nanopayments/adapter.py | 28 ++++- .../protocols/nanopayments/middleware.py | 46 ++++++++ src/omniclaw/protocols/nanopayments/types.py | 18 +++ src/omniclaw/protocols/transfer.py | 2 +- src/omniclaw/protocols/x402.py | 94 +++++++++++++++- src/omniclaw/seller/seller.py | 69 +++++++++++- tests/test_client.py | 2 +- tests/test_config.py | 12 ++ tests/test_facilitator_integration.py | 8 ++ tests/test_nanopayments_middleware.py | 104 ++++++++++++++++++ tests/test_payment_router.py | 11 +- tests/test_seller_side.py | 83 +++++++++++--- tests/test_x402_sdk_adapter.py | 57 ++++++++++ 25 files changed, 594 insertions(+), 44 deletions(-) create mode 100644 docs/omniclaw-cli-skill/agents/openai.yaml diff --git a/docs/PRODUCTION_HARDENING.md b/docs/PRODUCTION_HARDENING.md index 62b4f9d..ef90e0d 100644 --- a/docs/PRODUCTION_HARDENING.md +++ b/docs/PRODUCTION_HARDENING.md @@ -16,6 +16,8 @@ OMNICLAW_WEBHOOK_DEDUP_DB_PATH=/var/lib/omniclaw/webhook_dedup.sqlite3 Startup fails fast if these are missing or if strict settlement is disabled. +For non-production package usage, `OMNICLAW_STRICT_SETTLEMENT` defaults to `false` so compatible x402 resources can still unlock even when a seller omits or delays settlement response metadata. Production deployments must opt into strict settlement explicitly. + ## Webhook Security Model - Signature verification is enforced when `OMNICLAW_WEBHOOK_VERIFICATION_KEY` is configured. diff --git a/docs/agent-getting-started.md b/docs/agent-getting-started.md index 37597a2..7cb68ab 100644 --- a/docs/agent-getting-started.md +++ b/docs/agent-getting-started.md @@ -212,11 +212,13 @@ If an agent wants to temporarily sell access to a local Python script or data fi omniclaw-cli serve \ --price 0.01 \ --endpoint /api/data \ - --exec "python my_service.py" \ + --exec "python safe_readonly_service.py" \ --port 8000 ``` -This opens `http://localhost:8000/api/data` that requires a USDC payment to execute `my_service.py` and return its output. +This opens `http://localhost:8000/api/data` that requires a USDC payment to execute the approved command and return its output. + +`serve` binds to all interfaces and `--exec` runs a host command. Use it only when the owner explicitly wants an agent-run seller endpoint, and prefer an isolated container or private development network. > **Web developer or vendor:** If the paid route lives inside your application, use the Python SDK inside your FastAPI application instead of `omniclaw-cli serve`. Use `serve` when the seller surface itself is agent-run. See the [Developer Guide](developer-guide.md). @@ -235,7 +237,7 @@ This opens `http://localhost:8000/api/data` that requires a USDC payment to exec | `omniclaw-cli inspect-x402 --recipient URL` | Inspect seller requirements and buyer readiness | | `omniclaw-cli pay --recipient 0x... --amount X` | Pay another agent | | `omniclaw-cli pay --recipient URL` | Pay a seller x402 endpoint | -| `omniclaw-cli serve --price X --endpoint /api --exec "cmd"` | Start payment gate | +| `omniclaw-cli serve --price X --endpoint /api --exec "cmd"` | Start an owner-approved agent-run payment gate | --- diff --git a/docs/agent-skills.md b/docs/agent-skills.md index 4728726..41af2f0 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -149,11 +149,13 @@ Useful buyer commands: ### Expose a paid endpoint +Only use this path when the owner explicitly wants an agent-run seller endpoint. Vendor and enterprise APIs should use the SDK seller middleware instead. + ```bash omniclaw-cli serve \ --price 0.01 \ --endpoint /api/data \ - --exec "python app.py" \ + --exec "python safe_readonly_service.py" \ --port 8000 ``` @@ -169,6 +171,9 @@ Important implementation detail: - `serve` binds to `0.0.0.0` - the banner may print `localhost`, but the actual bind host is all interfaces +- `--exec` runs the supplied host command after payment verification +- do not invent the `--exec` command; run only a command supplied or approved by the owner +- prefer an isolated container or private development network for `serve` Useful seller commands: diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 6426824..8e6249d 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -17,7 +17,9 @@ python3 docs/omniclaw-cli-skill/scripts/generate_cli_reference.py - use `balance-detail` when Gateway state matters - use `--idempotency-key` for job-based payments - for x402 URLs, `--amount` can be omitted because the payment requirements come from the seller endpoint +- `serve` is for owner-approved agent-run seller endpoints only - `serve` binds to `0.0.0.0` even if the banner prints `localhost` +- `serve --exec` runs a host command; do not invent the command or expose it outside an isolated runtime ## Example Flows @@ -45,10 +47,12 @@ Seller exposing a paid endpoint: omniclaw-cli serve \ --price 0.01 \ --endpoint /api/data \ - --exec "python app.py" \ + --exec "python safe_readonly_service.py" \ --port 8000 ``` +Only run this after the owner explicitly asks for an agent-run seller endpoint and supplies or approves the `--exec` command. + ## Live Help Output ### `omniclaw-cli --help` diff --git a/docs/omniclaw-cli-skill/SKILL.md b/docs/omniclaw-cli-skill/SKILL.md index 976fcb6..a39d183 100644 --- a/docs/omniclaw-cli-skill/SKILL.md +++ b/docs/omniclaw-cli-skill/SKILL.md @@ -2,12 +2,14 @@ name: omniclaw description: > Use this skill whenever an agent needs to pay for an x402 URL, transfer USDC - to an address, inspect OmniClaw balances or ledger entries, or expose a - paid endpoint for other agents or automation with omniclaw-cli serve. OmniClaw is the + to an address, inspect OmniClaw balances or ledger entries, or explicitly + expose a paid endpoint for other agents or automation with omniclaw-cli serve. + OmniClaw is the Economic Execution and Control Layer for Agentic Systems. The CLI is the zero-trust execution layer for agents. Use this skill for the CLI execution path only, not for vendor SDK integration, owner setup, policy editing, wallet provisioning, or Financial Policy Engine administration. +metadata: '{"openclaw":{"requires":{"bins":["omniclaw-cli"],"env":["OMNICLAW_SERVER_URL","OMNICLAW_TOKEN"]},"primaryEnv":"OMNICLAW_TOKEN","required_env":["OMNICLAW_SERVER_URL","OMNICLAW_TOKEN"],"optional_env":["OMNICLAW_OWNER_TOKEN"],"required_secrets":["OMNICLAW_TOKEN"],"optional_secrets":["OMNICLAW_OWNER_TOKEN"],"required_binaries":["omniclaw-cli"],"network_access":"required","data_access":"payment URLs, recipient addresses, balances, ledger entries, and paid endpoint responses only when requested","security_notes":"Requires a trusted OmniClaw Financial Policy Engine URL and scoped agent token. OMNICLAW_OWNER_TOKEN is optional and must only be provided intentionally for owner approvals. omniclaw-cli serve binds to 0.0.0.0 and --exec runs a host command, so serve/--exec must only be used after an explicit owner request, preferably inside an isolated runtime."}}' requires: - env: OMNICLAW_SERVER_URL description: > @@ -17,7 +19,7 @@ requires: description: > Scoped agent token. Never print, log, or transmit it. If missing, stop and notify the owner. -version: 0.0.6 +version: 0.0.8 author: Omnuron AI --- @@ -31,7 +33,7 @@ Use `omniclaw-cli` only when the task is directly about one of these actions: - transfer USDC to an address - inspect wallet, Gateway, or Circle balances - inspect transaction history -- expose a paid endpoint for other agents or automation with `serve` +- expose a paid endpoint for other agents or automation with `serve`, only when the owner explicitly asks for it Do not use this skill for: @@ -62,6 +64,20 @@ Vendor and enterprise seller APIs should use the Python SDK with `client.sell(.. The agent does not control the private key. The Financial Policy Engine enforces policy and signs allowed actions. +## Dependency and Credential Contract + +The runtime must have: + +- `omniclaw-cli` installed from the official OmniClaw package +- `OMNICLAW_SERVER_URL` pointing to the trusted Financial Policy Engine +- `OMNICLAW_TOKEN` scoped to the agent wallet/policy + +Optional: + +- `OMNICLAW_OWNER_TOKEN`, only when the owner intentionally grants approval authority for this run + +Never print tokens, write tokens into generated files, or pass tokens to third-party services. + ## Inputs The Agent Should Expect The runtime should normally provide either: @@ -108,8 +124,21 @@ Do not invent or search for them yourself. ### For agent-run seller tasks 1. Inspect current state with `balance-detail`. -2. Start the paid endpoint with `omniclaw-cli serve`. -3. Remember that `serve` binds to `0.0.0.0` even if the banner prints `localhost`. +2. Confirm the owner explicitly asked this agent to expose a paid endpoint. +3. Start the paid endpoint with `omniclaw-cli serve` only for the approved endpoint, price, command, and port. +4. Remember that `serve` binds to `0.0.0.0` even if the banner prints `localhost`. + +## Serve Safety Rules + +`omniclaw-cli serve` is powerful because it starts a network-accessible service and requires `--exec`. + +Rules: + +- do not run `serve` unless the owner explicitly requested a seller endpoint in the current task +- do not invent the `--exec` command +- do not use `--exec` for shell pipelines, downloads, package installs, destructive commands, or credential access +- prefer an isolated container or private development network for `serve` +- disclose the port and endpoint before treating the service as ready ## Approval Handling @@ -134,6 +163,8 @@ Stop and notify the owner if any of these happen: - `pay` returns a policy or guard rejection - available or Gateway balance is insufficient - the exact command or flag is unclear +- `serve` is requested without an explicit owner instruction +- `serve --exec` is requested but the command is not supplied or approved by the owner ## Command Reference diff --git a/docs/omniclaw-cli-skill/agents/openai.yaml b/docs/omniclaw-cli-skill/agents/openai.yaml new file mode 100644 index 0000000..b38b28a --- /dev/null +++ b/docs/omniclaw-cli-skill/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "OmniClaw CLI" + short_description: "Policy-controlled agent payments through omniclaw-cli." + default_prompt: "Use $omniclaw to inspect balances, check policy, or pay an x402 URL through the OmniClaw CLI." +policy: + allow_implicit_invocation: false diff --git a/docs/omniclaw-cli-skill/references/cli-reference.md b/docs/omniclaw-cli-skill/references/cli-reference.md index a729bea..e466762 100644 --- a/docs/omniclaw-cli-skill/references/cli-reference.md +++ b/docs/omniclaw-cli-skill/references/cli-reference.md @@ -17,7 +17,9 @@ python3 scripts/generate_cli_reference.py - use `balance-detail` when Gateway state matters - use `--idempotency-key` for job-based payments - for x402 URLs, `--amount` can be omitted because the payment requirements come from the seller endpoint +- `serve` is for owner-approved agent-run seller endpoints only - `serve` binds to `0.0.0.0` even if the banner prints `localhost` +- `serve --exec` runs a host command; do not invent the command or expose it outside an isolated runtime ## Example Flows @@ -45,10 +47,12 @@ Seller exposing a paid endpoint: omniclaw-cli serve \ --price 0.01 \ --endpoint /api/data \ - --exec "python app.py" \ + --exec "python safe_readonly_service.py" \ --port 8000 ``` +Only run this after the owner explicitly asks for an agent-run seller endpoint and supplies or approves the `--exec` command. + ## Live Help Output ### `omniclaw-cli --help` diff --git a/docs/omniclaw-cli-skill/scripts/generate_cli_reference.py b/docs/omniclaw-cli-skill/scripts/generate_cli_reference.py index 0a1a753..6ca2857 100755 --- a/docs/omniclaw-cli-skill/scripts/generate_cli_reference.py +++ b/docs/omniclaw-cli-skill/scripts/generate_cli_reference.py @@ -77,7 +77,9 @@ - use `balance-detail` when Gateway state matters - use `--idempotency-key` for job-based payments - for x402 URLs, `--amount` can be omitted because the payment requirements come from the seller endpoint +- `serve` is for owner-approved agent-run seller endpoints only - `serve` binds to `0.0.0.0` even if the banner prints `localhost` +- `serve --exec` runs a host command; do not invent the command or expose it outside an isolated runtime ## Example Flows @@ -105,10 +107,12 @@ omniclaw-cli serve \\ --price 0.01 \\ --endpoint /api/data \\ - --exec "python app.py" \\ + --exec "python safe_readonly_service.py" \\ --port 8000 ``` +Only run this after the owner explicitly asks for an agent-run seller endpoint and supplies or approves the `--exec` command. + ## Live Help Output """ diff --git a/docs/operator-cli.md b/docs/operator-cli.md index d6bba1a..f11fbe4 100644 --- a/docs/operator-cli.md +++ b/docs/operator-cli.md @@ -24,11 +24,13 @@ Use `omniclaw-cli` when an agent is performing constrained financial actions: omniclaw-cli can-pay --recipient https://seller.example.com/compute omniclaw-cli inspect-x402 --recipient https://seller.example.com/compute omniclaw-cli pay --recipient https://seller.example.com/compute --idempotency-key job-123 -omniclaw-cli serve --price 0.01 --endpoint /api/data --exec "python app.py" +omniclaw-cli serve --price 0.01 --endpoint /api/data --exec "python safe_readonly_service.py" ``` `omniclaw-cli serve` is the agent-facing seller surface. Use it when an agent needs to expose a paid endpoint for other agents or automation. Vendor and enterprise APIs that live inside application code should use the Python SDK seller middleware (`client.sell(...)`) instead. +`serve` binds to all interfaces and `--exec` runs the supplied host command. Treat it as an explicit owner-approved action, preferably in an isolated runtime. + ## Responsibility Split The infrastructure CLI manages trusted configuration and settlement services. The agent CLI executes through the Financial Policy Engine and never needs raw wallet authority. diff --git a/src/omniclaw/agent/routes.py b/src/omniclaw/agent/routes.py index 6a73dab..c3cd987 100644 --- a/src/omniclaw/agent/routes.py +++ b/src/omniclaw/agent/routes.py @@ -46,7 +46,10 @@ def _fmt_amount(value: object) -> str: try: - return f"{Decimal(str(value)).quantize(Decimal('0.01'))}" + amount = Decimal(str(value)) + if amount.copy_abs() < Decimal("0.01") and amount != 0: + return f"{amount.quantize(Decimal('0.000001'))}".rstrip("0").rstrip(".") + return f"{amount.quantize(Decimal('0.01'))}" except Exception: return str(value) @@ -1333,6 +1336,11 @@ async def x402_verify( return {"valid": False, "error": "Nanopayment client not initialized"} sig_data = json.loads(base64.b64decode(request.signature)) + if int(sig_data.get("x402Version", 2)) == 2 and not sig_data.get("accepted"): + return { + "valid": False, + "error": "Missing accepted requirements in PAYMENT-SIGNATURE payload", + } from omniclaw.protocols.nanopayments.middleware import GatewayMiddleware from omniclaw.protocols.nanopayments.types import PaymentPayload, PaymentRequirements diff --git a/src/omniclaw/core/config.py b/src/omniclaw/core/config.py index fff9ea1..64528c1 100644 --- a/src/omniclaw/core/config.py +++ b/src/omniclaw/core/config.py @@ -91,7 +91,7 @@ class Config: nanopayments_private_key: str | None = None """Raw EOA private key for direct nanopayment signing (no vault needed).""" - payment_strict_settlement: bool = True + payment_strict_settlement: bool = False """If true, success=True is emitted only for irreversible settlement.""" auto_reconcile_pending_settlements: bool = False @@ -190,7 +190,7 @@ def override_or_env(name: str, env_name: str, default: Any = None) -> Any: payment_strict_settlement = ( overrides.get("payment_strict_settlement") if "payment_strict_settlement" in overrides - else (str(_get_env_var("OMNICLAW_STRICT_SETTLEMENT", "true")).lower() == "true") + else (str(_get_env_var("OMNICLAW_STRICT_SETTLEMENT", "false")).lower() == "true") ) auto_reconcile_pending_settlements = ( overrides.get("auto_reconcile_pending_settlements") diff --git a/src/omniclaw/protocols/gateway.py b/src/omniclaw/protocols/gateway.py index d23aa03..87321cd 100644 --- a/src/omniclaw/protocols/gateway.py +++ b/src/omniclaw/protocols/gateway.py @@ -80,7 +80,7 @@ async def execute( """Execute a cross-chain transfer.""" source_network = source_network or self._config.network - strict_settlement = bool(getattr(self._config, "payment_strict_settlement", True)) + strict_settlement = bool(getattr(self._config, "payment_strict_settlement", False)) canonical_idempotency_key = idempotency_key or derive_idempotency_key( "gateway", wallet_id, diff --git a/src/omniclaw/protocols/nanopayments/adapter.py b/src/omniclaw/protocols/nanopayments/adapter.py index c630f0f..379c74f 100644 --- a/src/omniclaw/protocols/nanopayments/adapter.py +++ b/src/omniclaw/protocols/nanopayments/adapter.py @@ -229,7 +229,7 @@ def __init__( circuit_breaker: NanopaymentCircuitBreaker | None = None, retry_attempts: int = 3, retry_base_delay: float = 0.5, - strict_settlement: bool = True, + strict_settlement: bool = False, ) -> None: self._signer = signer self._network = network @@ -322,6 +322,23 @@ def _sign( ) return payload + @staticmethod + def _encode_payment_signature_header( + payload: PaymentPayload, + accepted: PaymentRequirementsKind, + ) -> str: + """ + Encode the x402 v2 payment header sent back to the seller. + + x402 v2 requires the retry payload to include the selected `accepted` + requirement. Circle settlement also receives this requirement separately, + but external sellers validate the raw PAYMENT-SIGNATURE header before + accepting the paid retry. + """ + payment_payload = payload.to_dict() + payment_payload["accepted"] = accepted.to_dict() + return base64.b64encode(json.dumps(payment_payload).encode("utf-8")).decode("ascii") + # ------------------------------------------------------------------------- # x402 URL payment # ------------------------------------------------------------------------- @@ -508,9 +525,10 @@ async def pay_x402_url( ) # Step 8: Retry with payment header - payment_sig_header = base64.b64encode( - json.dumps(payload.to_dict()).encode("utf-8"), - ).decode("ascii") + payment_sig_header = self._encode_payment_signature_header( + payload=payload, + accepted=updated_kind, + ) retry_headers = dict(headers) retry_headers["PAYMENT-SIGNATURE"] = payment_sig_header @@ -1008,7 +1026,7 @@ async def execute( Returns: PaymentResult with nanopayment details. """ - strict_settlement = bool(getattr(self._adapter, "_strict_settlement", True)) + strict_settlement = bool(getattr(self._adapter, "_strict_settlement", False)) try: if _is_url(recipient): result = await self._adapter.pay_x402_url( diff --git a/src/omniclaw/protocols/nanopayments/middleware.py b/src/omniclaw/protocols/nanopayments/middleware.py index a42ccce..2aa724d 100644 --- a/src/omniclaw/protocols/nanopayments/middleware.py +++ b/src/omniclaw/protocols/nanopayments/middleware.py @@ -401,6 +401,10 @@ def _parse_payment_signature( try: decoded = base64.b64decode(header_value) data = json.loads(decoded) + if int(data.get("x402Version", X402_VERSION)) == X402_VERSION and not data.get( + "accepted" + ): + raise ValueError("Missing accepted requirements in PAYMENT-SIGNATURE payload") return PaymentPayload.from_dict(data) except Exception as exc: raise ValueError(f"Failed to parse PAYMENT-SIGNATURE: {exc}") from exc @@ -488,6 +492,31 @@ async def handle( }, headers={}, ) + accepted = payload.accepted.to_dict() if payload.accepted else None + if not accepted: + raise PaymentRequiredHTTPError( + status_code=402, + detail={"error": "Missing accepted requirements in PAYMENT-SIGNATURE payload"}, + headers={}, + ) + if str(accepted.get("network", "")) != payload.network: + raise PaymentRequiredHTTPError( + status_code=402, + detail={"error": "Accepted requirements network does not match payload"}, + headers={}, + ) + if str(accepted.get("amount", "")) != expected_amount: + raise PaymentRequiredHTTPError( + status_code=402, + detail={"error": "Accepted requirements amount does not match price"}, + headers={}, + ) + if str(accepted.get("payTo", "")).lower() != self._seller_address.lower(): + raise PaymentRequiredHTTPError( + status_code=402, + detail={"error": "Accepted requirements payTo does not match seller"}, + headers={}, + ) # Build requirements from payload from omniclaw.protocols.nanopayments.types import ( PaymentRequirementsExtra, @@ -530,6 +559,12 @@ async def handle( detail={"error": f"Missing contract addresses for network {payload.network}"}, headers={}, ) + if str(accepted.get("asset", "")).lower() != usdc_address.lower(): + raise PaymentRequiredHTTPError( + status_code=402, + detail={"error": "Accepted requirements asset does not match network"}, + headers={}, + ) if self._uses_gateway_batched_scheme(): if not verifying_contract: @@ -540,6 +575,17 @@ async def handle( }, headers={}, ) + accepted_extra = accepted.get("extra") or {} + if str(accepted_extra.get("verifyingContract", "")).lower() != ( + verifying_contract.lower() + ): + raise PaymentRequiredHTTPError( + status_code=402, + detail={ + "error": "Accepted requirements verifying contract does not match network" + }, + headers={}, + ) gateway_kind = PaymentRequirementsKind( scheme="exact", network=payload.network, diff --git a/src/omniclaw/protocols/nanopayments/types.py b/src/omniclaw/protocols/nanopayments/types.py index 44170e8..379e06c 100644 --- a/src/omniclaw/protocols/nanopayments/types.py +++ b/src/omniclaw/protocols/nanopayments/types.py @@ -382,6 +382,15 @@ class PaymentPayload: Required by Circle Gateway. Set this to identify the resource being accessed. """ + accepted: PaymentRequirementsKind | dict[str, Any] | None = None + """ + Selected x402 v2 payment requirement. + + External sellers and facilitators validate this field from the raw retry + payload. It must identify the exact requirement selected from the seller's + PAYMENT-REQUIRED accepts array. + """ + def to_dict(self) -> dict[str, Any]: """Convert to dict for JSON serialization.""" result: dict[str, Any] = { @@ -392,6 +401,12 @@ def to_dict(self) -> dict[str, Any]: } if self.resource is not None: result["resource"] = self.resource.to_dict() + if self.accepted is not None: + result["accepted"] = ( + self.accepted.to_dict() + if hasattr(self.accepted, "to_dict") + else dict(self.accepted) + ) return result @classmethod @@ -399,12 +414,15 @@ def from_dict(cls, data: dict[str, Any]) -> PaymentPayload: """Create from dict parsed from JSON.""" resource_data = data.get("resource") resource = ResourceInfo.from_dict(resource_data) if resource_data else None + accepted_data = data.get("accepted") + accepted = PaymentRequirementsKind.from_dict(accepted_data) if accepted_data else None return cls( x402_version=data.get("x402Version", 2), scheme=data.get("scheme", "exact"), network=data.get("network", ""), payload=PaymentPayloadInner.from_dict(data.get("payload", {})), resource=resource, + accepted=accepted, ) diff --git a/src/omniclaw/protocols/transfer.py b/src/omniclaw/protocols/transfer.py index 3faf9db..7feb11e 100644 --- a/src/omniclaw/protocols/transfer.py +++ b/src/omniclaw/protocols/transfer.py @@ -155,7 +155,7 @@ async def execute( error=transfer_result.error, ) - strict_settlement = bool(getattr(self._config, "payment_strict_settlement", True)) + strict_settlement = bool(getattr(self._config, "payment_strict_settlement", False)) tx = transfer_result.transaction status = PaymentStatus.PENDING_SETTLEMENT if strict_settlement else PaymentStatus.PROCESSING if tx: diff --git a/src/omniclaw/protocols/x402.py b/src/omniclaw/protocols/x402.py index 84ac425..05a2106 100644 --- a/src/omniclaw/protocols/x402.py +++ b/src/omniclaw/protocols/x402.py @@ -7,6 +7,7 @@ import json import os import re +from contextlib import suppress from dataclasses import dataclass from decimal import Decimal from typing import TYPE_CHECKING, Any @@ -544,6 +545,80 @@ async def _capture_selection(ctx: Any) -> None: x402_client.on_after_payment_creation(_capture_selection) return x402HTTPClient(x402_client), selection_state + @staticmethod + def _payload_has_accepted(payment_payload: Any) -> bool: + accepted = getattr(payment_payload, "accepted", None) + if accepted is not None: + return True + if hasattr(payment_payload, "model_dump"): + try: + dumped = payment_payload.model_dump(by_alias=True, exclude_none=True) + return bool(dumped.get("accepted")) + except Exception: + return False + return False + + @classmethod + def _ensure_v2_payload_has_accepted( + cls, + *, + payment_payload: Any, + selected_requirements: Any, + payment_required: Any, + ) -> Any: + """ + Ensure x402 v2 retry payloads include the selected `accepted` requirement. + + x402 v2 makes PaymentPayload.accepted required. Some SDK/client + combinations have returned a signed payload object without that field, + which causes strict sellers to reject the retry request before + settlement. + """ + if getattr(payment_payload, "x402_version", 2) != 2: + return payment_payload + if cls._payload_has_accepted(payment_payload): + return payment_payload + + try: + from x402.schemas import PaymentPayload as X402PaymentPayload + + payload_data: dict[str, Any] + if hasattr(payment_payload, "model_dump"): + payload_data = payment_payload.model_dump(by_alias=True, exclude_none=True) + else: + payload_data = dict(getattr(payment_payload, "__dict__", {})) + + payload_data["accepted"] = selected_requirements + if not payload_data.get("resource"): + resource = getattr(payment_required, "resource", None) + if resource is not None: + payload_data["resource"] = resource + + return X402PaymentPayload.model_validate(payload_data) + except Exception: + with suppress(Exception): + payment_payload.accepted = selected_requirements + return payment_payload + + @staticmethod + def _selected_requirements_from_payload_or_response( + *, + payment_payload: Any, + payment_required: Any, + selection_state: dict[str, Any], + ) -> Any: + selected_requirements = selection_state.get("requirements") or getattr( + payment_payload, "accepted", None + ) + if selected_requirements is not None: + return selected_requirements + + accepts = getattr(payment_required, "accepts", None) + if accepts: + return accepts[0] + + raise ProtocolError("x402 payment payload did not include accepted requirements") + def _resolve_exact_balance_rpc_url(self, selected_network: Network | None) -> str | None: config_rpc_url = getattr(self._config, "rpc_url", None) config_network = _resolve_network(str(getattr(self._config, "network", "") or "")) @@ -653,7 +728,7 @@ async def execute( ) -> PaymentResult: """Execute an x402 exact payment using the upstream x402 SDK.""" url = recipient - strict_settlement = bool(getattr(self._config, "payment_strict_settlement", True)) + strict_settlement = bool(getattr(self._config, "payment_strict_settlement", False)) request_method = str(kwargs.get("http_method", kwargs.get("method", "GET"))).upper() request_headers = kwargs.get("request_headers") or kwargs.get("headers") request_json = kwargs.get("request_json") @@ -704,7 +779,16 @@ async def execute( agent_caip2, ) payment_payload = await x402_http_client.create_payment_payload(payment_required) - selected_requirements = selection_state.get("requirements") or payment_payload.accepted + selected_requirements = self._selected_requirements_from_payload_or_response( + payment_payload=payment_payload, + payment_required=payment_required, + selection_state=selection_state, + ) + payment_payload = self._ensure_v2_payload_has_accepted( + payment_payload=payment_payload, + selected_requirements=selected_requirements, + payment_required=payment_required, + ) required_amount = self._atomic_to_decimal(str(selected_requirements.amount)) payment_address = str(selected_requirements.pay_to) @@ -877,7 +961,11 @@ async def simulate( ) payment_required = x402_http_client.get_payment_required_response(get_header, body_data) payment_payload = await x402_http_client.create_payment_payload(payment_required) - selected_requirements = selection_state.get("requirements") or payment_payload.accepted + selected_requirements = self._selected_requirements_from_payload_or_response( + payment_payload=payment_payload, + payment_required=payment_required, + selection_state=selection_state, + ) required_amount = self._atomic_to_decimal(str(selected_requirements.amount)) result["would_succeed"] = True diff --git a/src/omniclaw/seller/seller.py b/src/omniclaw/seller/seller.py index eaf740e..c54ab86 100644 --- a/src/omniclaw/seller/seller.py +++ b/src/omniclaw/seller/seller.py @@ -70,6 +70,49 @@ def _usd_to_atomic(price_usd: Decimal) -> int: return int(atomic) +def _accepted_requirements_match( + payment_payload: dict[str, Any], + accepted: dict[str, Any], +) -> tuple[bool, str]: + """Validate x402 v2 payload.accepted against the server-selected requirement.""" + payload_accepted = payment_payload.get("accepted") + if int(payment_payload.get("x402Version", 2)) == 2 and not isinstance( + payload_accepted, dict + ): + return False, "Missing accepted requirements in PAYMENT-SIGNATURE payload" + if not isinstance(payload_accepted, dict): + return True, "" + + checks = ( + ("scheme", False, False), + ("network", False, False), + ("asset", True, True), + ("amount", False, False), + ("payTo", True, True), + ) + for requirement_field, casefold, optional in checks: + expected = accepted.get(requirement_field) + actual = payload_accepted.get(requirement_field) + if optional and (expected is None or actual is None): + continue + expected_text = str(expected) + actual_text = str(actual) + if casefold: + expected_text = expected_text.lower() + actual_text = actual_text.lower() + if actual_text != expected_text: + return False, f"Accepted requirements mismatch: {requirement_field}" + + expected_extra = accepted.get("extra") or {} + actual_extra = payload_accepted.get("extra") or {} + expected_contract = expected_extra.get("verifyingContract") + actual_contract = actual_extra.get("verifyingContract") + if expected_contract and str(actual_contract).lower() != str(expected_contract).lower(): + return False, "Accepted requirements mismatch: verifyingContract" + + return True, "" + + # ============================================================================= # TYPES # ============================================================================= @@ -515,6 +558,10 @@ def verify_payment( authorization = payment_data.get("authorization", {}) signature = payment_data.get("signature", "") + accepted_ok, accepted_error = _accepted_requirements_match(payment_payload, accepted) + if not accepted_ok: + return False, accepted_error, None + # Use Circle Gateway facilitator if available if self._facilitator: return self._verify_with_facilitator( @@ -636,6 +683,10 @@ async def verify_payment_async( authorization = payment_data.get("authorization", {}) signature = payment_data.get("signature", "") + accepted_ok, accepted_error = _accepted_requirements_match(payment_payload, accepted) + if not accepted_ok: + return False, accepted_error, None + # Use Circle Gateway facilitator if available if self._facilitator: payment_requirements = { @@ -928,8 +979,12 @@ def _select_accepted_for_payload( if not accepts: return None - payload_network = str(payload.get("network", "")) - payload_scheme = str(payload.get("scheme", "exact")).lower() + payload_accepted = payload.get("accepted") + if int(payload.get("x402Version", 2)) == 2 and not isinstance(payload_accepted, dict): + return None + payload_accepted = payload_accepted or {} + payload_network = str(payload_accepted.get("network", payload.get("network", ""))) + payload_scheme = str(payload_accepted.get("scheme", payload.get("scheme", "exact"))).lower() payload_data = payload.get("payload", {}) or {} auth = payload_data.get("authorization", {}) or {} payload_value = str(auth.get("value", "")) @@ -939,7 +994,17 @@ def _select_accepted_for_payload( continue if payload_network and payload_network != str(accepted.get("network", "")): continue + if str(payload_accepted.get("asset", accepted.get("asset", ""))).lower() != str( + accepted.get("asset", "") + ).lower(): + continue + if str(payload_accepted.get("payTo", accepted.get("payTo", ""))).lower() != str( + accepted.get("payTo", "") + ).lower(): + continue accepted_amount = str(accepted.get("amount", "0")) + if str(payload_accepted.get("amount", accepted_amount)) != accepted_amount: + continue if payload_value and int(payload_value) < int(accepted_amount): continue return accepted diff --git a/tests/test_client.py b/tests/test_client.py index a599f1d..bd9db09 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,8 +4,8 @@ Tests the main SDK entry point with per-wallet/wallet-set guards. """ -import os import inspect +import os import tempfile from decimal import Decimal from pathlib import Path diff --git a/tests/test_config.py b/tests/test_config.py index 71c61ca..23ecffa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -22,6 +22,7 @@ def test_create_config_directly(self) -> None: assert config.circle_api_key == "test_api_key_123" assert config.entity_secret == "test_entity_secret_456" assert config.network == Network.ETH # default + assert config.payment_strict_settlement is False def test_create_config_with_all_options(self) -> None: """Test creating config with all options.""" @@ -85,6 +86,17 @@ def test_from_env(self) -> None: assert config.payment_strict_settlement is True assert config.auto_reconcile_pending_settlements is True + def test_strict_settlement_defaults_to_false_for_non_production(self) -> None: + env_vars = { + "CIRCLE_API_KEY": "env_api_key", + "ENTITY_SECRET": "env_entity_secret", + } + + with patch.dict(os.environ, env_vars, clear=True): + config = Config.from_env() + + assert config.payment_strict_settlement is False + def test_from_env_missing_api_key_raises(self) -> None: """Test from_env raises when API key not set.""" env_vars = { diff --git a/tests/test_facilitator_integration.py b/tests/test_facilitator_integration.py index 89caeef..2446ca1 100644 --- a/tests/test_facilitator_integration.py +++ b/tests/test_facilitator_integration.py @@ -103,6 +103,8 @@ def test_verify_payment_with_facilitator(self): "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", "maxTimeoutSeconds": 345600, } + payment_payload["network"] = accepted["network"] + payment_payload["accepted"] = accepted is_valid, error, record = seller.verify_payment( payment_payload=payment_payload, @@ -150,6 +152,8 @@ def test_settle_payment_with_facilitator(self): "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", "maxTimeoutSeconds": 345600, } + payment_payload["network"] = accepted["network"] + payment_payload["accepted"] = accepted is_valid, error, record = seller.verify_payment( payment_payload=payment_payload, @@ -200,6 +204,8 @@ def test_facilitator_verification_failure(self): "amount": "1000", "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", } + payment_payload["network"] = accepted["network"] + payment_payload["accepted"] = accepted is_valid, error, record = seller.verify_payment( payment_payload=payment_payload, @@ -250,6 +256,8 @@ def test_facilitator_settlement_failure(self): "amount": "1000", "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", } + payment_payload["network"] = accepted["network"] + payment_payload["accepted"] = accepted is_valid, error, record = seller.verify_payment( payment_payload=payment_payload, diff --git a/tests/test_nanopayments_middleware.py b/tests/test_nanopayments_middleware.py index 67774d7..987541e 100644 --- a/tests/test_nanopayments_middleware.py +++ b/tests/test_nanopayments_middleware.py @@ -19,6 +19,7 @@ MAX_TIMEOUT_SECONDS, X402_VERSION, ) +from omniclaw.protocols.nanopayments.adapter import NanopaymentAdapter from omniclaw.protocols.nanopayments.client import NanopaymentClient from omniclaw.protocols.nanopayments.exceptions import InvalidPriceError from omniclaw.protocols.nanopayments.middleware import ( @@ -30,6 +31,8 @@ EIP3009Authorization, PaymentPayload, PaymentPayloadInner, + PaymentRequirementsExtra, + PaymentRequirementsKind, SupportedKind, ) @@ -271,6 +274,49 @@ async def test_payment_required_header_is_valid_base64(self): class TestHandle: + def test_payment_signature_header_includes_accepted_requirements(self): + """Buyer retry header must include x402 v2 accepted requirements.""" + authorization = EIP3009Authorization.create( + from_address="0x" + "a" * 40, + to="0x" + "b" * 40, + value="440", + valid_before=9999999999, + nonce="0x" + "c" * 64, + ) + payload = PaymentPayload( + x402_version=2, + scheme="exact", + network="eip155:11155111", + payload=PaymentPayloadInner( + signature="0x" + "d" * 130, + authorization=authorization, + ), + ) + accepted = PaymentRequirementsKind( + scheme="exact", + network="eip155:11155111", + asset="0x1c7d4b196cb0c7b01d743fbc6116a902379c7238", + amount="440", + max_timeout_seconds=345600, + pay_to="0x" + "b" * 40, + extra=PaymentRequirementsExtra( + name="GatewayWalletBatched", + version="1", + verifying_contract="0x" + "e" * 40, + ), + ) + + sig_header = NanopaymentAdapter._encode_payment_signature_header(payload, accepted) + decoded = json.loads(base64.b64decode(sig_header)) + + assert decoded["x402Version"] == 2 + assert decoded["scheme"] == "exact" + assert decoded["network"] == "eip155:11155111" + assert decoded["accepted"]["scheme"] == "exact" + assert decoded["accepted"]["network"] == "eip155:11155111" + assert decoded["accepted"]["amount"] == "440" + assert decoded["accepted"]["extra"]["name"] == "GatewayWalletBatched" + @pytest.mark.asyncio async def test_handle_without_payment_raises_402(self): """Request without PAYMENT-SIGNATURE header returns 402.""" @@ -313,6 +359,19 @@ async def test_handle_with_valid_payment_returns_payment_info(self): signature="0x" + "c" * 130, authorization=authorization, ), + accepted=PaymentRequirementsKind( + scheme="exact", + network="eip155:5042002", + asset="0xUsdcArcTestnet", + amount="1000", + max_timeout_seconds=345600, + pay_to="0x" + "a" * 40, + extra=PaymentRequirementsExtra( + name="GatewayWalletBatched", + version="1", + verifying_contract="0x" + "c" * 40, + ), + ), ) sig_header = base64.b64encode(json.dumps(payload.to_dict()).encode()).decode() @@ -331,6 +390,38 @@ async def test_handle_with_valid_payment_returns_payment_info(self): assert info.verified is True assert info.transaction == "batch-123" + @pytest.mark.asyncio + async def test_handle_rejects_v2_payment_without_accepted(self): + """OmniClaw seller must reject malformed x402 v2 retry payloads.""" + authorization = EIP3009Authorization.create( + from_address="0x" + "a" * 40, + to="0x" + "a" * 40, + value="1000", + valid_before=9999999999, + nonce="0x" + "b" * 64, + ) + payload = PaymentPayload( + x402_version=2, + scheme="exact", + network="eip155:5042002", + payload=PaymentPayloadInner( + signature="0x" + "c" * 130, + authorization=authorization, + ), + ) + sig_header = base64.b64encode(json.dumps(payload.to_dict()).encode()).decode() + + middleware = GatewayMiddleware( + seller_address="0x" + "a" * 40, + nanopayment_client=_make_client(), + supported_kinds=_make_kinds(), + ) + + with pytest.raises(PaymentRequiredHTTPError) as exc_info: + await middleware.handle({"payment-signature": sig_header}, "$0.001") + + assert "Missing accepted requirements" in exc_info.value.detail["error"] + @pytest.mark.asyncio async def test_handle_with_invalid_signature_raises_402(self): """Invalid PAYMENT-SIGNATURE header returns 402.""" @@ -389,6 +480,19 @@ async def test_non_circle_facilitator_settle_uses_standard_exact_requirements(se signature="0x" + "c" * 130, authorization=authorization, ), + accepted=PaymentRequirementsKind( + scheme="exact", + network="eip155:5042002", + asset="0xUsdcArcTestnet", + amount="1000", + max_timeout_seconds=345600, + pay_to="0x" + "a" * 40, + extra=PaymentRequirementsExtra( + name="", + version="", + verifying_contract="", + ), + ), ) sig_header = base64.b64encode(json.dumps(payload.to_dict()).encode()).decode() diff --git a/tests/test_payment_router.py b/tests/test_payment_router.py index 522d6fb..e823b0a 100644 --- a/tests/test_payment_router.py +++ b/tests/test_payment_router.py @@ -235,10 +235,19 @@ async def test_execute_derives_deterministic_idempotency_key( async def test_execute_pending_transfer_reports_pending_settlement( self, - transfer_adapter: TransferAdapter, + mock_config: Config, mock_wallet_service: MagicMock, ) -> None: """Non-final provider state should be in-flight, not irreversible success.""" + transfer_adapter = TransferAdapter( + Config( + circle_api_key=mock_config.circle_api_key, + entity_secret=mock_config.entity_secret, + network=mock_config.network, + payment_strict_settlement=True, + ), + mock_wallet_service, + ) mock_wallet_service.transfer.return_value = TransferResult( success=True, transaction=TransactionInfo( diff --git a/tests/test_seller_side.py b/tests/test_seller_side.py index 085778c..9852037 100644 --- a/tests/test_seller_side.py +++ b/tests/test_seller_side.py @@ -22,6 +22,15 @@ from omniclaw.seller import PaymentScheme, Seller, create_seller + +def _with_accepted(payload: dict, accepted: dict) -> dict: + """Attach required x402 v2 selected requirements to a test payment payload.""" + payload["x402Version"] = 2 + payload["accepted"] = accepted + payload.setdefault("network", accepted.get("network")) + return payload + + # ============================================================================= # TEST PRICE PARSING (Decimal precision — critical fix) # ============================================================================= @@ -262,7 +271,8 @@ def test_replay_nonce_is_rejected(self): seller.add_endpoint("/test", "$0.001") accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - payload = { + payload = _with_accepted( + { "scheme": "exact", "network": accepted["network"], "payload": { @@ -276,7 +286,9 @@ def test_replay_nonce_is_rejected(self): }, "signature": "", }, - } + }, + accepted, + ) is_valid, _, record = seller.verify_payment(payload, accepted, verify_signature=False) assert is_valid is True @@ -286,6 +298,39 @@ def test_replay_nonce_is_rejected(self): assert is_valid_2 is False assert "nonce" in err_2.lower() + def test_v2_payment_without_accepted_is_rejected(self): + import time + + seller = Seller( + seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", + name="Test", + ) + seller.add_endpoint("/test", "$0.001") + accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] + + payload = { + "x402Version": 2, + "scheme": "exact", + "network": accepted["network"], + "payload": { + "authorization": { + "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", + "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", + "value": "1000", + "validAfter": "0", + "validBefore": str(int(time.time()) + 300), + "nonce": "0x" + "22" * 32, + }, + "signature": "", + }, + } + + is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) + + assert is_valid is False + assert record is None + assert "Missing accepted requirements" in error + def test_strict_gateway_contract_mode_rejects_missing_contract(self, monkeypatch): monkeypatch.setenv("OMNICLAW_SELLER_STRICT_GATEWAY_CONTRACT", "true") monkeypatch.delenv("CIRCLE_GATEWAY_CONTRACT", raising=False) @@ -354,7 +399,8 @@ def test_basic_verify_timeout_check(self): endpoints = seller.get_endpoints() accepted = seller._create_accepts(endpoints["/test"])[0] - payload = { + payload = _with_accepted( + { "scheme": "exact", "payload": { "authorization": { @@ -367,7 +413,9 @@ def test_basic_verify_timeout_check(self): }, "signature": "", }, - } + }, + accepted, + ) is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) assert is_valid is False @@ -384,7 +432,8 @@ def test_basic_verify_wrong_recipient(self): seller.add_endpoint("/test", "$0.001") accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - payload = { + payload = _with_accepted( + { "scheme": "exact", "payload": { "authorization": { @@ -397,7 +446,9 @@ def test_basic_verify_wrong_recipient(self): }, "signature": "", }, - } + }, + accepted, + ) is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) assert is_valid is False @@ -414,7 +465,8 @@ def test_basic_verify_insufficient_amount(self): seller.add_endpoint("/test", "$0.001") accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - payload = { + payload = _with_accepted( + { "scheme": "exact", "payload": { "authorization": { @@ -427,7 +479,9 @@ def test_basic_verify_insufficient_amount(self): }, "signature": "", }, - } + }, + accepted, + ) is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) assert is_valid is False @@ -444,7 +498,8 @@ def test_basic_verify_valid_payment(self): seller.add_endpoint("/test", "$0.001") accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - payload = { + payload = _with_accepted( + { "scheme": "exact", "payload": { "authorization": { @@ -457,7 +512,9 @@ def test_basic_verify_valid_payment(self): }, "signature": "", }, - } + }, + accepted, + ) is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) assert is_valid is True @@ -492,7 +549,7 @@ async def test_verify_routes_to_facilitator(self): accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] is_valid, error, record = await seller.verify_payment_async( - {"scheme": "exact", "payload": {}}, + _with_accepted({"scheme": "exact", "payload": {}}, accepted), accepted, ) @@ -518,7 +575,7 @@ async def test_settle_routes_to_facilitator(self): accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] is_valid, error, record = await seller.verify_payment_async( - {"scheme": "exact", "payload": {}}, + _with_accepted({"scheme": "exact", "payload": {}}, accepted), accepted, settle_payment=True, ) @@ -545,7 +602,7 @@ async def test_facilitator_rejection_returns_error(self): accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] is_valid, error, record = await seller.verify_payment_async( - {"scheme": "exact", "payload": {}}, + _with_accepted({"scheme": "exact", "payload": {}}, accepted), accepted, ) diff --git a/tests/test_x402_sdk_adapter.py b/tests/test_x402_sdk_adapter.py index 34f9941..bddc74c 100644 --- a/tests/test_x402_sdk_adapter.py +++ b/tests/test_x402_sdk_adapter.py @@ -1,5 +1,7 @@ from __future__ import annotations +import base64 +import json from decimal import Decimal from types import SimpleNamespace from unittest.mock import AsyncMock @@ -76,6 +78,12 @@ async def handler(request: httpx.Request) -> httpx.Response: ) assert request.headers["PAYMENT-SIGNATURE"] + payment_payload = json.loads(base64.b64decode(request.headers["PAYMENT-SIGNATURE"])) + assert payment_payload["x402Version"] == 2 + assert payment_payload["accepted"]["scheme"] == "exact" + assert payment_payload["accepted"]["network"] == "eip155:84532" + assert payment_payload["accepted"]["amount"] == "250000" + assert payment_payload["accepted"]["payTo"] == "0x742d35Cc6634C0532925a3b844Bc454e4438f44e" return httpx.Response( 200, headers={"PAYMENT-RESPONSE": _make_payment_response_header(success=True)}, @@ -105,6 +113,55 @@ async def handler(request: httpx.Request) -> httpx.Response: assert calls[1].method == "POST" +def test_ensure_v2_payload_has_accepted_patches_missing_sdk_field(): + from x402.schemas import PaymentRequired, PaymentRequirements, ResourceInfo + + selected = PaymentRequirements( + scheme="exact", + network="eip155:84532", + asset="0x036CbD53842c5426634e7929541eC2318f3dCF7e", + amount="250000", + pay_to="0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + max_timeout_seconds=300, + extra={"name": "USD Coin", "version": "2"}, + ) + payment_required = PaymentRequired( + error="Payment Required", + resource=ResourceInfo( + url="https://seller.example/compute", + description="paid resource", + mime_type="application/json", + ), + accepts=[selected], + ) + incomplete_payload = SimpleNamespace( + x402_version=2, + payload={ + "authorization": { + "from": "0xCa7fc646D249442404e43e9ce3B9015241ABcCEE", + "to": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "value": "250000", + "validAfter": "1776868697", + "validBefore": "1776869597", + "nonce": "0x1cd5a89daa51bb8b7f113f02dd5a7c4b833e5a17e5b653d41e09eef8c1189524", + }, + "signature": "0xsignature", + }, + ) + + patched = X402Adapter._ensure_v2_payload_has_accepted( + payment_payload=incomplete_payload, + selected_requirements=selected, + payment_required=payment_required, + ) + + dumped = patched.model_dump(by_alias=True) + assert dumped["accepted"]["scheme"] == "exact" + assert dumped["accepted"]["network"] == "eip155:84532" + assert dumped["accepted"]["amount"] == "250000" + assert dumped["resource"]["url"] == "https://seller.example/compute" + + @pytest.mark.asyncio async def test_execute_returns_failed_final_when_seller_rejects_payment(): url = "https://seller.example/compute" From 3caf020a8d8cf48494539f2ad24dc9c0b0aff53c Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 22 Apr 2026 17:06:59 +0100 Subject: [PATCH 3/3] fix lint --- src/omniclaw/seller/seller.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/omniclaw/seller/seller.py b/src/omniclaw/seller/seller.py index c54ab86..322ac98 100644 --- a/src/omniclaw/seller/seller.py +++ b/src/omniclaw/seller/seller.py @@ -76,9 +76,7 @@ def _accepted_requirements_match( ) -> tuple[bool, str]: """Validate x402 v2 payload.accepted against the server-selected requirement.""" payload_accepted = payment_payload.get("accepted") - if int(payment_payload.get("x402Version", 2)) == 2 and not isinstance( - payload_accepted, dict - ): + if int(payment_payload.get("x402Version", 2)) == 2 and not isinstance(payload_accepted, dict): return False, "Missing accepted requirements in PAYMENT-SIGNATURE payload" if not isinstance(payload_accepted, dict): return True, "" @@ -994,13 +992,15 @@ def _select_accepted_for_payload( continue if payload_network and payload_network != str(accepted.get("network", "")): continue - if str(payload_accepted.get("asset", accepted.get("asset", ""))).lower() != str( - accepted.get("asset", "") - ).lower(): + if ( + str(payload_accepted.get("asset", accepted.get("asset", ""))).lower() + != str(accepted.get("asset", "")).lower() + ): continue - if str(payload_accepted.get("payTo", accepted.get("payTo", ""))).lower() != str( - accepted.get("payTo", "") - ).lower(): + if ( + str(payload_accepted.get("payTo", accepted.get("payTo", ""))).lower() + != str(accepted.get("payTo", "")).lower() + ): continue accepted_amount = str(accepted.get("amount", "0")) if str(payload_accepted.get("amount", accepted_amount)) != accepted_amount: