@zmeel/adapter-openclaw-gateway 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/dist/cli/format-event.d.ts +2 -0
  4. package/dist/cli/format-event.d.ts.map +1 -0
  5. package/dist/cli/format-event.js +20 -0
  6. package/dist/cli/format-event.js.map +1 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +2 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +53 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/server/execute.d.ts +3 -0
  16. package/dist/server/execute.d.ts.map +1 -0
  17. package/dist/server/execute.js +1146 -0
  18. package/dist/server/execute.js.map +1 -0
  19. package/dist/server/index.d.ts +3 -0
  20. package/dist/server/index.d.ts.map +1 -0
  21. package/dist/server/index.js +3 -0
  22. package/dist/server/index.js.map +1 -0
  23. package/dist/server/test.d.ts +3 -0
  24. package/dist/server/test.d.ts.map +1 -0
  25. package/dist/server/test.js +290 -0
  26. package/dist/server/test.js.map +1 -0
  27. package/dist/shared/stream.d.ts +5 -0
  28. package/dist/shared/stream.d.ts.map +1 -0
  29. package/dist/shared/stream.js +13 -0
  30. package/dist/shared/stream.js.map +1 -0
  31. package/dist/ui/build-config.d.ts +3 -0
  32. package/dist/ui/build-config.d.ts.map +1 -0
  33. package/dist/ui/build-config.js +33 -0
  34. package/dist/ui/build-config.js.map +1 -0
  35. package/dist/ui/index.d.ts +3 -0
  36. package/dist/ui/index.d.ts.map +1 -0
  37. package/dist/ui/index.js +3 -0
  38. package/dist/ui/index.js.map +1 -0
  39. package/dist/ui/parse-stdout.d.ts +3 -0
  40. package/dist/ui/parse-stdout.d.ts.map +1 -0
  41. package/dist/ui/parse-stdout.js +64 -0
  42. package/dist/ui/parse-stdout.js.map +1 -0
  43. package/package.json +56 -0
@@ -0,0 +1,1146 @@
1
+ import { asNumber, asString, buildzmeelEnv, parseObject } from "@zmeel/adapter-utils/server-utils";
2
+ import crypto, { randomUUID } from "node:crypto";
3
+ import { WebSocket } from "ws";
4
+ const PROTOCOL_VERSION = 3;
5
+ const DEFAULT_SCOPES = ["operator.admin"];
6
+ const DEFAULT_CLIENT_ID = "gateway-client";
7
+ const DEFAULT_CLIENT_MODE = "backend";
8
+ const DEFAULT_CLIENT_VERSION = "zmeel";
9
+ const DEFAULT_ROLE = "operator";
10
+ const SENSITIVE_LOG_KEY_PATTERN = /(^|[_-])(auth|authorization|token|secret|password|api[_-]?key|private[_-]?key)([_-]|$)|^x-openclaw-(auth|token)$/i;
11
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
12
+ function asRecord(value) {
13
+ if (typeof value !== "object" || value === null || Array.isArray(value))
14
+ return null;
15
+ return value;
16
+ }
17
+ function nonEmpty(value) {
18
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
19
+ }
20
+ function parseOptionalPositiveInteger(value) {
21
+ if (typeof value === "number" && Number.isFinite(value)) {
22
+ return Math.max(1, Math.floor(value));
23
+ }
24
+ if (typeof value === "string" && value.trim().length > 0) {
25
+ const parsed = Number.parseInt(value.trim(), 10);
26
+ if (Number.isFinite(parsed))
27
+ return Math.max(1, Math.floor(parsed));
28
+ }
29
+ return null;
30
+ }
31
+ function parseBoolean(value, fallback = false) {
32
+ if (typeof value === "boolean")
33
+ return value;
34
+ if (typeof value === "string") {
35
+ const normalized = value.trim().toLowerCase();
36
+ if (normalized === "true" || normalized === "1")
37
+ return true;
38
+ if (normalized === "false" || normalized === "0")
39
+ return false;
40
+ }
41
+ return fallback;
42
+ }
43
+ function normalizeSessionKeyStrategy(value) {
44
+ const normalized = asString(value, "issue").trim().toLowerCase();
45
+ if (normalized === "fixed" || normalized === "run")
46
+ return normalized;
47
+ return "issue";
48
+ }
49
+ function resolveSessionKey(input) {
50
+ const fallback = input.configuredSessionKey ?? "zmeel";
51
+ if (input.strategy === "run")
52
+ return `zmeel:run:${input.runId}`;
53
+ if (input.strategy === "issue" && input.issueId)
54
+ return `zmeel:issue:${input.issueId}`;
55
+ return fallback;
56
+ }
57
+ function isLoopbackHost(hostname) {
58
+ const value = hostname.trim().toLowerCase();
59
+ return value === "localhost" || value === "127.0.0.1" || value === "::1";
60
+ }
61
+ function toStringRecord(value) {
62
+ const parsed = parseObject(value);
63
+ const out = {};
64
+ for (const [key, entry] of Object.entries(parsed)) {
65
+ if (typeof entry === "string")
66
+ out[key] = entry;
67
+ }
68
+ return out;
69
+ }
70
+ function toStringArray(value) {
71
+ if (Array.isArray(value)) {
72
+ return value
73
+ .filter((entry) => typeof entry === "string")
74
+ .map((entry) => entry.trim())
75
+ .filter(Boolean);
76
+ }
77
+ if (typeof value === "string") {
78
+ return value
79
+ .split(",")
80
+ .map((entry) => entry.trim())
81
+ .filter(Boolean);
82
+ }
83
+ return [];
84
+ }
85
+ function normalizeScopes(value) {
86
+ const parsed = toStringArray(value);
87
+ return parsed.length > 0 ? parsed : [...DEFAULT_SCOPES];
88
+ }
89
+ function uniqueScopes(scopes) {
90
+ return Array.from(new Set(scopes.map((scope) => scope.trim()).filter(Boolean)));
91
+ }
92
+ function headerMapGetIgnoreCase(headers, key) {
93
+ const match = Object.entries(headers).find(([entryKey]) => entryKey.toLowerCase() === key.toLowerCase());
94
+ return match ? match[1] : null;
95
+ }
96
+ function headerMapHasIgnoreCase(headers, key) {
97
+ return Object.keys(headers).some((entryKey) => entryKey.toLowerCase() === key.toLowerCase());
98
+ }
99
+ function getGatewayErrorDetails(err) {
100
+ if (!err || typeof err !== "object")
101
+ return null;
102
+ const candidate = err.gatewayDetails;
103
+ return asRecord(candidate);
104
+ }
105
+ function extractPairingRequestId(err) {
106
+ const details = getGatewayErrorDetails(err);
107
+ const fromDetails = nonEmpty(details?.requestId);
108
+ if (fromDetails)
109
+ return fromDetails;
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ const match = message.match(/requestId\s*[:=]\s*([A-Za-z0-9_-]+)/i);
112
+ return match?.[1] ?? null;
113
+ }
114
+ function toAuthorizationHeaderValue(rawToken) {
115
+ const trimmed = rawToken.trim();
116
+ if (!trimmed)
117
+ return trimmed;
118
+ return /^bearer\s+/i.test(trimmed) ? trimmed : `Bearer ${trimmed}`;
119
+ }
120
+ function tokenFromAuthHeader(rawHeader) {
121
+ if (!rawHeader)
122
+ return null;
123
+ const trimmed = rawHeader.trim();
124
+ if (!trimmed)
125
+ return null;
126
+ const match = trimmed.match(/^bearer\s+(.+)$/i);
127
+ return match ? nonEmpty(match[1]) : trimmed;
128
+ }
129
+ function resolveAuthToken(config, headers) {
130
+ const explicit = nonEmpty(config.authToken) ?? nonEmpty(config.token);
131
+ if (explicit)
132
+ return explicit;
133
+ const tokenHeader = headerMapGetIgnoreCase(headers, "x-openclaw-token");
134
+ if (nonEmpty(tokenHeader))
135
+ return nonEmpty(tokenHeader);
136
+ const authHeader = headerMapGetIgnoreCase(headers, "x-openclaw-auth") ??
137
+ headerMapGetIgnoreCase(headers, "authorization");
138
+ return tokenFromAuthHeader(authHeader);
139
+ }
140
+ function isSensitiveLogKey(key) {
141
+ return SENSITIVE_LOG_KEY_PATTERN.test(key.trim());
142
+ }
143
+ function sha256Prefix(value) {
144
+ return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12);
145
+ }
146
+ function redactSecretForLog(value) {
147
+ return `[redacted len=${value.length} sha256=${sha256Prefix(value)}]`;
148
+ }
149
+ function truncateForLog(value, maxChars = 320) {
150
+ if (value.length <= maxChars)
151
+ return value;
152
+ return `${value.slice(0, maxChars)}... [truncated ${value.length - maxChars} chars]`;
153
+ }
154
+ function redactForLog(value, keyPath = [], depth = 0) {
155
+ const currentKey = keyPath[keyPath.length - 1] ?? "";
156
+ if (typeof value === "string") {
157
+ if (isSensitiveLogKey(currentKey))
158
+ return redactSecretForLog(value);
159
+ return truncateForLog(value);
160
+ }
161
+ if (typeof value === "number" || typeof value === "boolean" || value == null) {
162
+ return value;
163
+ }
164
+ if (Array.isArray(value)) {
165
+ if (depth >= 6)
166
+ return "[array-truncated]";
167
+ const out = value.slice(0, 20).map((entry, index) => redactForLog(entry, [...keyPath, `${index}`], depth + 1));
168
+ if (value.length > 20)
169
+ out.push(`[+${value.length - 20} more items]`);
170
+ return out;
171
+ }
172
+ if (typeof value === "object") {
173
+ if (depth >= 6)
174
+ return "[object-truncated]";
175
+ const entries = Object.entries(value);
176
+ const out = {};
177
+ for (const [key, entry] of entries.slice(0, 80)) {
178
+ out[key] = redactForLog(entry, [...keyPath, key], depth + 1);
179
+ }
180
+ if (entries.length > 80) {
181
+ out.__truncated__ = `+${entries.length - 80} keys`;
182
+ }
183
+ return out;
184
+ }
185
+ return String(value);
186
+ }
187
+ function stringifyForLog(value, maxChars) {
188
+ const text = JSON.stringify(value);
189
+ if (text.length <= maxChars)
190
+ return text;
191
+ return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`;
192
+ }
193
+ function buildWakePayload(ctx) {
194
+ const { runId, agent, context } = ctx;
195
+ return {
196
+ runId,
197
+ agentId: agent.id,
198
+ companyId: agent.companyId,
199
+ taskId: nonEmpty(context.taskId) ?? nonEmpty(context.issueId),
200
+ issueId: nonEmpty(context.issueId),
201
+ wakeReason: nonEmpty(context.wakeReason),
202
+ wakeCommentId: nonEmpty(context.wakeCommentId) ?? nonEmpty(context.commentId),
203
+ approvalId: nonEmpty(context.approvalId),
204
+ approvalStatus: nonEmpty(context.approvalStatus),
205
+ issueIds: Array.isArray(context.issueIds)
206
+ ? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
207
+ : [],
208
+ };
209
+ }
210
+ function resolvezmeelApiUrlOverride(value) {
211
+ const raw = nonEmpty(value);
212
+ if (!raw)
213
+ return null;
214
+ try {
215
+ const parsed = new URL(raw);
216
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
217
+ return null;
218
+ return parsed.toString();
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ }
224
+ function buildzmeelEnvForWake(ctx, wakePayload) {
225
+ const zmeelApiUrlOverride = resolvezmeelApiUrlOverride(ctx.config.zmeelApiUrl);
226
+ const zmeelEnv = {
227
+ ...buildzmeelEnv(ctx.agent),
228
+ ZMEEL_RUN_ID: ctx.runId,
229
+ };
230
+ if (zmeelApiUrlOverride) {
231
+ zmeelEnv.ZMEEL_API_URL = zmeelApiUrlOverride;
232
+ }
233
+ if (wakePayload.taskId)
234
+ zmeelEnv.ZMEEL_TASK_ID = wakePayload.taskId;
235
+ if (wakePayload.wakeReason)
236
+ zmeelEnv.ZMEEL_WAKE_REASON = wakePayload.wakeReason;
237
+ if (wakePayload.wakeCommentId)
238
+ zmeelEnv.ZMEEL_WAKE_COMMENT_ID = wakePayload.wakeCommentId;
239
+ if (wakePayload.approvalId)
240
+ zmeelEnv.ZMEEL_APPROVAL_ID = wakePayload.approvalId;
241
+ if (wakePayload.approvalStatus)
242
+ zmeelEnv.ZMEEL_APPROVAL_STATUS = wakePayload.approvalStatus;
243
+ if (wakePayload.issueIds.length > 0) {
244
+ zmeelEnv.ZMEEL_LINKED_ISSUE_IDS = wakePayload.issueIds.join(",");
245
+ }
246
+ return zmeelEnv;
247
+ }
248
+ function buildWakeText(payload, zmeelEnv) {
249
+ const claimedApiKeyPath = "~/.openclaw/workspace/zmeel-claimed-api-key.json";
250
+ const orderedKeys = [
251
+ "ZMEEL_RUN_ID",
252
+ "ZMEEL_AGENT_ID",
253
+ "ZMEEL_COMPANY_ID",
254
+ "ZMEEL_API_URL",
255
+ "ZMEEL_TASK_ID",
256
+ "ZMEEL_WAKE_REASON",
257
+ "ZMEEL_WAKE_COMMENT_ID",
258
+ "ZMEEL_APPROVAL_ID",
259
+ "ZMEEL_APPROVAL_STATUS",
260
+ "ZMEEL_LINKED_ISSUE_IDS",
261
+ ];
262
+ const envLines = [];
263
+ for (const key of orderedKeys) {
264
+ const value = zmeelEnv[key];
265
+ if (!value)
266
+ continue;
267
+ envLines.push(`${key}=${value}`);
268
+ }
269
+ const issueIdHint = payload.taskId ?? payload.issueId ?? "";
270
+ const apiBaseHint = zmeelEnv.ZMEEL_API_URL ?? "<set ZMEEL_API_URL>";
271
+ const lines = [
272
+ "zmeel wake event for a cloud adapter.",
273
+ "",
274
+ "Run this procedure now. Do not guess undocumented endpoints and do not ask for additional heartbeat docs.",
275
+ "",
276
+ "Set these values in your run context:",
277
+ ...envLines,
278
+ `ZMEEL_API_KEY=<token from ${claimedApiKeyPath}>`,
279
+ "",
280
+ `Load ZMEEL_API_KEY from ${claimedApiKeyPath} (the token you saved after claim-api-key).`,
281
+ "",
282
+ `api_base=${apiBaseHint}`,
283
+ `task_id=${payload.taskId ?? ""}`,
284
+ `issue_id=${payload.issueId ?? ""}`,
285
+ `wake_reason=${payload.wakeReason ?? ""}`,
286
+ `wake_comment_id=${payload.wakeCommentId ?? ""}`,
287
+ `approval_id=${payload.approvalId ?? ""}`,
288
+ `approval_status=${payload.approvalStatus ?? ""}`,
289
+ `linked_issue_ids=${payload.issueIds.join(",")}`,
290
+ "",
291
+ "HTTP rules:",
292
+ "- Use Authorization: Bearer $ZMEEL_API_KEY on every API call.",
293
+ "- Use X-zmeel-Run-Id: $ZMEEL_RUN_ID on every mutating API call.",
294
+ "- Use only /api endpoints listed below.",
295
+ "- Do NOT call guessed endpoints like /api/cloud-adapter/*, /api/cloud-adapters/*, /api/adapters/cloud/*, or /api/heartbeat.",
296
+ "",
297
+ "Workflow:",
298
+ "1) GET /api/agents/me",
299
+ `2) Determine issueId: ZMEEL_TASK_ID if present, otherwise issue_id (${issueIdHint}).`,
300
+ "3) If issueId exists:",
301
+ " - POST /api/issues/{issueId}/checkout with {\"agentId\":\"$ZMEEL_AGENT_ID\",\"expectedStatuses\":[\"todo\",\"backlog\",\"blocked\"]}",
302
+ " - GET /api/issues/{issueId}",
303
+ " - GET /api/issues/{issueId}/comments",
304
+ " - Execute the issue instructions exactly.",
305
+ " - If instructions require a comment, POST /api/issues/{issueId}/comments with {\"body\":\"...\"}.",
306
+ " - PATCH /api/issues/{issueId} with {\"status\":\"done\",\"comment\":\"what changed and why\"}.",
307
+ "4) If issueId does not exist:",
308
+ " - GET /api/companies/$ZMEEL_COMPANY_ID/issues?assigneeAgentId=$ZMEEL_AGENT_ID&status=todo,in_progress,blocked",
309
+ " - Pick in_progress first, then todo, then blocked, then execute step 3.",
310
+ "",
311
+ "Useful endpoints for issue work:",
312
+ "- POST /api/issues/{issueId}/comments",
313
+ "- PATCH /api/issues/{issueId}",
314
+ "- POST /api/companies/{companyId}/issues (when asked to create a new issue)",
315
+ "",
316
+ "Complete the workflow in this run.",
317
+ ];
318
+ return lines.join("\n");
319
+ }
320
+ function appendWakeText(baseText, wakeText) {
321
+ const trimmedBase = baseText.trim();
322
+ return trimmedBase.length > 0 ? `${trimmedBase}\n\n${wakeText}` : wakeText;
323
+ }
324
+ function buildStandardzmeelPayload(ctx, wakePayload, zmeelEnv, payloadTemplate) {
325
+ const templatezmeel = parseObject(payloadTemplate.zmeel);
326
+ const workspace = asRecord(ctx.context.zmeelWorkspace);
327
+ const workspaces = Array.isArray(ctx.context.zmeelWorkspaces)
328
+ ? ctx.context.zmeelWorkspaces.filter((entry) => Boolean(asRecord(entry)))
329
+ : [];
330
+ const configuredWorkspaceRuntime = parseObject(ctx.config.workspaceRuntime);
331
+ const runtimeServiceIntents = Array.isArray(ctx.context.zmeelRuntimeServiceIntents)
332
+ ? ctx.context.zmeelRuntimeServiceIntents.filter((entry) => Boolean(asRecord(entry)))
333
+ : [];
334
+ const standardzmeel = {
335
+ runId: ctx.runId,
336
+ companyId: ctx.agent.companyId,
337
+ agentId: ctx.agent.id,
338
+ agentName: ctx.agent.name,
339
+ taskId: wakePayload.taskId,
340
+ issueId: wakePayload.issueId,
341
+ issueIds: wakePayload.issueIds,
342
+ wakeReason: wakePayload.wakeReason,
343
+ wakeCommentId: wakePayload.wakeCommentId,
344
+ approvalId: wakePayload.approvalId,
345
+ approvalStatus: wakePayload.approvalStatus,
346
+ apiUrl: zmeelEnv.ZMEEL_API_URL ?? null,
347
+ };
348
+ if (workspace) {
349
+ standardzmeel.workspace = workspace;
350
+ }
351
+ if (workspaces.length > 0) {
352
+ standardzmeel.workspaces = workspaces;
353
+ }
354
+ if (runtimeServiceIntents.length > 0 || Object.keys(configuredWorkspaceRuntime).length > 0) {
355
+ standardzmeel.workspaceRuntime = {
356
+ ...configuredWorkspaceRuntime,
357
+ ...(runtimeServiceIntents.length > 0 ? { services: runtimeServiceIntents } : {}),
358
+ };
359
+ }
360
+ return {
361
+ ...templatezmeel,
362
+ ...standardzmeel,
363
+ };
364
+ }
365
+ function normalizeUrl(input) {
366
+ try {
367
+ return new URL(input);
368
+ }
369
+ catch {
370
+ return null;
371
+ }
372
+ }
373
+ function rawDataToString(data) {
374
+ if (typeof data === "string")
375
+ return data;
376
+ if (Buffer.isBuffer(data))
377
+ return data.toString("utf8");
378
+ if (data instanceof ArrayBuffer)
379
+ return Buffer.from(data).toString("utf8");
380
+ if (Array.isArray(data)) {
381
+ return Buffer.concat(data.map((entry) => (Buffer.isBuffer(entry) ? entry : Buffer.from(String(entry), "utf8")))).toString("utf8");
382
+ }
383
+ return String(data ?? "");
384
+ }
385
+ function withTimeout(promise, timeoutMs, message) {
386
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
387
+ return promise;
388
+ return new Promise((resolve, reject) => {
389
+ const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
390
+ promise
391
+ .then((value) => {
392
+ clearTimeout(timer);
393
+ resolve(value);
394
+ })
395
+ .catch((err) => {
396
+ clearTimeout(timer);
397
+ reject(err);
398
+ });
399
+ });
400
+ }
401
+ function derivePublicKeyRaw(publicKeyPem) {
402
+ const key = crypto.createPublicKey(publicKeyPem);
403
+ const spki = key.export({ type: "spki", format: "der" });
404
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
405
+ spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
406
+ return spki.subarray(ED25519_SPKI_PREFIX.length);
407
+ }
408
+ return spki;
409
+ }
410
+ function base64UrlEncode(buf) {
411
+ return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
412
+ }
413
+ function signDevicePayload(privateKeyPem, payload) {
414
+ const key = crypto.createPrivateKey(privateKeyPem);
415
+ const sig = crypto.sign(null, Buffer.from(payload, "utf8"), key);
416
+ return base64UrlEncode(sig);
417
+ }
418
+ function buildDeviceAuthPayloadV3(params) {
419
+ const scopes = params.scopes.join(",");
420
+ const token = params.token ?? "";
421
+ const platform = params.platform?.trim() ?? "";
422
+ const deviceFamily = params.deviceFamily?.trim() ?? "";
423
+ return [
424
+ "v3",
425
+ params.deviceId,
426
+ params.clientId,
427
+ params.clientMode,
428
+ params.role,
429
+ scopes,
430
+ String(params.signedAtMs),
431
+ token,
432
+ params.nonce,
433
+ platform,
434
+ deviceFamily,
435
+ ].join("|");
436
+ }
437
+ function resolveDeviceIdentity(config) {
438
+ const configuredPrivateKey = nonEmpty(config.devicePrivateKeyPem);
439
+ if (configuredPrivateKey) {
440
+ const privateKey = crypto.createPrivateKey(configuredPrivateKey);
441
+ const publicKey = crypto.createPublicKey(privateKey);
442
+ const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
443
+ const raw = derivePublicKeyRaw(publicKeyPem);
444
+ return {
445
+ deviceId: crypto.createHash("sha256").update(raw).digest("hex"),
446
+ publicKeyRawBase64Url: base64UrlEncode(raw),
447
+ privateKeyPem: configuredPrivateKey,
448
+ source: "configured",
449
+ };
450
+ }
451
+ const generated = crypto.generateKeyPairSync("ed25519");
452
+ const publicKeyPem = generated.publicKey.export({ type: "spki", format: "pem" }).toString();
453
+ const privateKeyPem = generated.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
454
+ const raw = derivePublicKeyRaw(publicKeyPem);
455
+ return {
456
+ deviceId: crypto.createHash("sha256").update(raw).digest("hex"),
457
+ publicKeyRawBase64Url: base64UrlEncode(raw),
458
+ privateKeyPem,
459
+ source: "ephemeral",
460
+ };
461
+ }
462
+ function isResponseFrame(value) {
463
+ const record = asRecord(value);
464
+ return Boolean(record && record.type === "res" && typeof record.id === "string" && typeof record.ok === "boolean");
465
+ }
466
+ function isEventFrame(value) {
467
+ const record = asRecord(value);
468
+ return Boolean(record && record.type === "event" && typeof record.event === "string");
469
+ }
470
+ class GatewayWsClient {
471
+ opts;
472
+ ws = null;
473
+ pending = new Map();
474
+ challengePromise;
475
+ resolveChallenge;
476
+ rejectChallenge;
477
+ constructor(opts) {
478
+ this.opts = opts;
479
+ this.challengePromise = new Promise((resolve, reject) => {
480
+ this.resolveChallenge = resolve;
481
+ this.rejectChallenge = reject;
482
+ });
483
+ this.challengePromise.catch(() => { });
484
+ }
485
+ async connect(buildConnectParams, timeoutMs) {
486
+ this.ws = new WebSocket(this.opts.url, {
487
+ headers: this.opts.headers,
488
+ maxPayload: 25 * 1024 * 1024,
489
+ });
490
+ const ws = this.ws;
491
+ ws.on("message", (data) => {
492
+ this.handleMessage(rawDataToString(data));
493
+ });
494
+ ws.on("close", (code, reason) => {
495
+ const reasonText = rawDataToString(reason);
496
+ const err = new Error(`gateway closed (${code}): ${reasonText}`);
497
+ this.failPending(err);
498
+ this.rejectChallenge(err);
499
+ });
500
+ ws.on("error", (err) => {
501
+ const message = err instanceof Error ? err.message : String(err);
502
+ void this.opts.onLog("stderr", `[openclaw-gateway] websocket error: ${message}\n`);
503
+ });
504
+ await withTimeout(new Promise((resolve, reject) => {
505
+ const onOpen = () => {
506
+ cleanup();
507
+ resolve();
508
+ };
509
+ const onError = (err) => {
510
+ cleanup();
511
+ reject(err);
512
+ };
513
+ const onClose = (code, reason) => {
514
+ cleanup();
515
+ reject(new Error(`gateway closed before open (${code}): ${rawDataToString(reason)}`));
516
+ };
517
+ const cleanup = () => {
518
+ ws.off("open", onOpen);
519
+ ws.off("error", onError);
520
+ ws.off("close", onClose);
521
+ };
522
+ ws.once("open", onOpen);
523
+ ws.once("error", onError);
524
+ ws.once("close", onClose);
525
+ }), timeoutMs, "gateway websocket open timeout");
526
+ const nonce = await withTimeout(this.challengePromise, timeoutMs, "gateway connect challenge timeout");
527
+ const signedConnectParams = buildConnectParams(nonce);
528
+ const hello = await this.request("connect", signedConnectParams, {
529
+ timeoutMs,
530
+ });
531
+ return hello;
532
+ }
533
+ async request(method, params, opts) {
534
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
535
+ throw new Error("gateway not connected");
536
+ }
537
+ const id = randomUUID();
538
+ const frame = {
539
+ type: "req",
540
+ id,
541
+ method,
542
+ params,
543
+ };
544
+ const payload = JSON.stringify(frame);
545
+ const requestPromise = new Promise((resolve, reject) => {
546
+ const timer = opts.timeoutMs > 0
547
+ ? setTimeout(() => {
548
+ this.pending.delete(id);
549
+ reject(new Error(`gateway request timeout (${method})`));
550
+ }, opts.timeoutMs)
551
+ : null;
552
+ this.pending.set(id, {
553
+ resolve: (value) => resolve(value),
554
+ reject,
555
+ expectFinal: opts.expectFinal === true,
556
+ timer,
557
+ });
558
+ });
559
+ this.ws.send(payload);
560
+ return requestPromise;
561
+ }
562
+ close() {
563
+ if (!this.ws)
564
+ return;
565
+ this.ws.close(1000, "zmeel-complete");
566
+ this.ws = null;
567
+ }
568
+ failPending(err) {
569
+ for (const [, pending] of this.pending) {
570
+ if (pending.timer)
571
+ clearTimeout(pending.timer);
572
+ pending.reject(err);
573
+ }
574
+ this.pending.clear();
575
+ }
576
+ handleMessage(raw) {
577
+ let parsed;
578
+ try {
579
+ parsed = JSON.parse(raw);
580
+ }
581
+ catch {
582
+ return;
583
+ }
584
+ if (isEventFrame(parsed)) {
585
+ if (parsed.event === "connect.challenge") {
586
+ const payload = asRecord(parsed.payload);
587
+ const nonce = nonEmpty(payload?.nonce);
588
+ if (nonce) {
589
+ this.resolveChallenge(nonce);
590
+ return;
591
+ }
592
+ }
593
+ void Promise.resolve(this.opts.onEvent(parsed)).catch(() => {
594
+ // Ignore event callback failures and keep stream active.
595
+ });
596
+ return;
597
+ }
598
+ if (!isResponseFrame(parsed))
599
+ return;
600
+ const pending = this.pending.get(parsed.id);
601
+ if (!pending)
602
+ return;
603
+ const payload = asRecord(parsed.payload);
604
+ const status = nonEmpty(payload?.status)?.toLowerCase();
605
+ if (pending.expectFinal && status === "accepted") {
606
+ return;
607
+ }
608
+ if (pending.timer)
609
+ clearTimeout(pending.timer);
610
+ this.pending.delete(parsed.id);
611
+ if (parsed.ok) {
612
+ pending.resolve(parsed.payload ?? null);
613
+ return;
614
+ }
615
+ const errorRecord = asRecord(parsed.error);
616
+ const message = nonEmpty(errorRecord?.message) ??
617
+ nonEmpty(errorRecord?.code) ??
618
+ "gateway request failed";
619
+ const err = new Error(message);
620
+ const code = nonEmpty(errorRecord?.code);
621
+ const details = asRecord(errorRecord?.details);
622
+ if (code)
623
+ err.gatewayCode = code;
624
+ if (details)
625
+ err.gatewayDetails = details;
626
+ pending.reject(err);
627
+ }
628
+ }
629
+ async function autoApproveDevicePairing(params) {
630
+ if (!params.authToken && !params.password) {
631
+ return { ok: false, reason: "shared auth token/password is missing" };
632
+ }
633
+ const approvalScopes = uniqueScopes([...params.scopes, "operator.pairing"]);
634
+ const client = new GatewayWsClient({
635
+ url: params.url,
636
+ headers: params.headers,
637
+ onEvent: () => { },
638
+ onLog: params.onLog,
639
+ });
640
+ try {
641
+ await params.onLog("stdout", "[openclaw-gateway] pairing required; attempting automatic pairing approval via gateway methods\n");
642
+ await client.connect(() => ({
643
+ minProtocol: PROTOCOL_VERSION,
644
+ maxProtocol: PROTOCOL_VERSION,
645
+ client: {
646
+ id: params.clientId,
647
+ version: params.clientVersion,
648
+ platform: process.platform,
649
+ mode: params.clientMode,
650
+ },
651
+ role: params.role,
652
+ scopes: approvalScopes,
653
+ auth: {
654
+ ...(params.authToken ? { token: params.authToken } : {}),
655
+ ...(params.password ? { password: params.password } : {}),
656
+ },
657
+ }), params.connectTimeoutMs);
658
+ let requestId = params.requestId;
659
+ if (!requestId) {
660
+ const listPayload = await client.request("device.pair.list", {}, {
661
+ timeoutMs: params.connectTimeoutMs,
662
+ });
663
+ const pending = Array.isArray(listPayload.pending) ? listPayload.pending : [];
664
+ const pendingRecords = pending
665
+ .map((entry) => asRecord(entry))
666
+ .filter((entry) => Boolean(entry));
667
+ const matching = (params.deviceId
668
+ ? pendingRecords.find((entry) => nonEmpty(entry.deviceId) === params.deviceId)
669
+ : null) ?? pendingRecords[pendingRecords.length - 1];
670
+ requestId = nonEmpty(matching?.requestId);
671
+ }
672
+ if (!requestId) {
673
+ return { ok: false, reason: "no pending device pairing request found" };
674
+ }
675
+ await client.request("device.pair.approve", { requestId }, {
676
+ timeoutMs: params.connectTimeoutMs,
677
+ });
678
+ return { ok: true, requestId };
679
+ }
680
+ catch (err) {
681
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
682
+ }
683
+ finally {
684
+ client.close();
685
+ }
686
+ }
687
+ function parseUsage(value) {
688
+ const record = asRecord(value);
689
+ if (!record)
690
+ return undefined;
691
+ const inputTokens = asNumber(record.inputTokens ?? record.input, 0);
692
+ const outputTokens = asNumber(record.outputTokens ?? record.output, 0);
693
+ const cachedInputTokens = asNumber(record.cachedInputTokens ?? record.cached_input_tokens ?? record.cacheRead ?? record.cache_read, 0);
694
+ if (inputTokens <= 0 && outputTokens <= 0 && cachedInputTokens <= 0) {
695
+ return undefined;
696
+ }
697
+ return {
698
+ inputTokens,
699
+ outputTokens,
700
+ ...(cachedInputTokens > 0 ? { cachedInputTokens } : {}),
701
+ };
702
+ }
703
+ function extractRuntimeServicesFromMeta(meta) {
704
+ if (!meta)
705
+ return [];
706
+ const reports = [];
707
+ const runtimeServices = Array.isArray(meta.runtimeServices)
708
+ ? meta.runtimeServices.filter((entry) => Boolean(asRecord(entry)))
709
+ : [];
710
+ for (const entry of runtimeServices) {
711
+ const serviceName = nonEmpty(entry.serviceName) ?? nonEmpty(entry.name);
712
+ if (!serviceName)
713
+ continue;
714
+ const rawStatus = nonEmpty(entry.status)?.toLowerCase();
715
+ const status = rawStatus === "starting" || rawStatus === "running" || rawStatus === "stopped" || rawStatus === "failed"
716
+ ? rawStatus
717
+ : "running";
718
+ const rawLifecycle = nonEmpty(entry.lifecycle)?.toLowerCase();
719
+ const lifecycle = rawLifecycle === "shared" ? "shared" : "ephemeral";
720
+ const rawScopeType = nonEmpty(entry.scopeType)?.toLowerCase();
721
+ const scopeType = rawScopeType === "project_workspace" ||
722
+ rawScopeType === "execution_workspace" ||
723
+ rawScopeType === "agent"
724
+ ? rawScopeType
725
+ : "run";
726
+ const rawHealth = nonEmpty(entry.healthStatus)?.toLowerCase();
727
+ const healthStatus = rawHealth === "healthy" || rawHealth === "unhealthy" || rawHealth === "unknown"
728
+ ? rawHealth
729
+ : status === "running"
730
+ ? "healthy"
731
+ : "unknown";
732
+ reports.push({
733
+ id: nonEmpty(entry.id),
734
+ projectId: nonEmpty(entry.projectId),
735
+ projectWorkspaceId: nonEmpty(entry.projectWorkspaceId),
736
+ issueId: nonEmpty(entry.issueId),
737
+ scopeType,
738
+ scopeId: nonEmpty(entry.scopeId),
739
+ serviceName,
740
+ status,
741
+ lifecycle,
742
+ reuseKey: nonEmpty(entry.reuseKey),
743
+ command: nonEmpty(entry.command),
744
+ cwd: nonEmpty(entry.cwd),
745
+ port: parseOptionalPositiveInteger(entry.port),
746
+ url: nonEmpty(entry.url),
747
+ providerRef: nonEmpty(entry.providerRef) ?? nonEmpty(entry.previewId),
748
+ ownerAgentId: nonEmpty(entry.ownerAgentId),
749
+ stopPolicy: asRecord(entry.stopPolicy),
750
+ healthStatus,
751
+ });
752
+ }
753
+ const previewUrl = nonEmpty(meta.previewUrl);
754
+ if (previewUrl) {
755
+ reports.push({
756
+ serviceName: "preview",
757
+ status: "running",
758
+ lifecycle: "ephemeral",
759
+ scopeType: "run",
760
+ url: previewUrl,
761
+ providerRef: nonEmpty(meta.previewId) ?? previewUrl,
762
+ healthStatus: "healthy",
763
+ });
764
+ }
765
+ const previewUrls = Array.isArray(meta.previewUrls)
766
+ ? meta.previewUrls.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
767
+ : [];
768
+ previewUrls.forEach((url, index) => {
769
+ reports.push({
770
+ serviceName: index === 0 ? "preview" : `preview-${index + 1}`,
771
+ status: "running",
772
+ lifecycle: "ephemeral",
773
+ scopeType: "run",
774
+ url,
775
+ providerRef: `${url}#${index}`,
776
+ healthStatus: "healthy",
777
+ });
778
+ });
779
+ return reports;
780
+ }
781
+ function extractResultText(value) {
782
+ const record = asRecord(value);
783
+ if (!record)
784
+ return null;
785
+ const payloads = Array.isArray(record.payloads) ? record.payloads : [];
786
+ const texts = payloads
787
+ .map((entry) => {
788
+ const payload = asRecord(entry);
789
+ return nonEmpty(payload?.text);
790
+ })
791
+ .filter((entry) => Boolean(entry));
792
+ if (texts.length > 0)
793
+ return texts.join("\n\n");
794
+ return nonEmpty(record.text) ?? nonEmpty(record.summary) ?? null;
795
+ }
796
+ export async function execute(ctx) {
797
+ const urlValue = asString(ctx.config.url, "").trim();
798
+ if (!urlValue) {
799
+ return {
800
+ exitCode: 1,
801
+ signal: null,
802
+ timedOut: false,
803
+ errorMessage: "OpenClaw gateway adapter missing url",
804
+ errorCode: "openclaw_gateway_url_missing",
805
+ };
806
+ }
807
+ const parsedUrl = normalizeUrl(urlValue);
808
+ if (!parsedUrl) {
809
+ return {
810
+ exitCode: 1,
811
+ signal: null,
812
+ timedOut: false,
813
+ errorMessage: `Invalid gateway URL: ${urlValue}`,
814
+ errorCode: "openclaw_gateway_url_invalid",
815
+ };
816
+ }
817
+ if (parsedUrl.protocol !== "ws:" && parsedUrl.protocol !== "wss:") {
818
+ return {
819
+ exitCode: 1,
820
+ signal: null,
821
+ timedOut: false,
822
+ errorMessage: `Unsupported gateway URL protocol: ${parsedUrl.protocol}`,
823
+ errorCode: "openclaw_gateway_url_protocol",
824
+ };
825
+ }
826
+ const timeoutSec = Math.max(0, Math.floor(asNumber(ctx.config.timeoutSec, 120)));
827
+ const timeoutMs = timeoutSec > 0 ? timeoutSec * 1000 : 0;
828
+ const connectTimeoutMs = timeoutMs > 0 ? Math.min(timeoutMs, 15_000) : 10_000;
829
+ const waitTimeoutMs = parseOptionalPositiveInteger(ctx.config.waitTimeoutMs) ?? (timeoutMs > 0 ? timeoutMs : 30_000);
830
+ const payloadTemplate = parseObject(ctx.config.payloadTemplate);
831
+ const transportHint = nonEmpty(ctx.config.streamTransport) ?? nonEmpty(ctx.config.transport);
832
+ const headers = toStringRecord(ctx.config.headers);
833
+ const authToken = resolveAuthToken(parseObject(ctx.config), headers);
834
+ const password = nonEmpty(ctx.config.password);
835
+ const deviceToken = nonEmpty(ctx.config.deviceToken);
836
+ if (authToken && !headerMapHasIgnoreCase(headers, "authorization")) {
837
+ headers.authorization = toAuthorizationHeaderValue(authToken);
838
+ }
839
+ const clientId = nonEmpty(ctx.config.clientId) ?? DEFAULT_CLIENT_ID;
840
+ const clientMode = nonEmpty(ctx.config.clientMode) ?? DEFAULT_CLIENT_MODE;
841
+ const clientVersion = nonEmpty(ctx.config.clientVersion) ?? DEFAULT_CLIENT_VERSION;
842
+ const role = nonEmpty(ctx.config.role) ?? DEFAULT_ROLE;
843
+ const scopes = normalizeScopes(ctx.config.scopes);
844
+ const deviceFamily = nonEmpty(ctx.config.deviceFamily);
845
+ const disableDeviceAuth = parseBoolean(ctx.config.disableDeviceAuth, false);
846
+ const wakePayload = buildWakePayload(ctx);
847
+ const zmeelEnv = buildzmeelEnvForWake(ctx, wakePayload);
848
+ const wakeText = buildWakeText(wakePayload, zmeelEnv);
849
+ const sessionKeyStrategy = normalizeSessionKeyStrategy(ctx.config.sessionKeyStrategy);
850
+ const configuredSessionKey = nonEmpty(ctx.config.sessionKey);
851
+ const sessionKey = resolveSessionKey({
852
+ strategy: sessionKeyStrategy,
853
+ configuredSessionKey,
854
+ runId: ctx.runId,
855
+ issueId: wakePayload.issueId,
856
+ });
857
+ const templateMessage = nonEmpty(payloadTemplate.message) ?? nonEmpty(payloadTemplate.text);
858
+ const message = templateMessage ? appendWakeText(templateMessage, wakeText) : wakeText;
859
+ const zmeelPayload = buildStandardzmeelPayload(ctx, wakePayload, zmeelEnv, payloadTemplate);
860
+ const agentParams = {
861
+ ...payloadTemplate,
862
+ message,
863
+ sessionKey,
864
+ idempotencyKey: ctx.runId,
865
+ };
866
+ delete agentParams.text;
867
+ const configuredAgentId = nonEmpty(ctx.config.agentId);
868
+ if (configuredAgentId && !nonEmpty(agentParams.agentId)) {
869
+ agentParams.agentId = configuredAgentId;
870
+ }
871
+ if (typeof agentParams.timeout !== "number") {
872
+ agentParams.timeout = waitTimeoutMs;
873
+ }
874
+ if (ctx.onMeta) {
875
+ await ctx.onMeta({
876
+ adapterType: "openclaw_gateway",
877
+ command: "gateway",
878
+ commandArgs: ["ws", parsedUrl.toString(), "agent"],
879
+ context: ctx.context,
880
+ });
881
+ }
882
+ const outboundHeaderKeys = Object.keys(headers).sort();
883
+ await ctx.onLog("stdout", `[openclaw-gateway] outbound headers (redacted): ${stringifyForLog(redactForLog(headers), 4_000)}\n`);
884
+ await ctx.onLog("stdout", `[openclaw-gateway] outbound payload (redacted): ${stringifyForLog(redactForLog(agentParams), 12_000)}\n`);
885
+ await ctx.onLog("stdout", `[openclaw-gateway] outbound header keys: ${outboundHeaderKeys.join(", ")}\n`);
886
+ if (transportHint) {
887
+ await ctx.onLog("stdout", `[openclaw-gateway] ignoring streamTransport=${transportHint}; gateway adapter always uses websocket protocol\n`);
888
+ }
889
+ if (parsedUrl.protocol === "ws:" && !isLoopbackHost(parsedUrl.hostname)) {
890
+ await ctx.onLog("stdout", "[openclaw-gateway] warning: using plaintext ws:// to a non-loopback host; prefer wss:// for remote endpoints\n");
891
+ }
892
+ const autoPairOnFirstConnect = parseBoolean(ctx.config.autoPairOnFirstConnect, true);
893
+ let autoPairAttempted = false;
894
+ let latestResultPayload = null;
895
+ while (true) {
896
+ const trackedRunIds = new Set([ctx.runId]);
897
+ const assistantChunks = [];
898
+ let lifecycleError = null;
899
+ let deviceIdentity = null;
900
+ const onEvent = async (frame) => {
901
+ if (frame.event !== "agent") {
902
+ if (frame.event === "shutdown") {
903
+ await ctx.onLog("stdout", `[openclaw-gateway] gateway shutdown notice: ${stringifyForLog(frame.payload ?? {}, 2_000)}\n`);
904
+ }
905
+ return;
906
+ }
907
+ const payload = asRecord(frame.payload);
908
+ if (!payload)
909
+ return;
910
+ const runId = nonEmpty(payload.runId);
911
+ if (!runId || !trackedRunIds.has(runId))
912
+ return;
913
+ const stream = nonEmpty(payload.stream) ?? "unknown";
914
+ const data = asRecord(payload.data) ?? {};
915
+ await ctx.onLog("stdout", `[openclaw-gateway:event] run=${runId} stream=${stream} data=${stringifyForLog(data, 8_000)}\n`);
916
+ if (stream === "assistant") {
917
+ const delta = nonEmpty(data.delta);
918
+ const text = nonEmpty(data.text);
919
+ if (delta) {
920
+ assistantChunks.push(delta);
921
+ }
922
+ else if (text) {
923
+ assistantChunks.push(text);
924
+ }
925
+ return;
926
+ }
927
+ if (stream === "error") {
928
+ lifecycleError = nonEmpty(data.error) ?? nonEmpty(data.message) ?? lifecycleError;
929
+ return;
930
+ }
931
+ if (stream === "lifecycle") {
932
+ const phase = nonEmpty(data.phase)?.toLowerCase();
933
+ if (phase === "error" || phase === "failed" || phase === "cancelled") {
934
+ lifecycleError = nonEmpty(data.error) ?? nonEmpty(data.message) ?? lifecycleError;
935
+ }
936
+ }
937
+ };
938
+ const client = new GatewayWsClient({
939
+ url: parsedUrl.toString(),
940
+ headers,
941
+ onEvent,
942
+ onLog: ctx.onLog,
943
+ });
944
+ try {
945
+ deviceIdentity = disableDeviceAuth ? null : resolveDeviceIdentity(parseObject(ctx.config));
946
+ if (deviceIdentity) {
947
+ await ctx.onLog("stdout", `[openclaw-gateway] device auth enabled keySource=${deviceIdentity.source} deviceId=${deviceIdentity.deviceId}\n`);
948
+ }
949
+ else {
950
+ await ctx.onLog("stdout", "[openclaw-gateway] device auth disabled\n");
951
+ }
952
+ await ctx.onLog("stdout", `[openclaw-gateway] connecting to ${parsedUrl.toString()}\n`);
953
+ const hello = await client.connect((nonce) => {
954
+ const signedAtMs = Date.now();
955
+ const connectParams = {
956
+ minProtocol: PROTOCOL_VERSION,
957
+ maxProtocol: PROTOCOL_VERSION,
958
+ client: {
959
+ id: clientId,
960
+ version: clientVersion,
961
+ platform: process.platform,
962
+ ...(deviceFamily ? { deviceFamily } : {}),
963
+ mode: clientMode,
964
+ },
965
+ role,
966
+ scopes,
967
+ auth: authToken || password || deviceToken
968
+ ? {
969
+ ...(authToken ? { token: authToken } : {}),
970
+ ...(deviceToken ? { deviceToken } : {}),
971
+ ...(password ? { password } : {}),
972
+ }
973
+ : undefined,
974
+ };
975
+ if (deviceIdentity) {
976
+ const payload = buildDeviceAuthPayloadV3({
977
+ deviceId: deviceIdentity.deviceId,
978
+ clientId,
979
+ clientMode,
980
+ role,
981
+ scopes,
982
+ signedAtMs,
983
+ token: authToken,
984
+ nonce,
985
+ platform: process.platform,
986
+ deviceFamily,
987
+ });
988
+ connectParams.device = {
989
+ id: deviceIdentity.deviceId,
990
+ publicKey: deviceIdentity.publicKeyRawBase64Url,
991
+ signature: signDevicePayload(deviceIdentity.privateKeyPem, payload),
992
+ signedAt: signedAtMs,
993
+ nonce,
994
+ };
995
+ }
996
+ return connectParams;
997
+ }, connectTimeoutMs);
998
+ await ctx.onLog("stdout", `[openclaw-gateway] connected protocol=${asNumber(asRecord(hello)?.protocol, PROTOCOL_VERSION)}\n`);
999
+ const acceptedPayload = await client.request("agent", agentParams, {
1000
+ timeoutMs: connectTimeoutMs,
1001
+ });
1002
+ latestResultPayload = acceptedPayload;
1003
+ const acceptedStatus = nonEmpty(acceptedPayload?.status)?.toLowerCase() ?? "";
1004
+ const acceptedRunId = nonEmpty(acceptedPayload?.runId) ?? ctx.runId;
1005
+ trackedRunIds.add(acceptedRunId);
1006
+ await ctx.onLog("stdout", `[openclaw-gateway] agent accepted runId=${acceptedRunId} status=${acceptedStatus || "unknown"}\n`);
1007
+ if (acceptedStatus === "error") {
1008
+ const errorMessage = nonEmpty(acceptedPayload?.summary) ?? lifecycleError ?? "OpenClaw gateway agent request failed";
1009
+ return {
1010
+ exitCode: 1,
1011
+ signal: null,
1012
+ timedOut: false,
1013
+ errorMessage,
1014
+ errorCode: "openclaw_gateway_agent_error",
1015
+ resultJson: acceptedPayload,
1016
+ };
1017
+ }
1018
+ if (acceptedStatus !== "ok") {
1019
+ const waitPayload = await client.request("agent.wait", { runId: acceptedRunId, timeoutMs: waitTimeoutMs }, { timeoutMs: waitTimeoutMs + connectTimeoutMs });
1020
+ latestResultPayload = waitPayload;
1021
+ const waitStatus = nonEmpty(waitPayload?.status)?.toLowerCase() ?? "";
1022
+ if (waitStatus === "timeout") {
1023
+ return {
1024
+ exitCode: 1,
1025
+ signal: null,
1026
+ timedOut: true,
1027
+ errorMessage: `OpenClaw gateway run timed out after ${waitTimeoutMs}ms`,
1028
+ errorCode: "openclaw_gateway_wait_timeout",
1029
+ resultJson: waitPayload,
1030
+ };
1031
+ }
1032
+ if (waitStatus === "error") {
1033
+ return {
1034
+ exitCode: 1,
1035
+ signal: null,
1036
+ timedOut: false,
1037
+ errorMessage: nonEmpty(waitPayload?.error) ??
1038
+ lifecycleError ??
1039
+ "OpenClaw gateway run failed",
1040
+ errorCode: "openclaw_gateway_wait_error",
1041
+ resultJson: waitPayload,
1042
+ };
1043
+ }
1044
+ if (waitStatus && waitStatus !== "ok") {
1045
+ return {
1046
+ exitCode: 1,
1047
+ signal: null,
1048
+ timedOut: false,
1049
+ errorMessage: `Unexpected OpenClaw gateway agent.wait status: ${waitStatus}`,
1050
+ errorCode: "openclaw_gateway_wait_status_unexpected",
1051
+ resultJson: waitPayload,
1052
+ };
1053
+ }
1054
+ }
1055
+ const summaryFromEvents = assistantChunks.join("").trim();
1056
+ const summaryFromPayload = extractResultText(asRecord(acceptedPayload?.result)) ??
1057
+ extractResultText(acceptedPayload) ??
1058
+ extractResultText(asRecord(latestResultPayload)) ??
1059
+ null;
1060
+ const summary = summaryFromEvents || summaryFromPayload || null;
1061
+ const acceptedResult = asRecord(acceptedPayload?.result);
1062
+ const latestPayload = asRecord(latestResultPayload);
1063
+ const latestResult = asRecord(latestPayload?.result);
1064
+ const acceptedMeta = asRecord(acceptedResult?.meta) ?? asRecord(acceptedPayload?.meta);
1065
+ const latestMeta = asRecord(latestResult?.meta) ?? asRecord(latestPayload?.meta);
1066
+ const mergedMeta = {
1067
+ ...(acceptedMeta ?? {}),
1068
+ ...(latestMeta ?? {}),
1069
+ };
1070
+ const agentMeta = asRecord(mergedMeta.agentMeta) ??
1071
+ asRecord(acceptedMeta?.agentMeta) ??
1072
+ asRecord(latestMeta?.agentMeta);
1073
+ const usage = parseUsage(agentMeta?.usage ?? mergedMeta.usage);
1074
+ const runtimeServices = extractRuntimeServicesFromMeta(agentMeta ?? mergedMeta);
1075
+ const provider = nonEmpty(agentMeta?.provider) ?? nonEmpty(mergedMeta.provider) ?? "openclaw";
1076
+ const model = nonEmpty(agentMeta?.model) ?? nonEmpty(mergedMeta.model) ?? null;
1077
+ const costUsd = asNumber(agentMeta?.costUsd ?? mergedMeta.costUsd, 0);
1078
+ await ctx.onLog("stdout", `[openclaw-gateway] run completed runId=${Array.from(trackedRunIds).join(",")} status=ok\n`);
1079
+ return {
1080
+ exitCode: 0,
1081
+ signal: null,
1082
+ timedOut: false,
1083
+ provider,
1084
+ ...(model ? { model } : {}),
1085
+ ...(usage ? { usage } : {}),
1086
+ ...(costUsd > 0 ? { costUsd } : {}),
1087
+ resultJson: asRecord(latestResultPayload),
1088
+ ...(runtimeServices.length > 0 ? { runtimeServices } : {}),
1089
+ ...(summary ? { summary } : {}),
1090
+ };
1091
+ }
1092
+ catch (err) {
1093
+ const message = err instanceof Error ? err.message : String(err);
1094
+ const lower = message.toLowerCase();
1095
+ const timedOut = lower.includes("timeout");
1096
+ const pairingRequired = lower.includes("pairing required");
1097
+ if (pairingRequired &&
1098
+ !disableDeviceAuth &&
1099
+ autoPairOnFirstConnect &&
1100
+ !autoPairAttempted &&
1101
+ (authToken || password)) {
1102
+ autoPairAttempted = true;
1103
+ const pairResult = await autoApproveDevicePairing({
1104
+ url: parsedUrl.toString(),
1105
+ headers,
1106
+ connectTimeoutMs,
1107
+ clientId,
1108
+ clientMode,
1109
+ clientVersion,
1110
+ role,
1111
+ scopes,
1112
+ authToken,
1113
+ password,
1114
+ requestId: extractPairingRequestId(err),
1115
+ deviceId: deviceIdentity?.deviceId ?? null,
1116
+ onLog: ctx.onLog,
1117
+ });
1118
+ if (pairResult.ok) {
1119
+ await ctx.onLog("stdout", `[openclaw-gateway] auto-approved pairing request ${pairResult.requestId}; retrying\n`);
1120
+ continue;
1121
+ }
1122
+ await ctx.onLog("stderr", `[openclaw-gateway] auto-pairing failed: ${pairResult.reason}\n`);
1123
+ }
1124
+ const detailedMessage = pairingRequired
1125
+ ? `${message}. Approve the pending device in OpenClaw (for example: openclaw devices approve --latest --url <gateway-ws-url> --token <gateway-token>) and retry. Ensure this agent has a persisted adapterConfig.devicePrivateKeyPem so approvals are reused.`
1126
+ : message;
1127
+ await ctx.onLog("stderr", `[openclaw-gateway] request failed: ${detailedMessage}\n`);
1128
+ return {
1129
+ exitCode: 1,
1130
+ signal: null,
1131
+ timedOut,
1132
+ errorMessage: detailedMessage,
1133
+ errorCode: timedOut
1134
+ ? "openclaw_gateway_timeout"
1135
+ : pairingRequired
1136
+ ? "openclaw_gateway_pairing_required"
1137
+ : "openclaw_gateway_request_failed",
1138
+ resultJson: asRecord(latestResultPayload),
1139
+ };
1140
+ }
1141
+ finally {
1142
+ client.close();
1143
+ }
1144
+ }
1145
+ }
1146
+ //# sourceMappingURL=execute.js.map