@hypercli.com/sdk 0.9.1

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.
Files changed (193) hide show
  1. package/README.md +629 -0
  2. package/dist/acp-driver.d.ts +72 -0
  3. package/dist/acp-driver.d.ts.map +1 -0
  4. package/dist/acp-driver.js +147 -0
  5. package/dist/acp-driver.js.map +1 -0
  6. package/dist/acp-pool.d.ts +70 -0
  7. package/dist/acp-pool.d.ts.map +1 -0
  8. package/dist/acp-pool.js +181 -0
  9. package/dist/acp-pool.js.map +1 -0
  10. package/dist/acp.d.ts +179 -0
  11. package/dist/acp.d.ts.map +1 -0
  12. package/dist/acp.js +487 -0
  13. package/dist/acp.js.map +1 -0
  14. package/dist/agent-persona.d.ts +68 -0
  15. package/dist/agent-persona.d.ts.map +1 -0
  16. package/dist/agent-persona.js +78 -0
  17. package/dist/agent-persona.js.map +1 -0
  18. package/dist/agent-slots.d.ts +18 -0
  19. package/dist/agent-slots.d.ts.map +1 -0
  20. package/dist/agent-slots.js +24 -0
  21. package/dist/agent-slots.js.map +1 -0
  22. package/dist/agent-urls.d.ts +47 -0
  23. package/dist/agent-urls.d.ts.map +1 -0
  24. package/dist/agent-urls.js +178 -0
  25. package/dist/agent-urls.js.map +1 -0
  26. package/dist/agent.d.ts +482 -0
  27. package/dist/agent.d.ts.map +1 -0
  28. package/dist/agent.js +926 -0
  29. package/dist/agent.js.map +1 -0
  30. package/dist/agents.d.ts +1452 -0
  31. package/dist/agents.d.ts.map +1 -0
  32. package/dist/agents.js +4022 -0
  33. package/dist/agents.js.map +1 -0
  34. package/dist/billing.d.ts +72 -0
  35. package/dist/billing.d.ts.map +1 -0
  36. package/dist/billing.js +93 -0
  37. package/dist/billing.js.map +1 -0
  38. package/dist/browser-jobs.d.ts +24 -0
  39. package/dist/browser-jobs.d.ts.map +1 -0
  40. package/dist/browser-jobs.js +209 -0
  41. package/dist/browser-jobs.js.map +1 -0
  42. package/dist/browser.d.ts +41 -0
  43. package/dist/browser.d.ts.map +1 -0
  44. package/dist/browser.js +57 -0
  45. package/dist/browser.js.map +1 -0
  46. package/dist/buzz-activity.d.ts +169 -0
  47. package/dist/buzz-activity.d.ts.map +1 -0
  48. package/dist/buzz-activity.js +844 -0
  49. package/dist/buzz-activity.js.map +1 -0
  50. package/dist/channels.d.ts +99 -0
  51. package/dist/channels.d.ts.map +1 -0
  52. package/dist/channels.js +27 -0
  53. package/dist/channels.js.map +1 -0
  54. package/dist/chat-integrations.d.ts +44 -0
  55. package/dist/chat-integrations.d.ts.map +1 -0
  56. package/dist/chat-integrations.js +138 -0
  57. package/dist/chat-integrations.js.map +1 -0
  58. package/dist/client.d.ts +88 -0
  59. package/dist/client.d.ts.map +1 -0
  60. package/dist/client.js +144 -0
  61. package/dist/client.js.map +1 -0
  62. package/dist/config.d.ts +44 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +192 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/connectors.d.ts +80 -0
  67. package/dist/connectors.d.ts.map +1 -0
  68. package/dist/connectors.js +2 -0
  69. package/dist/connectors.js.map +1 -0
  70. package/dist/errors.d.ts +16 -0
  71. package/dist/errors.d.ts.map +1 -0
  72. package/dist/errors.js +26 -0
  73. package/dist/errors.js.map +1 -0
  74. package/dist/files.d.ts +60 -0
  75. package/dist/files.d.ts.map +1 -0
  76. package/dist/files.js +128 -0
  77. package/dist/files.js.map +1 -0
  78. package/dist/http.d.ts +56 -0
  79. package/dist/http.d.ts.map +1 -0
  80. package/dist/http.js +362 -0
  81. package/dist/http.js.map +1 -0
  82. package/dist/index.d.ts +49 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +64 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/instances.d.ts +94 -0
  87. package/dist/instances.d.ts.map +1 -0
  88. package/dist/instances.js +197 -0
  89. package/dist/instances.js.map +1 -0
  90. package/dist/integrations.d.ts +111 -0
  91. package/dist/integrations.d.ts.map +1 -0
  92. package/dist/integrations.js +206 -0
  93. package/dist/integrations.js.map +1 -0
  94. package/dist/job/base.d.ts +86 -0
  95. package/dist/job/base.d.ts.map +1 -0
  96. package/dist/job/base.js +206 -0
  97. package/dist/job/base.js.map +1 -0
  98. package/dist/job/comfyui.d.ts +259 -0
  99. package/dist/job/comfyui.d.ts.map +1 -0
  100. package/dist/job/comfyui.js +1563 -0
  101. package/dist/job/comfyui.js.map +1 -0
  102. package/dist/job/gradio.d.ts +48 -0
  103. package/dist/job/gradio.d.ts.map +1 -0
  104. package/dist/job/gradio.js +90 -0
  105. package/dist/job/gradio.js.map +1 -0
  106. package/dist/job-codecs.d.ts +66 -0
  107. package/dist/job-codecs.d.ts.map +1 -0
  108. package/dist/job-codecs.js +132 -0
  109. package/dist/job-codecs.js.map +1 -0
  110. package/dist/jobs.d.ts +148 -0
  111. package/dist/jobs.d.ts.map +1 -0
  112. package/dist/jobs.js +480 -0
  113. package/dist/jobs.js.map +1 -0
  114. package/dist/keys.d.ts +99 -0
  115. package/dist/keys.d.ts.map +1 -0
  116. package/dist/keys.js +145 -0
  117. package/dist/keys.js.map +1 -0
  118. package/dist/logs.d.ts +54 -0
  119. package/dist/logs.d.ts.map +1 -0
  120. package/dist/logs.js +256 -0
  121. package/dist/logs.js.map +1 -0
  122. package/dist/managed-context.d.ts +20 -0
  123. package/dist/managed-context.d.ts.map +1 -0
  124. package/dist/managed-context.js +95 -0
  125. package/dist/managed-context.js.map +1 -0
  126. package/dist/mcp.d.ts +7 -0
  127. package/dist/mcp.d.ts.map +1 -0
  128. package/dist/mcp.js +8 -0
  129. package/dist/mcp.js.map +1 -0
  130. package/dist/memory.d.ts +87 -0
  131. package/dist/memory.d.ts.map +1 -0
  132. package/dist/memory.js +79 -0
  133. package/dist/memory.js.map +1 -0
  134. package/dist/models.d.ts +15 -0
  135. package/dist/models.d.ts.map +1 -0
  136. package/dist/models.js +19 -0
  137. package/dist/models.js.map +1 -0
  138. package/dist/openclaw-control-ui-origin.d.ts +23 -0
  139. package/dist/openclaw-control-ui-origin.d.ts.map +1 -0
  140. package/dist/openclaw-control-ui-origin.js +53 -0
  141. package/dist/openclaw-control-ui-origin.js.map +1 -0
  142. package/dist/renders.d.ts +187 -0
  143. package/dist/renders.d.ts.map +1 -0
  144. package/dist/renders.js +360 -0
  145. package/dist/renders.js.map +1 -0
  146. package/dist/routines.d.ts +59 -0
  147. package/dist/routines.d.ts.map +1 -0
  148. package/dist/routines.js +135 -0
  149. package/dist/routines.js.map +1 -0
  150. package/dist/runners.d.ts +61 -0
  151. package/dist/runners.d.ts.map +1 -0
  152. package/dist/runners.js +127 -0
  153. package/dist/runners.js.map +1 -0
  154. package/dist/sessions.d.ts +280 -0
  155. package/dist/sessions.d.ts.map +1 -0
  156. package/dist/sessions.js +292 -0
  157. package/dist/sessions.js.map +1 -0
  158. package/dist/skills.d.ts +170 -0
  159. package/dist/skills.d.ts.map +1 -0
  160. package/dist/skills.js +2 -0
  161. package/dist/skills.js.map +1 -0
  162. package/dist/user.d.ts +78 -0
  163. package/dist/user.d.ts.map +1 -0
  164. package/dist/user.js +143 -0
  165. package/dist/user.js.map +1 -0
  166. package/dist/version.d.ts +2 -0
  167. package/dist/version.d.ts.map +1 -0
  168. package/dist/version.js +3 -0
  169. package/dist/version.js.map +1 -0
  170. package/dist/voice-session.d.ts +66 -0
  171. package/dist/voice-session.d.ts.map +1 -0
  172. package/dist/voice-session.js +366 -0
  173. package/dist/voice-session.js.map +1 -0
  174. package/dist/voice-transcription-session.d.ts +64 -0
  175. package/dist/voice-transcription-session.d.ts.map +1 -0
  176. package/dist/voice-transcription-session.js +228 -0
  177. package/dist/voice-transcription-session.js.map +1 -0
  178. package/dist/voice.d.ts +62 -0
  179. package/dist/voice.d.ts.map +1 -0
  180. package/dist/voice.js +134 -0
  181. package/dist/voice.js.map +1 -0
  182. package/dist/workspace-tomd.d.ts +61 -0
  183. package/dist/workspace-tomd.d.ts.map +1 -0
  184. package/dist/workspace-tomd.js +177 -0
  185. package/dist/workspace-tomd.js.map +1 -0
  186. package/dist/workspaces.d.ts +255 -0
  187. package/dist/workspaces.d.ts.map +1 -0
  188. package/dist/workspaces.js +653 -0
  189. package/dist/workspaces.js.map +1 -0
  190. package/package.json +115 -0
  191. package/runtime/config-require.browser.js +4 -0
  192. package/runtime/config-require.d.ts +2 -0
  193. package/runtime/config-require.node.js +8 -0
@@ -0,0 +1,844 @@
1
+ /**
2
+ * Buzz-activity transport: subscribe to a Buzz-backed agent's observer
3
+ * telemetry directly from its Nostr relay.
4
+ *
5
+ * Buzz-backed HyperCLI deployments run `hyper-acp` with Buzz credentials, which publishes observer
6
+ * frames as kind-24200 events signed by the agent key, with the content
7
+ * NIP-44-v2-encrypted to the owner pubkey. The SDK subscribes to the relay
8
+ * over a plain WebSocket and decrypts locally; no backend round-trips are
9
+ * involved beyond revealing the agent key.
10
+ *
11
+ * NIP-44 conversation keys are symmetric:
12
+ * `getConversationKey(ownerSecret, agentPub) === getConversationKey(agentSecret, ownerPub)`,
13
+ * so decryption works with either the owner's nsec or the agent's
14
+ * `BUZZ_PRIVATE_KEY` secret.
15
+ */
16
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
17
+ import { extract as hkdfExtract, expand as hkdfExpand } from '@noble/hashes/hkdf.js';
18
+ import { hmac } from '@noble/hashes/hmac.js';
19
+ import { sha256 } from '@noble/hashes/sha2.js';
20
+ import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
21
+ import { chacha20 } from '@noble/ciphers/chacha.js';
22
+ import { equalBytes } from '@noble/ciphers/utils.js';
23
+ import { base64, bech32 } from '@scure/base';
24
+ import { preferredWebSocket } from './agents.js';
25
+ export const BUZZ_OBSERVER_EVENT_KIND = 24200;
26
+ const NIP44_MIN_PAYLOAD_LEN = 99;
27
+ const NIP44_MAX_PAYLOAD_LEN = 87_472;
28
+ const NIP44_EXTENDED_PREFIX_THRESHOLD = 0x1_00_00;
29
+ const DEDUP_CAP = 4_096;
30
+ const RECONNECT_DELAYS_MS = [1_000, 2_000, 4_000];
31
+ function isPlainRecord(value) {
32
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ }
34
+ function isHex64(value) {
35
+ return /^[0-9a-f]{64}$/.test(value);
36
+ }
37
+ /**
38
+ * Decode an agent/owner secret given as an nsec bech32 string, a 64-char hex
39
+ * string, or a raw 32-byte array. Throws on anything else.
40
+ */
41
+ export function decodeBuzzSecret(nsecOrHex) {
42
+ let bytes;
43
+ if (nsecOrHex instanceof Uint8Array) {
44
+ bytes = nsecOrHex;
45
+ }
46
+ else if (typeof nsecOrHex === 'string') {
47
+ const trimmed = nsecOrHex.trim();
48
+ if (isHex64(trimmed)) {
49
+ bytes = hexToBytes(trimmed);
50
+ }
51
+ else if (trimmed.startsWith('nsec1')) {
52
+ let decoded;
53
+ try {
54
+ decoded = bech32.decode(trimmed, 1_000);
55
+ }
56
+ catch (error) {
57
+ throw new Error(`invalid nsec bech32: ${error.message}`);
58
+ }
59
+ if (decoded.prefix !== 'nsec') {
60
+ throw new Error(`expected an nsec bech32 string, got prefix "${decoded.prefix}"`);
61
+ }
62
+ bytes = new Uint8Array(bech32.fromWords(decoded.words));
63
+ }
64
+ else {
65
+ throw new Error('secret key must be an nsec bech32 string or 64-char hex');
66
+ }
67
+ }
68
+ else {
69
+ throw new Error('secret key must be an nsec/hex string or raw bytes');
70
+ }
71
+ if (bytes.length !== 32 || !secp256k1.utils.isValidPrivateKey(bytes)) {
72
+ throw new Error('secret key must be a valid 32-byte secp256k1 scalar');
73
+ }
74
+ return bytes;
75
+ }
76
+ /** Derive the x-only hex pubkey for a 32-byte secret key. */
77
+ export function buzzPublicKeyHex(secretKey) {
78
+ return bytesToHex(secp256k1.getPublicKey(secretKey, true).subarray(1, 33));
79
+ }
80
+ /**
81
+ * NIP-44 v2 conversation key: HKDF-extract(salt "nip44-v2", ECDH shared x).
82
+ * Symmetric across the (owner, agent) pair.
83
+ */
84
+ export function buzzConversationKey(secretKey, peerPubHex) {
85
+ if (!isHex64(peerPubHex)) {
86
+ throw new Error('peer pubkey must be 64-char hex');
87
+ }
88
+ const sharedX = secp256k1.getSharedSecret(secretKey, hexToBytes(`02${peerPubHex}`), true).subarray(1, 33);
89
+ return hkdfExtract(sha256, sharedX, utf8ToBytes('nip44-v2'));
90
+ }
91
+ function nip44MessageKeys(conversationKey, nonce) {
92
+ const keys = hkdfExpand(sha256, conversationKey, nonce, 76);
93
+ return {
94
+ chachaKey: keys.subarray(0, 32),
95
+ chachaNonce: keys.subarray(32, 44),
96
+ hmacKey: keys.subarray(44, 76),
97
+ };
98
+ }
99
+ function calcPaddedLen(len) {
100
+ if (!Number.isSafeInteger(len) || len < 1)
101
+ throw new Error('expected positive integer');
102
+ if (len <= 32)
103
+ return 32;
104
+ const nextPower = 2 ** (Math.floor(Math.log2(len - 1)) + 1);
105
+ const chunk = nextPower <= 256 ? 32 : nextPower / 8;
106
+ return chunk * (Math.floor((len - 1) / chunk) + 1);
107
+ }
108
+ function unpadNip44(padded) {
109
+ if (padded.length < 2)
110
+ throw new Error('invalid padding');
111
+ const view = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);
112
+ const firstTwo = view.getUint16(0);
113
+ let unpaddedLen;
114
+ let prefixLen;
115
+ if (firstTwo === 0) {
116
+ unpaddedLen = view.getUint32(2);
117
+ if (unpaddedLen < NIP44_EXTENDED_PREFIX_THRESHOLD)
118
+ throw new Error('invalid padding');
119
+ prefixLen = 6;
120
+ }
121
+ else {
122
+ unpaddedLen = firstTwo;
123
+ prefixLen = 2;
124
+ }
125
+ const unpadded = padded.subarray(prefixLen, prefixLen + unpaddedLen);
126
+ if (unpadded.length !== unpaddedLen
127
+ || padded.length !== prefixLen + calcPaddedLen(unpaddedLen)) {
128
+ throw new Error('invalid padding');
129
+ }
130
+ return unpadded;
131
+ }
132
+ /**
133
+ * Decrypt a NIP-44 v2 payload: base64 of [version=2][nonce32][ciphertext][mac32].
134
+ * Rejects non-v2 versions, bad MACs, bad lengths, and malformed padding.
135
+ */
136
+ export function decryptBuzzPayload(conversationKey, payload) {
137
+ if (typeof payload !== 'string')
138
+ throw new Error('payload must be a string');
139
+ let data;
140
+ try {
141
+ data = base64.decode(payload);
142
+ }
143
+ catch (error) {
144
+ throw new Error(`invalid base64 payload: ${error.message}`);
145
+ }
146
+ if (data.length < NIP44_MIN_PAYLOAD_LEN || data.length > NIP44_MAX_PAYLOAD_LEN) {
147
+ throw new Error(`invalid payload length: ${data.length}`);
148
+ }
149
+ if (data[0] !== 2) {
150
+ throw new Error(`unsupported payload version: ${data[0]}`);
151
+ }
152
+ const nonce = data.subarray(1, 33);
153
+ const ciphertext = data.subarray(33, data.length - 32);
154
+ const mac = data.subarray(data.length - 32);
155
+ const { chachaKey, chachaNonce, hmacKey } = nip44MessageKeys(conversationKey, nonce);
156
+ const expectedMac = hmac(sha256, hmacKey, concatBytes(nonce, ciphertext));
157
+ if (!equalBytes(expectedMac, mac)) {
158
+ throw new Error('invalid MAC');
159
+ }
160
+ const padded = chacha20(chachaKey, chachaNonce, ciphertext);
161
+ return new TextDecoder().decode(unpadNip44(padded));
162
+ }
163
+ function readEnvString(env, key) {
164
+ const value = env[key];
165
+ return typeof value === 'string' && value.length > 0 ? value : null;
166
+ }
167
+ function ownerFromAuthTag(authTag) {
168
+ let parsed;
169
+ try {
170
+ parsed = JSON.parse(authTag);
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ if (!Array.isArray(parsed) || parsed.length < 2 || parsed[0] !== 'auth')
176
+ return null;
177
+ const candidate = parsed[1];
178
+ return typeof candidate === 'string' && isHex64(candidate) ? candidate : null;
179
+ }
180
+ /**
181
+ * Resolve the Buzz relay URL and owner pubkey from a launch-config env map.
182
+ * Owner comes from `BUZZ_AUTH_TAG` (JSON `["auth", owner_pubkey_hex, ...]`,
183
+ * element [1]) with `BUZZ_ACP_AGENT_OWNER` (bare 64-hex) as fallback.
184
+ * Throws a clear "not Buzz-backed" error when no relay URL is present.
185
+ */
186
+ export function resolveBuzzOwnerFromEnv(env) {
187
+ const relayUrl = readEnvString(env, 'BUZZ_RELAY_URL');
188
+ if (!relayUrl) {
189
+ throw new Error('Agent is not Buzz-backed: launchConfig env has no BUZZ_RELAY_URL; ' +
190
+ 'observer activity is only published by Buzz deployments');
191
+ }
192
+ const authTag = readEnvString(env, 'BUZZ_AUTH_TAG');
193
+ const fromTag = authTag ? ownerFromAuthTag(authTag) : null;
194
+ const fallback = readEnvString(env, 'BUZZ_ACP_AGENT_OWNER');
195
+ const ownerPubHex = fromTag ?? (fallback && isHex64(fallback) ? fallback : null);
196
+ if (!ownerPubHex) {
197
+ throw new Error('Buzz owner pubkey not found: BUZZ_AUTH_TAG is missing/malformed and ' +
198
+ 'BUZZ_ACP_AGENT_OWNER is not a 64-char hex pubkey');
199
+ }
200
+ return { relayUrl, ownerPubHex };
201
+ }
202
+ function launchConfigEnv(launchConfig) {
203
+ if (!isPlainRecord(launchConfig))
204
+ return {};
205
+ const env = launchConfig.env;
206
+ return isPlainRecord(env) ? env : {};
207
+ }
208
+ function optionalString(value) {
209
+ return typeof value === 'string' && value.length > 0 ? value : null;
210
+ }
211
+ function toObserverFrame(value) {
212
+ if (!isPlainRecord(value))
213
+ return null;
214
+ const seq = value.seq;
215
+ const timestamp = value.timestamp;
216
+ const kind = value.kind;
217
+ if (typeof seq !== 'number' || !Number.isFinite(seq))
218
+ return null;
219
+ if (typeof timestamp !== 'string' || timestamp.length === 0)
220
+ return null;
221
+ if (typeof kind !== 'string' || kind.length === 0)
222
+ return null;
223
+ const agentIndex = value.agentIndex ?? value.agent_index;
224
+ const frame = {
225
+ seq,
226
+ timestamp,
227
+ kind,
228
+ agentIndex: typeof agentIndex === 'number' && Number.isInteger(agentIndex) ? agentIndex : null,
229
+ channelId: optionalString(value.channelId ?? value.channel_id),
230
+ sessionId: optionalString(value.sessionId ?? value.session_id),
231
+ turnId: optionalString(value.turnId ?? value.turn_id),
232
+ payload: value.payload,
233
+ };
234
+ const startedAt = optionalString(value.startedAt ?? value.started_at);
235
+ if (startedAt !== null)
236
+ frame.startedAt = startedAt;
237
+ return frame;
238
+ }
239
+ function hasTag(tags, name, value) {
240
+ if (!Array.isArray(tags))
241
+ return false;
242
+ return tags.some((tag) => Array.isArray(tag) && tag[0] === name && tag[1] === value);
243
+ }
244
+ /**
245
+ * Shared deduper for both transports: drop frames already seen, keyed on
246
+ * (timestamp, seq). Bounded memory — the oldest key is evicted past
247
+ * DEDUP_CAP. Replay/live overlap happens on every (re)connect, so this is
248
+ * load-bearing, not defensive.
249
+ */
250
+ function createBuzzActivityDeduper() {
251
+ const seen = new Map();
252
+ return (frame) => {
253
+ const dedupKey = `${frame.timestamp.length}:${frame.timestamp}:${frame.seq}`;
254
+ if (seen.has(dedupKey))
255
+ return false;
256
+ seen.set(dedupKey, true);
257
+ if (seen.size > DEDUP_CAP) {
258
+ const oldest = seen.keys().next().value;
259
+ if (oldest !== undefined)
260
+ seen.delete(oldest);
261
+ }
262
+ return true;
263
+ };
264
+ }
265
+ /**
266
+ * Shared frame tolerance: unwrap `batch` envelopes, drop (but never fail on)
267
+ * unparseable frames. Used by the relay transport after decryption and by the
268
+ * route transport after JSON.parse — the payloads are the same shape.
269
+ */
270
+ function emitObserverPayload(parsed, deliver, countDrop) {
271
+ const frame = toObserverFrame(parsed);
272
+ const payload = frame?.payload;
273
+ if (frame?.kind === 'batch' && isPlainRecord(payload) && Array.isArray(payload.events)) {
274
+ // Batch envelope: unwrap each inner event. A malformed batch (no events
275
+ // array) falls through and is emitted as-is.
276
+ for (const inner of payload.events) {
277
+ const innerFrame = toObserverFrame(inner);
278
+ if (!innerFrame) {
279
+ countDrop();
280
+ continue;
281
+ }
282
+ deliver(innerFrame);
283
+ }
284
+ return;
285
+ }
286
+ if (!frame) {
287
+ countDrop();
288
+ return;
289
+ }
290
+ deliver(frame);
291
+ }
292
+ /**
293
+ * Subscribe to a Buzz-backed agent's observer telemetry stream.
294
+ *
295
+ * Resolves the agent, reads `BUZZ_RELAY_URL`/owner pubkey from its
296
+ * launch-config env, reveals the agent `BUZZ_PRIVATE_KEY`, then REQ-subscribes
297
+ * the relay for kind-24200 frames and decrypts them locally. Reconnects with
298
+ * bounded backoff (1s/2s/4s) resuming from the last accepted frame; gives up
299
+ * via `onClose`.
300
+ */
301
+ export async function subscribeBuzzActivity(deployments, agentIdOrName, handlers) {
302
+ const agent = await deployments.get(agentIdOrName);
303
+ const env = launchConfigEnv(agent.launchConfig);
304
+ let relayUrl;
305
+ let ownerPubHex;
306
+ try {
307
+ ({ relayUrl, ownerPubHex } = resolveBuzzOwnerFromEnv(env));
308
+ }
309
+ catch (error) {
310
+ // Agents launched after BUZZ_AUTH_TAG moved from the launch-config env
311
+ // into the redacted secrets projection no longer carry the attestation
312
+ // here. The "not Buzz-backed" failure (no relay URL) is terminal; only
313
+ // the missing-owner case can be recovered by revealing the secret.
314
+ if (error instanceof Error && error.message.startsWith('Agent is not Buzz-backed')) {
315
+ throw error;
316
+ }
317
+ relayUrl = readEnvString(env, 'BUZZ_RELAY_URL') ?? '';
318
+ if (!relayUrl)
319
+ throw error;
320
+ const tagResponse = await deployments
321
+ .secret(agentIdOrName, 'BUZZ_AUTH_TAG')
322
+ .catch(() => null);
323
+ const tag = tagResponse
324
+ ? typeof tagResponse.value === 'string'
325
+ ? tagResponse.value
326
+ : new TextDecoder().decode(tagResponse.value)
327
+ : null;
328
+ ownerPubHex = tag ? ownerFromAuthTag(tag) : null;
329
+ if (!ownerPubHex)
330
+ throw error;
331
+ }
332
+ // The agent secret is revealed in both modes: even when the caller supplies
333
+ // an owner key for decryption, the agent pubkey is required for the REQ
334
+ // `authors` filter and for per-event sender validation.
335
+ const agentSecretResponse = await deployments.secret(agentIdOrName, 'BUZZ_PRIVATE_KEY');
336
+ const agentSecret = decodeBuzzSecret(agentSecretResponse.value);
337
+ const agentPubHex = buzzPublicKeyHex(agentSecret);
338
+ const conversationKey = handlers.ownerSecretKey !== undefined
339
+ ? buzzConversationKey(decodeBuzzSecret(handlers.ownerSecretKey), agentPubHex)
340
+ : buzzConversationKey(agentSecret, ownerPubHex);
341
+ const sinceSeconds = handlers.sinceSeconds ?? 86_400;
342
+ const limit = handlers.limit ?? 500;
343
+ let closed = false;
344
+ let ws = null;
345
+ let reconnectTimer = null;
346
+ let failures = 0;
347
+ let historyEndFired = false;
348
+ let lastFrameSince = null;
349
+ let connectionCounter = 0;
350
+ const isNewFrame = createBuzzActivityDeduper();
351
+ const onAbort = () => close();
352
+ function close() {
353
+ if (closed)
354
+ return;
355
+ closed = true;
356
+ handlers.signal?.removeEventListener('abort', onAbort);
357
+ if (reconnectTimer !== null) {
358
+ clearTimeout(reconnectTimer);
359
+ reconnectTimer = null;
360
+ }
361
+ const current = ws;
362
+ ws = null;
363
+ if (current) {
364
+ current.onopen = null;
365
+ current.onmessage = null;
366
+ current.onerror = null;
367
+ current.onclose = null;
368
+ try {
369
+ current.close();
370
+ }
371
+ catch {
372
+ // A socket already closed by the peer needs no local close.
373
+ }
374
+ }
375
+ }
376
+ function deliver(frame) {
377
+ if (!isNewFrame(frame))
378
+ return;
379
+ const parsed = Date.parse(frame.timestamp);
380
+ if (!Number.isNaN(parsed)) {
381
+ const seconds = Math.floor(parsed / 1_000);
382
+ if (lastFrameSince === null || seconds > lastFrameSince)
383
+ lastFrameSince = seconds;
384
+ }
385
+ handlers.onFrame(frame);
386
+ }
387
+ function emitDecrypted(plaintext) {
388
+ let parsed;
389
+ try {
390
+ parsed = JSON.parse(plaintext);
391
+ }
392
+ catch {
393
+ // Unparseable plaintext drops silently; it never kills the subscription.
394
+ return;
395
+ }
396
+ emitObserverPayload(parsed, deliver, () => {
397
+ // Unparseable observer frames drop silently, matching the route
398
+ // transport; malformed frames never kill the subscription.
399
+ });
400
+ }
401
+ function handleEvent(envelope, subId) {
402
+ if (envelope[1] !== subId)
403
+ return;
404
+ const event = envelope[2];
405
+ if (!isPlainRecord(event))
406
+ return;
407
+ if (event.kind !== BUZZ_OBSERVER_EVENT_KIND)
408
+ return;
409
+ if (event.pubkey !== agentPubHex)
410
+ return;
411
+ if (typeof event.content !== 'string')
412
+ return;
413
+ if (!hasTag(event.tags, 'agent', agentPubHex))
414
+ return;
415
+ if (!hasTag(event.tags, 'frame', 'telemetry'))
416
+ return;
417
+ let plaintext;
418
+ try {
419
+ plaintext = decryptBuzzPayload(conversationKey, event.content);
420
+ }
421
+ catch {
422
+ // Decryption failures skip the frame; they never kill the subscription.
423
+ return;
424
+ }
425
+ emitDecrypted(plaintext);
426
+ }
427
+ function connect() {
428
+ if (closed)
429
+ return;
430
+ connectionCounter += 1;
431
+ const subId = `buzz-activity-${connectionCounter}`;
432
+ const WebSocketImpl = preferredWebSocket();
433
+ const socket = new WebSocketImpl(relayUrl);
434
+ ws = socket;
435
+ socket.onopen = () => {
436
+ const since = lastFrameSince ?? Math.floor(Date.now() / 1_000) - sinceSeconds;
437
+ const filter = {
438
+ kinds: [BUZZ_OBSERVER_EVENT_KIND],
439
+ authors: [agentPubHex],
440
+ '#p': [ownerPubHex],
441
+ since,
442
+ limit,
443
+ };
444
+ socket.send(JSON.stringify(['REQ', subId, filter]));
445
+ };
446
+ socket.onmessage = (message) => {
447
+ const raw = typeof message.data === 'string'
448
+ ? message.data
449
+ : message.data instanceof Uint8Array
450
+ ? new TextDecoder().decode(message.data)
451
+ : '';
452
+ if (!raw)
453
+ return;
454
+ let envelope;
455
+ try {
456
+ envelope = JSON.parse(raw);
457
+ }
458
+ catch {
459
+ return;
460
+ }
461
+ if (!Array.isArray(envelope))
462
+ return;
463
+ switch (envelope[0]) {
464
+ case 'EVENT':
465
+ handleEvent(envelope, subId);
466
+ return;
467
+ case 'EOSE':
468
+ // A relay that served history is healthy; reset the backoff budget.
469
+ failures = 0;
470
+ if (!historyEndFired) {
471
+ historyEndFired = true;
472
+ handlers.onHistoryEnd?.();
473
+ }
474
+ return;
475
+ case 'NOTICE':
476
+ handlers.onError?.(new Error(`Buzz relay notice: ${String(envelope[1] ?? '')}`));
477
+ return;
478
+ default:
479
+ return;
480
+ }
481
+ };
482
+ socket.onerror = () => {
483
+ handlers.onError?.(new Error('Buzz relay WebSocket error'));
484
+ };
485
+ socket.onclose = (event) => {
486
+ if (closed)
487
+ return;
488
+ ws = null;
489
+ const code = event?.code ?? 1006;
490
+ const reason = event?.reason ?? '';
491
+ if (failures >= RECONNECT_DELAYS_MS.length) {
492
+ close();
493
+ handlers.onClose?.({ code, reason });
494
+ return;
495
+ }
496
+ const delay = RECONNECT_DELAYS_MS[failures];
497
+ failures += 1;
498
+ reconnectTimer = setTimeout(() => {
499
+ reconnectTimer = null;
500
+ connect();
501
+ }, delay);
502
+ };
503
+ }
504
+ if (handlers.signal?.aborted) {
505
+ return { close };
506
+ }
507
+ handlers.signal?.addEventListener('abort', onAbort, { once: true });
508
+ connect();
509
+ return { close };
510
+ }
511
+ // ── Legacy route transport: former hyper-acp read-only activity stream. ──
512
+ // ── app-level token auth ──
513
+ /**
514
+ * Pinned route name for the legacy hyper-acp introspection listener. Current
515
+ * hosted Buzz launches use raw outbound ACP over `HYPER_ACP_WS_URL` and do not
516
+ * provision this route.
517
+ */
518
+ export const HYPER_ACP_ROUTE_NAME = 'hyper-acp';
519
+ /** Thrown when the agent's launch config has no `hyper-acp` edge route. */
520
+ export class BuzzActivityRouteUnavailableError extends Error {
521
+ constructor(message) {
522
+ super(message);
523
+ this.name = 'BuzzActivityRouteUnavailableError';
524
+ }
525
+ }
526
+ /**
527
+ * Soft-signal error emitted via `onError` when the in-pod stream skipped a
528
+ * lagging client ahead. Transient by definition — the subscription is alive.
529
+ */
530
+ export class BuzzActivityGapError extends Error {
531
+ dropped;
532
+ constructor(dropped) {
533
+ super(`buzz activity stream skipped ${dropped} events after client lag`);
534
+ this.name = 'BuzzActivityGapError';
535
+ this.dropped = dropped;
536
+ }
537
+ }
538
+ /**
539
+ * Legacy deployment secret for the retired read-only route. Current hosted
540
+ * Buzz launches do not create this secret; callers should prefer the relay
541
+ * activity subscription or the raw outbound ACP launch contract.
542
+ */
543
+ const BUZZ_ACTIVITY_WS_TOKEN_SECRET = 'HYPER_ACP_WS_TOKEN';
544
+ /**
545
+ * Resolve the `hyper-acp` edge URL from an already-loaded Agent model.
546
+ * Host derivation mirrors the platform contract: prefix defaults to the route
547
+ * name on both sides, so `prefix` is `${prefix}-${agent.hostname}` and a bare
548
+ * prefix is the root host itself. Returns null when the route is absent or
549
+ * the agent hostname is not ready — callers use that to select the relay
550
+ * transport instead.
551
+ */
552
+ export function resolveBuzzActivityRouteTarget(agent) {
553
+ const route = agent.routes?.[HYPER_ACP_ROUTE_NAME];
554
+ if (!route || typeof route.port !== 'number')
555
+ return null;
556
+ const hostname = String(agent.hostname ?? '').trim().replace(/\.$/, '');
557
+ if (!hostname)
558
+ return null;
559
+ const prefix = String(route.prefix ?? HYPER_ACP_ROUTE_NAME).trim();
560
+ const host = prefix ? `${prefix}-${hostname}` : hostname;
561
+ return { wsUrl: `wss://${host}/`, port: route.port };
562
+ }
563
+ /**
564
+ * Open the route WebSocket. Uniform in every runtime: the app token travels
565
+ * in-band as the client's first text frame after the upgrade, so neither
566
+ * browser cookies nor ws-package request headers are involved. (The bare
567
+ * `ws`-constructor URL also sidesteps Node-undici's WebSocket quirks.)
568
+ */
569
+ function openBuzzActivityRouteSocket(wsUrl) {
570
+ const WebSocketImpl = preferredWebSocket();
571
+ return new WebSocketImpl(wsUrl);
572
+ }
573
+ /**
574
+ * Subscribe to a legacy Buzz-backed agent's observer stream through the
575
+ * agent's `hyper-acp` WS route. Current hosted Buzz launches do not provision
576
+ * this observer route; the active path uses raw outbound ACP over
577
+ * `HYPER_ACP_WS_URL`.
578
+ *
579
+ * Auth is app-level and entirely in-band: the route is `auth: false` at the
580
+ * edge, so no platform JWT, cookie priming, or header tricks are involved.
581
+ * The token is the deployment-provisioned `HYPER_ACP_WS_TOKEN`; each
582
+ * connect attempt reveals the secret through the OpenClaw-style per-secret
583
+ * endpoint — fresh per attempt, with a launch-epoch freshness guard
584
+ * mirroring the OpenClaw gateway flow. After the WebSocket upgrade the client's FIRST text
585
+ * frame is exactly `{"type":"auth","token":...}`; the server answers exactly
586
+ * one `{"type":"auth_ok"}` text frame and then replays. A close with code
587
+ * 4401 before `auth_ok` means the token was rejected: terminal for the
588
+ * subscription — the retry loop is skipped and `onClose` fires immediately
589
+ * with that code/reason (consumers treat 4401 as no-reconnect).
590
+ *
591
+ * Token-fetch failures on the FIRST attempt (empty value, stale launch
592
+ * epoch, or a `secret()` APIError 401/403/404) reject this promise as-is —
593
+ * they are never converted to {@link BuzzActivityRouteUnavailableError}. On
594
+ * a mid-life reconnect they surface via `onError` and end the subscription
595
+ * with a terminal `onClose`.
596
+ *
597
+ * A recognizable protocol message (`replay_end`, `replay_gap`, or an
598
+ * observer-shaped frame) arriving BEFORE `auth_ok` is a protocol violation
599
+ * — the signature of a skewed deployment (backend secret present, pod
600
+ * missing the token). It surfaces exactly one `onError` per subscription
601
+ * and never touches the retry budget, history-end, or dedup state (so a
602
+ * wedged stream cannot look healthy); the socket stays open so a healed pod
603
+ * recovers on its own.
604
+ *
605
+ * Reconnects with bounded backoff (1s/2s/4s). The server's session log
606
+ * re-plays from disk on reconnect; overlap dedup on (timestamp, seq) makes
607
+ * duplicates a no-op. A served `replay_end` also resets the retry budget,
608
+ * the route analogue of the relay transport's EOSE reset. A
609
+ * `{"type":"replay_gap"}` frame surfaces as a soft
610
+ * {@link BuzzActivityGapError}; the stream stays alive.
611
+ */
612
+ export async function subscribeBuzzActivityRoute(deployments, agentIdOrName, handlers) {
613
+ const agent = await deployments.get(agentIdOrName);
614
+ const target = resolveBuzzActivityRouteTarget(agent);
615
+ if (!target) {
616
+ throw new BuzzActivityRouteUnavailableError(`agent has no ${HYPER_ACP_ROUTE_NAME} route`);
617
+ }
618
+ // Hoisted function declarations below lose control-flow narrowing, so bind
619
+ // the post-guard values to fresh consts.
620
+ const targetWsUrl = target.wsUrl;
621
+ const agentId = String(agent.id ?? agentIdOrName);
622
+ // Launch-epoch freshness guard for the ws token: a 0/missing epoch on the
623
+ // agent side means "unknown", which disables the guard rather than failing
624
+ // it.
625
+ const agentLaunchEpoch = Number(agent.launchEpoch ?? 0);
626
+ let closed = false;
627
+ let ws = null;
628
+ let reconnectTimer = null;
629
+ let failures = 0;
630
+ let historyEndFired = false;
631
+ // One-shot latch for the pre-auth protocol-violation notice (per
632
+ // subscription, so reconnects don't re-report the wedge).
633
+ let preAuthProtocolNoticeFired = false;
634
+ let connectionCounter = 0;
635
+ const isNewFrame = createBuzzActivityDeduper();
636
+ const onAbort = () => close();
637
+ function close() {
638
+ if (closed)
639
+ return;
640
+ closed = true;
641
+ handlers.signal?.removeEventListener('abort', onAbort);
642
+ if (reconnectTimer !== null) {
643
+ clearTimeout(reconnectTimer);
644
+ reconnectTimer = null;
645
+ }
646
+ const current = ws;
647
+ ws = null;
648
+ if (current) {
649
+ current.onopen = null;
650
+ current.onmessage = null;
651
+ current.onerror = null;
652
+ current.onclose = null;
653
+ try {
654
+ current.close();
655
+ }
656
+ catch {
657
+ // A socket already closed by the peer needs no local close.
658
+ }
659
+ }
660
+ }
661
+ function deliver(frame) {
662
+ if (!isNewFrame(frame))
663
+ return;
664
+ handlers.onFrame(frame);
665
+ }
666
+ function handleMessage(data, handshake) {
667
+ const raw = typeof data === 'string'
668
+ ? data
669
+ : data instanceof Uint8Array
670
+ ? new TextDecoder().decode(data)
671
+ : '';
672
+ if (!raw)
673
+ return;
674
+ let parsed;
675
+ try {
676
+ parsed = JSON.parse(raw);
677
+ }
678
+ catch {
679
+ // Malformed wire frames drop silently; they never kill the subscription.
680
+ return;
681
+ }
682
+ if (isPlainRecord(parsed) && parsed.type === 'auth_ok') {
683
+ // First-frame auth accepted; replay follows. Nothing to surface.
684
+ handshake.markAuthed();
685
+ return;
686
+ }
687
+ if (!handshake.complete()) {
688
+ // Before auth_ok a healthy server sends NOTHING. A recognizable
689
+ // protocol message here (replay_end/replay_gap envelope or an
690
+ // observer-shaped frame) is a protocol violation — the signature of a
691
+ // skewed deployment (backend secret present, pod missing the token).
692
+ // Surface exactly one error per subscription; never reset the retry
693
+ // budget, never fire onHistoryEnd, never feed the deduper from this
694
+ // state (so an identical frame arriving post-handshake still
695
+ // delivers, and a wedged stream cannot look healthy via budget
696
+ // resets). The socket stays open: if the pod heals, the stream
697
+ // recovers on its own. Unrecognizable pre-auth noise (non-JSON,
698
+ // unknown JSON) drops silently as before. This is dedup/state
699
+ // hygiene, not client auth enforcement, which the server owns.
700
+ const recognizable = isPlainRecord(parsed) && (parsed.type === 'replay_end'
701
+ || parsed.type === 'replay_gap'
702
+ || (typeof parsed.seq === 'number' && typeof parsed.timestamp === 'string'));
703
+ if (recognizable && !preAuthProtocolNoticeFired) {
704
+ preAuthProtocolNoticeFired = true;
705
+ handlers.onError?.(new Error('hyper-acp endpoint spoke before completing the auth handshake'));
706
+ }
707
+ return;
708
+ }
709
+ if (isPlainRecord(parsed)) {
710
+ if (parsed.type === 'replay_end') {
711
+ // A route that served history is healthy; reset the backoff budget
712
+ // (route analogue of the relay transport's EOSE reset).
713
+ failures = 0;
714
+ if (!historyEndFired) {
715
+ historyEndFired = true;
716
+ handlers.onHistoryEnd?.();
717
+ }
718
+ return;
719
+ }
720
+ if (parsed.type === 'replay_gap') {
721
+ const dropped = typeof parsed.dropped === 'number' ? parsed.dropped : 0;
722
+ handlers.onError?.(new BuzzActivityGapError(dropped));
723
+ return;
724
+ }
725
+ }
726
+ emitObserverPayload(parsed, deliver, () => {
727
+ // Unparseable observer frames drop silently, matching the relay
728
+ // transport; the counter was removed with the old auth flow.
729
+ });
730
+ }
731
+ async function fetchWsToken() {
732
+ // Fresh reveal per attempt, no caching: a rotated token is honored
733
+ // without resubscribing. `secret()` APIError (401/403/404) propagates
734
+ // untouched.
735
+ const secretData = await deployments.secret(agentId, BUZZ_ACTIVITY_WS_TOKEN_SECRET);
736
+ const token = String(secretData?.value ?? '').trim();
737
+ if (!token)
738
+ throw new Error('activity ws token secret is empty');
739
+ if (agentLaunchEpoch > 0
740
+ && Number(secretData?.launch_epoch ?? 0) < agentLaunchEpoch) {
741
+ throw new Error('activity ws token belongs to an older launch epoch');
742
+ }
743
+ return token;
744
+ }
745
+ async function connect() {
746
+ if (closed)
747
+ return;
748
+ connectionCounter += 1;
749
+ const connection = connectionCounter;
750
+ let token;
751
+ try {
752
+ token = await fetchWsToken();
753
+ }
754
+ catch (error) {
755
+ if (closed || connection !== connectionCounter)
756
+ return;
757
+ const surfaced = error instanceof Error
758
+ ? error
759
+ : new Error('activity ws token request failed');
760
+ if (connection === 1) {
761
+ // Pre-first-connect: the subscribe promise rejects.
762
+ throw surfaced;
763
+ }
764
+ // Mid-life reconnect: surface, then terminate without retrying.
765
+ handlers.onError?.(surfaced);
766
+ close();
767
+ handlers.onClose?.({ code: 1006, reason: surfaced.message });
768
+ return;
769
+ }
770
+ if (closed || connection !== connectionCounter)
771
+ return;
772
+ const socket = openBuzzActivityRouteSocket(targetWsUrl);
773
+ ws = socket;
774
+ // Per-connection handshake completion; a 4401 close is terminal only
775
+ // before `auth_ok` (after it, 4401 cannot legitimately arrive).
776
+ let authed = false;
777
+ socket.onopen = () => {
778
+ if (closed || connection !== connectionCounter)
779
+ return;
780
+ // The first text frame must be the auth frame, within the server's
781
+ // 3s window; the shape is pinned by the wire contract.
782
+ socket.send(JSON.stringify({ type: 'auth', token }));
783
+ };
784
+ socket.onmessage = (message) => {
785
+ if (closed || connection !== connectionCounter)
786
+ return;
787
+ handleMessage(message.data, {
788
+ complete: () => authed,
789
+ markAuthed: () => {
790
+ authed = true;
791
+ },
792
+ });
793
+ };
794
+ socket.onerror = () => {
795
+ if (closed || connection !== connectionCounter)
796
+ return;
797
+ handlers.onError?.(new Error('Buzz activity route WebSocket error'));
798
+ };
799
+ socket.onclose = (event) => {
800
+ if (closed || connection !== connectionCounter)
801
+ return;
802
+ ws = null;
803
+ const code = event?.code ?? 1006;
804
+ const reason = event?.reason ?? '';
805
+ if (code === 4401 && !authed) {
806
+ // The server rejected the token. Terminal: skip the retry loop and
807
+ // report immediately.
808
+ close();
809
+ handlers.onClose?.({ code, reason });
810
+ return;
811
+ }
812
+ fail(code, reason);
813
+ };
814
+ function fail(code, reason = '') {
815
+ if (failures >= RECONNECT_DELAYS_MS.length) {
816
+ close();
817
+ handlers.onClose?.({ code: code ?? 1006, reason });
818
+ return;
819
+ }
820
+ const delay = RECONNECT_DELAYS_MS[failures];
821
+ failures += 1;
822
+ reconnectTimer = setTimeout(() => {
823
+ reconnectTimer = null;
824
+ void connect();
825
+ }, delay);
826
+ }
827
+ }
828
+ if (handlers.signal?.aborted) {
829
+ return { close };
830
+ }
831
+ handlers.signal?.addEventListener('abort', onAbort, { once: true });
832
+ try {
833
+ // Await the first attempt's token fetch so empty/404/stale-epoch secrets
834
+ // reject the subscribe promise; everything after the token resolves into
835
+ // the socket retry loop.
836
+ await connect();
837
+ }
838
+ catch (error) {
839
+ close();
840
+ throw error;
841
+ }
842
+ return { close };
843
+ }
844
+ //# sourceMappingURL=buzz-activity.js.map