@aramisfa/openclaw-a2a-outbound 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +261 -16
  2. package/dist/config.d.ts +17 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +214 -8
  5. package/dist/config.js.map +1 -1
  6. package/dist/constants.d.ts +1 -1
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +1 -1
  9. package/dist/constants.js.map +1 -1
  10. package/dist/errors.d.ts +4 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js +37 -1
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +44 -20
  17. package/dist/index.js.map +1 -1
  18. package/dist/request-normalization.d.ts +23 -0
  19. package/dist/request-normalization.d.ts.map +1 -0
  20. package/dist/request-normalization.js +109 -0
  21. package/dist/request-normalization.js.map +1 -0
  22. package/dist/result-shape.d.ts +68 -25
  23. package/dist/result-shape.d.ts.map +1 -1
  24. package/dist/result-shape.js +192 -59
  25. package/dist/result-shape.js.map +1 -1
  26. package/dist/schemas.d.ts +81 -779
  27. package/dist/schemas.d.ts.map +1 -1
  28. package/dist/schemas.js +308 -275
  29. package/dist/schemas.js.map +1 -1
  30. package/dist/sdk-client-pool.d.ts +8 -4
  31. package/dist/sdk-client-pool.d.ts.map +1 -1
  32. package/dist/sdk-client-pool.js +21 -1
  33. package/dist/sdk-client-pool.js.map +1 -1
  34. package/dist/service.d.ts +33 -4
  35. package/dist/service.d.ts.map +1 -1
  36. package/dist/service.js +381 -60
  37. package/dist/service.js.map +1 -1
  38. package/dist/target-catalog.d.ts +66 -0
  39. package/dist/target-catalog.d.ts.map +1 -0
  40. package/dist/target-catalog.js +309 -0
  41. package/dist/target-catalog.js.map +1 -0
  42. package/dist/task-handle-registry.d.ts +29 -0
  43. package/dist/task-handle-registry.d.ts.map +1 -0
  44. package/dist/task-handle-registry.js +141 -0
  45. package/dist/task-handle-registry.js.map +1 -0
  46. package/openclaw.plugin.json +68 -1
  47. package/package.json +2 -1
  48. package/skills/remote-agent/SKILL.md +93 -0
package/README.md CHANGED
@@ -2,11 +2,7 @@
2
2
 
3
3
  Native OpenClaw outbound A2A delegation plugin.
4
4
 
5
- This package registers three optional tools in OpenClaw:
6
-
7
- - `a2a_delegate`
8
- - `a2a_task_status`
9
- - `a2a_task_cancel`
5
+ This package registers exactly one optional OpenClaw tool, `remote_agent`. The tool exposes five actions: `list_targets`, `send`, `watch`, `status`, and `cancel`.
10
6
 
11
7
  ## Installation
12
8
 
@@ -21,7 +17,7 @@ npm install @aramisfa/openclaw-a2a-outbound
21
17
 
22
18
  ## OpenClaw Plugin Config
23
19
 
24
- `@aramisfa/openclaw-a2a-outbound` is disabled by default. Enable it in your plugin config:
20
+ `@aramisfa/openclaw-a2a-outbound` is disabled by default. Enable plugin id `openclaw-a2a-outbound` in your OpenClaw plugin config:
25
21
 
26
22
  ```json
27
23
  {
@@ -32,33 +28,263 @@ npm install @aramisfa/openclaw-a2a-outbound
32
28
  "preferredTransports": ["JSONRPC", "HTTP+JSON"],
33
29
  "serviceParameters": {}
34
30
  },
31
+ "targets": [
32
+ {
33
+ "alias": "support",
34
+ "baseUrl": "https://support.example",
35
+ "description": "Primary support lane",
36
+ "tags": ["support"],
37
+ "examples": ["Summarize this incident and propose next steps."],
38
+ "default": true
39
+ }
40
+ ],
41
+ "taskHandles": {
42
+ "ttlMs": 86400000,
43
+ "maxEntries": 1000
44
+ },
35
45
  "policy": {
36
46
  "acceptedOutputModes": [],
37
47
  "normalizeBaseUrl": true,
38
- "enforceSupportedTransports": true
48
+ "enforceSupportedTransports": true,
49
+ "allowTargetUrlOverride": false
50
+ }
51
+ }
52
+ ```
53
+
54
+ Call `list_targets` first to discover configured aliases and refreshed target-card metadata. Prefer `target_alias` over `target_url`; use `target_url` only when policy allows direct URL routing.
55
+
56
+ ## Unified Tool Contract
57
+
58
+ `remote_agent` accepts a flattened request object with top-level fields:
59
+
60
+ - `action`: required for every request.
61
+ - `target_alias`: preferred routing key for `send`, `watch`, `status`, and `cancel`.
62
+ - `target_url`: explicit remote base URL when policy allows it or when it matches a configured target.
63
+ - `input`: required for `send`; becomes the single user text part sent to the peer.
64
+ - `attachments`: optional file or data attachments for `send`.
65
+ - `task_handle`: opaque follow-up handle returned after delegated tasks are created.
66
+ - `task_id`: fallback follow-up key when no live `task_handle` is available.
67
+ - `follow_updates`: stream live updates during `send`.
68
+ - `history_length`: optional history window for `send` and `status`.
69
+ - `timeout_ms`: per-request timeout override.
70
+ - `service_parameters`: optional outbound service parameters.
71
+ - `metadata`: optional metadata payload for `send`.
72
+
73
+ Action-specific validation rejects unsupported fields for each action, so keep requests flat and action-focused.
74
+
75
+ ## Actions
76
+
77
+ - `list_targets`: discover configured targets, aliases, examples, and hydrated card metadata.
78
+ - `send`: send user input to a remote agent selected by `target_alias`, `target_url`, or a configured default target.
79
+ - `watch`: resubscribe to a running delegated task and stream updates.
80
+ - `status`: fetch the latest task snapshot.
81
+ - `cancel`: request cancellation for a delegated task.
82
+
83
+ For follow-up actions, prefer `task_handle` first. If the handle is expired or unavailable, fall back to `target_alias` + `task_id`.
84
+
85
+ ## Examples
86
+
87
+ ### Discover Available Targets
88
+
89
+ ```json
90
+ { "action": "list_targets" }
91
+ ```
92
+
93
+ ```json
94
+ {
95
+ "ok": true,
96
+ "operation": "remote_agent",
97
+ "action": "list_targets",
98
+ "summary": {
99
+ "targets": [
100
+ {
101
+ "target_alias": "support",
102
+ "target_url": "https://support.example/",
103
+ "default": true,
104
+ "tags": ["support"],
105
+ "examples": ["Summarize this incident and propose next steps."],
106
+ "target_name": "Support Agent",
107
+ "description": "Primary support lane",
108
+ "streaming_supported": true,
109
+ "skills": [
110
+ {
111
+ "id": "triage",
112
+ "name": "Incident Triage",
113
+ "description": "Summarize incidents and propose next actions.",
114
+ "tags": ["support"],
115
+ "examples": ["Summarize this incident and propose next steps."]
116
+ }
117
+ ]
118
+ }
119
+ ]
120
+ },
121
+ "raw": [
122
+ {
123
+ "default": true,
124
+ "tags": ["support"],
125
+ "examples": ["Summarize this incident and propose next steps."]
126
+ }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ ### Send To An Explicit `target_alias`
132
+
133
+ ```json
134
+ {
135
+ "action": "send",
136
+ "target_alias": "support",
137
+ "input": "Summarize this bug report for triage.",
138
+ "metadata": {
139
+ "ticket_id": "INC-42"
140
+ }
141
+ }
142
+ ```
143
+
144
+ ```json
145
+ {
146
+ "ok": true,
147
+ "operation": "remote_agent",
148
+ "action": "send",
149
+ "summary": {
150
+ "target_alias": "support",
151
+ "target_url": "https://support.example/",
152
+ "message_text": "Triage summary: reproduce, collect logs, and notify the on-call engineer."
153
+ },
154
+ "raw": {
155
+ "kind": "message"
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### Send Using The Configured Default Target
161
+
162
+ If one target is marked `"default": true`, `send` can omit `target_alias`:
163
+
164
+ ```json
165
+ {
166
+ "action": "send",
167
+ "input": "Draft a reply to the customer update.",
168
+ "follow_updates": true
169
+ }
170
+ ```
171
+
172
+ ```json
173
+ {
174
+ "ok": true,
175
+ "operation": "remote_agent",
176
+ "action": "send",
177
+ "summary": {
178
+ "target_alias": "support",
179
+ "target_url": "https://support.example/",
180
+ "task_handle": "rah_0a3ff8c2-4a6d-48cb-a57d-4ae6f3c589d0",
181
+ "task_id": "task-456",
182
+ "status": "completed",
183
+ "can_watch": true
184
+ },
185
+ "raw": {
186
+ "events": [
187
+ {
188
+ "kind": "task",
189
+ "id": "task-456",
190
+ "status": {
191
+ "state": "submitted"
192
+ }
193
+ },
194
+ {
195
+ "kind": "status-update",
196
+ "taskId": "task-456",
197
+ "status": {
198
+ "state": "completed"
199
+ },
200
+ "final": true
201
+ }
202
+ ],
203
+ "finalEvent": {
204
+ "kind": "status-update",
205
+ "taskId": "task-456",
206
+ "status": {
207
+ "state": "completed"
208
+ },
209
+ "final": true
210
+ }
39
211
  }
40
212
  }
41
213
  ```
42
214
 
43
- ## Validation Errors
215
+ ### Check Task Status With `task_handle`
44
216
 
45
- Tool input validation is powered by [Ajv](https://ajv.js.org/) in strict mode. When validation fails, the error envelope contains native Ajv error objects:
217
+ ```json
218
+ {
219
+ "action": "status",
220
+ "task_handle": "rah_0a3ff8c2-4a6d-48cb-a57d-4ae6f3c589d0",
221
+ "history_length": 2
222
+ }
223
+ ```
224
+
225
+ ```json
226
+ {
227
+ "ok": true,
228
+ "operation": "remote_agent",
229
+ "action": "status",
230
+ "summary": {
231
+ "target_alias": "support",
232
+ "target_url": "https://support.example/",
233
+ "task_handle": "rah_0a3ff8c2-4a6d-48cb-a57d-4ae6f3c589d0",
234
+ "task_id": "task-456",
235
+ "status": "completed",
236
+ "can_watch": true
237
+ },
238
+ "raw": {
239
+ "kind": "task",
240
+ "id": "task-456",
241
+ "status": {
242
+ "state": "completed"
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ When a handle is expired or unavailable, retry with `target_alias` + `task_id`:
249
+
250
+ ```json
251
+ {
252
+ "action": "status",
253
+ "target_alias": "support",
254
+ "task_id": "task-456"
255
+ }
256
+ ```
257
+
258
+ `watch` and `cancel` use the same follow-up targeting rules:
259
+
260
+ ```json
261
+ { "action": "watch", "task_handle": "rah_0a3ff8c2-4a6d-48cb-a57d-4ae6f3c589d0" }
262
+ ```
263
+
264
+ ```json
265
+ { "action": "cancel", "task_handle": "rah_0a3ff8c2-4a6d-48cb-a57d-4ae6f3c589d0" }
266
+ ```
267
+
268
+ ## Validation And Actionable Errors
269
+
270
+ Tool input validation uses Ajv in strict mode. Validation failures use `operation: "remote_agent"` and include native-style Ajv error objects:
46
271
 
47
272
  ```json
48
273
  {
49
274
  "ok": false,
50
- "operation": "a2a_delegate",
275
+ "operation": "remote_agent",
276
+ "action": "send",
51
277
  "error": {
52
278
  "code": "VALIDATION_ERROR",
53
- "message": "a2a_delegate input validation failed",
279
+ "message": "remote_agent input validation failed",
54
280
  "details": {
55
281
  "source": "ajv",
56
- "tool": "a2a_delegate",
282
+ "tool": "remote_agent",
57
283
  "errors": [
58
284
  {
59
- "keyword": "required",
60
- "instancePath": "/request/message",
61
- "params": { "missingProperty": "kind" }
285
+ "keyword": "anyOf",
286
+ "instancePath": "",
287
+ "message": "send requires target_alias, target_url, or a configured default target"
62
288
  }
63
289
  ]
64
290
  }
@@ -66,7 +292,26 @@ Tool input validation is powered by [Ajv](https://ajv.js.org/) in strict mode. W
66
292
  }
67
293
  ```
68
294
 
69
- The `error.details.errors` array contains Ajv `ErrorObject` entries. See the [Ajv error documentation](https://ajv.js.org/api.html#error-objects) for the full shape.
295
+ Expired handles return an actionable recovery envelope:
296
+
297
+ ```json
298
+ {
299
+ "ok": false,
300
+ "operation": "remote_agent",
301
+ "action": "status",
302
+ "error": {
303
+ "code": "EXPIRED_TASK_HANDLE",
304
+ "message": "task handle \"rah_0a3ff8c2-4a6d-48cb-a57d-4ae6f3c589d0\" has expired",
305
+ "details": {
306
+ "taskHandle": "rah_0a3ff8c2-4a6d-48cb-a57d-4ae6f3c589d0",
307
+ "retryHint": "Retry with explicit target plus taskId, or resend the original request after a restart to obtain a new handle.",
308
+ "restartInvalidatesHandles": true,
309
+ "suggested_actions": ["status", "send"],
310
+ "hint": "Retry with target_alias + task_id, or send a new request."
311
+ }
312
+ }
313
+ }
314
+ ```
70
315
 
71
316
  ## Development
72
317
 
package/dist/config.d.ts CHANGED
@@ -6,14 +6,31 @@ export interface A2AOutboundDefaultsConfig {
6
6
  preferredTransports: A2ATransport[];
7
7
  serviceParameters: Record<string, string>;
8
8
  }
9
+ export interface A2AOutboundTargetConfig {
10
+ alias: string;
11
+ baseUrl: string;
12
+ description?: string;
13
+ tags: string[];
14
+ cardPath: string;
15
+ preferredTransports: A2ATransport[];
16
+ examples: string[];
17
+ default: boolean;
18
+ }
19
+ export interface A2AOutboundTaskHandlesConfig {
20
+ ttlMs: number;
21
+ maxEntries: number;
22
+ }
9
23
  export interface A2AOutboundPolicyConfig {
10
24
  acceptedOutputModes: string[];
11
25
  normalizeBaseUrl: boolean;
12
26
  enforceSupportedTransports: boolean;
27
+ allowTargetUrlOverride: boolean;
13
28
  }
14
29
  export interface A2AOutboundPluginConfig {
15
30
  enabled: boolean;
16
31
  defaults: A2AOutboundDefaultsConfig;
32
+ targets: A2AOutboundTargetConfig[];
33
+ taskHandles: A2AOutboundTaskHandlesConfig;
17
34
  policy: A2AOutboundPolicyConfig;
18
35
  }
19
36
  export declare const A2A_OUTBOUND_DEFAULT_CONFIG: A2AOutboundPluginConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,uBAAuB;IACtC,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,0BAA0B,EAAE,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,yBAAyB,CAAC;IACpC,MAAM,EAAE,uBAAuB,CAAC;CACjC;AAED,eAAO,MAAM,2BAA2B,EAAE,uBAazC,CAAC;AAEF,eAAO,MAAM,+BAA+B,EAAE,WAAW,CACvD,0BAA0B,CAAC,YAAY,CAAC,CAyDzC,CAAC;AAEF,eAAO,MAAM,4BAA4B,EAAE,WAAW,CACpD,0BAA0B,CAAC,SAAS,CAAC,CA0CtC,CAAC;AAuFF,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,OAAO,GACb,uBAAuB,CA4CzB;AAED,eAAO,MAAM,mCAAmC,EAAE,0BAO/C,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,0BAA0B,EAAE,OAAO,CAAC;IACpC,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,yBAAyB,CAAC;IACpC,OAAO,EAAE,uBAAuB,EAAE,CAAC;IACnC,WAAW,EAAE,4BAA4B,CAAC;IAC1C,MAAM,EAAE,uBAAuB,CAAC;CACjC;AAED,eAAO,MAAM,2BAA2B,EAAE,uBAmBzC,CAAC;AAEF,eAAO,MAAM,+BAA+B,EAAE,WAAW,CACvD,0BAA0B,CAAC,YAAY,CAAC,CA2HzC,CAAC;AAEF,eAAO,MAAM,4BAA4B,EAAE,WAAW,CACpD,0BAA0B,CAAC,SAAS,CAAC,CA6DtC,CAAC;AAqPF,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,OAAO,GACb,uBAAuB,CA+DzB;AAED,eAAO,MAAM,mCAAmC,EAAE,0BAO/C,CAAC"}
package/dist/config.js CHANGED
@@ -7,10 +7,16 @@ export const A2A_OUTBOUND_DEFAULT_CONFIG = {
7
7
  preferredTransports: [...SUPPORTED_TRANSPORTS],
8
8
  serviceParameters: {},
9
9
  },
10
+ targets: [],
11
+ taskHandles: {
12
+ ttlMs: 86400000,
13
+ maxEntries: 1000,
14
+ },
10
15
  policy: {
11
16
  acceptedOutputModes: [],
12
17
  normalizeBaseUrl: true,
13
18
  enforceSupportedTransports: true,
19
+ allowTargetUrlOverride: false,
14
20
  },
15
21
  };
16
22
  export const A2A_OUTBOUND_CONFIG_JSON_SCHEMA = {
@@ -49,6 +55,68 @@ export const A2A_OUTBOUND_CONFIG_JSON_SCHEMA = {
49
55
  },
50
56
  },
51
57
  },
58
+ targets: {
59
+ type: "array",
60
+ default: [],
61
+ items: {
62
+ type: "object",
63
+ additionalProperties: false,
64
+ required: ["alias", "baseUrl"],
65
+ properties: {
66
+ alias: {
67
+ type: "string",
68
+ },
69
+ baseUrl: {
70
+ type: "string",
71
+ },
72
+ description: {
73
+ type: "string",
74
+ },
75
+ tags: {
76
+ type: "array",
77
+ items: { type: "string" },
78
+ default: [],
79
+ },
80
+ cardPath: {
81
+ type: "string",
82
+ default: A2A_OUTBOUND_DEFAULT_CONFIG.defaults.cardPath,
83
+ },
84
+ preferredTransports: {
85
+ type: "array",
86
+ items: {
87
+ type: "string",
88
+ enum: [...ALL_TRANSPORTS],
89
+ },
90
+ default: [...A2A_OUTBOUND_DEFAULT_CONFIG.defaults.preferredTransports],
91
+ },
92
+ examples: {
93
+ type: "array",
94
+ items: { type: "string" },
95
+ default: [],
96
+ },
97
+ default: {
98
+ type: "boolean",
99
+ default: false,
100
+ },
101
+ },
102
+ },
103
+ },
104
+ taskHandles: {
105
+ type: "object",
106
+ additionalProperties: false,
107
+ properties: {
108
+ ttlMs: {
109
+ type: "integer",
110
+ minimum: 1,
111
+ default: A2A_OUTBOUND_DEFAULT_CONFIG.taskHandles.ttlMs,
112
+ },
113
+ maxEntries: {
114
+ type: "integer",
115
+ minimum: 1,
116
+ default: A2A_OUTBOUND_DEFAULT_CONFIG.taskHandles.maxEntries,
117
+ },
118
+ },
119
+ },
52
120
  policy: {
53
121
  type: "object",
54
122
  additionalProperties: false,
@@ -66,6 +134,10 @@ export const A2A_OUTBOUND_CONFIG_JSON_SCHEMA = {
66
134
  type: "boolean",
67
135
  default: A2A_OUTBOUND_DEFAULT_CONFIG.policy.enforceSupportedTransports,
68
136
  },
137
+ allowTargetUrlOverride: {
138
+ type: "boolean",
139
+ default: A2A_OUTBOUND_DEFAULT_CONFIG.policy.allowTargetUrlOverride,
140
+ },
69
141
  },
70
142
  },
71
143
  },
@@ -96,6 +168,20 @@ export const A2A_OUTBOUND_CONFIG_UI_HINTS = {
96
168
  help: "Default headers or provider-specific request parameters.",
97
169
  advanced: true,
98
170
  },
171
+ targets: {
172
+ label: "Named Targets",
173
+ help: "Registry of reusable outbound A2A targets for future routing phases.",
174
+ },
175
+ "taskHandles.ttlMs": {
176
+ label: "Task Handle TTL (ms)",
177
+ help: "Retention window for cached delegated task handles.",
178
+ advanced: true,
179
+ },
180
+ "taskHandles.maxEntries": {
181
+ label: "Task Handle Cache Size",
182
+ help: "Maximum number of delegated task handles retained locally.",
183
+ advanced: true,
184
+ },
99
185
  "policy.acceptedOutputModes": {
100
186
  label: "Accepted Output Modes",
101
187
  help: "Allowed response media types for outbound client requests.",
@@ -111,7 +197,24 @@ export const A2A_OUTBOUND_CONFIG_UI_HINTS = {
111
197
  help: "Rejects targets requesting transports unsupported by this build.",
112
198
  advanced: true,
113
199
  },
200
+ "policy.allowTargetUrlOverride": {
201
+ label: "Allow Target URL Override",
202
+ help: "Permits explicit request URLs to bypass the named target registry.",
203
+ advanced: true,
204
+ },
114
205
  };
206
+ function cloneTargetConfig(target) {
207
+ return {
208
+ alias: target.alias,
209
+ baseUrl: target.baseUrl,
210
+ ...(target.description !== undefined ? { description: target.description } : {}),
211
+ tags: [...target.tags],
212
+ cardPath: target.cardPath,
213
+ preferredTransports: [...target.preferredTransports],
214
+ examples: [...target.examples],
215
+ default: target.default,
216
+ };
217
+ }
115
218
  function cloneConfig(config) {
116
219
  return {
117
220
  enabled: config.enabled,
@@ -121,10 +224,16 @@ function cloneConfig(config) {
121
224
  preferredTransports: [...config.defaults.preferredTransports],
122
225
  serviceParameters: { ...config.defaults.serviceParameters },
123
226
  },
227
+ targets: config.targets.map(cloneTargetConfig),
228
+ taskHandles: {
229
+ ttlMs: config.taskHandles.ttlMs,
230
+ maxEntries: config.taskHandles.maxEntries,
231
+ },
124
232
  policy: {
125
233
  acceptedOutputModes: [...config.policy.acceptedOutputModes],
126
234
  normalizeBaseUrl: config.policy.normalizeBaseUrl,
127
235
  enforceSupportedTransports: config.policy.enforceSupportedTransports,
236
+ allowTargetUrlOverride: config.policy.allowTargetUrlOverride,
128
237
  },
129
238
  };
130
239
  }
@@ -146,10 +255,33 @@ function normalizeStringArray(value, fallback = []) {
146
255
  }
147
256
  return normalized;
148
257
  }
258
+ function normalizeTrimmedStringArray(value, fallback = []) {
259
+ if (!Array.isArray(value)) {
260
+ return [...fallback];
261
+ }
262
+ const normalized = [];
263
+ for (const entry of value) {
264
+ if (typeof entry !== "string") {
265
+ continue;
266
+ }
267
+ const trimmed = entry.trim();
268
+ if (trimmed === "") {
269
+ continue;
270
+ }
271
+ if (!normalized.includes(trimmed)) {
272
+ normalized.push(trimmed);
273
+ }
274
+ }
275
+ return normalized;
276
+ }
149
277
  function normalizeTransports(value, fallback) {
150
278
  const normalized = normalizeStringArray(value).filter((entry) => ALL_TRANSPORTS.includes(entry));
151
279
  return normalized.length > 0 ? normalized : [...fallback];
152
280
  }
281
+ function normalizeTrimmedTransports(value, fallback) {
282
+ const normalized = normalizeTrimmedStringArray(value).filter((entry) => ALL_TRANSPORTS.includes(entry));
283
+ return normalized.length > 0 ? normalized : [...fallback];
284
+ }
153
285
  function normalizeStringMap(value, fallback = {}) {
154
286
  if (!isPlainObject(value)) {
155
287
  return { ...fallback };
@@ -170,27 +302,101 @@ function readPositiveInteger(value, fallback) {
170
302
  ? value
171
303
  : fallback;
172
304
  }
305
+ function readString(value, fallback) {
306
+ return typeof value === "string" && value.trim() !== "" ? value : fallback;
307
+ }
308
+ function readTrimmedString(value, fallback) {
309
+ if (typeof value !== "string") {
310
+ return fallback;
311
+ }
312
+ const trimmed = value.trim();
313
+ return trimmed !== "" ? trimmed : fallback;
314
+ }
315
+ function readOptionalTrimmedString(value) {
316
+ if (typeof value !== "string") {
317
+ return undefined;
318
+ }
319
+ const trimmed = value.trim();
320
+ return trimmed !== "" ? trimmed : undefined;
321
+ }
322
+ function invalidConfig(message) {
323
+ return new TypeError(`Invalid openclaw-a2a-outbound config: ${message}`);
324
+ }
325
+ function normalizeTargets(value, defaults) {
326
+ if (value === undefined) {
327
+ return A2A_OUTBOUND_DEFAULT_CONFIG.targets.map(cloneTargetConfig);
328
+ }
329
+ if (!Array.isArray(value)) {
330
+ throw invalidConfig("targets must be an array");
331
+ }
332
+ const aliases = new Set();
333
+ let defaultTargetAlias;
334
+ return value.map((entry, index) => {
335
+ if (!isPlainObject(entry)) {
336
+ throw invalidConfig(`targets[${index}] must be an object`);
337
+ }
338
+ const alias = typeof entry.alias === "string" ? entry.alias.trim() : "";
339
+ if (alias === "") {
340
+ throw invalidConfig(`targets[${index}].alias must be a non-empty string`);
341
+ }
342
+ const baseUrl = typeof entry.baseUrl === "string" ? entry.baseUrl.trim() : "";
343
+ if (baseUrl === "") {
344
+ throw invalidConfig(`targets[${index}].baseUrl must be a non-empty string`);
345
+ }
346
+ if (aliases.has(alias)) {
347
+ throw invalidConfig(`targets contains duplicate alias "${alias}"`);
348
+ }
349
+ aliases.add(alias);
350
+ const normalizedTarget = {
351
+ alias,
352
+ baseUrl,
353
+ tags: normalizeTrimmedStringArray(entry.tags),
354
+ cardPath: readTrimmedString(entry.cardPath, defaults.cardPath.trim()),
355
+ preferredTransports: normalizeTrimmedTransports(entry.preferredTransports, defaults.preferredTransports),
356
+ examples: normalizeTrimmedStringArray(entry.examples),
357
+ default: readBoolean(entry.default, false),
358
+ };
359
+ const description = readOptionalTrimmedString(entry.description);
360
+ if (description !== undefined) {
361
+ normalizedTarget.description = description;
362
+ }
363
+ if (normalizedTarget.default) {
364
+ if (defaultTargetAlias !== undefined) {
365
+ throw invalidConfig(`targets contains multiple default entries ("${defaultTargetAlias}" and "${alias}")`);
366
+ }
367
+ defaultTargetAlias = alias;
368
+ }
369
+ return normalizedTarget;
370
+ });
371
+ }
173
372
  export function parseA2AOutboundPluginConfig(input) {
174
373
  if (!isPlainObject(input)) {
175
374
  return cloneConfig(A2A_OUTBOUND_DEFAULT_CONFIG);
176
375
  }
177
376
  const rawDefaults = isPlainObject(input.defaults) ? input.defaults : {};
377
+ const normalizedDefaults = {
378
+ timeoutMs: readPositiveInteger(rawDefaults.timeoutMs, A2A_OUTBOUND_DEFAULT_CONFIG.defaults.timeoutMs),
379
+ cardPath: readString(rawDefaults.cardPath, A2A_OUTBOUND_DEFAULT_CONFIG.defaults.cardPath),
380
+ preferredTransports: normalizeTransports(rawDefaults.preferredTransports, A2A_OUTBOUND_DEFAULT_CONFIG.defaults.preferredTransports),
381
+ serviceParameters: normalizeStringMap(rawDefaults.serviceParameters, A2A_OUTBOUND_DEFAULT_CONFIG.defaults.serviceParameters),
382
+ };
383
+ const rawTaskHandles = isPlainObject(input.taskHandles)
384
+ ? input.taskHandles
385
+ : {};
178
386
  const rawPolicy = isPlainObject(input.policy) ? input.policy : {};
179
387
  return {
180
388
  enabled: readBoolean(input.enabled, A2A_OUTBOUND_DEFAULT_CONFIG.enabled),
181
- defaults: {
182
- timeoutMs: readPositiveInteger(rawDefaults.timeoutMs, A2A_OUTBOUND_DEFAULT_CONFIG.defaults.timeoutMs),
183
- cardPath: typeof rawDefaults.cardPath === "string" &&
184
- rawDefaults.cardPath.trim() !== ""
185
- ? rawDefaults.cardPath
186
- : A2A_OUTBOUND_DEFAULT_CONFIG.defaults.cardPath,
187
- preferredTransports: normalizeTransports(rawDefaults.preferredTransports, A2A_OUTBOUND_DEFAULT_CONFIG.defaults.preferredTransports),
188
- serviceParameters: normalizeStringMap(rawDefaults.serviceParameters, A2A_OUTBOUND_DEFAULT_CONFIG.defaults.serviceParameters),
389
+ defaults: normalizedDefaults,
390
+ targets: normalizeTargets(input.targets, normalizedDefaults),
391
+ taskHandles: {
392
+ ttlMs: readPositiveInteger(rawTaskHandles.ttlMs, A2A_OUTBOUND_DEFAULT_CONFIG.taskHandles.ttlMs),
393
+ maxEntries: readPositiveInteger(rawTaskHandles.maxEntries, A2A_OUTBOUND_DEFAULT_CONFIG.taskHandles.maxEntries),
189
394
  },
190
395
  policy: {
191
396
  acceptedOutputModes: normalizeStringArray(rawPolicy.acceptedOutputModes, A2A_OUTBOUND_DEFAULT_CONFIG.policy.acceptedOutputModes),
192
397
  normalizeBaseUrl: readBoolean(rawPolicy.normalizeBaseUrl, A2A_OUTBOUND_DEFAULT_CONFIG.policy.normalizeBaseUrl),
193
398
  enforceSupportedTransports: readBoolean(rawPolicy.enforceSupportedTransports, A2A_OUTBOUND_DEFAULT_CONFIG.policy.enforceSupportedTransports),
399
+ allowTargetUrlOverride: readBoolean(rawPolicy.allowTargetUrlOverride, A2A_OUTBOUND_DEFAULT_CONFIG.policy.allowTargetUrlOverride),
194
400
  },
195
401
  };
196
402
  }