@ai_mind/dsh-a2a 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +444 -2
- package/client.js +868 -0
- package/cordis.patch.yml +21 -0
- package/icon.svg +15 -0
- package/index.js +59 -0
- package/lib/auth.js +196 -0
- package/lib/bridge.js +518 -0
- package/lib/card.js +214 -0
- package/lib/client-discovery.js +58 -0
- package/lib/config.js +565 -0
- package/lib/executor.js +357 -0
- package/lib/files.js +52 -0
- package/lib/limits.js +133 -0
- package/lib/panel.js +357 -0
- package/lib/prompt.js +161 -0
- package/lib/server.js +456 -0
- package/lib/store.js +288 -0
- package/lib/util.js +221 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +54 -4
package/lib/card.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Card construction and JWS signing for A2A v1.0.
|
|
3
|
+
*
|
|
4
|
+
* The public card is served at the well-known URI. Optional JWS signatures
|
|
5
|
+
* follow RFC 7515 over the RFC 8785 canonical form computed by the official
|
|
6
|
+
* SDK, so a card signed here verifies with any conforming A2A client.
|
|
7
|
+
*
|
|
8
|
+
* @module @ai_mind/dsh-a2a/card
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { createPrivateKey } from 'node:crypto'
|
|
13
|
+
import { A2A_PROTOCOL_VERSION, generateAgentCardSignature } from '@a2a-js/sdk'
|
|
14
|
+
import { trimTrailingSlash } from './util.js'
|
|
15
|
+
|
|
16
|
+
/** Security scheme name advertised for bearer or API-key authentication. */
|
|
17
|
+
export const SECURITY_SCHEME_NAME = 'a2a'
|
|
18
|
+
|
|
19
|
+
/** @returns {boolean} whether the deployment requires credentials. */
|
|
20
|
+
export function isAuthEnabled(config, credentials) {
|
|
21
|
+
return config.auth.mode !== 'none' && credentials.length > 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the `securitySchemes` / `securityRequirements` pair for the public
|
|
26
|
+
* card. An unsecured deployment returns empty objects.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} config
|
|
29
|
+
* @param {{ id: string, token: string }[]} credentials
|
|
30
|
+
* @returns {{ securitySchemes: object, securityRequirements: object[] }}
|
|
31
|
+
*/
|
|
32
|
+
export function buildSecurity(config, credentials) {
|
|
33
|
+
if (!isAuthEnabled(config, credentials)) {
|
|
34
|
+
return { securitySchemes: {}, securityRequirements: [] }
|
|
35
|
+
}
|
|
36
|
+
if (config.auth.mode === 'bearer') {
|
|
37
|
+
return {
|
|
38
|
+
securitySchemes: {
|
|
39
|
+
[SECURITY_SCHEME_NAME]: {
|
|
40
|
+
httpAuthSecurityScheme: {
|
|
41
|
+
scheme: 'Bearer',
|
|
42
|
+
bearerFormat: 'opaque',
|
|
43
|
+
description: 'Send the configured A2A access token as an HTTP Bearer credential.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
securityRequirements: [{ schemes: { [SECURITY_SCHEME_NAME]: { list: [] } } }],
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
securitySchemes: {
|
|
52
|
+
[SECURITY_SCHEME_NAME]: {
|
|
53
|
+
apiKeySecurityScheme: {
|
|
54
|
+
location: 'header',
|
|
55
|
+
name: config.auth.headerName,
|
|
56
|
+
description: `Send the configured A2A access token in the ${config.auth.headerName} header.`,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
securityRequirements: [{ schemes: { [SECURITY_SCHEME_NAME]: { list: [] } } }],
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Drop absent optional entries so the served card stays ProtoJSON-clean. */
|
|
65
|
+
function compactSkill(skill) {
|
|
66
|
+
const result = {
|
|
67
|
+
id: skill.id,
|
|
68
|
+
name: skill.name,
|
|
69
|
+
description: skill.description,
|
|
70
|
+
tags: skill.tags ?? [],
|
|
71
|
+
}
|
|
72
|
+
if (skill.examples?.length) result.examples = skill.examples
|
|
73
|
+
if (skill.inputModes?.length) result.inputModes = skill.inputModes
|
|
74
|
+
if (skill.outputModes?.length) result.outputModes = skill.outputModes
|
|
75
|
+
if (skill.securityRequirements?.length) result.securityRequirements = skill.securityRequirements
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Assemble the Agent Card for this deployment.
|
|
81
|
+
*
|
|
82
|
+
* @param {Readonly<object>} config
|
|
83
|
+
* @param {object} options
|
|
84
|
+
* @param {string} options.publicBaseUrl absolute origin, no trailing slash
|
|
85
|
+
* @param {{ id: string, token: string }[]} options.credentials
|
|
86
|
+
* @returns {object} an A2A v1.0 AgentCard in ProtoJSON shape
|
|
87
|
+
*/
|
|
88
|
+
export function buildAgentCard(config, { publicBaseUrl, credentials }) {
|
|
89
|
+
const base = trimTrailingSlash(publicBaseUrl)
|
|
90
|
+
const endpoint = `${base}${config.server.mountPath}`
|
|
91
|
+
const security = buildSecurity(config, credentials)
|
|
92
|
+
|
|
93
|
+
const card = {
|
|
94
|
+
name: config.card.name,
|
|
95
|
+
description: config.card.description,
|
|
96
|
+
supportedInterfaces: [
|
|
97
|
+
{
|
|
98
|
+
url: endpoint,
|
|
99
|
+
protocolBinding: 'JSONRPC',
|
|
100
|
+
tenant: '',
|
|
101
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
url: `${endpoint}/rest`,
|
|
105
|
+
protocolBinding: 'HTTP+JSON',
|
|
106
|
+
tenant: '',
|
|
107
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
version: config.card.version,
|
|
111
|
+
capabilities: {
|
|
112
|
+
streaming: config.server.streaming,
|
|
113
|
+
pushNotifications: config.server.pushNotifications,
|
|
114
|
+
extendedAgentCard: config.server.extendedCard,
|
|
115
|
+
},
|
|
116
|
+
securitySchemes: security.securitySchemes,
|
|
117
|
+
securityRequirements: security.securityRequirements,
|
|
118
|
+
defaultInputModes: config.card.defaultInputModes,
|
|
119
|
+
defaultOutputModes: config.card.defaultOutputModes,
|
|
120
|
+
skills: config.card.skills.map(compactSkill),
|
|
121
|
+
signatures: [],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (config.card.extensions.length > 0) {
|
|
125
|
+
card.capabilities.extensions = config.card.extensions.map((extension) => {
|
|
126
|
+
const value = {
|
|
127
|
+
uri: extension.uri,
|
|
128
|
+
description: extension.description ?? '',
|
|
129
|
+
required: extension.required === true,
|
|
130
|
+
}
|
|
131
|
+
if (extension.params !== undefined) value.params = extension.params
|
|
132
|
+
return value
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
if (config.card.provider) card.provider = config.card.provider
|
|
136
|
+
if (config.card.documentationUrl) card.documentationUrl = config.card.documentationUrl
|
|
137
|
+
if (config.card.iconUrl) card.iconUrl = config.card.iconUrl
|
|
138
|
+
|
|
139
|
+
return card
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build the authenticated extended card: the public card with additional
|
|
144
|
+
* skills and optional description overrides.
|
|
145
|
+
*
|
|
146
|
+
* @param {Readonly<object>} config
|
|
147
|
+
* @param {object} options same options as {@link buildAgentCard}
|
|
148
|
+
* @returns {object}
|
|
149
|
+
*/
|
|
150
|
+
export function buildExtendedCard(config, options) {
|
|
151
|
+
const card = buildAgentCard(config, options)
|
|
152
|
+
const extended = config.card.extended
|
|
153
|
+
if (extended.description) card.description = extended.description
|
|
154
|
+
if (extended.documentationUrl) card.documentationUrl = extended.documentationUrl
|
|
155
|
+
if (extended.skills.length > 0) {
|
|
156
|
+
const seen = new Set(card.skills.map((skill) => skill.id))
|
|
157
|
+
for (const skill of extended.skills) {
|
|
158
|
+
if (seen.has(skill.id)) continue
|
|
159
|
+
card.skills.push(compactSkill(skill))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return card
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create the JWS signature generator, or `null` when signing is disabled.
|
|
167
|
+
* Signatures are cached for `signing.cacheSeconds` keyed on the card content.
|
|
168
|
+
*
|
|
169
|
+
* @param {Readonly<object>} config
|
|
170
|
+
* @param {{ warn: (message: string) => void }} logger
|
|
171
|
+
* @returns {Promise<((card: object) => Promise<object>) | null>}
|
|
172
|
+
*/
|
|
173
|
+
export function createCardSigner(config) {
|
|
174
|
+
if (!config.signing.enabled) return null
|
|
175
|
+
|
|
176
|
+
let pem = config.signing.privateKeyPem
|
|
177
|
+
if (!pem) {
|
|
178
|
+
try {
|
|
179
|
+
pem = readFileSync(config.signing.privateKeyFile, 'utf8')
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new Error(`a2a: cannot read signing.privateKeyFile "${config.signing.privateKeyFile}": ${error.message}`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let key
|
|
185
|
+
try {
|
|
186
|
+
key = createPrivateKey({ key: pem, format: 'pem' })
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(`a2a: signing key is not a valid PEM private key: ${error.message}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const header = { alg: config.signing.algorithm, typ: 'JOSE', kid: config.signing.keyId }
|
|
192
|
+
if (config.signing.jku) header.jku = config.signing.jku
|
|
193
|
+
const sign = generateAgentCardSignature(key, header)
|
|
194
|
+
|
|
195
|
+
const cacheMs = config.signing.cacheSeconds * 1000
|
|
196
|
+
let cacheKey
|
|
197
|
+
let cacheValue
|
|
198
|
+
let cacheAt = 0
|
|
199
|
+
|
|
200
|
+
return async (card) => {
|
|
201
|
+
const body = JSON.stringify(card)
|
|
202
|
+
const now = Date.now()
|
|
203
|
+
if (cacheMs > 0 && cacheValue !== undefined && cacheKey === body && now - cacheAt < cacheMs) {
|
|
204
|
+
return cacheValue
|
|
205
|
+
}
|
|
206
|
+
const signed = await sign(structuredClone(card))
|
|
207
|
+
if (cacheMs > 0) {
|
|
208
|
+
cacheKey = body
|
|
209
|
+
cacheValue = signed
|
|
210
|
+
cacheAt = now
|
|
211
|
+
}
|
|
212
|
+
return signed
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot-install discovery for the browser half.
|
|
3
|
+
*
|
|
4
|
+
* `@deepseek-ai/dsh-client-modules` caches, per Loader specifier, whether that
|
|
5
|
+
* package carries a `dsh.client` half. A profile that installed this bundle
|
|
6
|
+
* while it had no browser half keeps a negative verdict for the process
|
|
7
|
+
* lifetime, so adding `client.js` to an already-loaded package would otherwise
|
|
8
|
+
* only appear after a restart.
|
|
9
|
+
*
|
|
10
|
+
* This shim clears that one negative verdict and asks the scanner to reconcile
|
|
11
|
+
* the row once. It touches only the scanner's public-ish compiled fields and
|
|
12
|
+
* silently degrades to "client half appears after restart" when they are
|
|
13
|
+
* absent or changed. On a fresh boot the package is scanned with `dsh.client`
|
|
14
|
+
* already present and the shim is a no-op.
|
|
15
|
+
*
|
|
16
|
+
* @module @ai_mind/dsh-a2a/client-discovery
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { errorMessage } from './util.js'
|
|
20
|
+
|
|
21
|
+
const PACKAGE_NAME = '@ai_mind/dsh-a2a'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ask the client-module scanner to re-read this package's manifest.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} ctx owning Cordis context
|
|
27
|
+
* @param {{ info: Function, warn: Function }} logger
|
|
28
|
+
* @returns {boolean} whether a reconciliation was triggered
|
|
29
|
+
*/
|
|
30
|
+
export function ensureClientHalfDiscovered(ctx, logger) {
|
|
31
|
+
const scanner = ctx.get?.('clientModules')
|
|
32
|
+
if (scanner === undefined || scanner === null) return false
|
|
33
|
+
if (!(scanner.pkgMeta instanceof Map) || !(scanner.dirty instanceof Set) || typeof scanner.flush !== 'function') {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
if (scanner.table instanceof Map && scanner.table.has(PACKAGE_NAME)) return false
|
|
37
|
+
|
|
38
|
+
const suffix = `\u0000${PACKAGE_NAME}`
|
|
39
|
+
let cleared = 0
|
|
40
|
+
for (const key of [...scanner.pkgMeta.keys()]) {
|
|
41
|
+
if (typeof key === 'string' && key.endsWith(suffix)) {
|
|
42
|
+
scanner.pkgMeta.delete(key)
|
|
43
|
+
cleared += 1
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
scanner.dirty.add(PACKAGE_NAME)
|
|
48
|
+
try {
|
|
49
|
+
scanner.flush((error) => {
|
|
50
|
+
logger.warn(`[a2a] client module discovery failed: ${errorMessage(error)}`)
|
|
51
|
+
})
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.warn(`[a2a] client module discovery failed: ${errorMessage(error)}`)
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
if (cleared > 0) logger.info('[a2a] cleared a cached "no browser half" verdict and re-scanned the package')
|
|
57
|
+
return true
|
|
58
|
+
}
|