@aramisfa/openclaw-a2a-outbound 0.1.2 → 0.2.1

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