@agentsquared/cli 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 +420 -0
- package/a2_cli.mjs +1576 -0
- package/adapters/index.mjs +79 -0
- package/adapters/openclaw/adapter.mjs +1020 -0
- package/adapters/openclaw/cli.mjs +89 -0
- package/adapters/openclaw/detect.mjs +259 -0
- package/adapters/openclaw/helpers.mjs +827 -0
- package/adapters/openclaw/ws_client.mjs +740 -0
- package/bin/a2-cli.js +8 -0
- package/lib/conversation/policy.mjs +122 -0
- package/lib/conversation/store.mjs +223 -0
- package/lib/conversation/templates.mjs +419 -0
- package/lib/gateway/api.mjs +28 -0
- package/lib/gateway/inbox.mjs +344 -0
- package/lib/gateway/lifecycle.mjs +602 -0
- package/lib/gateway/runtime_state.mjs +388 -0
- package/lib/gateway/server.mjs +883 -0
- package/lib/gateway/state.mjs +175 -0
- package/lib/routing/agent_router.mjs +511 -0
- package/lib/runtime/executor.mjs +380 -0
- package/lib/runtime/keys.mjs +85 -0
- package/lib/runtime/report.mjs +302 -0
- package/lib/runtime/safety.mjs +72 -0
- package/lib/shared/paths.mjs +155 -0
- package/lib/shared/primitives.mjs +43 -0
- package/lib/transport/http_json.mjs +96 -0
- package/lib/transport/libp2p.mjs +397 -0
- package/lib/transport/peer_session.mjs +857 -0
- package/lib/transport/relay_http.mjs +110 -0
- package/package.json +53 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { extractInboundText } from '../routing/agent_router.mjs'
|
|
2
|
+
import { normalizeConversationControl } from '../conversation/policy.mjs'
|
|
3
|
+
import { createHostRuntimeAdapter } from '../../adapters/index.mjs'
|
|
4
|
+
|
|
5
|
+
function clean(value) {
|
|
6
|
+
return `${value ?? ''}`.trim()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function buildTextMessageResult(text, metadata = {}) {
|
|
10
|
+
return {
|
|
11
|
+
message: {
|
|
12
|
+
kind: 'message',
|
|
13
|
+
role: 'agent',
|
|
14
|
+
parts: [{ kind: 'text', text: clean(text) }]
|
|
15
|
+
},
|
|
16
|
+
metadata
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function excerpt(text, maxLength = 180) {
|
|
21
|
+
const compact = clean(text).replace(/\s+/g, ' ').trim()
|
|
22
|
+
if (!compact) {
|
|
23
|
+
return ''
|
|
24
|
+
}
|
|
25
|
+
return compact.length > maxLength ? `${compact.slice(0, maxLength - 3)}...` : compact
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizePeerResponse(value, metadata = {}) {
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
return buildTextMessageResult(value, metadata)
|
|
31
|
+
}
|
|
32
|
+
if (value && typeof value === 'object') {
|
|
33
|
+
return {
|
|
34
|
+
...value,
|
|
35
|
+
metadata: {
|
|
36
|
+
...(value.metadata ?? {}),
|
|
37
|
+
...metadata
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeExecutionResult(raw, {
|
|
45
|
+
selectedSkill,
|
|
46
|
+
mailboxKey
|
|
47
|
+
} = {}) {
|
|
48
|
+
const baseMetadata = {
|
|
49
|
+
selectedSkill: clean(selectedSkill),
|
|
50
|
+
mailboxKey: clean(mailboxKey)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof raw === 'string') {
|
|
54
|
+
const conversation = normalizeConversationControl({}, {
|
|
55
|
+
defaultTurnIndex: 1,
|
|
56
|
+
defaultDecision: 'done',
|
|
57
|
+
defaultStopReason: 'single-turn',
|
|
58
|
+
defaultFinalize: true
|
|
59
|
+
})
|
|
60
|
+
return {
|
|
61
|
+
peerResponse: buildTextMessageResult(raw, {
|
|
62
|
+
...baseMetadata,
|
|
63
|
+
turnIndex: conversation.turnIndex,
|
|
64
|
+
decision: conversation.decision,
|
|
65
|
+
stopReason: conversation.stopReason,
|
|
66
|
+
finalize: conversation.finalize
|
|
67
|
+
}),
|
|
68
|
+
ownerReport: null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!raw || typeof raw !== 'object') {
|
|
73
|
+
throw new Error('local runtime executor returned an invalid result')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (raw.reject && typeof raw.reject === 'object') {
|
|
77
|
+
return {
|
|
78
|
+
reject: {
|
|
79
|
+
code: Number.parseInt(`${raw.reject.code ?? 500}`, 10) || 500,
|
|
80
|
+
message: clean(raw.reject.message) || 'local runtime rejected the inbound request'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const directPeerResponse = normalizePeerResponse(raw.peerResponse, baseMetadata)
|
|
86
|
+
if (directPeerResponse) {
|
|
87
|
+
const conversation = normalizeConversationControl({
|
|
88
|
+
...(raw.peerResponse?.metadata ?? {}),
|
|
89
|
+
...(raw ?? {})
|
|
90
|
+
}, {
|
|
91
|
+
defaultTurnIndex: 1,
|
|
92
|
+
defaultDecision: 'done',
|
|
93
|
+
defaultStopReason: '',
|
|
94
|
+
defaultFinalize: true
|
|
95
|
+
})
|
|
96
|
+
return {
|
|
97
|
+
peerResponse: {
|
|
98
|
+
...directPeerResponse,
|
|
99
|
+
metadata: {
|
|
100
|
+
...(directPeerResponse.metadata ?? {}),
|
|
101
|
+
turnIndex: conversation.turnIndex,
|
|
102
|
+
decision: conversation.decision,
|
|
103
|
+
stopReason: conversation.stopReason,
|
|
104
|
+
finalize: conversation.finalize
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
ownerReport: raw.ownerReport ?? null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const shorthandPeerResponse = normalizePeerResponse(
|
|
112
|
+
raw.message || raw.result || (raw.parts ? { message: raw } : null),
|
|
113
|
+
baseMetadata
|
|
114
|
+
)
|
|
115
|
+
if (shorthandPeerResponse) {
|
|
116
|
+
const conversation = normalizeConversationControl(raw, {
|
|
117
|
+
defaultTurnIndex: 1,
|
|
118
|
+
defaultDecision: 'done',
|
|
119
|
+
defaultStopReason: '',
|
|
120
|
+
defaultFinalize: true
|
|
121
|
+
})
|
|
122
|
+
return {
|
|
123
|
+
peerResponse: {
|
|
124
|
+
...shorthandPeerResponse,
|
|
125
|
+
metadata: {
|
|
126
|
+
...(shorthandPeerResponse.metadata ?? {}),
|
|
127
|
+
turnIndex: conversation.turnIndex,
|
|
128
|
+
decision: conversation.decision,
|
|
129
|
+
stopReason: conversation.stopReason,
|
|
130
|
+
finalize: conversation.finalize
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
ownerReport: raw.ownerReport ?? null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error('local runtime executor did not provide peerResponse or reject')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createLocalRuntimeExecutor({
|
|
141
|
+
agentId,
|
|
142
|
+
mode = 'none',
|
|
143
|
+
hostRuntime = 'none',
|
|
144
|
+
conversationStore = null,
|
|
145
|
+
openclawStateDir = '',
|
|
146
|
+
openclawCommand = 'openclaw',
|
|
147
|
+
openclawCwd = '',
|
|
148
|
+
openclawConfigPath = '',
|
|
149
|
+
openclawAgent = '',
|
|
150
|
+
openclawSessionPrefix = 'agentsquared:',
|
|
151
|
+
openclawTimeoutMs = 180000,
|
|
152
|
+
openclawGatewayUrl = '',
|
|
153
|
+
openclawGatewayToken = '',
|
|
154
|
+
openclawGatewayPassword = ''
|
|
155
|
+
} = {}) {
|
|
156
|
+
const normalizedMode = clean(mode).toLowerCase() || 'none'
|
|
157
|
+
const normalizedHostRuntime = clean(hostRuntime).toLowerCase() || 'none'
|
|
158
|
+
const hostAdapter = normalizedMode === 'host'
|
|
159
|
+
? createHostRuntimeAdapter({
|
|
160
|
+
hostRuntime: normalizedHostRuntime || 'openclaw',
|
|
161
|
+
localAgentId: agentId,
|
|
162
|
+
openclaw: {
|
|
163
|
+
conversationStore,
|
|
164
|
+
stateDir: openclawStateDir,
|
|
165
|
+
openclawAgent,
|
|
166
|
+
command: openclawCommand,
|
|
167
|
+
cwd: openclawCwd,
|
|
168
|
+
configPath: openclawConfigPath,
|
|
169
|
+
sessionPrefix: openclawSessionPrefix,
|
|
170
|
+
timeoutMs: openclawTimeoutMs,
|
|
171
|
+
gatewayUrl: openclawGatewayUrl,
|
|
172
|
+
gatewayToken: openclawGatewayToken,
|
|
173
|
+
gatewayPassword: openclawGatewayPassword
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
: null
|
|
177
|
+
|
|
178
|
+
async function executeViaHost(context) {
|
|
179
|
+
if (!hostAdapter) {
|
|
180
|
+
throw new Error('host runtime adapter was not configured')
|
|
181
|
+
}
|
|
182
|
+
return normalizeExecutionResult(
|
|
183
|
+
await hostAdapter.executeInbound(context),
|
|
184
|
+
context
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function rejectExecution() {
|
|
189
|
+
return {
|
|
190
|
+
reject: {
|
|
191
|
+
code: 503,
|
|
192
|
+
message: 'no local agent runtime adapter is configured; inbound request cannot be handled yet'
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const execute = normalizedMode === 'host'
|
|
198
|
+
? executeViaHost
|
|
199
|
+
: rejectExecution
|
|
200
|
+
|
|
201
|
+
execute.mode = normalizedMode
|
|
202
|
+
execute.preflight = async () => {
|
|
203
|
+
if (!hostAdapter?.preflight) {
|
|
204
|
+
return { ok: normalizedMode !== 'host', mode: normalizedMode }
|
|
205
|
+
}
|
|
206
|
+
return hostAdapter.preflight()
|
|
207
|
+
}
|
|
208
|
+
return execute
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createOwnerNotifier({
|
|
212
|
+
agentId,
|
|
213
|
+
mode = 'inbox',
|
|
214
|
+
hostRuntime = 'none',
|
|
215
|
+
inbox = null,
|
|
216
|
+
openclawStateDir = '',
|
|
217
|
+
openclawCommand = 'openclaw',
|
|
218
|
+
openclawCwd = '',
|
|
219
|
+
openclawConfigPath = '',
|
|
220
|
+
openclawAgent = '',
|
|
221
|
+
openclawSessionPrefix = 'agentsquared:',
|
|
222
|
+
openclawTimeoutMs = 180000,
|
|
223
|
+
openclawGatewayUrl = '',
|
|
224
|
+
openclawGatewayToken = '',
|
|
225
|
+
openclawGatewayPassword = ''
|
|
226
|
+
} = {}) {
|
|
227
|
+
const normalizedMode = clean(mode).toLowerCase() || 'inbox'
|
|
228
|
+
const normalizedHostRuntime = clean(hostRuntime).toLowerCase() || 'none'
|
|
229
|
+
const hostAdapter = normalizedMode === 'host'
|
|
230
|
+
? createHostRuntimeAdapter({
|
|
231
|
+
hostRuntime: normalizedHostRuntime || 'openclaw',
|
|
232
|
+
localAgentId: agentId,
|
|
233
|
+
openclaw: {
|
|
234
|
+
stateDir: openclawStateDir,
|
|
235
|
+
openclawAgent,
|
|
236
|
+
command: openclawCommand,
|
|
237
|
+
cwd: openclawCwd,
|
|
238
|
+
configPath: openclawConfigPath,
|
|
239
|
+
sessionPrefix: openclawSessionPrefix,
|
|
240
|
+
timeoutMs: openclawTimeoutMs,
|
|
241
|
+
gatewayUrl: openclawGatewayUrl,
|
|
242
|
+
gatewayToken: openclawGatewayToken,
|
|
243
|
+
gatewayPassword: openclawGatewayPassword
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
: null
|
|
247
|
+
const deliveredFinalConversationKeys = new Set()
|
|
248
|
+
const pendingFinalConversationKeys = new Set()
|
|
249
|
+
|
|
250
|
+
async function notifyViaInbox(context) {
|
|
251
|
+
if (!inbox?.appendEntry) {
|
|
252
|
+
throw new Error('inbox store is required when AgentSquared falls back to local audit-only owner reporting')
|
|
253
|
+
}
|
|
254
|
+
const value = inbox.appendEntry({
|
|
255
|
+
agentId,
|
|
256
|
+
selectedSkill: context.selectedSkill,
|
|
257
|
+
mailboxKey: context.mailboxKey,
|
|
258
|
+
item: context.item,
|
|
259
|
+
ownerReport: context.ownerReport,
|
|
260
|
+
peerResponse: context.peerResponse ?? null,
|
|
261
|
+
ownerDelivery: {
|
|
262
|
+
mode: 'inbox',
|
|
263
|
+
attempted: false,
|
|
264
|
+
delivered: false,
|
|
265
|
+
reason: context.notifyOwnerNow ? 'inbox-only' : 'conversation-intermediate'
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
return {
|
|
269
|
+
delivered: true,
|
|
270
|
+
mode: 'inbox',
|
|
271
|
+
entryId: value.entry.id,
|
|
272
|
+
totalCount: value.index.totalCount
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function notifyViaHost(context) {
|
|
277
|
+
let ownerResult
|
|
278
|
+
const shouldDeliverToOwner = context.notifyOwnerNow !== false
|
|
279
|
+
const finalConversationKey = clean(context?.ownerReport?.conversationKey)
|
|
280
|
+
const duplicateDeliveredFinal = shouldDeliverToOwner && finalConversationKey
|
|
281
|
+
? pendingFinalConversationKeys.has(finalConversationKey)
|
|
282
|
+
|| deliveredFinalConversationKeys.has(finalConversationKey)
|
|
283
|
+
|| inbox?.findDeliveredFinalConversationReport?.(finalConversationKey)
|
|
284
|
+
: null
|
|
285
|
+
if (duplicateDeliveredFinal) {
|
|
286
|
+
ownerResult = {
|
|
287
|
+
delivered: false,
|
|
288
|
+
attempted: false,
|
|
289
|
+
mode: normalizedHostRuntime || 'host',
|
|
290
|
+
reason: 'duplicate-final-report-suppressed'
|
|
291
|
+
}
|
|
292
|
+
} else if (!shouldDeliverToOwner) {
|
|
293
|
+
ownerResult = {
|
|
294
|
+
delivered: false,
|
|
295
|
+
attempted: false,
|
|
296
|
+
mode: normalizedHostRuntime || 'host',
|
|
297
|
+
reason: 'conversation-intermediate'
|
|
298
|
+
}
|
|
299
|
+
} else if (!hostAdapter) {
|
|
300
|
+
ownerResult = {
|
|
301
|
+
delivered: false,
|
|
302
|
+
attempted: false,
|
|
303
|
+
mode: normalizedHostRuntime || 'host',
|
|
304
|
+
reason: 'adapter-not-configured'
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
try {
|
|
308
|
+
if (finalConversationKey && context?.ownerReport?.finalize) {
|
|
309
|
+
pendingFinalConversationKeys.add(finalConversationKey)
|
|
310
|
+
}
|
|
311
|
+
ownerResult = await hostAdapter.pushOwnerReport({
|
|
312
|
+
item: context.item,
|
|
313
|
+
selectedSkill: context.selectedSkill,
|
|
314
|
+
ownerReport: context.ownerReport
|
|
315
|
+
})
|
|
316
|
+
} catch (error) {
|
|
317
|
+
ownerResult = {
|
|
318
|
+
delivered: false,
|
|
319
|
+
attempted: true,
|
|
320
|
+
mode: 'openclaw',
|
|
321
|
+
reason: clean(error?.message) || 'owner-push-failed'
|
|
322
|
+
}
|
|
323
|
+
} finally {
|
|
324
|
+
if (finalConversationKey && context?.ownerReport?.finalize) {
|
|
325
|
+
pendingFinalConversationKeys.delete(finalConversationKey)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const value = inbox.appendEntry({
|
|
330
|
+
agentId,
|
|
331
|
+
selectedSkill: context.selectedSkill,
|
|
332
|
+
mailboxKey: context.mailboxKey,
|
|
333
|
+
item: context.item,
|
|
334
|
+
ownerReport: context.ownerReport,
|
|
335
|
+
peerResponse: context.peerResponse ?? null,
|
|
336
|
+
ownerDelivery: {
|
|
337
|
+
attempted: Boolean(ownerResult?.attempted ?? true),
|
|
338
|
+
delivered: Boolean(ownerResult?.delivered),
|
|
339
|
+
mode: clean(ownerResult?.mode) || 'openclaw',
|
|
340
|
+
reason: clean(ownerResult?.reason),
|
|
341
|
+
stdout: ownerResult?.stdout ?? ''
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
if (finalConversationKey && Boolean(ownerResult?.delivered) && context?.ownerReport?.finalize) {
|
|
345
|
+
deliveredFinalConversationKeys.add(finalConversationKey)
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
delivered: true,
|
|
349
|
+
mode: 'openclaw',
|
|
350
|
+
entryId: value.entry.id,
|
|
351
|
+
totalCount: value.index.totalCount,
|
|
352
|
+
deliveredToOwner: Boolean(ownerResult?.delivered),
|
|
353
|
+
ownerDelivery: ownerResult
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const notify = normalizedMode === 'host'
|
|
358
|
+
? notifyViaHost
|
|
359
|
+
: notifyViaInbox
|
|
360
|
+
|
|
361
|
+
notify.mode = normalizedMode
|
|
362
|
+
notify.preflight = async () => ({ ok: true, mode: normalizedMode })
|
|
363
|
+
return notify
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function buildOwnerSummary(context) {
|
|
367
|
+
const remoteAgentId = clean(context?.item?.remoteAgentId) || 'unknown'
|
|
368
|
+
const selectedSkill = clean(context?.selectedSkill) || 'friend-im'
|
|
369
|
+
const incoming = excerpt(extractInboundText(context?.item))
|
|
370
|
+
if (selectedSkill === 'agent-mutual-learning') {
|
|
371
|
+
if (incoming) {
|
|
372
|
+
return `${remoteAgentId} sent a mutual-learning request: ${incoming}`
|
|
373
|
+
}
|
|
374
|
+
return `${remoteAgentId} opened a mutual-learning request.`
|
|
375
|
+
}
|
|
376
|
+
if (incoming) {
|
|
377
|
+
return `${remoteAgentId} sent a message: ${incoming}`
|
|
378
|
+
}
|
|
379
|
+
return `${remoteAgentId} opened an inbound ${selectedSkill} request.`
|
|
380
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { resolveUserPath } from '../shared/paths.mjs'
|
|
5
|
+
|
|
6
|
+
function toBase64Url(buffer) {
|
|
7
|
+
return Buffer.from(buffer).toString('base64url')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadRuntimeKeyBundle(keyFile) {
|
|
11
|
+
return JSON.parse(fs.readFileSync(resolveUserPath(keyFile), 'utf8'))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createPrivateKey(privateKeyPem) {
|
|
15
|
+
return crypto.createPrivateKey(privateKeyPem)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function signText(bundle, message) {
|
|
19
|
+
const key = createPrivateKey(bundle.privateKeyPem)
|
|
20
|
+
const payload = Buffer.from(message, 'utf8')
|
|
21
|
+
if (bundle.keyType === 2) {
|
|
22
|
+
return toBase64Url(crypto.sign(null, payload, key))
|
|
23
|
+
}
|
|
24
|
+
if (bundle.keyType === 3) {
|
|
25
|
+
return toBase64Url(crypto.sign('sha256', payload, key))
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unsupported keyType: ${bundle.keyType}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function publicKeyFingerprint(bundle) {
|
|
31
|
+
const digest = crypto.createHash('sha256').update(String(bundle.publicKey)).digest('hex')
|
|
32
|
+
return `sha256:${digest.slice(0, 16)}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildEd25519Bundle() {
|
|
36
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
|
|
37
|
+
const publicJwk = publicKey.export({ format: 'jwk' })
|
|
38
|
+
const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' })
|
|
39
|
+
const publicBytes = Buffer.from(publicJwk.x, 'base64url')
|
|
40
|
+
return {
|
|
41
|
+
keyType: 2,
|
|
42
|
+
keyTypeName: 'agent_runtime_ed25519',
|
|
43
|
+
publicKey: publicJwk.x,
|
|
44
|
+
publicKeyEncoding: 'base64url-raw-32',
|
|
45
|
+
publicKeyHex: publicBytes.toString('hex'),
|
|
46
|
+
privateKeyPem: privatePem.toString(),
|
|
47
|
+
privateKeyEncoding: 'pkcs8-pem',
|
|
48
|
+
signingAlgorithm: 'ed25519'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildSecp256k1Bundle() {
|
|
53
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' })
|
|
54
|
+
const publicJwk = publicKey.export({ format: 'jwk' })
|
|
55
|
+
const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' })
|
|
56
|
+
const x = Buffer.from(publicJwk.x, 'base64url')
|
|
57
|
+
const y = Buffer.from(publicJwk.y, 'base64url')
|
|
58
|
+
const prefix = (y[y.length - 1] & 1) === 0 ? 0x02 : 0x03
|
|
59
|
+
const compressed = Buffer.concat([Buffer.from([prefix]), x])
|
|
60
|
+
return {
|
|
61
|
+
keyType: 3,
|
|
62
|
+
keyTypeName: 'agent_runtime_secp256k1',
|
|
63
|
+
publicKey: compressed.toString('hex'),
|
|
64
|
+
publicKeyEncoding: 'hex-compressed-33',
|
|
65
|
+
publicKeyHex: compressed.toString('hex'),
|
|
66
|
+
privateKeyPem: privatePem.toString(),
|
|
67
|
+
privateKeyEncoding: 'pkcs8-pem',
|
|
68
|
+
signingAlgorithm: 'ecdsa-secp256k1-sha256'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function writeRuntimeKeyBundle(outFile, bundle) {
|
|
73
|
+
const outPath = resolveUserPath(outFile)
|
|
74
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
|
75
|
+
fs.writeFileSync(outPath, `${JSON.stringify(bundle, null, 2)}\n`, { mode: 0o600 })
|
|
76
|
+
fs.chmodSync(outPath, 0o600)
|
|
77
|
+
return outPath
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function generateRuntimeKeyBundle(keyType = 'ed25519') {
|
|
81
|
+
const normalized = `${keyType}`.trim()
|
|
82
|
+
const bundle = normalized === 'secp256k1' ? buildSecp256k1Bundle() : buildEd25519Bundle()
|
|
83
|
+
bundle.generatedAt = new Date().toISOString()
|
|
84
|
+
return bundle
|
|
85
|
+
}
|