@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,397 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { createLibp2p } from 'libp2p'
|
|
5
|
+
import { tcp } from '@libp2p/tcp'
|
|
6
|
+
import { noise } from '@libp2p/noise'
|
|
7
|
+
import { yamux } from '@chainsafe/libp2p-yamux'
|
|
8
|
+
import { identify } from '@libp2p/identify'
|
|
9
|
+
import { autoNAT } from '@libp2p/autonat'
|
|
10
|
+
import { dcutr } from '@libp2p/dcutr'
|
|
11
|
+
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
|
12
|
+
import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys'
|
|
13
|
+
import { peerIdFromString } from '@libp2p/peer-id'
|
|
14
|
+
import { multiaddr } from '@multiformats/multiaddr'
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_LISTEN_ADDRS = ['/ip6/::/tcp/0', '/ip4/0.0.0.0/tcp/0']
|
|
17
|
+
const DEFAULT_DIRECT_UPGRADE_TIMEOUT_MS = 12000
|
|
18
|
+
const DEFAULT_TRANSPORT_READY_TIMEOUT_MS = 20000
|
|
19
|
+
const streamReaders = new WeakMap()
|
|
20
|
+
|
|
21
|
+
function unique(values = []) {
|
|
22
|
+
return [...new Set(values.map((value) => `${value}`.trim()).filter(Boolean))]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addrPriority(value) {
|
|
26
|
+
const text = `${value}`.trim().toLowerCase()
|
|
27
|
+
if (text.includes('/dns6/')) return 0
|
|
28
|
+
if (text.includes('/ip6/')) return 1
|
|
29
|
+
if (text.includes('/dnsaddr/')) return 2
|
|
30
|
+
if (text.includes('/dns4/')) return 3
|
|
31
|
+
if (text.includes('/ip4/')) return 4
|
|
32
|
+
return 5
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function prioritizeAddrs(values = []) {
|
|
36
|
+
return unique(values)
|
|
37
|
+
.map((value, index) => ({ value, index, priority: addrPriority(value) }))
|
|
38
|
+
.sort((left, right) => {
|
|
39
|
+
if (left.priority !== right.priority) {
|
|
40
|
+
return left.priority - right.priority
|
|
41
|
+
}
|
|
42
|
+
return left.index - right.index
|
|
43
|
+
})
|
|
44
|
+
.map((item) => item.value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureParentDir(filePath) {
|
|
48
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function loadOrCreatePeerPrivateKey(peerKeyFile) {
|
|
52
|
+
if (!peerKeyFile) {
|
|
53
|
+
throw new Error('peerKeyFile is required for gateway node identity')
|
|
54
|
+
}
|
|
55
|
+
const cleaned = path.resolve(peerKeyFile)
|
|
56
|
+
if (fs.existsSync(cleaned)) {
|
|
57
|
+
return privateKeyFromProtobuf(fs.readFileSync(cleaned))
|
|
58
|
+
}
|
|
59
|
+
ensureParentDir(cleaned)
|
|
60
|
+
const privateKey = await generateKeyPair('Ed25519')
|
|
61
|
+
fs.writeFileSync(cleaned, Buffer.from(privateKeyToProtobuf(privateKey)), { mode: 0o600 })
|
|
62
|
+
fs.chmodSync(cleaned, 0o600)
|
|
63
|
+
return privateKey
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildRelayListenAddrs(relayMultiaddrs = []) {
|
|
67
|
+
return prioritizeAddrs(relayMultiaddrs.map((value) => `${value}`.trim()).filter(Boolean).map((value) => `${value}/p2p-circuit`))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseFailedListenAddrs(error) {
|
|
71
|
+
const message = `${error?.message ?? ''}`
|
|
72
|
+
if (!message.includes('Some configured addresses failed to be listened on')) {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
return message
|
|
76
|
+
.split('\n')
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.filter((line) => line.startsWith('/'))
|
|
79
|
+
.map((line) => line.split(':', 1)[0].trim())
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function createNode({
|
|
84
|
+
listenAddrs = DEFAULT_LISTEN_ADDRS,
|
|
85
|
+
relayListenAddrs = [],
|
|
86
|
+
peerKeyFile
|
|
87
|
+
} = {}) {
|
|
88
|
+
const privateKey = await loadOrCreatePeerPrivateKey(peerKeyFile)
|
|
89
|
+
let activeListenAddrs = unique([...listenAddrs, ...relayListenAddrs])
|
|
90
|
+
let lastError = null
|
|
91
|
+
|
|
92
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
93
|
+
try {
|
|
94
|
+
return await createLibp2p({
|
|
95
|
+
privateKey,
|
|
96
|
+
addresses: {
|
|
97
|
+
listen: activeListenAddrs
|
|
98
|
+
},
|
|
99
|
+
transports: [
|
|
100
|
+
tcp(),
|
|
101
|
+
circuitRelayTransport()
|
|
102
|
+
],
|
|
103
|
+
connectionEncrypters: [noise()],
|
|
104
|
+
streamMuxers: [yamux()],
|
|
105
|
+
services: {
|
|
106
|
+
identify: identify(),
|
|
107
|
+
autoNAT: autoNAT(),
|
|
108
|
+
dcutr: dcutr()
|
|
109
|
+
},
|
|
110
|
+
start: true
|
|
111
|
+
})
|
|
112
|
+
} catch (error) {
|
|
113
|
+
lastError = error
|
|
114
|
+
const failedListenAddrs = parseFailedListenAddrs(error)
|
|
115
|
+
if (failedListenAddrs.length === 0) {
|
|
116
|
+
throw error
|
|
117
|
+
}
|
|
118
|
+
const failedSet = new Set(failedListenAddrs)
|
|
119
|
+
const nextListenAddrs = activeListenAddrs.filter((value) => !failedSet.has(value))
|
|
120
|
+
const removedAny = nextListenAddrs.length < activeListenAddrs.length
|
|
121
|
+
if (!removedAny || nextListenAddrs.length === 0) {
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
activeListenAddrs = prioritizeAddrs(nextListenAddrs)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw lastError ?? new Error('gateway node failed to start')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function advertisedAddrs(node) {
|
|
132
|
+
return prioritizeAddrs(node.getMultiaddrs().map((addr) => addr.toString()))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function relayReservationAddrs(node) {
|
|
136
|
+
return prioritizeAddrs(advertisedAddrs(node).filter((addr) => addr.includes('/p2p-circuit')))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function directListenAddrs(node) {
|
|
140
|
+
return prioritizeAddrs(advertisedAddrs(node).filter((addr) => !addr.includes('/p2p-circuit')))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function requireListeningTransport(node, binding) {
|
|
144
|
+
const peerId = node?.peerId?.toString?.() ?? ''
|
|
145
|
+
const listenAddrs = directListenAddrs(node)
|
|
146
|
+
const relayAddrs = relayReservationAddrs(node)
|
|
147
|
+
const supportedBindings = binding?.binding ? [binding.binding] : []
|
|
148
|
+
const streamProtocol = `${binding?.streamProtocol ?? ''}`.trim()
|
|
149
|
+
const a2aProtocolVersion = `${binding?.a2aProtocolVersion ?? ''}`.trim()
|
|
150
|
+
|
|
151
|
+
if (!peerId) {
|
|
152
|
+
throw new Error('local gateway is not ready: peerId is unavailable')
|
|
153
|
+
}
|
|
154
|
+
if (listenAddrs.length === 0 && relayAddrs.length === 0) {
|
|
155
|
+
throw new Error('local gateway is not ready: no direct or relay-backed addresses were published')
|
|
156
|
+
}
|
|
157
|
+
if (!streamProtocol) {
|
|
158
|
+
throw new Error('local gateway is not ready: streamProtocol is unavailable')
|
|
159
|
+
}
|
|
160
|
+
if (supportedBindings.length === 0) {
|
|
161
|
+
throw new Error('local gateway is not ready: supportedBindings are unavailable')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
peerId,
|
|
166
|
+
dialAddrs: prioritizeAddrs(relayAddrs.length > 0 ? relayAddrs : listenAddrs),
|
|
167
|
+
listenAddrs,
|
|
168
|
+
relayAddrs,
|
|
169
|
+
supportedBindings,
|
|
170
|
+
streamProtocol,
|
|
171
|
+
a2aProtocolVersion
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function waitForPublishedTransport(node, binding, {
|
|
176
|
+
requireRelayReservation = false,
|
|
177
|
+
timeoutMs = DEFAULT_TRANSPORT_READY_TIMEOUT_MS
|
|
178
|
+
} = {}) {
|
|
179
|
+
const startedAt = Date.now()
|
|
180
|
+
let lastError = null
|
|
181
|
+
|
|
182
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
183
|
+
try {
|
|
184
|
+
const transport = requireListeningTransport(node, binding)
|
|
185
|
+
if (requireRelayReservation && transport.relayAddrs.length === 0) {
|
|
186
|
+
throw new Error('waiting for relay reservation-backed transport')
|
|
187
|
+
}
|
|
188
|
+
return transport
|
|
189
|
+
} catch (error) {
|
|
190
|
+
lastError = error
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw lastError ?? new Error('gateway transport did not become ready before timeout')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isDirectConnection(connection) {
|
|
199
|
+
const remoteAddr = connection?.remoteAddr?.toString?.() ?? ''
|
|
200
|
+
return !remoteAddr.includes('/p2p-circuit') && connection?.limits == null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function newStreamOptions(connection) {
|
|
204
|
+
if (connection?.limits != null) {
|
|
205
|
+
return { runOnLimitedConnection: true }
|
|
206
|
+
}
|
|
207
|
+
const remoteAddr = connection?.remoteAddr?.toString?.() ?? ''
|
|
208
|
+
if (remoteAddr.includes('/p2p-circuit')) {
|
|
209
|
+
return { runOnLimitedConnection: true }
|
|
210
|
+
}
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function currentPeerConnection(node, peerId) {
|
|
215
|
+
if (!peerId?.trim?.()) return null
|
|
216
|
+
const remotePeer = peerIdFromString(peerId)
|
|
217
|
+
const connections = node.getConnections(remotePeer)
|
|
218
|
+
return connections.find(isDirectConnection) ?? connections[0] ?? null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function chooseDialAddrs(transport) {
|
|
222
|
+
return prioritizeAddrs(
|
|
223
|
+
transport?.dialAddrs?.length
|
|
224
|
+
? transport.dialAddrs
|
|
225
|
+
: transport?.relayAddrs?.length
|
|
226
|
+
? transport.relayAddrs
|
|
227
|
+
: transport?.listenAddrs ?? []
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function waitForDirectConnection(node, peerId, timeoutMs = DEFAULT_DIRECT_UPGRADE_TIMEOUT_MS) {
|
|
232
|
+
const remotePeer = peerIdFromString(peerId)
|
|
233
|
+
const startedAt = Date.now()
|
|
234
|
+
let relayedConnection = null
|
|
235
|
+
|
|
236
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
237
|
+
const connections = node.getConnections(remotePeer)
|
|
238
|
+
const directConnection = connections.find(isDirectConnection)
|
|
239
|
+
if (directConnection) {
|
|
240
|
+
return directConnection
|
|
241
|
+
}
|
|
242
|
+
relayedConnection = connections.find((connection) => connection?.remoteAddr?.toString?.().includes('/p2p-circuit'))
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (relayedConnection) {
|
|
247
|
+
try {
|
|
248
|
+
await relayedConnection.close()
|
|
249
|
+
} catch {
|
|
250
|
+
// best-effort cleanup only
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`direct P2P upgrade did not complete for ${peerId}`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function dialProtocol(node, transport, {
|
|
257
|
+
requireDirect = false,
|
|
258
|
+
timeoutMs = DEFAULT_DIRECT_UPGRADE_TIMEOUT_MS
|
|
259
|
+
} = {}) {
|
|
260
|
+
if (!transport?.streamProtocol) {
|
|
261
|
+
throw new Error('target transport is missing streamProtocol')
|
|
262
|
+
}
|
|
263
|
+
if (!transport?.peerId?.trim()) {
|
|
264
|
+
throw new Error('target transport is missing peerId')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const dialAddrs = chooseDialAddrs(transport)
|
|
268
|
+
if (dialAddrs.length === 0) {
|
|
269
|
+
throw new Error('target transport is missing dialAddrs')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let lastError = null
|
|
273
|
+
for (const value of dialAddrs) {
|
|
274
|
+
try {
|
|
275
|
+
await node.dial(multiaddr(value))
|
|
276
|
+
break
|
|
277
|
+
} catch (error) {
|
|
278
|
+
lastError = error
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (lastError && node.getConnections(peerIdFromString(transport.peerId)).length === 0) {
|
|
283
|
+
throw lastError
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const connections = node.getConnections(peerIdFromString(transport.peerId))
|
|
287
|
+
const connection = requireDirect
|
|
288
|
+
? await waitForDirectConnection(node, transport.peerId, timeoutMs)
|
|
289
|
+
: connections.find(isDirectConnection) ?? connections[0]
|
|
290
|
+
|
|
291
|
+
if (!connection) {
|
|
292
|
+
throw new Error(`no connection was available for ${transport.peerId}`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return connection.newStream([transport.streamProtocol], newStreamOptions(connection))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function openStreamOnExistingConnection(node, transport) {
|
|
299
|
+
if (!transport?.streamProtocol) {
|
|
300
|
+
throw new Error('target transport is missing streamProtocol')
|
|
301
|
+
}
|
|
302
|
+
if (!transport?.peerId?.trim()) {
|
|
303
|
+
throw new Error('target transport is missing peerId')
|
|
304
|
+
}
|
|
305
|
+
const connection = currentPeerConnection(node, transport.peerId)
|
|
306
|
+
if (!connection) {
|
|
307
|
+
throw new Error(`no existing peer connection is available for ${transport.peerId}`)
|
|
308
|
+
}
|
|
309
|
+
return connection.newStream([transport.streamProtocol], newStreamOptions(connection))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function writeLine(stream, line) {
|
|
313
|
+
const payload = Buffer.from(`${line}\n`, 'utf8')
|
|
314
|
+
const accepted = stream.send(payload)
|
|
315
|
+
if (accepted) return
|
|
316
|
+
await new Promise((resolve, reject) => {
|
|
317
|
+
const cleanup = () => {
|
|
318
|
+
stream.removeEventListener('drain', onDrain)
|
|
319
|
+
stream.removeEventListener('close', onClose)
|
|
320
|
+
}
|
|
321
|
+
const onDrain = () => {
|
|
322
|
+
cleanup()
|
|
323
|
+
resolve()
|
|
324
|
+
}
|
|
325
|
+
const onClose = (event) => {
|
|
326
|
+
cleanup()
|
|
327
|
+
reject(event?.error ?? new Error('stream closed before drain'))
|
|
328
|
+
}
|
|
329
|
+
stream.addEventListener('drain', onDrain, { once: true })
|
|
330
|
+
stream.addEventListener('close', onClose, { once: true })
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function readSingleLine(stream) {
|
|
335
|
+
const chunks = []
|
|
336
|
+
for await (const chunk of stream) {
|
|
337
|
+
chunks.push(Buffer.from(chunk.subarray ? chunk.subarray(0) : chunk.slice()))
|
|
338
|
+
const buffer = Buffer.concat(chunks)
|
|
339
|
+
const newline = buffer.indexOf(0x0a)
|
|
340
|
+
if (newline >= 0) {
|
|
341
|
+
return buffer.subarray(0, newline).toString('utf8')
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isUnexpectedEndJson(error) {
|
|
348
|
+
return error instanceof SyntaxError && /Unexpected end of JSON input/i.test(`${error.message ?? ''}`)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function readJsonMessage(stream) {
|
|
352
|
+
if (!stream) {
|
|
353
|
+
throw new Error('stream is required')
|
|
354
|
+
}
|
|
355
|
+
let reader = streamReaders.get(stream)
|
|
356
|
+
if (!reader) {
|
|
357
|
+
reader = {
|
|
358
|
+
iterator: stream[Symbol.asyncIterator](),
|
|
359
|
+
buffer: ''
|
|
360
|
+
}
|
|
361
|
+
streamReaders.set(stream, reader)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
while (true) {
|
|
365
|
+
while (true) {
|
|
366
|
+
const newlineIndex = reader.buffer.indexOf('\n')
|
|
367
|
+
if (newlineIndex < 0) {
|
|
368
|
+
break
|
|
369
|
+
}
|
|
370
|
+
const candidate = reader.buffer.slice(0, newlineIndex).trim()
|
|
371
|
+
reader.buffer = reader.buffer.slice(newlineIndex + 1)
|
|
372
|
+
if (!candidate) {
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
return JSON.parse(candidate)
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (candidate.startsWith('{') || candidate.startsWith('[')) {
|
|
379
|
+
reader.buffer = `${candidate}\n${reader.buffer}`
|
|
380
|
+
break
|
|
381
|
+
}
|
|
382
|
+
throw error
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const next = await reader.iterator.next()
|
|
387
|
+
if (next.done) {
|
|
388
|
+
const finalText = reader.buffer.trim()
|
|
389
|
+
streamReaders.delete(stream)
|
|
390
|
+
if (!finalText) {
|
|
391
|
+
throw new Error('empty JSON message')
|
|
392
|
+
}
|
|
393
|
+
return JSON.parse(finalText)
|
|
394
|
+
}
|
|
395
|
+
reader.buffer += Buffer.from(next.value?.subarray ? next.value.subarray(0) : next.value.slice()).toString('utf8')
|
|
396
|
+
}
|
|
397
|
+
}
|