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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to Agents.KT are documented here. The format follows [Keep a

## [Unreleased]

### Added — seller-side x402 payments: `X402PaymentGate` (#4527, PRD §12.8) — experimental

`X402PaymentGate(requirements, facilitator).gate(handler)` wraps any JDK `HttpHandler` so a resource is served
only after a valid, settled stablecoin (USDC) payment over the [x402](https://github.com/x402-foundation/x402)
protocol (HTTP `402 Payment Required`). Our agentic-web serve surfaces (`McpServer` / `A2AServer` /
`NlWebServer` / `AgUiServer`) are loopback `HttpServer`s, so this fronts any of them — letting an agent
**monetize itself**. The **safe half** of x402: **the seller holds no key and takes no custody** — the buyer
signs an EIP-3009 authorization and a *hosted* `FacilitatorClient` (injected seam; `HttpFacilitatorClient` for
production) verifies + settles on-chain; we only configure a public `payTo`. The LLM never touches money
(gating is at the HTTP layer, outside the agent loop). **Fails closed** — missing/invalid payment, settle
failure, or an unreachable facilitator all return `402`, never serving the resource unpaid. Per request: no
`X-PAYMENT` → `402` with `{x402Version, error, accepts:[requirements]}`; `X-PAYMENT` present → verify → settle
→ set `X-PAYMENT-RESPONSE` → serve. New package `agents_engine.x402` (core, no deps). 5 hermetic tests (fake
facilitator + in-process `HttpServer`). **Buyer-side autonomous payment is deliberately not included** (it
concentrates the irreversible-money risk — gated on scoped session keys with signing kept below the model
layer); first-class serve-surface wiring + an MCP `paidTool()` / `a2a-x402` extension are follow-ups (#4526).

### Added — `AgUiServer`: serve an agent to a frontend over AG-UI (#4523, PRD §12.7)

`AgUiServer.from(agent).start()` exposes an agent over the [AG-UI](https://github.com/ag-ui-protocol/ag-ui)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ These APIs work in `main`, are unit-tested, and are exercised by integration tes
- **NLWeb endpoint tool (`nlwebSearch`)** — `tools { +nlwebSearchTool(baseUrl = "https://example.com") }` lets an agent query an [NLWeb](https://github.com/nlweb-ai/NLWeb) endpoint — a website's natural-language interface over its **schema.org**-structured content — and fold the ranked, typed results into context (#4541, PRD §12.9). Like `perplexitySearch` it is `untrustedOutput = true` (fetched web content is treated as data, not instructions). `nlwebSearchOptions`-style args via `NlWebSearchOptions(site = "podcasts", mode = NlWebMode.GENERATE)`. NLWeb endpoints need no API key. (Every NLWeb endpoint is also an MCP server, so an NLWeb `/mcp` URL is equally consumable through the existing MCP client — this tool is the zero-wiring `/ask`-over-HTTP path.)
- **Serve an NLWeb endpoint (`NlWebServer`)** — `NlWebServer.from(agent).start()` exposes the NLWeb `POST /ask` contract (`{query, site?, mode}` → ranked schema.org `results[]`), so agents.kt is consumable by NLWeb clients — the **serve** side to `nlwebSearch`'s **consume** side (#4542). Same `from(agent)` shape, loopback-only JDK-`HttpServer` posture, and threat model as `McpServer.from(agent)` / `A2AServer.from(agent)` (`127.0.0.1`, optional bearer, front with a gateway). The query is the agent's input; an `NlWebSearchResult` output is served verbatim (ranked schema.org results), any other output becomes the `summary` answer — back the agent's retrieval with the RAG `EmbeddingStore` seam (`:agents-kt-rag`) or whatever you like.
- **Serve a frontend over AG-UI (`AgUiServer`)** — `AgUiServer.from(agent).start()` exposes an agent over the [AG-UI](https://github.com/ag-ui-protocol/ag-ui) protocol — the **agent↔user/frontend** layer (e.g. a CopilotKit React chat), the only interop surface that reaches an end-user UI without us building a frontend (#4523). Not a descriptor exporter — a runtime streaming surface: `POST` a `RunAgentInput` and get an **SSE stream of typed AG-UI events**, bridged live from the agent's `AgentSession` (`Token` → `TEXT_MESSAGE_*`, `ToolCall*` → `TOOL_CALL_*`, `Skill*` → `STEP_*`, wrapped in `RUN_STARTED … RUN_FINISHED`). Same `from(agent)` shape, loopback-only posture, and threat model as the others; hand-rolled SSE, no AG-UI SDK. **agents.kt now serves the agentic web four ways: MCP, A2A, NLWeb, and AG-UI.**
- **Charge for an agent endpoint over x402 (`X402PaymentGate`, experimental)** — `X402PaymentGate(requirements, facilitator).gate(handler)` wraps any JDK `HttpHandler` so a resource is served only after a settled stablecoin (USDC) payment over the [x402](https://github.com/x402-foundation/x402) protocol (`402 Payment Required`) — front it on any of the serve surfaces above to let an agent **monetize itself** (#4527). The **safe, seller-side** half: **we hold no key and take no custody** — the buyer signs an EIP-3009 authorization and a *hosted* `FacilitatorClient` verifies + settles on-chain; we only configure a public `payTo`, and the LLM never touches money (gating is at the HTTP layer). **Fails closed** (any failure → `402`, never served unpaid). Buyer-side autonomous payment is deliberately *not* included (it concentrates the irreversible-money risk). New `agents_engine.x402` package, no deps.
- **AGNTCY interop (OASF record + DIR directory + Identity badge)** — `agent.toOasfRecord(version, authors, locators)` exports an [AGNTCY](https://github.com/agntcy) [OASF](https://github.com/agntcy/oasf) 1.0.0 discovery record (the third exporter beside `agent.json` and the A2A AgentCard; skills carry taxonomy uids via the opt-in `.oasf("agent_orchestration/multi_agent_planning")` annotation against a vendored, drift-checked taxonomy), and `fromOasfRecord(json)` imports + fail-closed-validates it back (#4518/#4519, PRD §12.6). The `:agents-kt-dir` module publishes/discovers records in the AGNTCY **DIR** content-addressed directory over generated grpc-kotlin stubs for three services — `StoreService` (CRUD: `dir.push(agent.toOasfRecord(...))` → CID, `dir.pull(cid)`), `SearchService` (local content search by typed `DirQuery` facet — skill/domain/author/…), and `RoutingService` (`publish`/`routeSearch` for cross-peer network discovery) (#4520). The trust side ships in `:agents-kt-identity`: `IdentityVerifier.verify(compactJws, jwks)` validates an AGNTCY Identity **badge** (a JOSE/JWS-secured W3C Verifiable Credential) against an issuer's `/.well-known/jwks.json`, fail-closed via `nimbus-jose-jwt` (rejects `alg: none`, `HS*` algorithm-confusion, expiry, tamper, wrong/unknown key — #4521). Verify-only; issuance deferred. DIR Routing/Search + OCI referrers are follow-ups under epic #4517.
- **Prompt caching across providers** — `agent { caching { enabled = true; cacheSystemPrompt = true; cacheToolDefs = true; cacheConversation = Rolling; ttl = 1.hours; cacheable("doc-id") { ... } } }`. Vendor-neutral DSL drives Anthropic's explicit `cache_control` breakpoints (#2658), OpenAI / DeepSeek automatic prefix caching with a stable `prompt_cache_key` routing hint (#2659 / #2661), Ollama / vLLM / SGLang engine-level KV-cache reuse (no-op hints, #2662), and surfaces cache reads + writes + hit-rate on `TokenUsage` (#2663). A prefix-stability guard (#2657) detects silent cache-busters — timestamps, UUIDs, non-deterministic ordering inside cacheable segments — and warns before you pay for a single non-cached run. Off by default; non-breaking. See [docs/caching.md](docs/caching.md).
- **JSONL audit exporter** — `:agents-kt-observability` writes append-only, one-line-per-event audit rows with `requestId`, `sessionId`, `manifestHash`, agent/skill/tool ids, event type, provider, and model; raw arguments/results are omitted by default (#1914). See [docs/observability.md](docs/observability.md).
Expand Down
6 changes: 3 additions & 3 deletions docs/prd.md
Original file line number Diff line number Diff line change
Expand Up @@ -2924,7 +2924,7 @@ The whole job is: emit our stream wrapped in the `RUN_STARTED … RUN_FINISHED`

Tracking: epic #4523 `[interop] AG-UI support (agent↔frontend serving)`. **Serve side shipped** — `AgUiServer.from(agent)` (package `agents_engine.agui`): `RunAgentInput` POST → SSE over the JDK `HttpServer`, `AgUiEventBridge` mapping `AgentSession` events into the `RUN_STARTED … RUN_FINISHED` envelope (lifecycle/text/tool/step families). Hand-rolled, no SDK (as planned). Follow-ups: STATE_SNAPSHOT/STATE_DELTA (needs a shared agent↔UI state model), REASONING/THINKING, and client-tool round-trips.

### 12.8 x402 — Agent Payments / Settlement Layer *(planned, deferred — money-handling)*
### 12.8 x402 — Agent Payments / Settlement Layer *(seller-side shipped experimental, #4527 — `X402PaymentGate`; buyer-side deferred — money-handling)*

[x402](https://github.com/x402-foundation/x402) revives HTTP `402 Payment Required` to let agents pay for gated resources in **stablecoins (USDC), gaslessly**. Unlike §12.5–12.7 (which carry no money), x402 is a **settlement layer** — and it sits *beneath* the protocols we already target, not beside them. As of April 2026 it is **Linux-Foundation-governed** (x402 Foundation, 22 orgs incl. Coinbase, Cloudflare, AWS, Google, Circle, Visa, Mastercard, Amex, Stripe, Shopify); Apache-2.0.

Expand All @@ -2933,14 +2933,14 @@ Tracking: epic #4523 `[interop] AG-UI support (agent↔frontend serving)`. **Ser
**Where it fits.** x402 is the settlement *rail* under our existing seams: an official **`a2a-x402` extension** (Google + Coinbase) and an **MCP `paidTool()`** wrapper already exist; Google's **AP2** is the *authorization* layer and x402 is its blessed *crypto rail*. AGNTCY and AG-UI have no payment dimension. So our insertion point is **an A2A x402 extension + an MCP paid-tool wrapper**, not a standalone payments module.

**Seller-side first (safe); buyer-side deferred behind hard custody guardrails.** These are different risk animals:
- **Seller-side** — our agents expose *paid* endpoints. We *receive* USDC via a hosted facilitator: **no custody, no money-transmitter exposure, no LLM-holds-key problem.** "Emit `402` → verify facilitator settlement → deliver" — a thin Micronaut middleware; may lean on the official Java SDK (`org.x402:x402`, SNAPSHOT — a servlet `PaymentFilter` + client; servlet/reactive impedance to weigh). This makes agents that can **monetize themselves** — a real differentiator. Ship **experimental**.
- **Seller-side *(shipped, #4527)*** — our agents expose *paid* endpoints. We *receive* USDC via a hosted facilitator: **no custody, no money-transmitter exposure, no LLM-holds-key problem.** `X402PaymentGate(requirements, facilitator).gate(handler)` (package `agents_engine.x402`) wraps any JDK `HttpHandler` ("emit `402` with terms → facilitator `verify` → `settle` → `X-PAYMENT-RESPONSE` → deliver"), so it fronts any of our `HttpServer`-based serve surfaces — agents that **monetize themselves**. Hand-rolled (JDK `HttpHandler` + a `FacilitatorClient` seam), **not** the servlet-based `org.x402:x402` SDK (servlet/JDK impedance). Fails closed. Shipped **experimental**.
- **Buyer-side** — our agents autonomously *pay*. **Deferred.** All real-money risk lives here. Only behind scoped ERC-4337 session keys (on-chain per-tx caps, payee allowlists, velocity limits), **signing isolated from the model layer**, and human-in-the-loop for settlement. Separate, later, opt-in module.

**Non-negotiable design constraint:** **keep signing and spending limits below the model layer** — the LLM must never hold a key or carry a spend limit in its prompt. x402 moves **irreversible** money (no chargebacks; liability lands on the deployer), and prompt-injection drains are confirmed-real (Grok/Bankr ~$150–200k, Freysa $47k; peer-reviewed *Five Attacks on x402*). A self-hosted *custodial* facilitator likely triggers money-transmitter / stablecoin regulation (FinCEN MSB, GENIUS Act, MiCA) — prefer a hosted facilitator and never take custody on the seller path.

**Why deferred.** Lower strategic priority than the interop trio (A2A/AGNTCY/AG-UI are table-stakes with no money/custody/regulatory surface). Adoption is also early — real settled volume is ~\$28k/day (much of it wash-traded) over 13 months — so this is a forward-looking, on-trend bet, sequenced **after** the interop work, seller-side first.

Tracking: epic `[interop] x402 agent payments`, deferred — seller-side experimental, buyer-side gated.
Tracking: epic #4526 `[interop] x402 agent payments`. **Seller-side shipped** (#4527, `X402PaymentGate`, experimental). Buyer-side (#4528) deferred behind hard custody guardrails (scoped session keys, signing below the model layer, HITL); first-class serve-surface wiring + MCP `paidTool()` / `a2a-x402` extension are follow-ups.

### 12.9 NLWeb — Agent↔Web-Content / External Knowledge *(client ≈ free via MCP; server deferred)*

Expand Down
18 changes: 18 additions & 0 deletions src/main/kotlin/agents_engine/x402/FacilitatorClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package agents_engine.x402

/**
* `agents_engine/x402/FacilitatorClient.kt` — #4527 (PRD §12.8). The x402 **facilitator** seam: the service
* that cryptographically verifies a buyer's signed payment and settles it on-chain. Keeping it an interface
* (a) keeps the seller crypto-free and key-free, (b) lets [X402PaymentGate] be tested hermetically with a
* fake, and (c) means production stays on a **hosted** facilitator (e.g. Coinbase CDP) — the seller never
* runs a custodial settler, which is what would trip money-transmitter regulation.
*
* [paymentHeader] is the raw value of the buyer's `X-PAYMENT` request header (base64-encoded payload).
*/
interface FacilitatorClient {
/** Check the signed payment against [requirements] without moving money. */
fun verify(paymentHeader: String, requirements: PaymentRequirements): FacilitatorVerification

/** Submit the verified payment on-chain. */
fun settle(paymentHeader: String, requirements: PaymentRequirements): FacilitatorSettlement
}
14 changes: 14 additions & 0 deletions src/main/kotlin/agents_engine/x402/FacilitatorSettlement.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package agents_engine.x402

/**
* `agents_engine/x402/FacilitatorSettlement.kt` — #4527 (PRD §12.8). Result of a facilitator `/settle`: the
* facilitator submitted the buyer's EIP-3009 authorization on-chain. [transaction] is the settled tx hash.
* The seller surfaces this back to the buyer as the `X-PAYMENT-RESPONSE` header.
*/
data class FacilitatorSettlement(
val success: Boolean,
val transaction: String? = null,
val network: String? = null,
val payer: String? = null,
val errorReason: String? = null,
)
12 changes: 12 additions & 0 deletions src/main/kotlin/agents_engine/x402/FacilitatorVerification.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package agents_engine.x402

/**
* `agents_engine/x402/FacilitatorVerification.kt` — #4527 (PRD §12.8). Result of a facilitator `/verify`:
* whether a buyer's signed payment payload is valid for the seller's [PaymentRequirements], without yet
* moving money. The facilitator (not the seller) does the EIP-712/EIP-3009 cryptographic checks.
*/
data class FacilitatorVerification(
val isValid: Boolean,
val invalidReason: String? = null,
val payer: String? = null,
)
98 changes: 98 additions & 0 deletions src/main/kotlin/agents_engine/x402/HttpFacilitatorClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package agents_engine.x402

import agents_engine.generation.LenientJsonParser
import agents_engine.mcp.McpJson
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import java.util.Base64

/**
* `agents_engine/x402/HttpFacilitatorClient.kt` — #4527 (PRD §12.8). A [FacilitatorClient] backed by a
* **hosted** x402 facilitator's REST API (`POST <baseUrl>/verify`, `POST <baseUrl>/settle`). The buyer's
* `X-PAYMENT` header (base64 JSON) is decoded and forwarded as `paymentPayload` alongside the seller's
* `paymentRequirements`; the facilitator performs the EIP-712/EIP-3009 checks and the on-chain settle.
*
* Bounded timeouts; failures surface as [X402Exception] so [X402PaymentGate] can fail closed (deny access).
* Using a hosted facilitator is deliberate — the seller never takes custody (see [FacilitatorClient]).
*/
class HttpFacilitatorClient(
private val baseUrl: String,
private val bearerToken: String? = null,
private val x402Version: Int = 1,
connectTimeout: Duration = Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS),
private val requestTimeout: Duration = Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS),
) : FacilitatorClient {

private val http: HttpClient = HttpClient.newBuilder().connectTimeout(connectTimeout).build()

override fun verify(paymentHeader: String, requirements: PaymentRequirements): FacilitatorVerification {
val json = post("/verify", paymentHeader, requirements)
return FacilitatorVerification(
isValid = json["isValid"] as? Boolean ?: false,
invalidReason = json["invalidReason"] as? String,
payer = json["payer"] as? String,
)
}

override fun settle(paymentHeader: String, requirements: PaymentRequirements): FacilitatorSettlement {
val json = post("/settle", paymentHeader, requirements)
return FacilitatorSettlement(
success = json["success"] as? Boolean ?: false,
transaction = json["transaction"] as? String,
network = json["network"] as? String,
payer = json["payer"] as? String,
errorReason = json["errorReason"] as? String,
)
}

private fun post(path: String, paymentHeader: String, requirements: PaymentRequirements): Map<*, *> {
val body = McpJson.encode(
linkedMapOf(
"x402Version" to x402Version,
"paymentPayload" to decodePaymentPayload(paymentHeader),
"paymentRequirements" to requirements.toJsonObject(),
),
)
val responseBody = send(path, body)
return LenientJsonParser.parse(responseBody) as? Map<*, *>
?: throw X402Exception("facilitator $path returned a non-object body")
}

private fun send(path: String, body: String): String {
val builder = HttpRequest.newBuilder()
.uri(URI.create(baseUrl.trimEnd('/') + path))
.timeout(requestTimeout)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
bearerToken?.let { builder.header("Authorization", "Bearer $it") }
val resp = try {
http.send(builder.build(), HttpResponse.BodyHandlers.ofString())
} catch (e: Exception) {
throw X402Exception("facilitator $path request failed: ${e.message}", e)
}
if (resp.statusCode() !in HTTP_OK_RANGE) {
val detail = resp.body().take(MAX_ERROR_BODY)
throw X402Exception("facilitator $path returned HTTP ${resp.statusCode()}: $detail")
}
return resp.body()
}

private fun decodePaymentPayload(paymentHeader: String): Any? {
val decoded = try {
String(Base64.getDecoder().decode(paymentHeader.trim()), Charsets.UTF_8)
} catch (e: IllegalArgumentException) {
throw X402Exception("X-PAYMENT header is not valid base64", e)
}
return LenientJsonParser.parse(decoded) ?: throw X402Exception("X-PAYMENT payload is not valid JSON")
}

private companion object {
const val CONNECT_TIMEOUT_SECONDS = 10L
const val REQUEST_TIMEOUT_SECONDS = 30L
const val MAX_ERROR_BODY = 300
val HTTP_OK_RANGE = 200..299
}
}
Loading
Loading