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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory
A2AT_CRED_KEY=your-32-byte-hex-key-here

# Keep true for production HTTPS. Set false only for local/self-signed integration testing.
# For LoadPsop HTTPS, false skips both server trust and hostname checks; no local CA file is needed.
# The server still needs a TLS certificate. This does not bypass mTLS or change forwarding-platform TLS.
A2A_SSL_VERIFY=false
# Comma-separated classpath:, file: URI, or absolute file locations. Leave empty for bundled cards.
# For a live OMC, point the city AgentCards at files exported by the registry center.
A2A_AGENT_CARD_LOCATIONS=
# Embedded startup validates both effective AgentCards against local network interfaces.
# Set false for an external OMC, including one running on the same host. Empty defaults to true.
A2A_EMBEDDED_OMC_ENABLED=
14 changes: 13 additions & 1 deletion docs/en/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ Override to receive real-time execution events. Event types are defined in `Even
| `AUTHORIZATION_RESOLVED` | Authorization decision was made |
| `NOTIFICATION` | Notification received from agent |
| `ROUTE_DECISION` | Route decision was made |
| `WORKFLOW_COMPLETE` | The workflow completed (all steps finished) |
| `WORKFLOW_COMPLETE` | DAG scheduling ended; check success, not all nodes necessarily ran |
| `START` | Workflow execution started |
| `COMPLETE` | Workflow execution completed successfully |
| `ERROR` | Workflow execution failed |
Expand Down Expand Up @@ -321,6 +321,12 @@ static List<WorkflowSearchResult> search(

POST `/api/v1/orchestrate/search`. Returns ranked workflow summaries matched by natural-language intent.

LoadPsop convenience overloads default to `sslVerify=true`, using JVM trust and hostname checks.
Explicit `false` skips certificate-chain and hostname verification on that orchestration HTTPS connection only,
allowing development without a local CA file. The server still needs a TLS certificate; this does not bypass
mTLS, modify JVM-wide defaults, or change other clients' TLS policies. Production must verify trust and matching SANs.
This is distinct from the engine's southbound HTTP/JSON-RPC verification policy.

### RegistryClient

The two-argument constructor uses a 30-second complete-response deadline. Override it with
Expand Down Expand Up @@ -555,6 +561,12 @@ Canonical negotiation metadata is `templateUri` plus

## Error Handling

- Top-level remote problem responses retain their fields in `RemoteProblemException`;
`findIn(Throwable)` finds it through wrappers. Workflow failures use `remote.problem.<status>`,
with details in history / TASK_RESPONSE rather than business outputs.
Executor node events include executionId; task response events and history include the logical taskId.
See [integration guidance](INTEGRATION_GUIDE.md#remote-problem-responses) for propagation and logging.

- Missing/null/exceptional/timed-out callbacks fail explicitly. Host SDK errors may be mapped to BusinessFailure with
safe code/details.
- Exhausted maxNegotiationExchanges or local Stop does not generate Abort. Business Send (Abort) is never diagnosis
Expand Down
73 changes: 73 additions & 0 deletions docs/en/INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,79 @@ Construct final MessageContent (parts, metadata, extensions), with host-owned co
handler or SDK instance registration. A2atMessages.from copies A2A-T metadata; other extensions can directly supply
metadata and activation URIs. AgentCard declarations never cause implicit generation.

## External OMC startup checks

### Orchestration-center HTTPS development

The demo passes `A2A_SSL_VERIFY` (Spring property `a2a.ssl-verify`) to LoadPsop.search/load.
For controlled development, set `A2A_SSL_VERIFY=false` or `--a2a.ssl-verify=false`.
No local orchestration CA file is required: both calls skip chain and hostname checks, including
self-signed certificates, missing SANs and mismatched SANs, and emit a `[Registry] INSECURE_TLS` warning.

The HTTPS server must still present a certificate, and client-certificate requirements (mTLS) still apply.
This mode cannot authenticate the server and is vulnerable to interception; do not use it in production.
Keep verification enabled in production, configure matching server SANs and trust its CA in the running JVM.
The southbound `caCertsPath` setting is not forwarded to LoadPsop.
LoadPsop never changes JVM-wide SSLContext, SocketFactory or HostnameVerifier defaults.
Southbound HTTP/JSON-RPC retains hostname checks when chain verification is disabled; vendor forwarding TLS
remains managed by the vendor SDK/platform.

### Downstream OMC startup

Set `A2A_AGENT_CARD_LOCATIONS` in the environment or local .env to comma-separated external
AgentCard file locations. For SpringSpnDemo, set `A2A_EMBEDDED_OMC_ENABLED=false` or pass
`--a2a.embedded-omc-enabled=false` when using externally managed OMCs.
Use SpringWorkbenchApplication for a persistent host; the demo exits after its task.

Embedded startup validates both effective city cards before starting either OMC, then uses those
cards to bind. Missing cards, invalid URLs and non-local addresses fail early. Loopback and local
network-interface addresses are accepted; non-loopback does not automatically mean remote.
This does not probe OMCs or detect existing services. Disable embedded startup even when an externally
managed OMC runs on this same machine. Keep real cards and credentials outside tracked sample resources.

## Remote problem responses

A successful HTTP envelope does not guarantee a successful task: SSE data can contain a top-level
problem object such as `{"status":429,"detail":"Active task limit reached","type":""}`.
Top-level numeric status 400–599 with title/detail becomes `RemoteProblemException`, preserving
status, title, detail, type and timestamp. Both synchronous and streaming responses are checked.
Nested business data is not classified as a protocol error.
Detection is limited to bodies actually delivered by the SDK. A non-2xx HTTP response may be rejected
first as an HTTP/authentication error by the SDK or adapter, without exposing its problem body;
such failures retain the generic failure path rather than fabricating errorDetails.

The error fails the call without fabricating a successful task, invoking onNegotiation, or automatically
resubmitting it. For 400, inspect business inputs; for 429, inspect OMC capacity and existing tasks or
subscriptions before deciding whether to retry. Negotiation still requires a valid Negotiation-T Propose.
Use `RemoteProblemException.findIn(error)` to inspect the cause chain (null means no problem found);
workflow execution follows its existing failure path.
Task results and execution history / TASK_RESPONSE events expose `remote.problem.400`,
`remote.problem.429`, etc. as errorCode, the reason as error, the five fields above as errorDetails,
and empty outputs. Known credential fields use the protocol logger's redaction rules;
unrecognized SDK exceptions still expose only the exception type.
Independent authorization/subscription failures do not determine the workflow result.
Pretty logging does not modify the received problem payload.

In the two-city sample, one failed diagnosis does not interrupt the other already dispatched call:
its result or timeout is collected before the workflow returns `success=false`.
The merge_analysis onSelfTask/LLM is not invoked; successful peer results remain in history,
not in a fabricated overall diagnosis. There is no automatic retry, queuing or partial-success merge.
Generic AnySuccess nodes retain their existing first-success semantics.

Observe failures through TASK_RESPONSE or inspect result.history in `onFinish(result, events)`;
onTask prepares outbound content and is not an error-retry callback.
Executor node events include executionId; TASK_RESPONSE and history also include the logical taskId.
The sample's northbound A2A task returns FAILED with per-agent errorCode, error and errorDetails
in its status message, without a success-summary artifact. Demo shutdown closes subscriptions;
a persistent host owns them independently of workflow results.

Logging responsibilities: PROTOCOL captures observed traffic, correlated by requestId rather than
adjacent log entries; REMOTE_PROBLEM reports transport rejection; TASK_FAILED identifies the execution,
step, logical task, agent, code and reason; WORKFLOW_STOPPED lists unexecuted steps.
Sample TASK_RESPONSE logs bridge contextId and executionId. Recognized remote problems use WARN summaries,
while unexpected exceptions retain ERROR stack traces. Known credentials and Bearer/Basic values are redacted.
Logging configuration, pretty display and observer callbacks do not determine task outcomes.

## 13. Interface Reference

| Interface/Class | Purpose |
Expand Down
12 changes: 11 additions & 1 deletion docs/zh/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public class EventCallback {
| `AUTHORIZATION_RESOLVED` | 授权决策已做出 |
| `NOTIFICATION` | 收到智能体通知 |
| `ROUTE_DECISION` | 路由决策已做出 |
| `WORKFLOW_COMPLETE` | 工作流完成(所有步骤结束) |
| `WORKFLOW_COMPLETE` | DAG 调度结束,需检查 success;不代表全部节点成功执行 |
| `START` | 工作流执行开始 |
| `COMPLETE` | 工作流执行成功完成 |
| `ERROR` | 工作流执行失败 |
Expand Down Expand Up @@ -310,6 +310,11 @@ static List<WorkflowSearchResult> search(

POST `/api/v1/orchestrate/search`。返回按自然语言意图匹配的工作流摘要列表。

LoadPsop 的简便重载默认 `sslVerify=true`,使用 JVM 信任库并校验主机名。
显式传 `false` 时,仅该编排中心 HTTPS 连接跳过证书链和主机名校验,可免配本地 CA 文件联调。
服务端仍须提供 HTTPS 证书;此开关不绕过 mTLS,也不修改其他客户端或 JVM 的全局 TLS 策略。
生产环境必须保持验证并配置正确的服务端 SAN 与信任库;本设置不是引擎南向 HTTP/JSON-RPC 的 TLS 策略变更。

### RegistryClient

双参数构造默认使用 30 秒完整响应截止时间。可通过
Expand Down Expand Up @@ -536,6 +541,11 @@ getReceivedMessages () 是保留层级的响应来源,getOutputs () 为便利

## 错误处理

- 顶层远端 problem 错误通过 `RemoteProblemException` 保留;可用 `findIn(Throwable)` 查找包装异常中的原因。
工作流任务失败信息为 `remote.problem.<status>`,详情进入 history / TASK_RESPONSE,不作为业务 outputs。
引擎节点事件携带 executionId,任务结果事件和 history 携带逻辑 taskId。
失败传播、双地市汇总规则和日志职责见 [集成指南](INTEGRATION_GUIDE.md#远端错误响应)。

- 回调异常/空值/超时明确失败;SDK 内容错误由宿主转换为 BusinessFailure,必要时保留安全的 code/details。
- maxNegotiationExchanges 耗尽或本地 Stop 只结束本地交互,不自动发送 Abort。业务 Send (Abort) 不算诊断成功。
- 远端状态和错误与业务输出分开,失败状态消息不成为 outputs,已有部分 artifact 保留。
Expand Down
73 changes: 73 additions & 0 deletions docs/zh/INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,79 @@ Workflow workflow = LoadPsop.load(
实例。A2atMessages.from 是 A2A-T metadata 复制辅助;非 A2A-T 扩展也可直接提供 metadata 和激活 URI。引擎不会仅因 AgentCard
声明而自动生成内容。

## 远端 OMC 联调与启动校验

### 编排中心 HTTPS 联调

Demo 将 `A2A_SSL_VERIFY`(Spring 属性 `a2a.ssl-verify`)传给 LoadPsop.search/load。
受控开发环境可设置 `A2A_SSL_VERIFY=false` 或启动参数 `--a2a.ssl-verify=false`:
执行引擎端不用下载或配置编排中心证书,搜索与加载连接均跳过证书链及主机名校验,
包括自签名、缺少 SAN 或 SAN 不匹配的开发证书,并打印 `[Registry] INSECURE_TLS` 警告。

“免配本地证书”不表示 HTTPS 服务端不需要证书,也不能绕过要求客户端证书的 mTLS。
该非验证模式不能确认服务端身份,存在中间人风险,仅用于受控联调。
生产保持 `true`,给服务端配置匹配实际 URL 的 SAN,并让运行 JVM 信任其 CA;
引擎南向 `caCertsPath` 不会自动传给 LoadPsop。
LoadPsop 不修改 JVM 全局 SSLContext、默认 SocketFactory 或 HostnameVerifier;
引擎南向 HTTP/JSON-RPC 的 `false` 仍仅跳过证书链校验,自定义转发适配器的 TLS 由其 SDK/平台管理。

### 下游 OMC 启动

在 IDEA 环境变量或本地 .env 中设置 `A2A_AGENT_CARD_LOCATIONS`,以逗号分隔外部 AgentCard 文件路径。
运行 SpringSpnDemo 对接外部 OMC 时,显式设置 `A2A_EMBEDDED_OMC_ENABLED=false`,
或使用 Program arguments `--a2a.embedded-omc-enabled=false`;不会为这些下游节点创建本地 JDK Server。
持续接收任务和保持订阅应运行 SpringWorkbenchApplication,而不是执行一次后退出的 Demo。

启用内嵌 OMC 时,Demo 在启动任意 OMC 前校验两张实际加载的地市卡片,并使用其地址启动。
非本机地址、缺失卡片或非法 URL 会提前失败;回环地址和本机网卡地址都可绑定,
不以“是否 127.0.0.1”判断远端。校验不探测 OMC,也不能判定本机某端口是否已有真实服务:
即使外部 OMC 恰好部署在本机,也必须显式关闭内嵌服务。
真实地址与凭据应放在本地外部文件,不覆盖仓库示例,不提交。

## 远端错误响应

响应头成功不代表任务成功。HTTP 200 的 SSE data 也可能返回顶层错误对象,例如:

```json
{"status":429,"detail":"Current active tasks have reached the maximum limit 10.","type":""}
```

通信层识别顶层数字 status 为 400–599 且携带 title/detail 的错误对象,
以 `RemoteProblemException` 保留 status、title、detail、type、timestamp。
同步响应和流式响应均检查;普通任务内容或 artifact 内嵌的同名字段不被当成协议错误。
该识别以 SDK 实际交付的正文为限:真实 HTTP 非 2xx 可能先被 SDK/适配器作为 HTTP 或认证错误拒绝,
不保证可取得 problem 正文;没有该正文时仍走原有通用失败路径,不伪造 errorDetails。
错误不生成成功结果、不触发 onNegotiation,也不自动重新提交请求。400 应核对业务参数;
429 应检查 OMC 活跃任务配额及已有任务/订阅,再由业务决定是否重试。
只有合法 Negotiation-T Propose 才进入协商回调。

直接调用客户端时可用 `RemoteProblemException.findIn(error)` 检查 cause 链(没有则返回 null);
工作流执行时沿现有失败路径报告错误:
任务结果及执行 history / TASK_RESPONSE 事件中的 errorCode 为 `remote.problem.400`、
`remote.problem.429` 等,error 包含原因,errorDetails 包含上述五个字段,outputs 为空。
错误内容中的已知凭据字段按协议日志的规则脱敏;未识别的 SDK 异常仍只暴露异常类型。
预置授权、通知订阅的错误仍由其独立生命周期处理,不决定工作流执行结果。
协议 pretty 日志仅用于展示,不改写原始收到的错误对象。

**当前双地市示例的失败流程**:两个诊断并行下发,一方失败后记录该方错误;
已经下发的另一方继续等待结果或超时。当前批次结束后返回 `success=false`,
不进入 `merge_analysis`,也不调用该节点的 onSelfTask/LLM。成功地市的结果留在 history 中,
不冒充整体诊断结论。此处没有自动重试、排队或“部分成功后汇总”策略;
通用引擎的 AnySuccess 节点仍按原有任一成功规则执行。

集成方通过 TASK_RESPONSE 事件及时观察任务失败,或在 `onFinish(result, events)`
检查 `result.history`;onTask 是发送前准备消息的回调,不是失败重试回调。
引擎节点事件带 executionId;TASK_RESPONSE 和 history 还带逻辑 taskId。
Demo 的北向 A2A 任务返回 FAILED,状态消息包含失败地市的 errorCode、error 和 errorDetails,
不产生成功汇总 artifact。整个演示退出时会关闭订阅;常驻宿主中的订阅由独立生命周期管理。

**日志职责**:PROTOCOL 记录已观察到的请求/响应,按 requestId 关联并发报文;
REMOTE_PROBLEM 记录通信层识别的远端拒绝,TASK_FAILED 记录执行 ID、节点、逻辑任务 ID、
智能体、错误码和原因,WORKFLOW_STOPPED 列出未执行节点。示例 TASK_RESPONSE 日志同时包含
contextId 与 executionId,供跨层定位。已识别的远端问题使用 WARN 摘要,
未知异常保留 ERROR 堆栈;错误详情中的已知凭据及 Bearer/Basic 值脱敏。
日志开关、pretty 展示和观察回调不会改变任务结果。

## 13. 你需要使用的接口一览

| 接口/类 | 用途 |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2026 Huawei Technologies Co., Ltd.
* SPDX-License-Identifier: Apache-2.0
*/
package dev.openan.workflow.engine.examples.demo;

import dev.openan.workflow.engine.examples.server.LocalServerAddress;
import dev.openan.workflow.engine.examples.workbench.WorkbenchAgentCatalog;
import java.util.List;
import org.a2aproject.sdk.spec.AgentCard;

/** Preflights both demo OMCs before either local server or business executor starts. */
final class EmbeddedOmcSupport {
private static final List<String> AGENTS =
List.of("SPN Domain Agent City1", "SPN Domain Agent City2");

private EmbeddedOmcSupport() {}

static boolean enabled(String[] args, boolean defaultValue) {
String configured = null;
for (String arg : args) {
if (arg.startsWith("--a2a.embedded-omc-enabled=")) {
configured = arg.substring("--a2a.embedded-omc-enabled=".length());
if (configured.isBlank()) {
throw new IllegalArgumentException("--a2a.embedded-omc-enabled must be true or false");
}
}
}
if (configured == null) configured = System.getProperty("A2A_EMBEDDED_OMC_ENABLED");
if (configured == null || configured.isBlank()) configured = System.getenv("A2A_EMBEDDED_OMC_ENABLED");
if (configured == null || configured.isBlank()) return defaultValue;
if ("true".equalsIgnoreCase(configured.trim())) return true;
if ("false".equalsIgnoreCase(configured.trim())) return false;
throw new IllegalArgumentException("A2A_EMBEDDED_OMC_ENABLED must be true or false");
}

static List<AgentCard> prepare(boolean enabled) {
return enabled ? validateTargets(new WorkbenchAgentCatalog().load()) : List.of();
}

static List<AgentCard> validateTargets(List<AgentCard> cards) {
return AGENTS.stream()
.map(name -> {
AgentCard card = cards.stream().filter(candidate -> name.equals(candidate.name()))
.findFirst().orElseThrow(() -> new IllegalArgumentException(
"Embedded OMC AgentCard missing: " + name
+ "; provide both demo cards or set A2A_EMBEDDED_OMC_ENABLED=false"));
if (card.supportedInterfaces() == null || card.supportedInterfaces().isEmpty()) {
throw new IllegalArgumentException("Embedded OMC AgentCard has no interface: " + name);
}
LocalServerAddress.requireLocalEndpoint(
card.supportedInterfaces().get(0).url(), "Embedded OMC " + name);
return card;
})
.toList();
}
}
Loading
Loading