From 707c52d6851786b23a7a388ff94dc62e874e1805 Mon Sep 17 00:00:00 2001 From: geobelsky Date: Fri, 6 Mar 2026 16:37:57 +0000 Subject: [PATCH] feat: release Java SDK 0.1.1 with actor-token auth semantics Finalize Java SDK platform key plus actor token behavior in client configuration and request headers, and align README/pom release metadata for 0.1.1. Made-with: Cursor --- README.md | 68 +++++++++++++++++-- pom.xml | 16 +++-- src/main/java/dev/axme/sdk/AxmeClient.java | 12 +++- .../java/dev/axme/sdk/AxmeClientConfig.java | 25 +++++++ .../java/dev/axme/sdk/AxmeClientTest.java | 47 ++++++++++++- 5 files changed, 151 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f117d7a..5c64b34 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,43 @@ --- +## What Is AXME? + +AXME is a coordination infrastructure for durable execution of long-running intents across distributed systems. + +It provides a model for executing **intents** — requests that may take minutes, hours, or longer to complete — across services, agents, and human participants. + +## AXP — the Intent Protocol + +At the core of AXME is **AXP (Intent Protocol)** — an open protocol that defines contracts and lifecycle rules for intent processing. + +AXP can be implemented independently. +The open part of the platform includes: + +- the protocol specification and schemas +- SDKs and CLI for integration +- conformance tests +- implementation and integration documentation + +## AXME Cloud + +**AXME Cloud** is the managed service that runs AXP in production together with **The Registry** (identity and routing). + +It removes operational complexity by providing: + +- reliable intent delivery and retries +- lifecycle management for long-running operations +- handling of timeouts, waits, reminders, and escalation +- observability of intent status and execution history + +State and events can be accessed through: + +- API and SDKs +- event streams and webhooks +- the cloud console + +--- + ## What You Can Do With This SDK - **Send intents** — create typed, durable actions with delivery guarantees @@ -19,16 +56,26 @@ ## Install -Add to your `pom.xml`: +Build and install from source (local Maven repository): + +```bash +git clone https://github.com/AxmeAI/axme-sdk-java.git +cd axme-sdk-java +mvn -q -DskipTests install +``` + +Then add to your `pom.xml`: ```xml ai.axme axme - 0.1.0 + 0.1.1 ``` +Maven Central publication target: `ai.axme:axme`. + --- ## Quickstart @@ -42,9 +89,16 @@ import java.util.Map; public class Quickstart { public static void main(String[] args) throws Exception { AxmeClient client = new AxmeClient( - new AxmeClientConfig("https://gateway.axme.ai", "YOUR_API_KEY") + new AxmeClientConfig( + "https://gateway.axme.ai", + "YOUR_PLATFORM_API_KEY", // sent as x-api-key + "OPTIONAL_USER_OR_SESSION_TOKEN" // sent as Authorization: Bearer + ) ); + // Check connectivity / discover available capabilities + System.out.println(client.getCapabilities(RequestOptions.none())); + // Send an intent Map intent = client.createIntent( Map.of( @@ -71,9 +125,9 @@ The SDK covers the full public API surface: --- -## Pagination, Filtering, and Sorting +## Pagination and Cursor Flows -List endpoints return paginated results. The SDK handles cursor-based pagination: +Cursor-based list endpoints are available for inbox change streams: ![Pagination, Filtering, and Sorting Patterns](docs/diagrams/03-pagination-filtering-sorting-patterns.svg) @@ -155,12 +209,12 @@ Map sa = client.createServiceAccount( // Issue a key Map key = client.createServiceAccountKey( (String) sa.get("id"), - Map.of("name", "ci-key"), + Map.of(), new RequestOptions(null, null) ); // List service accounts -client.listServiceAccounts("org_abc", null, RequestOptions.none()); +client.listServiceAccounts("org_abc", "", RequestOptions.none()); // Revoke a key client.revokeServiceAccountKey( diff --git a/pom.xml b/pom.xml index 622adf5..0e4a6fc 100644 --- a/pom.xml +++ b/pom.xml @@ -6,15 +6,15 @@ ai.axme axme - 0.1.0 - Axme Java SDK + 0.1.1 + axme Official Java SDK for Axme APIs and workflows. https://github.com/AxmeAI/axme-sdk-java MIT License - https://opensource.org/license/mit + https://opensource.org/licenses/MIT repo @@ -89,7 +89,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.7.0 + 3.10.0 11 UTF-8 @@ -108,6 +108,12 @@ org.apache.maven.plugins maven-gpg-plugin 3.2.7 + + + --pinentry-mode + loopback + + sign-artifacts @@ -128,7 +134,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.9.0 true central diff --git a/src/main/java/dev/axme/sdk/AxmeClient.java b/src/main/java/dev/axme/sdk/AxmeClient.java index 6ed306b..a7f1658 100644 --- a/src/main/java/dev/axme/sdk/AxmeClient.java +++ b/src/main/java/dev/axme/sdk/AxmeClient.java @@ -15,6 +15,7 @@ public final class AxmeClient { private final String baseUrl; private final String apiKey; + private final String actorToken; private final HttpClient httpClient; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -25,6 +26,7 @@ public AxmeClient(AxmeClientConfig config) { public AxmeClient(AxmeClientConfig config, HttpClient httpClient) { this.baseUrl = config.getBaseUrl(); this.apiKey = config.getApiKey(); + this.actorToken = config.getActorToken(); this.httpClient = httpClient; } @@ -648,8 +650,14 @@ private Map requestJson( .method(method, payload == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload))) .header("Accept", "application/json"); - String resolvedAuthorization = isBlank(options.getAuthorization()) ? "Bearer " + apiKey : options.getAuthorization(); - builder.header("Authorization", resolvedAuthorization); + builder.header("x-api-key", apiKey); + String resolvedAuthorization = options.getAuthorization(); + if (isBlank(resolvedAuthorization) && !isBlank(actorToken)) { + resolvedAuthorization = "Bearer " + actorToken; + } + if (!isBlank(resolvedAuthorization)) { + builder.header("Authorization", resolvedAuthorization); + } if (payload != null) { builder.header("Content-Type", "application/json"); diff --git a/src/main/java/dev/axme/sdk/AxmeClientConfig.java b/src/main/java/dev/axme/sdk/AxmeClientConfig.java index a5521e4..8cc41c6 100644 --- a/src/main/java/dev/axme/sdk/AxmeClientConfig.java +++ b/src/main/java/dev/axme/sdk/AxmeClientConfig.java @@ -3,16 +3,33 @@ public final class AxmeClientConfig { private final String baseUrl; private final String apiKey; + private final String actorToken; public AxmeClientConfig(String baseUrl, String apiKey) { + this(baseUrl, apiKey, null, null); + } + + public AxmeClientConfig(String baseUrl, String apiKey, String actorToken) { + this(baseUrl, apiKey, actorToken, null); + } + + public AxmeClientConfig(String baseUrl, String apiKey, String actorToken, String bearerToken) { if (baseUrl == null || baseUrl.trim().isEmpty()) { throw new IllegalArgumentException("baseUrl is required"); } if (apiKey == null || apiKey.trim().isEmpty()) { throw new IllegalArgumentException("apiKey is required"); } + String normalizedActorToken = actorToken == null ? null : actorToken.trim(); + String normalizedBearerToken = bearerToken == null ? null : bearerToken.trim(); + if (isNonBlank(normalizedActorToken) + && isNonBlank(normalizedBearerToken) + && !normalizedActorToken.equals(normalizedBearerToken)) { + throw new IllegalArgumentException("actorToken and bearerToken must match when both are provided"); + } this.baseUrl = trimTrailingSlash(baseUrl.trim()); this.apiKey = apiKey.trim(); + this.actorToken = isNonBlank(normalizedActorToken) ? normalizedActorToken : normalizedBearerToken; } public String getBaseUrl() { @@ -23,6 +40,14 @@ public String getApiKey() { return apiKey; } + public String getActorToken() { + return actorToken; + } + + private static boolean isNonBlank(String value) { + return value != null && !value.isBlank(); + } + private static String trimTrailingSlash(String value) { if (value.endsWith("/")) { return value.substring(0, value.length() - 1); diff --git a/src/test/java/dev/axme/sdk/AxmeClientTest.java b/src/test/java/dev/axme/sdk/AxmeClientTest.java index 4a608d9..a1fa333 100644 --- a/src/test/java/dev/axme/sdk/AxmeClientTest.java +++ b/src/test/java/dev/axme/sdk/AxmeClientTest.java @@ -1,6 +1,7 @@ package dev.axme.sdk; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.core.type.TypeReference; @@ -42,7 +43,7 @@ void registerNickSendsPayloadAndHeaders() throws Exception { RecordedRequest request = server.takeRequest(); assertEquals("POST", request.getMethod()); assertEquals("/v1/users/register-nick", request.getPath()); - assertEquals("Bearer token", request.getHeader("Authorization")); + assertEquals("token", request.getHeader("x-api-key")); assertEquals("register-1", request.getHeader("Idempotency-Key")); Map body = @@ -51,6 +52,31 @@ void registerNickSendsPayloadAndHeaders() throws Exception { assertTrue((Boolean) response.get("ok")); } + @Test + void clientSendsConfiguredActorToken() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"ok\":true,\"available\":true}")); + + AxmeClient actorClient = + new AxmeClient(new AxmeClientConfig(server.url("/").toString(), "platform-token", "actor-token")); + actorClient.checkNick("@partner.user", RequestOptions.none()); + + RecordedRequest request = server.takeRequest(); + assertEquals("platform-token", request.getHeader("x-api-key")); + assertEquals("Bearer actor-token", request.getHeader("Authorization")); + } + + @Test + void configRejectsConflictingActorTokenAliases() { + assertThrows( + IllegalArgumentException.class, + () -> + new AxmeClientConfig( + "https://api.axme.test", + "platform-token", + "actor-a", + "actor-b")); + } + @Test void checkNickSendsQueryParameter() throws Exception { server.enqueue( @@ -190,7 +216,13 @@ void intentLifecycleAndControlEndpointsAreReachable() throws Exception { "it_123", Map.of("controls_patch", Map.of("timeout_seconds", 120), "expected_policy_generation", 5), RequestOptions.none()); - assertEquals("/v1/intents/it_123/controls", server.takeRequest().getPath()); + RecordedRequest controlsRequest = server.takeRequest(); + assertEquals("/v1/intents/it_123/controls", controlsRequest.getPath()); + Map controlsBody = + objectMapper.readValue(controlsRequest.getBody().readUtf8(), new TypeReference>() {}); + Map controlsPatch = (Map) controlsBody.get("controls_patch"); + assertEquals(120, ((Number) controlsPatch.get("timeout_seconds")).intValue()); + assertEquals(5, ((Number) controlsBody.get("expected_policy_generation")).intValue()); client.updateIntentPolicy( "it_123", @@ -200,7 +232,16 @@ void intentLifecycleAndControlEndpointsAreReachable() throws Exception { "envelope_patch", Map.of("max_retry_count", 10)), new RequestOptions(null, null, "agent://creator", null, null)); - assertEquals("/v1/intents/it_123/policy?owner_agent=agent%3A%2F%2Fcreator", server.takeRequest().getPath()); + RecordedRequest policyRequest = server.takeRequest(); + assertEquals("/v1/intents/it_123/policy?owner_agent=agent%3A%2F%2Fcreator", policyRequest.getPath()); + Map policyBody = + objectMapper.readValue(policyRequest.getBody().readUtf8(), new TypeReference>() {}); + Map grantsPatch = (Map) policyBody.get("grants_patch"); + Map delegateGrant = (Map) grantsPatch.get("delegate:agent://ops"); + assertTrue(((java.util.List) delegateGrant.get("allow")).contains("resume")); + assertTrue(((java.util.List) delegateGrant.get("allow")).contains("update_controls")); + Map envelopePatch = (Map) policyBody.get("envelope_patch"); + assertEquals(10, ((Number) envelopePatch.get("max_retry_count")).intValue()); } @Test