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
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
# A2A-T SDK Configuration
# Copy this file to .env and fill in your values

# Full A2A/Order protocol messages are emitted by the dedicated PROTOCOL DEBUG logger.
# Keep sensitive values hidden by default; set true only during controlled local debugging.
WORKFLOW_ENGINE_PROTOCOL_INCLUDE_SENSITIVE_HEADERS=false
# Set false to omit request/response bodies; large bodies are truncated at this character count.
WORKFLOW_ENGINE_PROTOCOL_INCLUDE_BODY=true
WORKFLOW_ENGINE_PROTOCOL_MAX_BODY_CHARS=100000

# Prompt runtime
A2AT_LANGUAGE=zh-CN
# classpath: prompt_resources are bundled inside the a2a-t-resources jar on the classpath
Expand Down Expand Up @@ -47,3 +54,22 @@ A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory
# Credential encryption key (32-byte hex, used for enc: prefixed passwords in credentials JSON)
# Generate with: openssl rand -hex 32
A2AT_CRED_KEY=your-32-byte-hex-key-here

# Workbench outbound transport: order (Eastcom SDK), mock, or direct
A2A_TRANSPORT_MODE=order
A2A_MOCK_GATEWAY_URL=http://127.0.0.1:26400

# Eastcom instruction platform. Configure either a default NE or per-agent NE routes.
# true starts the local RSocket-RPC protocol simulator; never enable it in production.
EASTCOM_ORDER_SIMULATOR_ENABLED=false
EASTCOM_ORDER_HOST=10.x.x.x
EASTCOM_ORDER_PORT=0
EASTCOM_ORDER_USERNAME=your-username
EASTCOM_ORDER_PASSWORD=your-password
EASTCOM_ORDER_CLIENT_ID=
EASTCOM_ORDER_CLIENT_SECRET=
EASTCOM_ORDER_DEFAULT_NE=
EASTCOM_ORDER_CITY1_NE=city1-omc
EASTCOM_ORDER_CITY2_NE=city2-omc
EASTCOM_ORDER_LOGIN_TIMEOUT_SECONDS=15
EASTCOM_ORDER_TIMEOUT_SECONDS=600
32 changes: 29 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Client TLS options for custom CA, mTLS client certificate/private key, encrypted PKCS#8 keys, and CRL checking
- Configurable bounded executors for client sends and Spring Boot server request handling
- Configurable Notification-T acknowledgement timeout and protocol-log body controls

### Changed

- TLS configuration is now per client; it no longer changes JVM-wide hostname-verification properties
- Credential login uses the same TLS policy as agent traffic and encrypted credentials now fail closed
- AgentCards that declare security requirements now fail closed when neither matching credentials nor an `AuthProvider` is configured
- `securitySchemes` no longer implies non-empty `securityRequirements`; an `AuthProvider` can be the sole authentication source
- Notification-T distinguishes acknowledgement timeout from real stream failure and keeps its lifecycle independent from
workflow request transports
- Workflow execution validates graph targets and cycles up front and waits only for activated predecessors at joins
- Protocol headers are redacted by default; sensitive values require an explicit diagnostic opt-in
- Agent clients and transport executors are reused and closed through their owning runtime

### Fixed

- Task success is derived from the A2A task state instead of non-empty response text alone
- Conflicting authentication headers from `AuthProvider` and credential configuration now fail fast
- Extension URI matching now uses exact URI path segments instead of substring matching
- Notification callback implementations can no longer silently discard the supplied callback
- Header construction is split into isolated auth, credential, and extension contributors with conflict detection

## [1.0.0] - 2026-07-28

First public release. The SDK ships a clean transport-facade architecture with single-responsibility decision interfaces
Expand All @@ -16,8 +42,8 @@ and full A2A-T extension support. Full A2A-T extension support and multi-protoco

- `A2ATransport`: shared wire layer owning the A2A client runtime, auth manager, agent-card map, and streaming event
extraction
- `ExtensionSender` / `DefaultExtensionSender`: one-shot pre-positioning facade for Authorization-T and Notification-T
(long-lived SSE subscription)
- `ExtensionSender` / `DefaultExtensionSender`: pre-positioning facade for one-shot Authorization-T and long-lived
Notification-T SSE subscriptions
- `ControlPoint` / `ExtensionSender` split: flow decisions (`onTask` / `onSelfTask` / `onRoute` / `onNegotiation`) on `ControlPoint`; Authorization-T / Notification-T pre-positioning on `ExtensionSender`
- `NegotiationStrategy`: pluggable clarification strategy injected into
`DefaultControlPoint`
Expand All @@ -37,7 +63,7 @@ and full A2A-T extension support. Full A2A-T extension support and multi-protoco
generation, Negotiation-T auto-loop, event callback, ControlPoint/ExtensionCallback wiring)
- Pre-positioning sends moved from `WorkflowEngineClient` to `ExtensionSender`
- `ExtensionRegistry` auto-registers only Task-T and Negotiation-T (in-workflow handlers); Authorization-T /
Notification-T are one-shot pre-positioning operations
Notification-T are pre-positioning operations with request and subscription lifecycles respectively

## [0.3.0] - 2026-07-25

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ List<AgentCard> agentCards = registry.fetchAgentCards();
// 3. Create transport + engine client
A2ATransport transport = new A2ATransport(agentCards, null,
WorkflowEngineClientConfig.builder()
.sslVerify(false)
.sslVerify(true)
.a2atEnvPath(".env")
.credentialsConfigPath("credentials.json")
.build());
Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ within 90 days. Valid reports will be credited in the release notes.
See [Integration Guide](docs/en/INTEGRATION_GUIDE.md#53-custom-authentication-authprovider).

- **TLS/HTTPS**: All agent communication supports HTTPS with configurable
certificate verification. Self-signed certificates are supported for
development via `sslVerify(false)`.
certificate verification. Self-signed certificates should be trusted through
`caCertsPath`; `sslVerify(false)` is limited to controlled local diagnostics and
does not disable hostname verification.
66 changes: 43 additions & 23 deletions docs/en/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ public interface WorkflowEngineClient {
}
```

> One-shot pre-positioning (Authorization-T / Notification-T) lives on
> `ExtensionSender`, not here. See the `ExtensionSender` section below.
> Pre-positioning lives on `ExtensionSender`, not here: Authorization-T is a one-shot request and Notification-T is a
> long-lived subscription. See the `ExtensionSender` section below.

#### sendMessage

Expand Down Expand Up @@ -108,10 +108,9 @@ After receiving:

### ExtensionSender

One-shot pre-positioning facade over the same `A2ATransport`. Sends Authorization-T / Notification-T (and any one-shot
extension) to agents before the workflow starts. Bypasses Task-T prompt generation and the Negotiation-T auto-loop, and
does not emit events through the global
`EventCallback` (the returned `CompletableFuture` is the callback).
Pre-positioning facade over the same `A2ATransport`. Sends one-shot Authorization-T requests, establishes long-lived
Notification-T subscriptions, and sends other one-shot extensions before a workflow starts. It bypasses Task-T prompt
generation and the Negotiation-T auto-loop; later Notification-T events are delivered through the subscription callback.

```java
public interface ExtensionSender {
Expand All @@ -127,7 +126,7 @@ public interface ExtensionSender {
CompletableFuture<SendMessageResult> sendNotification(
String agentName, String instruction, String naturalLanguageInput);

// Convenience: Notification-T (long-lived SSE + event callback)
// Interface contract: Notification-T (long-lived SSE + event callback)
CompletableFuture<SendMessageResult> sendNotification(
String agentName, String instruction,
String naturalLanguageInput, Consumer<Map<String, Object>> eventCallback);
Expand All @@ -138,11 +137,11 @@ public interface ExtensionSender {
|------------------------|-----------------|-------------------------------------------------|
| `agentName` | `String` | Target agent name (must match `AgentCard.name`) |
| `instruction` | `String` | Short instruction text; becomes `parts[].text` in the A2A message body |
| `naturalLanguageInput` | `String` | Natural language input passed to the A2A-T SDK to generate a structured extension prompt. The generated value is placed in the message `metadata` under the extension URI key (e.g. `https://.../Authorization-T/v1`). Falls back to this text as-is when SDK generation is unavailable |
| `naturalLanguageInput` | `String` | Natural-language extension metadata. Task-T is structured by the A2A-T SDK; Authorization-T and Notification-T currently use this value directly |
| `extension` | `A2ATExtension` | Extension enum (never hardcode URIs) |
| `eventCallback` | `Consumer<Map<String, Object>>` | Optional SSE event callback (`sendNotification` only). Recovery results pushed by the agent are received here in real time; the Map contains `agent`, `text`, `metadata`, `state`. Null drops subsequent events |

**Wire format**: The resulting A2A message sent to the agent has `parts[].text = instruction` and `metadata = { "<extension-URI>": "<SDK-generated structured prompt>" }`. For example, Authorization-T produces:
**Wire format**: The resulting A2A message sent to the agent has `parts[].text = instruction` and `metadata = { "<extension-URI>": "<extension value>" }`. For example, Authorization-T produces:

```json
{
Expand All @@ -157,21 +156,32 @@ public interface ExtensionSender {

Builder-based configuration for the workflow engine client.

| Property | Type | Default | Description |
|-------------------------|--------------------------|---------|-------------------------------------|
| `sslVerify` | `boolean` | `true` | TLS certificate verification |
| `caCertsPath` | `String` | null | Path to CA certs PEM file |
| `sendTimeoutSeconds` | `long` | `600` | SSE stream timeout (10 min default) |
| `authProvider` | `AuthProvider` | null | Custom auth provider |
| `credentialsConfigPath` | `String` | null | Path to credentials JSON |
| `credentialsConfig` | `Map` | null | Inline credentials config |
| `a2atEnvPath` | `String` | null | Path to `.env` file |
| `maxNegotiationRounds` | `int` | `3` | Max negotiation auto-loop rounds |
| `customHandlers` | `List<ExtensionHandler>` | null | Custom extension handlers |
| Property | Type | Default | Description |
|---------------------------------|--------------------------|---------|----------------------------------------------|
| `sslVerify` | `boolean` | `true` | TLS chain verification for HTTP/JSON-RPC; disabling it keeps hostname checks and the mTLS client identity. Disabling it uses plaintext for default gRPC |
| `caCertsPath` | `String` | null | Path to CA certs PEM file |
| `clientCertPath` | `String` | null | Path to the mTLS client certificate chain; default gRPC requires `sslVerify=true` |
| `clientKeyPath` | `String` | null | Path to a PKCS#8 PEM/DER mTLS private key |
| `clientKeyPassword` | `String` | null | Password for an encrypted PKCS#8 private key |
| `crlPath` | `String` | null | X.509 CRL for HTTP/JSON-RPC; the default gRPC runtime rejects this unsupported combination |
| `sendTimeoutSeconds` | `long` | `600` | SSE stream timeout (10 min default) |
| `notificationAckTimeoutSeconds` | `long` | `5` | Wait for the first Notification-T ACK/event |
| `sendExecutorCoreSize` | `int` | `4` | Send executor core threads |
| `sendExecutorMaxSize` | `int` | `16` | Send executor maximum threads |
| `sendExecutorQueueCapacity` | `int` | `256` | Bounded send executor queue capacity |
| `authProvider` | `AuthProvider` | null | Custom auth provider |
| `credentialsConfigPath` | `String` | null | Path to credentials JSON; explicit missing or malformed files fail startup |
| `credentialsConfig` | `Map` | null | Inline credentials config; must match AgentCard security requirements |
| `a2atEnvPath` | `String` | null | Path to `.env` file |
| `maxNegotiationRounds` | `int` | `3` | Max negotiation auto-loop rounds |
| `customHandlers` | `List<ExtensionHandler>` | null | Custom extension handlers |

```java
WorkflowEngineClientConfig config = WorkflowEngineClientConfig.builder()
.sslVerify(false)
.sslVerify(true)
.caCertsPath("ca.pem")
.clientCertPath("client-cert.pem")
.clientKeyPath("client-key.pem")
.sendTimeoutSeconds(900)
.a2atEnvPath(".env")
.credentialsConfigPath("creds.json")
Expand All @@ -191,8 +201,7 @@ public interface AuthProvider {
}
```

Called for every message send. The `headers` map is mutable; add
`Authorization`, custom headers, etc. Runs before credentials-based auth.
Called for every message send. The `headers` map is mutable; add `Authorization`, custom headers, etc. `AuthProvider` can be the sole authentication source, including when `securityRequirements` is non-empty and credentials are not configured. When credentials are also configured, both sets are computed independently and merged; different values for the same header name throw `SecurityException` instead of silently overwriting either value.

### ExtensionHandler

Expand Down Expand Up @@ -610,12 +619,23 @@ Configuration properties prefixed with `a2at.server`:
|----------|---------|-------------|
| `a2at.server.agent-card` | `classpath:agentcard.json` | Path to the AgentCard JSON file (classpath: or file: prefix supported) |
| `a2at.server.path-prefix` | `/a2a/json` | URL path prefix for A2A endpoints |
| `a2at.server.agent-timeout-seconds` | `30` | Agent execution timeout in seconds |
| `a2at.server.consumption-timeout-seconds` | `5` | Consumption timeout in seconds |
| `a2at.server.reconciliation-timeout-seconds` | `1` | Reconciliation wait timeout in seconds |
| `a2at.server.executor-core-size` | `8` | Server executor core threads |
| `a2at.server.executor-max-size` | `8` | Server executor maximum threads |
| `a2at.server.executor-queue-capacity` | `100` | Bounded server executor queue capacity |
| `a2at.server.executor-keep-alive-seconds` | `60` | Non-core thread keep-alive in seconds |

```yaml
a2at:
server:
agent-card: classpath:agentcard/my_agent.json
path-prefix: /a2a/json
agent-timeout-seconds: 30
executor-core-size: 8
executor-max-size: 16
executor-queue-capacity: 200
```

### A2AAutoConfiguration
Expand Down
26 changes: 12 additions & 14 deletions docs/en/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ state, and metadata.
- **`WorkflowEngineClient`** - the workflow execution send path. Owns Task-T prompt generation (before send), the
Negotiation-T auto-loop (after receive), the global `EventCallback`, and the `ControlPoint` wiring.
This is the facade the executor calls during workflow execution.
- **`ExtensionSender`** - one-shot pre-positioning. Sends Authorization-T and Notification-T messages to agents *before*
the workflow starts. It bypasses Task-T generation and the negotiation loop and does not emit events through the
global callback - the returned result *is* the callback.
- **`ExtensionSender`** - pre-positioning facade. Sends one-shot Authorization-T requests or establishes long-lived
Notification-T subscriptions *before* the workflow starts. It bypasses Task-T generation and the negotiation loop;
Notification-T events use the subscription callback rather than the workflow-global callback.

#### Why a shared transport with two facades?

Both the workflow send path and the one-shot pre-positioning path need the same wire-level machinery: an HTTP client,
Both the workflow send path and the pre-positioning path need the same wire-level machinery: an HTTP client,
TLS configuration, auth interceptors, agent-card resolution, and SSE parsing. Putting that machinery on either facade
would either (a) force a caller that only wants to pre-position to hold the full workflow facade, or (b) duplicate the
wire code across two classes. The shared-transport / two-facade design avoids both:
Expand Down Expand Up @@ -133,10 +133,9 @@ both):

### 4.2 Pre-positioning extensions

One-shot sends that happen before the workflow starts, via `ExtensionSender`:
Established through `ExtensionSender` before a workflow and decoupled from any single workflow transport:

- **Authorization-T** - Sends an authorization pre-positioning request. The prompt value is generated by the A2A-T SDK;
the engine falls back to the raw natural-language input when SDK generation is unavailable.
- **Authorization-T** - Sends a one-shot authorization request. The current implementation uses the caller-supplied natural-language input directly as the metadata value.
- **Notification-T** - Establishes a result subscription. Opens a long-lived SSE stream so later recovery results flow
back through the response stream.

Expand All @@ -145,9 +144,8 @@ stream, not through
`onNotification`. That hook only fires when an agent voluntarily includes a Notification-T payload in a `sendMessage`
task response.

Prompt generation methods for Authorization-T and Notification-T are reserved on `ExtensionSender`; Task-T uses the
SDK's `generateTaskPrompt`. Negotiation-T and the reserved generators will be wired to the A2A-T SDK as that support
lands upstream.
Task-T uses the SDK's `generateTaskPrompt`. Authorization-T and Notification-T currently use caller-supplied metadata
values because the upstream SDK does not expose corresponding prompt generators.

### 4.3 Extension handler chain

Expand Down Expand Up @@ -176,8 +174,8 @@ A step's `next` list holds `JumpCondition(step, condition)` entries. The routing
- **No `next`** - terminal; the step completes the branch.
- **All conditions empty** - unconditional fan-out: dispatch every non-terminal next step in parallel.
- **Has conditions** - conditional: call `ControlPoint.onRoute`, which returns a single `RouteDecision.nextStep`. The
engine enforces that the returned step is among the declared conditions; an invalid step ends the workflow with a
warning.
engine enforces that the returned step is among the declared conditions; an invalid return fails the workflow with an
error.

This makes conditional branches an N-choose-1 selection and keeps unconditional fan-out as automatic parallel dispatch.

Expand Down Expand Up @@ -280,8 +278,8 @@ The SDK is standalone: it does not depend on the orchestration center.
work. Avoids both forced-facade coupling and wire-code duplication.

2. **In-workflow vs pre-positioning extensions** - Task-T and Negotiation-T are part of the `sendMessage` chain;
Authorization-T and Notification-T are one-shot sends before the workflow. The registry auto-registers only the
in-workflow pair.
Authorization-T is a one-shot request sent before the workflow, while Notification-T is a long-lived subscription
established before the workflow. The registry auto-registers only the in-workflow pair.

3. **Auto-negotiation loop** - the engine owns the resend loop so hosts only supply clarification text
(`onNegotiation`), never the protocol mechanics of resending.
Expand Down
Loading
Loading