@zhin.js/a2a 1.0.2 → 2.0.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.
- package/CHANGELOG.md +42 -0
- package/lib/card-builder.d.ts +1 -1
- package/lib/card-builder.d.ts.map +1 -1
- package/lib/card-builder.js +11 -4
- package/lib/card-builder.js.map +1 -1
- package/lib/runtime.d.ts +19 -0
- package/lib/runtime.d.ts.map +1 -0
- package/lib/runtime.js +109 -0
- package/lib/runtime.js.map +1 -0
- package/package.json +15 -9
- package/src/card-builder.ts +11 -3
- package/src/runtime.ts +143 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @zhin.js/a2a
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [7db69c1]
|
|
8
|
+
- Updated dependencies [e5c84ed]
|
|
9
|
+
- Updated dependencies [3ea84a0]
|
|
10
|
+
- Updated dependencies [1ddcd70]
|
|
11
|
+
- Updated dependencies [ac9da66]
|
|
12
|
+
- @zhin.js/core@1.4.0
|
|
13
|
+
- @zhin.js/host-router@3.0.0
|
|
14
|
+
- @zhin.js/agent@1.0.5
|
|
15
|
+
- @zhin.js/host-http@1.0.2
|
|
16
|
+
- zhin.js@5.0.0
|
|
17
|
+
|
|
18
|
+
## 1.0.3
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 16ec4e8: Harden Plugin Runtime migration boundaries: make process-level registries, the game
|
|
23
|
+
catalog, and game-record storage generation-owned so HMR replacement cannot unregister the
|
|
24
|
+
active generation, discover workspace agents without mutating `process.cwd()`, and
|
|
25
|
+
require authentication for production A2A endpoints.
|
|
26
|
+
|
|
27
|
+
Game SessionServices and Group Suite mutable state now live in owner-scoped
|
|
28
|
+
resources. Lottery database, Agent dependencies, and outbound push bindings use
|
|
29
|
+
generation registrations with rollback-safe disposal.
|
|
30
|
+
|
|
31
|
+
The Plugin Runtime Console demo scope now follows ADR 0016 and rejects project
|
|
32
|
+
file, environment, and database RPCs, closing both direct and file-manager paths
|
|
33
|
+
that could expose `.env` values.
|
|
34
|
+
|
|
35
|
+
- Updated dependencies [16ec4e8]
|
|
36
|
+
- Updated dependencies [cc5c94d]
|
|
37
|
+
- Updated dependencies [447f3e2]
|
|
38
|
+
- @zhin.js/core@1.3.5
|
|
39
|
+
- @zhin.js/agent@1.0.4
|
|
40
|
+
- @zhin.js/host-http@1.0.1
|
|
41
|
+
- zhin.js@4.1.3
|
|
42
|
+
- @zhin.js/logger@1.0.75
|
|
43
|
+
- @zhin.js/host-router@2.0.4
|
|
44
|
+
|
|
3
45
|
## 1.0.2
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/lib/card-builder.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { type AgentCard } from '@a2a-js/sdk';
|
|
5
5
|
import type { AgentBindingRegistry } from '@zhin.js/agent/config';
|
|
6
|
-
export declare function buildAgentCardForBinding(agentName: string, registry: AgentBindingRegistry, publicBaseUrl: string): AgentCard | null;
|
|
6
|
+
export declare function buildAgentCardForBinding(agentName: string, registry: AgentBindingRegistry, publicBaseUrl: string, basePath?: string): AgentCard | null;
|
|
7
7
|
export declare function listExposableAgentNames(registry: AgentBindingRegistry): string[];
|
|
8
8
|
//# sourceMappingURL=card-builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card-builder.d.ts","sourceRoot":"","sources":["../src/card-builder.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAwB,KAAK,SAAS,EAAmB,MAAM,aAAa,CAAC;AAEpF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAuBlE,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"card-builder.d.ts","sourceRoot":"","sources":["../src/card-builder.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAwB,KAAK,SAAS,EAAmB,MAAM,aAAa,CAAC;AAEpF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAuBlE,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,MAAM,EACrB,QAAQ,SAAS,GAChB,SAAS,GAAG,IAAI,CAuElB;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAEhF"}
|
package/lib/card-builder.js
CHANGED
|
@@ -21,13 +21,18 @@ const DEFAULT_SKILLS = [
|
|
|
21
21
|
tags: ['orchestration', 'delegate'],
|
|
22
22
|
}),
|
|
23
23
|
];
|
|
24
|
-
export function buildAgentCardForBinding(agentName, registry, publicBaseUrl) {
|
|
24
|
+
export function buildAgentCardForBinding(agentName, registry, publicBaseUrl, basePath = '/a2a') {
|
|
25
25
|
const binding = registry.getBinding(agentName);
|
|
26
26
|
if (!binding)
|
|
27
27
|
return null;
|
|
28
28
|
const nickname = binding.nickname ?? agentName;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
29
|
+
const normalizedBasePath = basePath === '/a2a' ? undefined : basePath;
|
|
30
|
+
const jsonRpcUrl = normalizedBasePath
|
|
31
|
+
? `${publicBaseUrl}${normalizedBasePath}/${encodeURIComponent(agentName)}/jsonrpc`
|
|
32
|
+
: a2aJsonRpcUrl(publicBaseUrl, agentName);
|
|
33
|
+
const restUrl = normalizedBasePath
|
|
34
|
+
? `${publicBaseUrl}${normalizedBasePath}/${encodeURIComponent(agentName)}/rest`
|
|
35
|
+
: a2aRestUrl(publicBaseUrl, agentName);
|
|
31
36
|
const skills = [
|
|
32
37
|
makeSkill({
|
|
33
38
|
id: agentName,
|
|
@@ -81,7 +86,9 @@ export function buildAgentCardForBinding(agentName, registry, publicBaseUrl) {
|
|
|
81
86
|
defaultOutputModes: ['text/plain'],
|
|
82
87
|
skills,
|
|
83
88
|
signatures: [],
|
|
84
|
-
documentationUrl:
|
|
89
|
+
documentationUrl: normalizedBasePath
|
|
90
|
+
? `${publicBaseUrl}${normalizedBasePath}/${encodeURIComponent(agentName)}/.well-known/agent-card.json`
|
|
91
|
+
: a2aAgentCardUrl(publicBaseUrl, agentName),
|
|
85
92
|
};
|
|
86
93
|
}
|
|
87
94
|
export function listExposableAgentNames(registry) {
|
package/lib/card-builder.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card-builder.js","sourceRoot":"","sources":["../src/card-builder.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,oBAAoB,EAAmC,MAAM,aAAa,CAAC;AAGpF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzE,SAAS,SAAS,CAAC,OAA8E;IAC/F,OAAO;QACL,GAAG,OAAO;QACV,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE;QACxB,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,CAAC,YAAY,CAAC;QAC1B,WAAW,EAAE,CAAC,YAAY,CAAC;QAC3B,oBAAoB,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;KAC9D,CAAC;AACJ,CAAC;AAED,MAAM,cAAc,GAAiB;IACnC,SAAS,CAAC;QACR,EAAE,EAAE,UAAU;QACd,IAAI,EAAE,eAAe;QACrB,WAAW,EAAE,wDAAwD;QACrE,IAAI,EAAE,CAAC,eAAe,EAAE,UAAU,CAAC;KACpC,CAAC;CACH,CAAC;AAEF,MAAM,UAAU,wBAAwB,CACtC,SAAiB,EACjB,QAA8B,EAC9B,aAAqB;
|
|
1
|
+
{"version":3,"file":"card-builder.js","sourceRoot":"","sources":["../src/card-builder.ts"],"names":[],"mappings":"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"}
|
package/lib/runtime.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
2
|
+
import type { AgentHostPort } from '@zhin.js/agent/runtime';
|
|
3
|
+
export interface RuntimeA2aConfig {
|
|
4
|
+
readonly enabled?: boolean;
|
|
5
|
+
readonly path?: string;
|
|
6
|
+
readonly token?: string;
|
|
7
|
+
readonly publicUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface InstallRuntimeA2aOptions {
|
|
10
|
+
readonly http: HttpHost;
|
|
11
|
+
readonly agentHost: AgentHostPort;
|
|
12
|
+
readonly config?: RuntimeA2aConfig;
|
|
13
|
+
readonly fallbackToken?: string;
|
|
14
|
+
readonly fallbackPublicUrl: string;
|
|
15
|
+
readonly production?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Mount Agent Card, JSON-RPC, and HTTP+JSON routes on the Runtime HTTP Host. */
|
|
18
|
+
export declare function installRuntimeA2a(options: InstallRuntimeA2aOptions): () => void;
|
|
19
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAQ5D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAOD,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAyD/E"}
|
package/lib/runtime.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { DefaultRequestHandler, InMemoryTaskStore, } from '@a2a-js/sdk/server';
|
|
2
|
+
import { AGENT_CARD_PATH } from '@a2a-js/sdk';
|
|
3
|
+
import { buildAgentCardForBinding, listExposableAgentNames } from './card-builder.js';
|
|
4
|
+
import { ZhinA2AExecutor } from './agent-executor.js';
|
|
5
|
+
import { handleAgentCard, handleJsonRpc, handleRest } from './http-handlers.js';
|
|
6
|
+
import { verifyA2aBearer } from './auth.js';
|
|
7
|
+
/** Mount Agent Card, JSON-RPC, and HTTP+JSON routes on the Runtime HTTP Host. */
|
|
8
|
+
export function installRuntimeA2a(options) {
|
|
9
|
+
if (options.config?.enabled === false)
|
|
10
|
+
return () => undefined;
|
|
11
|
+
const basePath = normalizePath(options.config?.path ?? '/a2a');
|
|
12
|
+
const publicBaseUrl = (options.config?.publicUrl ?? options.fallbackPublicUrl).replace(/\/+$/u, '');
|
|
13
|
+
const token = options.config?.token ?? options.fallbackToken ?? '';
|
|
14
|
+
if (options.production === true && !token) {
|
|
15
|
+
throw new Error('A2A requires a2a.token or http.token in production');
|
|
16
|
+
}
|
|
17
|
+
const service = options.agentHost.service;
|
|
18
|
+
const agent = options.agentHost.agent;
|
|
19
|
+
const registry = service.getBindingRegistry();
|
|
20
|
+
const stacks = new Map();
|
|
21
|
+
for (const agentName of listExposableAgentNames(registry)) {
|
|
22
|
+
const card = buildAgentCardForBinding(agentName, registry, publicBaseUrl, basePath);
|
|
23
|
+
if (!card)
|
|
24
|
+
continue;
|
|
25
|
+
const executor = new ZhinA2AExecutor({
|
|
26
|
+
agentName,
|
|
27
|
+
getAgent: () => agent,
|
|
28
|
+
resolveBinding: () => registry.getBinding(agentName),
|
|
29
|
+
});
|
|
30
|
+
stacks.set(agentName, {
|
|
31
|
+
card,
|
|
32
|
+
handler: new DefaultRequestHandler(card, new InMemoryTaskStore(), executor),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const unregister = options.http.route('ALL', `${basePath}/*`, async (request, response, url) => {
|
|
36
|
+
if (token && !verifyA2aBearer(request, token)) {
|
|
37
|
+
writeJson(response, 401, { error: 'Unauthorized - Bearer token required' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const route = parseRoute(url.pathname, basePath);
|
|
41
|
+
if (!route) {
|
|
42
|
+
writeJson(response, 404, { error: 'Not found' });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const stack = stacks.get(route.agentName);
|
|
46
|
+
if (!stack) {
|
|
47
|
+
writeJson(response, 404, { error: `A2A agent "${route.agentName}" not found` });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (route.kind === 'card') {
|
|
51
|
+
await handleAgentCard(request, response, stack.handler);
|
|
52
|
+
}
|
|
53
|
+
else if (route.kind === 'jsonrpc') {
|
|
54
|
+
await handleJsonRpc(request, response, stack.handler);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
await handleRest(request, response, stack.handler, route.restPath);
|
|
58
|
+
}
|
|
59
|
+
}, { summary: 'Agent-to-Agent protocol', tags: ['a2a'] });
|
|
60
|
+
return () => {
|
|
61
|
+
unregister();
|
|
62
|
+
stacks.clear();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function parseRoute(pathname, basePath) {
|
|
66
|
+
const prefix = `${basePath}/`;
|
|
67
|
+
if (!pathname.startsWith(prefix))
|
|
68
|
+
return null;
|
|
69
|
+
const slash = pathname.indexOf('/', prefix.length);
|
|
70
|
+
if (slash < 0)
|
|
71
|
+
return null;
|
|
72
|
+
let agentName;
|
|
73
|
+
try {
|
|
74
|
+
agentName = decodeURIComponent(pathname.slice(prefix.length, slash));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (!agentName)
|
|
80
|
+
return null;
|
|
81
|
+
const tail = pathname.slice(slash + 1).replace(/^\/+|\/+$/gu, '');
|
|
82
|
+
if (tail === `.well-known/${AGENT_CARD_PATH}` || tail === '.well-known/agent-card.json') {
|
|
83
|
+
return { agentName, kind: 'card' };
|
|
84
|
+
}
|
|
85
|
+
if (tail === 'jsonrpc' || tail.startsWith('jsonrpc/')) {
|
|
86
|
+
return { agentName, kind: 'jsonrpc' };
|
|
87
|
+
}
|
|
88
|
+
if (tail === 'rest' || tail.startsWith('rest/')) {
|
|
89
|
+
return {
|
|
90
|
+
agentName,
|
|
91
|
+
kind: 'rest',
|
|
92
|
+
restPath: tail === 'rest' ? '' : tail.slice('rest/'.length),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function writeJson(response, status, body) {
|
|
98
|
+
const payload = JSON.stringify(body);
|
|
99
|
+
response.writeHead(status, {
|
|
100
|
+
'content-type': 'application/json; charset=utf-8',
|
|
101
|
+
'content-length': String(Buffer.byteLength(payload)),
|
|
102
|
+
});
|
|
103
|
+
response.end(payload);
|
|
104
|
+
}
|
|
105
|
+
function normalizePath(path) {
|
|
106
|
+
const leading = path.startsWith('/') ? path : `/${path}`;
|
|
107
|
+
return leading.replace(/\/+$/u, '') || '/a2a';
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAkB,MAAM,aAAa,CAAC;AAK9D,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAuB5C,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,OAAiC;IACjE,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,KAAK,KAAK;QAAE,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;IAC9D,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,IAAI,MAAM,CAAC,CAAC;IAC/D,MAAM,aAAa,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACpG,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,IAAI,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;IACnE,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,OAEjC,CAAC;IACF,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,KAAkB,CAAC;IACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAC;IAE7C,KAAK,MAAM,SAAS,IAAI,uBAAuB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,GAAG,wBAAwB,CAAC,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC;QACpF,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,QAAQ,GAAG,IAAI,eAAe,CAAC;YACnC,SAAS;YACT,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK;YACrB,cAAc,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC;SACrD,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE;YACpB,IAAI;YACJ,OAAO,EAAE,IAAI,qBAAqB,CAAC,IAAI,EAAE,IAAI,iBAAiB,EAAE,EAAE,QAAQ,CAAC;SAC5E,CAAC,CAAC;IACL,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,QAAQ,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE;QAC7F,IAAI,KAAK,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;YAC9C,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC,CAAC;YAC5E,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACjD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,cAAc,KAAK,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;YAChF,OAAO;QACT,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,MAAM,eAAe,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC1D,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACpC,MAAM,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,MAAM,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;QACrE,CAAC;IACH,CAAC,EAAE,EAAE,OAAO,EAAE,yBAAyB,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAE1D,OAAO,GAAG,EAAE;QACV,UAAU,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC;AAOD,SAAS,UAAU,CAAC,QAAgB,EAAE,QAAgB;IACpD,MAAM,MAAM,GAAG,GAAG,QAAQ,GAAG,CAAC;IAC9B,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,GAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAClE,IAAI,IAAI,KAAK,eAAe,eAAe,EAAE,IAAI,IAAI,KAAK,6BAA6B,EAAE,CAAC;QACxF,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACrC,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACtD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IACxC,CAAC;IACD,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAChD,OAAO;YACL,SAAS;YACT,IAAI,EAAE,MAAM;YACZ,QAAQ,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;SAC5D,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,SAAS,CAAC,QAA4C,EAAE,MAAc,EAAE,IAAa;IAC5F,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE;QACzB,cAAc,EAAE,iCAAiC;QACjD,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;KACrD,CAAC,CAAC;IACH,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IACzD,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC;AAChD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/a2a",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A2A Protocol server for Zhin.js — multi-agent Agent Cards and task delegation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
11
11
|
"development": "./src/index.ts",
|
|
12
12
|
"import": "./lib/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./runtime": {
|
|
15
|
+
"types": "./lib/runtime.d.ts",
|
|
16
|
+
"development": "./src/runtime.ts",
|
|
17
|
+
"import": "./lib/runtime.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"keywords": [
|
|
@@ -31,21 +36,22 @@
|
|
|
31
36
|
},
|
|
32
37
|
"dependencies": {
|
|
33
38
|
"@a2a-js/sdk": "1.0.0-beta.0",
|
|
34
|
-
"@zhin.js/core": "1.
|
|
35
|
-
"@zhin.js/
|
|
39
|
+
"@zhin.js/core": "1.4.0",
|
|
40
|
+
"@zhin.js/host-http": "1.0.2",
|
|
41
|
+
"@zhin.js/logger": "1.0.75"
|
|
36
42
|
},
|
|
37
43
|
"peerDependencies": {
|
|
38
|
-
"@zhin.js/agent": "1.0.
|
|
39
|
-
"@zhin.js/host-router": "
|
|
40
|
-
"zhin.js": "
|
|
44
|
+
"@zhin.js/agent": "1.0.5",
|
|
45
|
+
"@zhin.js/host-router": "3.0.0",
|
|
46
|
+
"zhin.js": "5.0.0"
|
|
41
47
|
},
|
|
42
48
|
"devDependencies": {
|
|
43
49
|
"@types/node": "^26.1.0",
|
|
44
50
|
"rimraf": "^6.0.0",
|
|
45
51
|
"typescript": "^6.0.3",
|
|
46
|
-
"@zhin.js/agent": "1.0.
|
|
47
|
-
"@zhin.js/host-router": "
|
|
48
|
-
"zhin.js": "
|
|
52
|
+
"@zhin.js/agent": "1.0.5",
|
|
53
|
+
"@zhin.js/host-router": "3.0.0",
|
|
54
|
+
"zhin.js": "5.0.0"
|
|
49
55
|
},
|
|
50
56
|
"files": [
|
|
51
57
|
"src",
|
package/src/card-builder.ts
CHANGED
|
@@ -30,13 +30,19 @@ export function buildAgentCardForBinding(
|
|
|
30
30
|
agentName: string,
|
|
31
31
|
registry: AgentBindingRegistry,
|
|
32
32
|
publicBaseUrl: string,
|
|
33
|
+
basePath = '/a2a',
|
|
33
34
|
): AgentCard | null {
|
|
34
35
|
const binding = registry.getBinding(agentName);
|
|
35
36
|
if (!binding) return null;
|
|
36
37
|
|
|
37
38
|
const nickname = binding.nickname ?? agentName;
|
|
38
|
-
const
|
|
39
|
-
const
|
|
39
|
+
const normalizedBasePath = basePath === '/a2a' ? undefined : basePath;
|
|
40
|
+
const jsonRpcUrl = normalizedBasePath
|
|
41
|
+
? `${publicBaseUrl}${normalizedBasePath}/${encodeURIComponent(agentName)}/jsonrpc`
|
|
42
|
+
: a2aJsonRpcUrl(publicBaseUrl, agentName);
|
|
43
|
+
const restUrl = normalizedBasePath
|
|
44
|
+
? `${publicBaseUrl}${normalizedBasePath}/${encodeURIComponent(agentName)}/rest`
|
|
45
|
+
: a2aRestUrl(publicBaseUrl, agentName);
|
|
40
46
|
|
|
41
47
|
const skills: AgentSkill[] = [
|
|
42
48
|
makeSkill({
|
|
@@ -92,7 +98,9 @@ export function buildAgentCardForBinding(
|
|
|
92
98
|
defaultOutputModes: ['text/plain'],
|
|
93
99
|
skills,
|
|
94
100
|
signatures: [],
|
|
95
|
-
documentationUrl:
|
|
101
|
+
documentationUrl: normalizedBasePath
|
|
102
|
+
? `${publicBaseUrl}${normalizedBasePath}/${encodeURIComponent(agentName)}/.well-known/agent-card.json`
|
|
103
|
+
: a2aAgentCardUrl(publicBaseUrl, agentName),
|
|
96
104
|
};
|
|
97
105
|
}
|
|
98
106
|
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultRequestHandler,
|
|
3
|
+
InMemoryTaskStore,
|
|
4
|
+
type A2ARequestHandler,
|
|
5
|
+
} from '@a2a-js/sdk/server';
|
|
6
|
+
import { AGENT_CARD_PATH, type AgentCard } from '@a2a-js/sdk';
|
|
7
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
8
|
+
import type { AgentHostPort } from '@zhin.js/agent/runtime';
|
|
9
|
+
import type { ZhinAgent } from '@zhin.js/agent';
|
|
10
|
+
import type { AgentBindingRegistry } from '@zhin.js/agent/config';
|
|
11
|
+
import { buildAgentCardForBinding, listExposableAgentNames } from './card-builder.js';
|
|
12
|
+
import { ZhinA2AExecutor } from './agent-executor.js';
|
|
13
|
+
import { handleAgentCard, handleJsonRpc, handleRest } from './http-handlers.js';
|
|
14
|
+
import { verifyA2aBearer } from './auth.js';
|
|
15
|
+
|
|
16
|
+
export interface RuntimeA2aConfig {
|
|
17
|
+
readonly enabled?: boolean;
|
|
18
|
+
readonly path?: string;
|
|
19
|
+
readonly token?: string;
|
|
20
|
+
readonly publicUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface InstallRuntimeA2aOptions {
|
|
24
|
+
readonly http: HttpHost;
|
|
25
|
+
readonly agentHost: AgentHostPort;
|
|
26
|
+
readonly config?: RuntimeA2aConfig;
|
|
27
|
+
readonly fallbackToken?: string;
|
|
28
|
+
readonly fallbackPublicUrl: string;
|
|
29
|
+
readonly production?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface AgentStack {
|
|
33
|
+
readonly card: AgentCard;
|
|
34
|
+
readonly handler: A2ARequestHandler;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Mount Agent Card, JSON-RPC, and HTTP+JSON routes on the Runtime HTTP Host. */
|
|
38
|
+
export function installRuntimeA2a(options: InstallRuntimeA2aOptions): () => void {
|
|
39
|
+
if (options.config?.enabled === false) return () => undefined;
|
|
40
|
+
const basePath = normalizePath(options.config?.path ?? '/a2a');
|
|
41
|
+
const publicBaseUrl = (options.config?.publicUrl ?? options.fallbackPublicUrl).replace(/\/+$/u, '');
|
|
42
|
+
const token = options.config?.token ?? options.fallbackToken ?? '';
|
|
43
|
+
if (options.production === true && !token) {
|
|
44
|
+
throw new Error('A2A requires a2a.token or http.token in production');
|
|
45
|
+
}
|
|
46
|
+
const service = options.agentHost.service as {
|
|
47
|
+
getBindingRegistry(): AgentBindingRegistry;
|
|
48
|
+
};
|
|
49
|
+
const agent = options.agentHost.agent as ZhinAgent;
|
|
50
|
+
const registry = service.getBindingRegistry();
|
|
51
|
+
const stacks = new Map<string, AgentStack>();
|
|
52
|
+
|
|
53
|
+
for (const agentName of listExposableAgentNames(registry)) {
|
|
54
|
+
const card = buildAgentCardForBinding(agentName, registry, publicBaseUrl, basePath);
|
|
55
|
+
if (!card) continue;
|
|
56
|
+
const executor = new ZhinA2AExecutor({
|
|
57
|
+
agentName,
|
|
58
|
+
getAgent: () => agent,
|
|
59
|
+
resolveBinding: () => registry.getBinding(agentName),
|
|
60
|
+
});
|
|
61
|
+
stacks.set(agentName, {
|
|
62
|
+
card,
|
|
63
|
+
handler: new DefaultRequestHandler(card, new InMemoryTaskStore(), executor),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const unregister = options.http.route('ALL', `${basePath}/*`, async (request, response, url) => {
|
|
68
|
+
if (token && !verifyA2aBearer(request, token)) {
|
|
69
|
+
writeJson(response, 401, { error: 'Unauthorized - Bearer token required' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const route = parseRoute(url.pathname, basePath);
|
|
73
|
+
if (!route) {
|
|
74
|
+
writeJson(response, 404, { error: 'Not found' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const stack = stacks.get(route.agentName);
|
|
78
|
+
if (!stack) {
|
|
79
|
+
writeJson(response, 404, { error: `A2A agent "${route.agentName}" not found` });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (route.kind === 'card') {
|
|
83
|
+
await handleAgentCard(request, response, stack.handler);
|
|
84
|
+
} else if (route.kind === 'jsonrpc') {
|
|
85
|
+
await handleJsonRpc(request, response, stack.handler);
|
|
86
|
+
} else {
|
|
87
|
+
await handleRest(request, response, stack.handler, route.restPath);
|
|
88
|
+
}
|
|
89
|
+
}, { summary: 'Agent-to-Agent protocol', tags: ['a2a'] });
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
unregister();
|
|
93
|
+
stacks.clear();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type ParsedRoute =
|
|
98
|
+
| { readonly agentName: string; readonly kind: 'card' }
|
|
99
|
+
| { readonly agentName: string; readonly kind: 'jsonrpc' }
|
|
100
|
+
| { readonly agentName: string; readonly kind: 'rest'; readonly restPath: string };
|
|
101
|
+
|
|
102
|
+
function parseRoute(pathname: string, basePath: string): ParsedRoute | null {
|
|
103
|
+
const prefix = `${basePath}/`;
|
|
104
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
105
|
+
const slash = pathname.indexOf('/', prefix.length);
|
|
106
|
+
if (slash < 0) return null;
|
|
107
|
+
let agentName: string;
|
|
108
|
+
try {
|
|
109
|
+
agentName = decodeURIComponent(pathname.slice(prefix.length, slash));
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!agentName) return null;
|
|
114
|
+
const tail = pathname.slice(slash + 1).replace(/^\/+|\/+$/gu, '');
|
|
115
|
+
if (tail === `.well-known/${AGENT_CARD_PATH}` || tail === '.well-known/agent-card.json') {
|
|
116
|
+
return { agentName, kind: 'card' };
|
|
117
|
+
}
|
|
118
|
+
if (tail === 'jsonrpc' || tail.startsWith('jsonrpc/')) {
|
|
119
|
+
return { agentName, kind: 'jsonrpc' };
|
|
120
|
+
}
|
|
121
|
+
if (tail === 'rest' || tail.startsWith('rest/')) {
|
|
122
|
+
return {
|
|
123
|
+
agentName,
|
|
124
|
+
kind: 'rest',
|
|
125
|
+
restPath: tail === 'rest' ? '' : tail.slice('rest/'.length),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeJson(response: import('node:http').ServerResponse, status: number, body: unknown): void {
|
|
132
|
+
const payload = JSON.stringify(body);
|
|
133
|
+
response.writeHead(status, {
|
|
134
|
+
'content-type': 'application/json; charset=utf-8',
|
|
135
|
+
'content-length': String(Buffer.byteLength(payload)),
|
|
136
|
+
});
|
|
137
|
+
response.end(payload);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizePath(path: string): string {
|
|
141
|
+
const leading = path.startsWith('/') ? path : `/${path}`;
|
|
142
|
+
return leading.replace(/\/+$/u, '') || '/a2a';
|
|
143
|
+
}
|