kxco-pq-agent 1.0.6 → 1.0.7
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/package.json +62 -62
- package/src/agent-identity.js +299 -299
package/package.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"name": "John Heffernan"
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"homepage": "https://kxco.ai",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/KnightsbridgeAIQ/kxco-pq-agent.git"
|
|
2
|
+
"name": "kxco-pq-agent",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "Post-quantum identity for AI agents and autonomous systems: a KYC-verified institution sponsors an ML-DSA-65 keypair and locked capability scope for any agent that cannot pass KYC itself.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"post-quantum",
|
|
7
|
+
"pqc",
|
|
8
|
+
"ml-dsa",
|
|
9
|
+
"nist",
|
|
10
|
+
"fips-204",
|
|
11
|
+
"kxco",
|
|
12
|
+
"agent-identity",
|
|
13
|
+
"robot-identity",
|
|
14
|
+
"machine-identity",
|
|
15
|
+
"ai-agent",
|
|
16
|
+
"capability-scope",
|
|
17
|
+
"armature",
|
|
18
|
+
"relay",
|
|
19
|
+
"blockchain"
|
|
20
|
+
],
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"author": "Shayne Heffernan and John Heffernan",
|
|
23
|
+
"contributors": [
|
|
24
|
+
{
|
|
25
|
+
"name": "Shayne Heffernan"
|
|
35
26
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"dependencies": {
|
|
57
|
-
"kxco-post-quantum": "^1.3.0"
|
|
58
|
-
},
|
|
59
|
-
"scripts": {
|
|
60
|
-
"test": "node --test --test-timeout=30000 test/agent.test.js"
|
|
61
|
-
},
|
|
62
|
-
"funding": "https://kxco.ai",
|
|
63
|
-
"publishConfig": {
|
|
64
|
-
"access": "public"
|
|
27
|
+
{
|
|
28
|
+
"name": "John Heffernan"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"homepage": "https://kxco.ai",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/KnightsbridgeAIQ/kxco-pq-agent.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/KnightsbridgeAIQ/kxco-pq-agent/issues"
|
|
38
|
+
},
|
|
39
|
+
"type": "module",
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"main": "./src/index.js",
|
|
42
|
+
"types": "./src/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./src/index.d.ts",
|
|
46
|
+
"import": "./src/index.js"
|
|
65
47
|
}
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20.19"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"kxco-post-quantum": "^1.3.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"test": "node --test --test-timeout=30000 test/agent.test.js"
|
|
61
|
+
},
|
|
62
|
+
"funding": "https://kxco.ai",
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
66
|
}
|
package/src/agent-identity.js
CHANGED
|
@@ -1,299 +1,299 @@
|
|
|
1
|
-
import { mlDsa, fingerprint } from 'kxco-post-quantum'
|
|
2
|
-
import { validateScope, hashScope } from './scope.js'
|
|
3
|
-
import { canonicalize } from './jcs.js'
|
|
4
|
-
import { KxcoPqAgentError } from './errors.js'
|
|
5
|
-
import { AgentChainClient } from './agent-client.js'
|
|
6
|
-
|
|
7
|
-
const CREDENTIAL_VERSION = '1'
|
|
8
|
-
const IDENTITY_VERSION = '1'
|
|
9
|
-
|
|
10
|
-
const enc = new TextEncoder()
|
|
11
|
-
|
|
12
|
-
function b64url(bytes) {
|
|
13
|
-
return Buffer.from(bytes).toString('base64url')
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function fromB64url(s) {
|
|
17
|
-
return new Uint8Array(Buffer.from(s, 'base64url'))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseDuration(val) {
|
|
21
|
-
if (typeof val === 'number') return val * 1000
|
|
22
|
-
const m = String(val).match(/^(\d+)(d|h|m|s|y)$/)
|
|
23
|
-
if (!m) throw new KxcoPqAgentError(`invalid expiresIn '${val}' — use '30d', '1y', or seconds as a number`)
|
|
24
|
-
const n = parseInt(m[1], 10)
|
|
25
|
-
const ms = { d: 86400000, h: 3600000, m: 60000, s: 1000, y: 31536000000 }
|
|
26
|
-
return n * ms[m[2]]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt }) {
|
|
30
|
-
return enc.encode([
|
|
31
|
-
'kxco-agent-credential-v1',
|
|
32
|
-
agentKid,
|
|
33
|
-
agentPublicKey,
|
|
34
|
-
sponsorKid,
|
|
35
|
-
agentType,
|
|
36
|
-
label,
|
|
37
|
-
model ?? '',
|
|
38
|
-
canonicalize(scope),
|
|
39
|
-
issuedAt,
|
|
40
|
-
expiresAt,
|
|
41
|
-
].join('\n'))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const VALID_AGENT_TYPES = new Set(['llm', 'robot', 'iot', 'process'])
|
|
45
|
-
|
|
46
|
-
export class KxcoAgentIdentity {
|
|
47
|
-
#kid
|
|
48
|
-
#keypair
|
|
49
|
-
#sponsorKid
|
|
50
|
-
#agentType
|
|
51
|
-
#label
|
|
52
|
-
#model
|
|
53
|
-
#scope
|
|
54
|
-
#issuedAt
|
|
55
|
-
#expiresAt
|
|
56
|
-
#credential
|
|
57
|
-
|
|
58
|
-
constructor(opts) {
|
|
59
|
-
this.#kid = opts.kid
|
|
60
|
-
this.#keypair = opts.keypair
|
|
61
|
-
this.#sponsorKid = opts.sponsorKid
|
|
62
|
-
this.#agentType = opts.agentType
|
|
63
|
-
this.#label = opts.label
|
|
64
|
-
this.#model = opts.model ?? null
|
|
65
|
-
this.#scope = opts.scope
|
|
66
|
-
this.#issuedAt = opts.issuedAt
|
|
67
|
-
this.#expiresAt = opts.expiresAt
|
|
68
|
-
this.#credential = opts.credential
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
get kid() { return this.#kid }
|
|
72
|
-
get sponsorKid() { return this.#sponsorKid }
|
|
73
|
-
get agentType() { return this.#agentType }
|
|
74
|
-
get label() { return this.#label }
|
|
75
|
-
get model() { return this.#model }
|
|
76
|
-
get scope() { return JSON.parse(JSON.stringify(this.#scope)) }
|
|
77
|
-
get issuedAt() { return this.#issuedAt }
|
|
78
|
-
get expiresAt() { return this.#expiresAt }
|
|
79
|
-
get credential() { return JSON.parse(JSON.stringify(this.#credential)) }
|
|
80
|
-
|
|
81
|
-
// ── Factory ───────────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Create a new agent identity. Any KxcoIdentity holder may sponsor an agent.
|
|
85
|
-
*
|
|
86
|
-
* @param {object} opts
|
|
87
|
-
* @param {{ kid: string, sign(msg: Uint8Array): Promise<Uint8Array> }} opts.sponsor
|
|
88
|
-
* @param {string} opts.label — human-readable name for this agent
|
|
89
|
-
* @param {'llm'|'robot'|'iot'|'process'} opts.agentType
|
|
90
|
-
* @param {string} [opts.model] — model/hardware identifier (optional)
|
|
91
|
-
* @param {object} opts.scope — locked capability manifest (see scope.js)
|
|
92
|
-
* @param {string|number} opts.expiresIn — '30d', '1y', or seconds as number (mandatory)
|
|
93
|
-
* @param {object} [opts.chain] — KxcoChain instance for on-chain registration
|
|
94
|
-
*/
|
|
95
|
-
static async create({ sponsor, label, agentType, model, scope, expiresIn, chain } = {}) {
|
|
96
|
-
if (!sponsor?.kid || typeof sponsor.sign !== 'function') {
|
|
97
|
-
throw new KxcoPqAgentError('create: sponsor must have .kid and .sign(message)')
|
|
98
|
-
}
|
|
99
|
-
if (!label) throw new KxcoPqAgentError('create: label is required')
|
|
100
|
-
if (!agentType) throw new KxcoPqAgentError('create: agentType is required')
|
|
101
|
-
if (!VALID_AGENT_TYPES.has(agentType)) {
|
|
102
|
-
throw new KxcoPqAgentError(`create: agentType must be one of: ${[...VALID_AGENT_TYPES].join(', ')}`)
|
|
103
|
-
}
|
|
104
|
-
if (!scope) throw new KxcoPqAgentError('create: scope is required')
|
|
105
|
-
if (expiresIn == null) throw new KxcoPqAgentError('create: expiresIn is required — agents must have an expiry')
|
|
106
|
-
|
|
107
|
-
validateScope(scope)
|
|
108
|
-
|
|
109
|
-
const keypair = mlDsa.ml_dsa65.keygen()
|
|
110
|
-
const agentKid = fingerprint(keypair.publicKey)
|
|
111
|
-
const agentPubB64 = b64url(keypair.publicKey)
|
|
112
|
-
const issuedAt = new Date().toISOString()
|
|
113
|
-
const expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString()
|
|
114
|
-
|
|
115
|
-
const sigMsg = credentialSigningMsg({
|
|
116
|
-
agentKid,
|
|
117
|
-
agentPublicKey: agentPubB64,
|
|
118
|
-
sponsorKid: sponsor.kid,
|
|
119
|
-
agentType,
|
|
120
|
-
label,
|
|
121
|
-
model,
|
|
122
|
-
scope,
|
|
123
|
-
issuedAt,
|
|
124
|
-
expiresAt,
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
const sigBytes = await sponsor.sign(sigMsg)
|
|
128
|
-
const credential = {
|
|
129
|
-
'kxco-agent': CREDENTIAL_VERSION,
|
|
130
|
-
agentKid,
|
|
131
|
-
agentPublicKey: agentPubB64,
|
|
132
|
-
sponsorKid: sponsor.kid,
|
|
133
|
-
agentType,
|
|
134
|
-
label,
|
|
135
|
-
...(model && { model }),
|
|
136
|
-
scope,
|
|
137
|
-
issuedAt,
|
|
138
|
-
expiresAt,
|
|
139
|
-
sponsorSignature: b64url(sigBytes),
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (chain) {
|
|
143
|
-
const scopeHash = await hashScope(scope)
|
|
144
|
-
const expiresAtSec = Math.floor(new Date(expiresAt).getTime() / 1000)
|
|
145
|
-
await chain.issueAgentCredential({
|
|
146
|
-
agentKid,
|
|
147
|
-
agentPublicKeyHex: Buffer.from(keypair.publicKey).toString('hex'),
|
|
148
|
-
agentType,
|
|
149
|
-
scopeHash,
|
|
150
|
-
expiresAt: expiresAtSec,
|
|
151
|
-
})
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return new KxcoAgentIdentity({
|
|
155
|
-
kid: agentKid,
|
|
156
|
-
keypair,
|
|
157
|
-
sponsorKid: sponsor.kid,
|
|
158
|
-
agentType,
|
|
159
|
-
label,
|
|
160
|
-
model: model ?? null,
|
|
161
|
-
scope,
|
|
162
|
-
issuedAt,
|
|
163
|
-
expiresAt,
|
|
164
|
-
credential,
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── Signing ───────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
async sign(message) {
|
|
171
|
-
if (!this.#keypair?.secretKey) {
|
|
172
|
-
throw new KxcoPqAgentError('no signing key — reconstruct with KxcoAgentIdentity.import()')
|
|
173
|
-
}
|
|
174
|
-
return Buffer.from(mlDsa.sign(
|
|
175
|
-
new Uint8Array(this.#keypair.secretKey),
|
|
176
|
-
new Uint8Array(message),
|
|
177
|
-
), 'hex')
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async getPublicKey() {
|
|
181
|
-
return this.#keypair.publicKey
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── Export / import ───────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
export() {
|
|
187
|
-
return {
|
|
188
|
-
'kxco-agent-identity': IDENTITY_VERSION,
|
|
189
|
-
kid: this.#kid,
|
|
190
|
-
sponsorKid: this.#sponsorKid,
|
|
191
|
-
agentType: this.#agentType,
|
|
192
|
-
label: this.#label,
|
|
193
|
-
...(this.#model && { model: this.#model }),
|
|
194
|
-
scope: this.#scope,
|
|
195
|
-
issuedAt: this.#issuedAt,
|
|
196
|
-
expiresAt: this.#expiresAt,
|
|
197
|
-
secretKey: b64url(this.#keypair.secretKey),
|
|
198
|
-
publicKey: b64url(this.#keypair.publicKey),
|
|
199
|
-
credential: this.#credential,
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
static async import(exported) {
|
|
204
|
-
if (!exported || exported['kxco-agent-identity'] !== IDENTITY_VERSION) {
|
|
205
|
-
throw new KxcoPqAgentError('import: invalid or unsupported agent identity format')
|
|
206
|
-
}
|
|
207
|
-
return new KxcoAgentIdentity({
|
|
208
|
-
kid: exported.kid,
|
|
209
|
-
keypair: { secretKey: fromB64url(exported.secretKey), publicKey: fromB64url(exported.publicKey) },
|
|
210
|
-
sponsorKid: exported.sponsorKid,
|
|
211
|
-
agentType: exported.agentType,
|
|
212
|
-
label: exported.label,
|
|
213
|
-
model: exported.model ?? null,
|
|
214
|
-
scope: exported.scope,
|
|
215
|
-
issuedAt: exported.issuedAt,
|
|
216
|
-
expiresAt: exported.expiresAt,
|
|
217
|
-
credential: exported.credential,
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ── Chain client ──────────────────────────────────────────────────────────
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Returns an AgentChainClient that sends agent-signed intents to the relay.
|
|
225
|
-
* Each request automatically includes this agent's credential and kid.
|
|
226
|
-
* @param {string} relay — relay base URL, e.g. 'https://relay.kxco.ai'
|
|
227
|
-
* @param {{ timeout?: number }} [opts]
|
|
228
|
-
*/
|
|
229
|
-
toChainClient(relay, { timeout } = {}) {
|
|
230
|
-
return new AgentChainClient({ relay, agent: this, ...(timeout && { timeout }) })
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ── Static: verify a credential ──────────────────────────────────────────
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Verify an agent credential envelope.
|
|
237
|
-
* Pass sponsorPublicKey (Uint8Array) to perform full ML-DSA-65 signature verification.
|
|
238
|
-
* Without it, only expiry and format are checked.
|
|
239
|
-
*
|
|
240
|
-
* @param {object} credential
|
|
241
|
-
* @param {{ sponsorPublicKey?: Uint8Array }} [opts]
|
|
242
|
-
*/
|
|
243
|
-
static async verify(credential, { sponsorPublicKey } = {}) {
|
|
244
|
-
if (!credential || credential['kxco-agent'] !== CREDENTIAL_VERSION) {
|
|
245
|
-
return { valid: false, error: 'invalid or unsupported credential format' }
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const {
|
|
249
|
-
agentKid, agentPublicKey, sponsorKid, agentType,
|
|
250
|
-
label, model, scope, issuedAt, expiresAt, sponsorSignature,
|
|
251
|
-
} = credential
|
|
252
|
-
|
|
253
|
-
if (!agentKid || !agentPublicKey || !sponsorKid || !agentType || !issuedAt || !expiresAt || !sponsorSignature) {
|
|
254
|
-
return { valid: false, error: 'malformed credential — missing required fields' }
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (new Date(expiresAt) < new Date()) {
|
|
258
|
-
return { valid: false, error: 'agent credential has expired' }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (sponsorPublicKey) {
|
|
262
|
-
const msg = credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt })
|
|
263
|
-
let ok
|
|
264
|
-
try {
|
|
265
|
-
ok = mlDsa.verify(new Uint8Array(sponsorPublicKey), msg, Buffer.from(fromB64url(sponsorSignature)).toString('hex'))
|
|
266
|
-
} catch {
|
|
267
|
-
ok = false
|
|
268
|
-
}
|
|
269
|
-
if (!ok) return { valid: false, error: 'sponsor signature invalid' }
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
valid: true,
|
|
274
|
-
agentKid,
|
|
275
|
-
sponsorKid,
|
|
276
|
-
agentType,
|
|
277
|
-
label,
|
|
278
|
-
...(model && { model }),
|
|
279
|
-
scope,
|
|
280
|
-
issuedAt,
|
|
281
|
-
expiresAt,
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ── Static: revoke on-chain ───────────────────────────────────────────────
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Revoke an agent credential on-chain.
|
|
289
|
-
* The chain parameter must be a KxcoChain instance belonging to the sponsor.
|
|
290
|
-
*
|
|
291
|
-
* @param {string} agentKid
|
|
292
|
-
* @param {{ chain: object, reason?: string }} opts
|
|
293
|
-
*/
|
|
294
|
-
static async revoke(agentKid, { chain, reason = '' } = {}) {
|
|
295
|
-
if (!agentKid) throw new KxcoPqAgentError('revoke: agentKid is required')
|
|
296
|
-
if (!chain) throw new KxcoPqAgentError('revoke: chain is required for on-chain revocation')
|
|
297
|
-
return chain.revokeAgentCredential({ agentKid, reason })
|
|
298
|
-
}
|
|
299
|
-
}
|
|
1
|
+
import { mlDsa, fingerprint } from 'kxco-post-quantum'
|
|
2
|
+
import { validateScope, hashScope } from './scope.js'
|
|
3
|
+
import { canonicalize } from './jcs.js'
|
|
4
|
+
import { KxcoPqAgentError } from './errors.js'
|
|
5
|
+
import { AgentChainClient } from './agent-client.js'
|
|
6
|
+
|
|
7
|
+
const CREDENTIAL_VERSION = '1'
|
|
8
|
+
const IDENTITY_VERSION = '1'
|
|
9
|
+
|
|
10
|
+
const enc = new TextEncoder()
|
|
11
|
+
|
|
12
|
+
function b64url(bytes) {
|
|
13
|
+
return Buffer.from(bytes).toString('base64url')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fromB64url(s) {
|
|
17
|
+
return new Uint8Array(Buffer.from(s, 'base64url'))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseDuration(val) {
|
|
21
|
+
if (typeof val === 'number') return val * 1000
|
|
22
|
+
const m = String(val).match(/^(\d+)(d|h|m|s|y)$/)
|
|
23
|
+
if (!m) throw new KxcoPqAgentError(`invalid expiresIn '${val}' — use '30d', '1y', or seconds as a number`)
|
|
24
|
+
const n = parseInt(m[1], 10)
|
|
25
|
+
const ms = { d: 86400000, h: 3600000, m: 60000, s: 1000, y: 31536000000 }
|
|
26
|
+
return n * ms[m[2]]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt }) {
|
|
30
|
+
return enc.encode([
|
|
31
|
+
'kxco-agent-credential-v1',
|
|
32
|
+
agentKid,
|
|
33
|
+
agentPublicKey,
|
|
34
|
+
sponsorKid,
|
|
35
|
+
agentType,
|
|
36
|
+
label,
|
|
37
|
+
model ?? '',
|
|
38
|
+
canonicalize(scope),
|
|
39
|
+
issuedAt,
|
|
40
|
+
expiresAt,
|
|
41
|
+
].join('\n'))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const VALID_AGENT_TYPES = new Set(['llm', 'robot', 'iot', 'process'])
|
|
45
|
+
|
|
46
|
+
export class KxcoAgentIdentity {
|
|
47
|
+
#kid
|
|
48
|
+
#keypair
|
|
49
|
+
#sponsorKid
|
|
50
|
+
#agentType
|
|
51
|
+
#label
|
|
52
|
+
#model
|
|
53
|
+
#scope
|
|
54
|
+
#issuedAt
|
|
55
|
+
#expiresAt
|
|
56
|
+
#credential
|
|
57
|
+
|
|
58
|
+
constructor(opts) {
|
|
59
|
+
this.#kid = opts.kid
|
|
60
|
+
this.#keypair = opts.keypair
|
|
61
|
+
this.#sponsorKid = opts.sponsorKid
|
|
62
|
+
this.#agentType = opts.agentType
|
|
63
|
+
this.#label = opts.label
|
|
64
|
+
this.#model = opts.model ?? null
|
|
65
|
+
this.#scope = opts.scope
|
|
66
|
+
this.#issuedAt = opts.issuedAt
|
|
67
|
+
this.#expiresAt = opts.expiresAt
|
|
68
|
+
this.#credential = opts.credential
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get kid() { return this.#kid }
|
|
72
|
+
get sponsorKid() { return this.#sponsorKid }
|
|
73
|
+
get agentType() { return this.#agentType }
|
|
74
|
+
get label() { return this.#label }
|
|
75
|
+
get model() { return this.#model }
|
|
76
|
+
get scope() { return JSON.parse(JSON.stringify(this.#scope)) }
|
|
77
|
+
get issuedAt() { return this.#issuedAt }
|
|
78
|
+
get expiresAt() { return this.#expiresAt }
|
|
79
|
+
get credential() { return JSON.parse(JSON.stringify(this.#credential)) }
|
|
80
|
+
|
|
81
|
+
// ── Factory ───────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a new agent identity. Any KxcoIdentity holder may sponsor an agent.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} opts
|
|
87
|
+
* @param {{ kid: string, sign(msg: Uint8Array): Promise<Uint8Array> }} opts.sponsor
|
|
88
|
+
* @param {string} opts.label — human-readable name for this agent
|
|
89
|
+
* @param {'llm'|'robot'|'iot'|'process'} opts.agentType
|
|
90
|
+
* @param {string} [opts.model] — model/hardware identifier (optional)
|
|
91
|
+
* @param {object} opts.scope — locked capability manifest (see scope.js)
|
|
92
|
+
* @param {string|number} opts.expiresIn — '30d', '1y', or seconds as number (mandatory)
|
|
93
|
+
* @param {object} [opts.chain] — KxcoChain instance for on-chain registration
|
|
94
|
+
*/
|
|
95
|
+
static async create({ sponsor, label, agentType, model, scope, expiresIn, chain } = {}) {
|
|
96
|
+
if (!sponsor?.kid || typeof sponsor.sign !== 'function') {
|
|
97
|
+
throw new KxcoPqAgentError('create: sponsor must have .kid and .sign(message)')
|
|
98
|
+
}
|
|
99
|
+
if (!label) throw new KxcoPqAgentError('create: label is required')
|
|
100
|
+
if (!agentType) throw new KxcoPqAgentError('create: agentType is required')
|
|
101
|
+
if (!VALID_AGENT_TYPES.has(agentType)) {
|
|
102
|
+
throw new KxcoPqAgentError(`create: agentType must be one of: ${[...VALID_AGENT_TYPES].join(', ')}`)
|
|
103
|
+
}
|
|
104
|
+
if (!scope) throw new KxcoPqAgentError('create: scope is required')
|
|
105
|
+
if (expiresIn == null) throw new KxcoPqAgentError('create: expiresIn is required — agents must have an expiry')
|
|
106
|
+
|
|
107
|
+
validateScope(scope)
|
|
108
|
+
|
|
109
|
+
const keypair = mlDsa.ml_dsa65.keygen()
|
|
110
|
+
const agentKid = fingerprint(keypair.publicKey)
|
|
111
|
+
const agentPubB64 = b64url(keypair.publicKey)
|
|
112
|
+
const issuedAt = new Date().toISOString()
|
|
113
|
+
const expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString()
|
|
114
|
+
|
|
115
|
+
const sigMsg = credentialSigningMsg({
|
|
116
|
+
agentKid,
|
|
117
|
+
agentPublicKey: agentPubB64,
|
|
118
|
+
sponsorKid: sponsor.kid,
|
|
119
|
+
agentType,
|
|
120
|
+
label,
|
|
121
|
+
model,
|
|
122
|
+
scope,
|
|
123
|
+
issuedAt,
|
|
124
|
+
expiresAt,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const sigBytes = await sponsor.sign(sigMsg)
|
|
128
|
+
const credential = {
|
|
129
|
+
'kxco-agent': CREDENTIAL_VERSION,
|
|
130
|
+
agentKid,
|
|
131
|
+
agentPublicKey: agentPubB64,
|
|
132
|
+
sponsorKid: sponsor.kid,
|
|
133
|
+
agentType,
|
|
134
|
+
label,
|
|
135
|
+
...(model && { model }),
|
|
136
|
+
scope,
|
|
137
|
+
issuedAt,
|
|
138
|
+
expiresAt,
|
|
139
|
+
sponsorSignature: b64url(sigBytes),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (chain) {
|
|
143
|
+
const scopeHash = await hashScope(scope)
|
|
144
|
+
const expiresAtSec = Math.floor(new Date(expiresAt).getTime() / 1000)
|
|
145
|
+
await chain.issueAgentCredential({
|
|
146
|
+
agentKid,
|
|
147
|
+
agentPublicKeyHex: Buffer.from(keypair.publicKey).toString('hex'),
|
|
148
|
+
agentType,
|
|
149
|
+
scopeHash,
|
|
150
|
+
expiresAt: expiresAtSec,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new KxcoAgentIdentity({
|
|
155
|
+
kid: agentKid,
|
|
156
|
+
keypair,
|
|
157
|
+
sponsorKid: sponsor.kid,
|
|
158
|
+
agentType,
|
|
159
|
+
label,
|
|
160
|
+
model: model ?? null,
|
|
161
|
+
scope,
|
|
162
|
+
issuedAt,
|
|
163
|
+
expiresAt,
|
|
164
|
+
credential,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Signing ───────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
async sign(message) {
|
|
171
|
+
if (!this.#keypair?.secretKey) {
|
|
172
|
+
throw new KxcoPqAgentError('no signing key — reconstruct with KxcoAgentIdentity.import()')
|
|
173
|
+
}
|
|
174
|
+
return Buffer.from(mlDsa.sign(
|
|
175
|
+
new Uint8Array(this.#keypair.secretKey),
|
|
176
|
+
new Uint8Array(message),
|
|
177
|
+
), 'hex')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async getPublicKey() {
|
|
181
|
+
return this.#keypair.publicKey
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Export / import ───────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
export() {
|
|
187
|
+
return {
|
|
188
|
+
'kxco-agent-identity': IDENTITY_VERSION,
|
|
189
|
+
kid: this.#kid,
|
|
190
|
+
sponsorKid: this.#sponsorKid,
|
|
191
|
+
agentType: this.#agentType,
|
|
192
|
+
label: this.#label,
|
|
193
|
+
...(this.#model && { model: this.#model }),
|
|
194
|
+
scope: this.#scope,
|
|
195
|
+
issuedAt: this.#issuedAt,
|
|
196
|
+
expiresAt: this.#expiresAt,
|
|
197
|
+
secretKey: b64url(this.#keypair.secretKey),
|
|
198
|
+
publicKey: b64url(this.#keypair.publicKey),
|
|
199
|
+
credential: this.#credential,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static async import(exported) {
|
|
204
|
+
if (!exported || exported['kxco-agent-identity'] !== IDENTITY_VERSION) {
|
|
205
|
+
throw new KxcoPqAgentError('import: invalid or unsupported agent identity format')
|
|
206
|
+
}
|
|
207
|
+
return new KxcoAgentIdentity({
|
|
208
|
+
kid: exported.kid,
|
|
209
|
+
keypair: { secretKey: fromB64url(exported.secretKey), publicKey: fromB64url(exported.publicKey) },
|
|
210
|
+
sponsorKid: exported.sponsorKid,
|
|
211
|
+
agentType: exported.agentType,
|
|
212
|
+
label: exported.label,
|
|
213
|
+
model: exported.model ?? null,
|
|
214
|
+
scope: exported.scope,
|
|
215
|
+
issuedAt: exported.issuedAt,
|
|
216
|
+
expiresAt: exported.expiresAt,
|
|
217
|
+
credential: exported.credential,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Chain client ──────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns an AgentChainClient that sends agent-signed intents to the relay.
|
|
225
|
+
* Each request automatically includes this agent's credential and kid.
|
|
226
|
+
* @param {string} relay — relay base URL, e.g. 'https://relay.kxco.ai'
|
|
227
|
+
* @param {{ timeout?: number }} [opts]
|
|
228
|
+
*/
|
|
229
|
+
toChainClient(relay, { timeout } = {}) {
|
|
230
|
+
return new AgentChainClient({ relay, agent: this, ...(timeout && { timeout }) })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Static: verify a credential ──────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Verify an agent credential envelope.
|
|
237
|
+
* Pass sponsorPublicKey (Uint8Array) to perform full ML-DSA-65 signature verification.
|
|
238
|
+
* Without it, only expiry and format are checked.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} credential
|
|
241
|
+
* @param {{ sponsorPublicKey?: Uint8Array }} [opts]
|
|
242
|
+
*/
|
|
243
|
+
static async verify(credential, { sponsorPublicKey } = {}) {
|
|
244
|
+
if (!credential || credential['kxco-agent'] !== CREDENTIAL_VERSION) {
|
|
245
|
+
return { valid: false, error: 'invalid or unsupported credential format' }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const {
|
|
249
|
+
agentKid, agentPublicKey, sponsorKid, agentType,
|
|
250
|
+
label, model, scope, issuedAt, expiresAt, sponsorSignature,
|
|
251
|
+
} = credential
|
|
252
|
+
|
|
253
|
+
if (!agentKid || !agentPublicKey || !sponsorKid || !agentType || !issuedAt || !expiresAt || !sponsorSignature) {
|
|
254
|
+
return { valid: false, error: 'malformed credential — missing required fields' }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (new Date(expiresAt) < new Date()) {
|
|
258
|
+
return { valid: false, error: 'agent credential has expired' }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (sponsorPublicKey) {
|
|
262
|
+
const msg = credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt })
|
|
263
|
+
let ok
|
|
264
|
+
try {
|
|
265
|
+
ok = mlDsa.verify(new Uint8Array(sponsorPublicKey), msg, Buffer.from(fromB64url(sponsorSignature)).toString('hex'))
|
|
266
|
+
} catch {
|
|
267
|
+
ok = false
|
|
268
|
+
}
|
|
269
|
+
if (!ok) return { valid: false, error: 'sponsor signature invalid' }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
valid: true,
|
|
274
|
+
agentKid,
|
|
275
|
+
sponsorKid,
|
|
276
|
+
agentType,
|
|
277
|
+
label,
|
|
278
|
+
...(model && { model }),
|
|
279
|
+
scope,
|
|
280
|
+
issuedAt,
|
|
281
|
+
expiresAt,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Static: revoke on-chain ───────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Revoke an agent credential on-chain.
|
|
289
|
+
* The chain parameter must be a KxcoChain instance belonging to the sponsor.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} agentKid
|
|
292
|
+
* @param {{ chain: object, reason?: string }} opts
|
|
293
|
+
*/
|
|
294
|
+
static async revoke(agentKid, { chain, reason = '' } = {}) {
|
|
295
|
+
if (!agentKid) throw new KxcoPqAgentError('revoke: agentKid is required')
|
|
296
|
+
if (!chain) throw new KxcoPqAgentError('revoke: chain is required for on-chain revocation')
|
|
297
|
+
return chain.revokeAgentCredential({ agentKid, reason })
|
|
298
|
+
}
|
|
299
|
+
}
|