@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.
@@ -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
+ }