@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,602 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { gatewayHealth } from './api.mjs'
|
|
7
|
+
import { currentRuntimeRevision, defaultGatewayStateFile, discoverGatewayStateFiles, readGatewayState, resolveGatewayBase } from './state.mjs'
|
|
8
|
+
import {
|
|
9
|
+
defaultGatewayLogFile as defaultGatewayLogFileFromLayout,
|
|
10
|
+
inferAgentSquaredScopeFromArtifact,
|
|
11
|
+
resolveUserPath
|
|
12
|
+
} from '../shared/paths.mjs'
|
|
13
|
+
import { loadRuntimeKeyBundle } from '../runtime/keys.mjs'
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const ROOT = path.resolve(path.dirname(__filename), '../..')
|
|
17
|
+
|
|
18
|
+
function clean(value) {
|
|
19
|
+
return `${value ?? ''}`.trim()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unique(values = []) {
|
|
23
|
+
return Array.from(new Set(values.filter(Boolean)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function walkLocalFiles(dirPath, out, depth = 0, maxDepth = 4) {
|
|
27
|
+
if (!dirPath || !fs.existsSync(dirPath) || depth > maxDepth) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
31
|
+
const entryPath = path.join(dirPath, entry.name)
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
walkLocalFiles(entryPath, out, depth + 1, maxDepth)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if (entry.isFile()) {
|
|
40
|
+
out.push(entryPath)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseJwtPayloadUnverified(token) {
|
|
46
|
+
const serialized = clean(token)
|
|
47
|
+
if (!serialized) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
const parts = serialized.split('.')
|
|
51
|
+
if (parts.length < 2) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/')
|
|
56
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
|
|
57
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'))
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function safeReadJson(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pidExists(pid) {
|
|
72
|
+
const numeric = Number.parseInt(`${pid ?? ''}`, 10)
|
|
73
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
process.kill(numeric, 0)
|
|
78
|
+
return true
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return error?.code !== 'ESRCH'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parsePid(value) {
|
|
85
|
+
const numeric = Number.parseInt(`${value ?? ''}`, 10)
|
|
86
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultGatewaySearchRoots(rootDir = process.cwd()) {
|
|
90
|
+
return unique([
|
|
91
|
+
path.join(rootDir, 'AgentSquared'),
|
|
92
|
+
process.env.HOME ? path.join(process.env.HOME, '.openclaw', 'workspace', 'AgentSquared') : '',
|
|
93
|
+
process.env.HOME ? path.join(process.env.HOME, '.nanobot', 'workspace', 'AgentSquared') : ''
|
|
94
|
+
])
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findSingletonAgentProfile(searchRoots) {
|
|
98
|
+
const profiles = discoverLocalAgentProfiles(searchRoots).filter((item) => item.agentId && item.keyFile)
|
|
99
|
+
if (profiles.length === 1) {
|
|
100
|
+
return profiles[0]
|
|
101
|
+
}
|
|
102
|
+
if (profiles.length > 1) {
|
|
103
|
+
throw new Error('Multiple local AgentSquared agent profiles were discovered. Pass --agent-id and --key-file explicitly.')
|
|
104
|
+
}
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function localActivationArtifacts(searchRoots) {
|
|
109
|
+
return discoverLocalAgentProfiles(searchRoots).filter((item) =>
|
|
110
|
+
item.gatewayRunning ||
|
|
111
|
+
clean(item.gatewayStateFile) ||
|
|
112
|
+
clean(item.keyFile) ||
|
|
113
|
+
clean(item.receiptFile) ||
|
|
114
|
+
clean(item.onboardingSummaryFile)
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onboardingTokenTargetAgentId(authorizationToken) {
|
|
119
|
+
const payload = parseJwtPayloadUnverified(authorizationToken)
|
|
120
|
+
const humanName = clean(payload?.hnm)
|
|
121
|
+
const agentName = clean(payload?.anm)
|
|
122
|
+
if (!humanName || !agentName) {
|
|
123
|
+
return ''
|
|
124
|
+
}
|
|
125
|
+
return `${agentName}@${humanName}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findSingletonGatewayState(searchRoots) {
|
|
129
|
+
const candidates = discoverGatewayStateFiles(searchRoots)
|
|
130
|
+
const valid = []
|
|
131
|
+
for (const stateFile of candidates) {
|
|
132
|
+
try {
|
|
133
|
+
const state = readGatewayState(stateFile)
|
|
134
|
+
if (!state?.agentId || !state?.keyFile || !state?.gatewayBase) {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
if (!state?.runtimeRevision || state.runtimeRevision !== currentRuntimeRevision()) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
valid.push({
|
|
141
|
+
stateFile,
|
|
142
|
+
state
|
|
143
|
+
})
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore malformed state files
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (valid.length === 1) {
|
|
149
|
+
return valid[0]
|
|
150
|
+
}
|
|
151
|
+
if (valid.length > 1) {
|
|
152
|
+
throw new Error('Multiple local AgentSquared gateway instances were discovered. Pass --agent-id and --key-file explicitly.')
|
|
153
|
+
}
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveGatewayBaseIfAvailable(args, searchRoots) {
|
|
158
|
+
try {
|
|
159
|
+
const context = resolveAgentContext(args, { searchRoots })
|
|
160
|
+
return resolveGatewayBase({
|
|
161
|
+
gatewayBase: args['gateway-base'],
|
|
162
|
+
keyFile: context.keyFile,
|
|
163
|
+
agentId: context.agentId,
|
|
164
|
+
gatewayStateFile: clean(args['gateway-state-file']) || context.gatewayStateFile
|
|
165
|
+
})
|
|
166
|
+
} catch {
|
|
167
|
+
return ''
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function resolveGatewayTransport(args, searchRoots) {
|
|
172
|
+
const gatewayBase = resolveGatewayBaseIfAvailable(args, searchRoots)
|
|
173
|
+
if (!gatewayBase) {
|
|
174
|
+
return { gatewayBase: '', transport: null, health: null }
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const health = await gatewayHealth(gatewayBase)
|
|
178
|
+
if (health?.peerId && health?.streamProtocol) {
|
|
179
|
+
return {
|
|
180
|
+
gatewayBase,
|
|
181
|
+
health,
|
|
182
|
+
transport: {
|
|
183
|
+
peerId: health.peerId,
|
|
184
|
+
listenAddrs: health.listenAddrs ?? [],
|
|
185
|
+
relayAddrs: health.relayAddrs ?? [],
|
|
186
|
+
supportedBindings: health.supportedBindings ?? [],
|
|
187
|
+
streamProtocol: health.streamProtocol,
|
|
188
|
+
a2aProtocolVersion: health.a2aProtocolVersion ?? ''
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { gatewayBase, health, transport: null }
|
|
193
|
+
} catch {
|
|
194
|
+
return { gatewayBase, health: null, transport: null }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resolvedHostRuntimeFromHealth(health = null) {
|
|
199
|
+
return clean(health?.hostRuntime?.resolved || health?.hostRuntime?.id) || 'none'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function toOwnerFacingText(lines = []) {
|
|
203
|
+
return lines.filter(Boolean).join('\n')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildGatewayArgs(args, fullName, keyFile, detectedHostRuntime) {
|
|
207
|
+
const forwarded = [
|
|
208
|
+
'--api-base', clean(args['api-base']) || 'https://api.agentsquared.net',
|
|
209
|
+
'--agent-id', fullName,
|
|
210
|
+
'--key-file', keyFile
|
|
211
|
+
]
|
|
212
|
+
for (const key of [
|
|
213
|
+
'gateway-host',
|
|
214
|
+
'gateway-port',
|
|
215
|
+
'presence-refresh-ms',
|
|
216
|
+
'health-check-ms',
|
|
217
|
+
'transport-check-timeout-ms',
|
|
218
|
+
'recovery-idle-wait-ms',
|
|
219
|
+
'failures-before-recover',
|
|
220
|
+
'router-mode',
|
|
221
|
+
'wait-ms',
|
|
222
|
+
'max-active-mailboxes',
|
|
223
|
+
'router-skills',
|
|
224
|
+
'default-skill',
|
|
225
|
+
'peer-key-file',
|
|
226
|
+
'gateway-state-file',
|
|
227
|
+
'inbox-dir',
|
|
228
|
+
'listen-addrs',
|
|
229
|
+
'openclaw-agent',
|
|
230
|
+
'openclaw-command',
|
|
231
|
+
'openclaw-cwd',
|
|
232
|
+
'openclaw-session-prefix',
|
|
233
|
+
'openclaw-timeout-ms',
|
|
234
|
+
'openclaw-gateway-url',
|
|
235
|
+
'openclaw-gateway-token',
|
|
236
|
+
'openclaw-gateway-password',
|
|
237
|
+
'host-runtime'
|
|
238
|
+
]) {
|
|
239
|
+
const value = clean(args[key])
|
|
240
|
+
if (value) {
|
|
241
|
+
forwarded.push(`--${key}`, value)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!forwarded.includes('--host-runtime') && detectedHostRuntime?.resolved && detectedHostRuntime.resolved !== 'none') {
|
|
245
|
+
forwarded.push('--host-runtime', detectedHostRuntime.resolved)
|
|
246
|
+
}
|
|
247
|
+
return forwarded
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function gatewayLogFileFor(keyFile, agentId) {
|
|
251
|
+
return defaultGatewayLogFileFromLayout(keyFile, agentId)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function archiveGatewayStateFile(gatewayStateFile, reason = 'stale') {
|
|
255
|
+
const resolved = clean(gatewayStateFile) ? resolveUserPath(gatewayStateFile) : ''
|
|
256
|
+
if (!resolved || !fs.existsSync(resolved)) {
|
|
257
|
+
return ''
|
|
258
|
+
}
|
|
259
|
+
const archived = `${resolved}.${clean(reason) || 'archived'}.${Date.now()}.bak`
|
|
260
|
+
fs.renameSync(resolved, archived)
|
|
261
|
+
return archived
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function spawnDetachedGatewayProcess({ args, agentId, keyFile, gatewayLogFile }) {
|
|
265
|
+
const gatewayArgs = buildGatewayArgs(args, agentId, keyFile, null)
|
|
266
|
+
fs.mkdirSync(path.dirname(gatewayLogFile), { recursive: true })
|
|
267
|
+
const stdoutFd = fs.openSync(gatewayLogFile, 'a')
|
|
268
|
+
const stderrFd = fs.openSync(gatewayLogFile, 'a')
|
|
269
|
+
try {
|
|
270
|
+
const child = spawn(process.execPath, [path.join(ROOT, 'a2_cli.mjs'), 'gateway', ...gatewayArgs], {
|
|
271
|
+
detached: true,
|
|
272
|
+
cwd: ROOT,
|
|
273
|
+
stdio: ['ignore', stdoutFd, stderrFd]
|
|
274
|
+
})
|
|
275
|
+
child.unref()
|
|
276
|
+
return child
|
|
277
|
+
} finally {
|
|
278
|
+
fs.closeSync(stdoutFd)
|
|
279
|
+
fs.closeSync(stderrFd)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function terminateGatewayProcess(pid, {
|
|
284
|
+
signal = 'SIGTERM',
|
|
285
|
+
waitMs = 8000
|
|
286
|
+
} = {}) {
|
|
287
|
+
const numeric = parsePid(pid)
|
|
288
|
+
if (!numeric) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
process.kill(numeric, signal)
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error?.code === 'ESRCH') {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
throw error
|
|
298
|
+
}
|
|
299
|
+
const deadline = Date.now() + Math.max(0, waitMs)
|
|
300
|
+
while (Date.now() < deadline) {
|
|
301
|
+
try {
|
|
302
|
+
process.kill(numeric, 0)
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error?.code === 'ESRCH') {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
throw error
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function waitForGatewayReady({ gatewayBase = '', keyFile = '', agentId = '', gatewayStateFile = '', timeoutMs = 30000 }) {
|
|
314
|
+
const startedAt = Date.now()
|
|
315
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
316
|
+
try {
|
|
317
|
+
const resolvedBase = gatewayBase || resolveGatewayBase({
|
|
318
|
+
gatewayBase,
|
|
319
|
+
keyFile,
|
|
320
|
+
agentId,
|
|
321
|
+
gatewayStateFile
|
|
322
|
+
})
|
|
323
|
+
const health = await gatewayHealth(resolvedBase)
|
|
324
|
+
if (health?.peerId) {
|
|
325
|
+
return { gatewayBase: resolvedBase, health }
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// keep waiting
|
|
329
|
+
}
|
|
330
|
+
await new Promise((resolve) => setTimeout(resolve, 750))
|
|
331
|
+
}
|
|
332
|
+
throw new Error('Timed out waiting for the local AgentSquared gateway to become healthy.')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function discoverLocalAgentProfiles(searchRoots = defaultGatewaySearchRoots()) {
|
|
336
|
+
const files = []
|
|
337
|
+
for (const root of searchRoots) {
|
|
338
|
+
walkLocalFiles(root, files)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const grouped = new Map()
|
|
342
|
+
|
|
343
|
+
function bucketFor(baseKey) {
|
|
344
|
+
if (!grouped.has(baseKey)) {
|
|
345
|
+
grouped.set(baseKey, {
|
|
346
|
+
baseKey,
|
|
347
|
+
agentId: '',
|
|
348
|
+
keyFile: '',
|
|
349
|
+
receiptFile: '',
|
|
350
|
+
onboardingSummaryFile: '',
|
|
351
|
+
gatewayStateFile: '',
|
|
352
|
+
gatewayBase: '',
|
|
353
|
+
gatewayPid: null
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
return grouped.get(baseKey)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const filePath of unique(files)) {
|
|
360
|
+
const name = path.basename(filePath)
|
|
361
|
+
const agentsquaredScope = inferAgentSquaredScopeFromArtifact(filePath)
|
|
362
|
+
if (name === 'registration-receipt.json' && agentsquaredScope) {
|
|
363
|
+
const baseKey = agentsquaredScope
|
|
364
|
+
const bucket = bucketFor(baseKey)
|
|
365
|
+
const payload = safeReadJson(filePath)
|
|
366
|
+
bucket.receiptFile = filePath
|
|
367
|
+
bucket.agentId = bucket.agentId || clean(payload?.fullName)
|
|
368
|
+
} else if (name === 'onboarding-summary.json' && agentsquaredScope) {
|
|
369
|
+
const baseKey = agentsquaredScope
|
|
370
|
+
const bucket = bucketFor(baseKey)
|
|
371
|
+
const payload = safeReadJson(filePath)
|
|
372
|
+
bucket.onboardingSummaryFile = filePath
|
|
373
|
+
bucket.agentId = bucket.agentId || clean(payload?.registration?.fullName)
|
|
374
|
+
bucket.keyFile = bucket.keyFile || clean(payload?.keyFile)
|
|
375
|
+
} else if (name === 'gateway.json' && agentsquaredScope) {
|
|
376
|
+
const baseKey = agentsquaredScope
|
|
377
|
+
const bucket = bucketFor(baseKey)
|
|
378
|
+
const payload = safeReadJson(filePath)
|
|
379
|
+
bucket.gatewayStateFile = filePath
|
|
380
|
+
bucket.agentId = bucket.agentId || clean(payload?.agentId)
|
|
381
|
+
bucket.keyFile = bucket.keyFile || clean(payload?.keyFile)
|
|
382
|
+
bucket.gatewayBase = bucket.gatewayBase || clean(payload?.gatewayBase)
|
|
383
|
+
bucket.gatewayPid = bucket.gatewayPid || parsePid(payload?.gatewayPid)
|
|
384
|
+
} else if (name === 'runtime-key.json' && agentsquaredScope) {
|
|
385
|
+
const baseKey = agentsquaredScope
|
|
386
|
+
const bucket = bucketFor(baseKey)
|
|
387
|
+
bucket.keyFile = bucket.keyFile || filePath
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const normalized = Array.from(grouped.values())
|
|
392
|
+
.filter((item) => item.agentId || item.keyFile || item.gatewayStateFile || item.receiptFile)
|
|
393
|
+
.map((item) => ({
|
|
394
|
+
...item,
|
|
395
|
+
keyFile: item.keyFile ? resolveUserPath(item.keyFile) : '',
|
|
396
|
+
gatewayStateFile: item.gatewayStateFile ? resolveUserPath(item.gatewayStateFile) : '',
|
|
397
|
+
receiptFile: item.receiptFile ? resolveUserPath(item.receiptFile) : '',
|
|
398
|
+
onboardingSummaryFile: item.onboardingSummaryFile ? resolveUserPath(item.onboardingSummaryFile) : '',
|
|
399
|
+
gatewayRunning: pidExists(item.gatewayPid)
|
|
400
|
+
}))
|
|
401
|
+
|
|
402
|
+
const merged = new Map()
|
|
403
|
+
|
|
404
|
+
function mergedKeyFor(item) {
|
|
405
|
+
return item.agentId || item.keyFile || item.baseKey
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const item of normalized) {
|
|
409
|
+
const mergedKey = mergedKeyFor(item)
|
|
410
|
+
if (!merged.has(mergedKey)) {
|
|
411
|
+
merged.set(mergedKey, { ...item })
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
const existing = merged.get(mergedKey)
|
|
415
|
+
existing.baseKey = existing.baseKey || item.baseKey
|
|
416
|
+
existing.agentId = existing.agentId || item.agentId
|
|
417
|
+
existing.keyFile = existing.keyFile || item.keyFile
|
|
418
|
+
existing.receiptFile = existing.receiptFile || item.receiptFile
|
|
419
|
+
existing.onboardingSummaryFile = existing.onboardingSummaryFile || item.onboardingSummaryFile
|
|
420
|
+
existing.gatewayStateFile = existing.gatewayStateFile || item.gatewayStateFile
|
|
421
|
+
existing.gatewayBase = existing.gatewayBase || item.gatewayBase
|
|
422
|
+
existing.gatewayPid = existing.gatewayPid || item.gatewayPid
|
|
423
|
+
existing.gatewayRunning = existing.gatewayRunning || item.gatewayRunning
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return Array.from(merged.values()).sort((left, right) => left.baseKey.localeCompare(right.baseKey))
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function assertNoExistingLocalActivation(authorizationToken, { searchRoots = defaultGatewaySearchRoots() } = {}) {
|
|
430
|
+
const artifacts = localActivationArtifacts(searchRoots)
|
|
431
|
+
if (artifacts.length === 0) {
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const profiles = artifacts.filter((item) => item.agentId && item.keyFile)
|
|
436
|
+
const tokenTargetAgentId = onboardingTokenTargetAgentId(authorizationToken)
|
|
437
|
+
if (profiles.length === 0) {
|
|
438
|
+
const artifact = artifacts[0]
|
|
439
|
+
const artifactPath = clean(artifact.gatewayStateFile) || clean(artifact.keyFile) || clean(artifact.receiptFile) || clean(artifact.onboardingSummaryFile)
|
|
440
|
+
throw new Error(`Local AgentSquared activation artifacts already exist${artifactPath ? ` at ${artifactPath}` : ''}. Do not start onboarding again on this host runtime. Reuse the existing local setup or clean up the abandoned local activation intentionally before retrying.`)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (tokenTargetAgentId) {
|
|
444
|
+
const matchingProfile = profiles.find((item) => item.agentId === tokenTargetAgentId)
|
|
445
|
+
if (matchingProfile) {
|
|
446
|
+
throw new Error(`AgentSquared is already activated locally for ${matchingProfile.agentId}. Do not activate the same agent again. Run \`a2-cli local inspect\` and then choose the existing profile instead of onboarding again.`)
|
|
447
|
+
}
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (profiles.length === 1) {
|
|
452
|
+
const profile = profiles[0]
|
|
453
|
+
throw new Error(`A reusable local AgentSquared profile already exists for ${profile.agentId}, but the onboarding token did not clearly identify a different target agent. Run \`a2-cli local inspect\` first and only onboard another agent when the token clearly targets a new local agent id.`)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
throw new Error('Multiple reusable local AgentSquared profiles already exist, and the onboarding token did not clearly identify which new local agent to create. Run `a2-cli local inspect` first and only onboard another agent when the token clearly targets a brand-new local agent id.')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function resolveAgentContext(args = {}, { searchRoots = defaultGatewaySearchRoots() } = {}) {
|
|
460
|
+
const explicitAgentId = clean(args['agent-id'])
|
|
461
|
+
const explicitKeyFile = clean(args['key-file'])
|
|
462
|
+
const explicitGatewayStateFile = clean(args['gateway-state-file'])
|
|
463
|
+
|
|
464
|
+
if (explicitAgentId && explicitKeyFile) {
|
|
465
|
+
return {
|
|
466
|
+
agentId: explicitAgentId,
|
|
467
|
+
keyFile: resolveUserPath(explicitKeyFile),
|
|
468
|
+
gatewayStateFile: explicitGatewayStateFile || defaultGatewayStateFile(explicitKeyFile, explicitAgentId)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const singleton = findSingletonGatewayState(searchRoots)
|
|
473
|
+
if (singleton) {
|
|
474
|
+
return {
|
|
475
|
+
agentId: clean(singleton.state.agentId),
|
|
476
|
+
keyFile: resolveUserPath(singleton.state.keyFile),
|
|
477
|
+
gatewayStateFile: singleton.stateFile
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const profile = findSingletonAgentProfile(searchRoots)
|
|
482
|
+
if (!profile) {
|
|
483
|
+
throw new Error('No local AgentSquared gateway or agent profile could be discovered automatically. Pass --agent-id and --key-file explicitly.')
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
agentId: clean(profile.agentId),
|
|
488
|
+
keyFile: resolveUserPath(profile.keyFile),
|
|
489
|
+
gatewayStateFile: profile.gatewayStateFile || defaultGatewayStateFile(profile.keyFile, profile.agentId)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function inspectExistingGateway({ gatewayBase = '', keyFile = '', agentId = '', gatewayStateFile = '' } = {}) {
|
|
494
|
+
const stateFile = clean(gatewayStateFile) || (keyFile && agentId ? defaultGatewayStateFile(keyFile, agentId) : '')
|
|
495
|
+
const state = stateFile ? readGatewayState(stateFile) : null
|
|
496
|
+
const pid = parsePid(state?.gatewayPid)
|
|
497
|
+
const discoveredBase = clean(gatewayBase) || clean(state?.gatewayBase)
|
|
498
|
+
const running = pidExists(pid)
|
|
499
|
+
const expectedRevision = currentRuntimeRevision()
|
|
500
|
+
const stateRevision = clean(state?.runtimeRevision)
|
|
501
|
+
const revisionMatches = !state || (stateRevision && stateRevision === expectedRevision)
|
|
502
|
+
let health = null
|
|
503
|
+
let healthy = false
|
|
504
|
+
|
|
505
|
+
if (running && discoveredBase && revisionMatches) {
|
|
506
|
+
try {
|
|
507
|
+
health = await gatewayHealth(discoveredBase)
|
|
508
|
+
healthy = Boolean(health?.peerId)
|
|
509
|
+
} catch {
|
|
510
|
+
healthy = false
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
stateFile,
|
|
516
|
+
state,
|
|
517
|
+
pid,
|
|
518
|
+
running,
|
|
519
|
+
healthy,
|
|
520
|
+
expectedRevision,
|
|
521
|
+
stateRevision,
|
|
522
|
+
revisionMatches,
|
|
523
|
+
gatewayBase: discoveredBase,
|
|
524
|
+
health
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export async function ensureGatewayForUse(args = {}, {
|
|
529
|
+
searchRoots = defaultGatewaySearchRoots(),
|
|
530
|
+
timeoutMs = 30000,
|
|
531
|
+
spawnGatewayProcess = spawnDetachedGatewayProcess,
|
|
532
|
+
waitForReady = waitForGatewayReady,
|
|
533
|
+
stopGatewayProcess = terminateGatewayProcess
|
|
534
|
+
} = {}) {
|
|
535
|
+
const context = resolveAgentContext(args, { searchRoots })
|
|
536
|
+
const gatewayStateFile = clean(args['gateway-state-file']) || context.gatewayStateFile
|
|
537
|
+
const existing = await inspectExistingGateway({
|
|
538
|
+
gatewayBase: args['gateway-base'],
|
|
539
|
+
keyFile: context.keyFile,
|
|
540
|
+
agentId: context.agentId,
|
|
541
|
+
gatewayStateFile
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
if (existing.running && existing.gatewayBase && existing.healthy) {
|
|
545
|
+
return {
|
|
546
|
+
...context,
|
|
547
|
+
gatewayBase: existing.gatewayBase,
|
|
548
|
+
gatewayHealth: existing.health,
|
|
549
|
+
gatewayPid: existing.pid,
|
|
550
|
+
autoStarted: false,
|
|
551
|
+
gatewayLogFile: gatewayLogFileFor(context.keyFile, context.agentId)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (existing.running && (!existing.healthy || !existing.revisionMatches)) {
|
|
556
|
+
await stopGatewayProcess(existing.pid)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (existing.state && (!existing.revisionMatches || !clean(existing.state?.gatewayBase))) {
|
|
560
|
+
archiveGatewayStateFile(gatewayStateFile, 'restart-required')
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const gatewayLogFile = gatewayLogFileFor(context.keyFile, context.agentId)
|
|
564
|
+
const child = await spawnGatewayProcess({
|
|
565
|
+
args,
|
|
566
|
+
agentId: context.agentId,
|
|
567
|
+
keyFile: context.keyFile,
|
|
568
|
+
gatewayLogFile
|
|
569
|
+
})
|
|
570
|
+
const ready = await waitForReady({
|
|
571
|
+
keyFile: context.keyFile,
|
|
572
|
+
agentId: context.agentId,
|
|
573
|
+
gatewayStateFile,
|
|
574
|
+
timeoutMs: Number.parseInt(args['gateway-wait-ms'] ?? `${timeoutMs}`, 10) || timeoutMs
|
|
575
|
+
})
|
|
576
|
+
return {
|
|
577
|
+
...context,
|
|
578
|
+
gatewayBase: ready.gatewayBase,
|
|
579
|
+
gatewayHealth: ready.health,
|
|
580
|
+
gatewayPid: child?.pid ?? null,
|
|
581
|
+
autoStarted: true,
|
|
582
|
+
gatewayLogFile
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export async function signedRelayContext(args, { searchRoots = defaultGatewaySearchRoots() } = {}) {
|
|
587
|
+
const apiBase = clean(args['api-base']) || 'https://api.agentsquared.net'
|
|
588
|
+
const context = resolveAgentContext(args, { searchRoots })
|
|
589
|
+
const agentId = context.agentId
|
|
590
|
+
const keyFile = context.keyFile
|
|
591
|
+
const bundle = loadRuntimeKeyBundle(keyFile)
|
|
592
|
+
const { gatewayBase, health, transport } = await resolveGatewayTransport(args, searchRoots)
|
|
593
|
+
return {
|
|
594
|
+
apiBase,
|
|
595
|
+
agentId,
|
|
596
|
+
keyFile,
|
|
597
|
+
bundle,
|
|
598
|
+
gatewayBase,
|
|
599
|
+
gatewayHealth: health,
|
|
600
|
+
transport
|
|
601
|
+
}
|
|
602
|
+
}
|