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