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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,50 @@ The agent picks its behaviour from a prefix on the message text -- `stream: <tex
runs a task with artifacts, `slow: <seconds>` runs a cancellable long task, and
anything else gets a plain message reply. See `EchoAgentExecutor`.

## Identity and Node Connections

`SlimHelper.createServer(instance, org, ns, secret, serverAddr)` and the matching
`createChannel` overload are shortcuts for a shared-secret identity over an insecure
connection. With a shared secret, anyone holding the secret can claim any SLIM name,
so production deployments should use per-agent identities.

The general overloads take the identity as an `IdentityProviderConfig` /
`IdentityVerifierConfig` pair (shared secret, JWT, static JWT or SPIRE) and the node
connection as a `ClientConfig`:

```java
Name local = new Name("agntcy", "demo", "server");

// Sign our tokens with an Ed25519 key; kid = sub = did:key works for a did:key mesh.
IdentityProviderConfig provider = new IdentityProviderConfig.Jwt(new ClientJwtAuth(
new JwtKeyType.Encoding(new JwtKeyConfig(JwtAlgorithm.ED_DSA, JwtKeyFormat.PEM,
new JwtKeyData.File("/etc/agent/ed25519.pem"))),
List.of("slim-a2a"), "my-issuer", "did:key:z6Mk...", Duration.ofHours(1)));

// Accept peers whose public keys are in a JWKS file (acts as a trust allowlist).
IdentityVerifierConfig verifier = new IdentityVerifierConfig.Jwt(new JwtAuth(
new JwtKeyType.Decoding(new JwtKeyConfig(JwtAlgorithm.ED_DSA, JwtKeyFormat.JWKS,
new JwtKeyData.File("/etc/agent/trusted.jwks.json"))),
List.of("slim-a2a"), "my-issuer", null, Duration.ofHours(1)));

// TLS to the node (system CA roots) plus a bearer token read from a file.
ClientConfig node = SlimBindings.newSecureClientConfig("https://slim.example.com:443");
node.setAuth(new ClientAuthenticationConfig.StaticJwt(
new StaticJwtAuth("/etc/agent/node-token.jwt", Duration.ofMinutes(5))));

Server server = SlimHelper.createServer(local, provider, verifier, node);
Channel channel = SlimHelper.createChannel(
new Name("agntcy", "demo", "client"), local, provider, verifier, node);
```

Every member of a SLIM session needs a compatible identity type, so the server and
its clients must agree on the provider/verifier kind. `SlimHelper.sharedSecretProvider`
and `sharedSecretVerifier` build the shared-secret configs used by the shortcut overloads.

With slim-bindings-java 2.0, the SlimRPC handler context does not expose the caller's
verified identity, so `SlimA2AHandler` still reports an anonymous user to the A2A
`RequestHandler`.

## Architecture

The library follows the same adapter pattern as
Expand Down
15 changes: 15 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<a2a.java.sdk.version>1.1.0.Final</a2a.java.sdk.version>
<protobuf.version>4.33.2</protobuf.version>
<jna.version>5.14.0</jna.version>
<junit.version>5.11.4</junit.version>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -87,6 +88,11 @@
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand All @@ -105,6 +111,15 @@
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<!-- slim-bindings-java uses the FFM API, a preview feature on Java 21 -->
<argLine>--enable-preview --enable-native-access=ALL-UNNAMED</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
Expand Down
5 changes: 5 additions & 0 deletions slim-a2a/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
110 changes: 95 additions & 15 deletions slim-a2a/src/main/java/io/agntcy/slim/a2a/SlimHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

package io.agntcy.slim.a2a;

import java.util.Objects;

import io.agntcy.slim.bindings.App;
import io.agntcy.slim.bindings.ClientConfig;
import io.agntcy.slim.bindings.IdentityProviderConfig;
import io.agntcy.slim.bindings.IdentityVerifierConfig;
import io.agntcy.slim.bindings.Name;
import io.agntcy.slim.bindings.Service;
import io.agntcy.slim.bindings.SlimBindings;
Expand All @@ -13,6 +17,13 @@

/**
* Convenience helpers for bootstrapping SLIM RPC server and client instances.
*
* <p>The general overloads take the SLIM app identity as an
* {@link IdentityProviderConfig} / {@link IdentityVerifierConfig} pair (shared
* secret, JWT, static JWT or SPIRE) and the node connection as a
* {@link ClientConfig} (plain, TLS, mTLS, with optional basic/JWT auth). The
* {@code secret}/{@code serverAddr} overloads are shortcuts for a shared-secret
* identity over an insecure connection.
*/
public final class SlimHelper {

Expand All @@ -23,7 +34,25 @@ public final class SlimHelper {
private SlimHelper() {}

/**
* Bootstraps a SLIM RPC server.
* Bootstraps a SLIM RPC server with a caller-supplied identity and node connection.
*
* @param localName the SLIM name this server registers under
* @param identityProvider how this app proves its identity to peers
* @param identityVerifier how this app verifies the identity of peers
* @param clientConfig how to connect to the SLIM node (endpoint, TLS, auth)
* @return a ready-to-register {@link Server}
*/
public static Server createServer(Name localName,
IdentityProviderConfig identityProvider,
IdentityVerifierConfig identityVerifier,
ClientConfig clientConfig) throws Exception {
Objects.requireNonNull(localName, "localName");
ConnectedApp connected = connect(localName, identityProvider, identityVerifier, clientConfig);
return Server.newWithConnection(connected.app(), localName, connected.connId());
}

/**
* Bootstraps a SLIM RPC server using a shared secret over an insecure connection.
*
* @param instanceName unique instance name for this server (e.g. "server")
* @param org the SLIM name org component
Expand All @@ -34,17 +63,35 @@ private SlimHelper() {}
*/
public static Server createServer(String instanceName, String org, String ns,
String secret, String serverAddr) throws Exception {
Service service = SlimBindings.getGlobalService();
Name localName = new Name(org, ns, instanceName);
App app = service.createAppWithSecret(localName, secret);
ClientConfig clientConfig = SlimBindings.newInsecureClientConfig(serverAddr);
long connId = service.connect(clientConfig);
app.subscribe(app.name(), connId);
return Server.newWithConnection(app, localName, connId);
return createServer(localName,
sharedSecretProvider(localName, secret),
sharedSecretVerifier(localName, secret),
SlimBindings.newInsecureClientConfig(serverAddr));
}

/**
* Bootstraps a SLIM RPC channel (client) with a caller-supplied identity and node connection.
*
* @param localName the SLIM name of this client
* @param remoteName the SLIM name of the remote server
* @param identityProvider how this app proves its identity to peers
* @param identityVerifier how this app verifies the identity of peers
* @param clientConfig how to connect to the SLIM node (endpoint, TLS, auth)
* @return a connected {@link Channel}
*/
public static Channel createChannel(Name localName, Name remoteName,
IdentityProviderConfig identityProvider,
IdentityVerifierConfig identityVerifier,
ClientConfig clientConfig) throws Exception {
Objects.requireNonNull(localName, "localName");
Objects.requireNonNull(remoteName, "remoteName");
ConnectedApp connected = connect(localName, identityProvider, identityVerifier, clientConfig);
return Channel.newWithConnection(connected.app(), remoteName, connected.connId());
}

/**
* Bootstraps a SLIM RPC channel (client).
* Bootstraps a SLIM RPC channel (client) using a shared secret over an insecure connection.
*
* @param instanceName unique instance name for this client (e.g. "client")
* @param org the SLIM name org component
Expand All @@ -56,14 +103,31 @@ public static Server createServer(String instanceName, String org, String ns,
*/
public static Channel createChannel(String instanceName, String org, String ns,
String remoteName, String secret, String serverAddr) throws Exception {
Service service = SlimBindings.getGlobalService();
Name localName = new Name(org, ns, instanceName);
Name remote = new Name(org, ns, remoteName);
App app = service.createAppWithSecret(localName, secret);
ClientConfig clientConfig = SlimBindings.newInsecureClientConfig(serverAddr);
long connId = service.connect(clientConfig);
app.subscribe(app.name(), connId);
return Channel.newWithConnection(app, remote, connId);
return createChannel(localName, new Name(org, ns, remoteName),
sharedSecretProvider(localName, secret),
sharedSecretVerifier(localName, secret),
SlimBindings.newInsecureClientConfig(serverAddr));
}

/**
* Shared-secret identity provider for {@code name}, equivalent to what
* {@code Service.createAppWithSecret} uses.
*/
public static IdentityProviderConfig sharedSecretProvider(Name name, String secret) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(secret, "secret");
return new IdentityProviderConfig.SharedSecret(name.toString(), secret);
}

/**
* Shared-secret identity verifier for {@code name}, equivalent to what
* {@code Service.createAppWithSecret} uses.
*/
public static IdentityVerifierConfig sharedSecretVerifier(Name name, String secret) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(secret, "secret");
return new IdentityVerifierConfig.SharedSecret(name.toString(), secret);
}

/**
Expand All @@ -73,4 +137,20 @@ public static Channel createChannel(String instanceName, String org, String ns,
public static void initializeDefaults() {
SlimBindings.initializeWithDefaults();
}

private record ConnectedApp(App app, long connId) {}

private static ConnectedApp connect(Name localName,
IdentityProviderConfig identityProvider,
IdentityVerifierConfig identityVerifier,
ClientConfig clientConfig) throws Exception {
Objects.requireNonNull(identityProvider, "identityProvider");
Objects.requireNonNull(identityVerifier, "identityVerifier");
Objects.requireNonNull(clientConfig, "clientConfig");
Service service = SlimBindings.getGlobalService();
App app = service.createApp(localName, identityProvider, identityVerifier);
long connId = service.connect(clientConfig);
app.subscribe(app.name(), connId);
return new ConnectedApp(app, connId);
}
}
Loading
Loading