@hypercli.com/sdk 2026.6.26 → 2026.7.31

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 (87) hide show
  1. package/README.md +78 -0
  2. package/dist/agent.d.ts +19 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +24 -0
  5. package/dist/agent.js.map +1 -1
  6. package/dist/agents.d.ts +534 -43
  7. package/dist/agents.d.ts.map +1 -1
  8. package/dist/agents.js +1890 -110
  9. package/dist/agents.js.map +1 -1
  10. package/dist/browser.d.ts +3 -1
  11. package/dist/browser.d.ts.map +1 -1
  12. package/dist/browser.js +9 -2
  13. package/dist/browser.js.map +1 -1
  14. package/dist/channels.d.ts +156 -0
  15. package/dist/channels.d.ts.map +1 -0
  16. package/dist/channels.js +110 -0
  17. package/dist/channels.js.map +1 -0
  18. package/dist/client.d.ts +2 -0
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +9 -3
  21. package/dist/client.js.map +1 -1
  22. package/dist/connectors.d.ts +79 -0
  23. package/dist/connectors.d.ts.map +1 -0
  24. package/dist/connectors.js +2 -0
  25. package/dist/connectors.js.map +1 -0
  26. package/dist/http.d.ts +10 -3
  27. package/dist/http.d.ts.map +1 -1
  28. package/dist/http.js +135 -61
  29. package/dist/http.js.map +1 -1
  30. package/dist/index.d.ts +12 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +8 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/keys.d.ts +6 -1
  35. package/dist/keys.d.ts.map +1 -1
  36. package/dist/keys.js +8 -1
  37. package/dist/keys.js.map +1 -1
  38. package/dist/openclaw/channels.d.ts +86 -0
  39. package/dist/openclaw/channels.d.ts.map +1 -0
  40. package/dist/openclaw/channels.js +290 -0
  41. package/dist/openclaw/channels.js.map +1 -0
  42. package/dist/openclaw/connectors.d.ts +61 -0
  43. package/dist/openclaw/connectors.d.ts.map +1 -0
  44. package/dist/openclaw/connectors.js +174 -0
  45. package/dist/openclaw/connectors.js.map +1 -0
  46. package/dist/openclaw/gateway.d.ts +650 -15
  47. package/dist/openclaw/gateway.d.ts.map +1 -1
  48. package/dist/openclaw/gateway.js +1708 -181
  49. package/dist/openclaw/gateway.js.map +1 -1
  50. package/dist/openclaw/skills.d.ts +106 -0
  51. package/dist/openclaw/skills.d.ts.map +1 -0
  52. package/dist/openclaw/skills.js +575 -0
  53. package/dist/openclaw/skills.js.map +1 -0
  54. package/dist/openclaw/slack.d.ts +818 -0
  55. package/dist/openclaw/slack.d.ts.map +1 -0
  56. package/dist/openclaw/slack.js +536 -0
  57. package/dist/openclaw/slack.js.map +1 -0
  58. package/dist/openclaw/whatsapp.d.ts +63 -0
  59. package/dist/openclaw/whatsapp.d.ts.map +1 -0
  60. package/dist/openclaw/whatsapp.js +264 -0
  61. package/dist/openclaw/whatsapp.js.map +1 -0
  62. package/dist/renders.js +11 -11
  63. package/dist/renders.js.map +1 -1
  64. package/dist/skills.d.ts +126 -0
  65. package/dist/skills.d.ts.map +1 -0
  66. package/dist/skills.js +2 -0
  67. package/dist/skills.js.map +1 -0
  68. package/dist/user.d.ts +15 -2
  69. package/dist/user.d.ts.map +1 -1
  70. package/dist/user.js +51 -4
  71. package/dist/user.js.map +1 -1
  72. package/dist/voice-session.d.ts +74 -0
  73. package/dist/voice-session.d.ts.map +1 -0
  74. package/dist/voice-session.js +255 -0
  75. package/dist/voice-session.js.map +1 -0
  76. package/dist/voice.d.ts +27 -3
  77. package/dist/voice.d.ts.map +1 -1
  78. package/dist/voice.js +89 -6
  79. package/dist/voice.js.map +1 -1
  80. package/dist/workspaces.d.ts +181 -0
  81. package/dist/workspaces.d.ts.map +1 -0
  82. package/dist/workspaces.js +495 -0
  83. package/dist/workspaces.js.map +1 -0
  84. package/dist/x402.d.ts.map +1 -1
  85. package/dist/x402.js +1 -2
  86. package/dist/x402.js.map +1 -1
  87. package/package.json +38 -2
package/dist/agents.js CHANGED
@@ -6,13 +6,96 @@ import { getAgentsApiBaseUrl, getConfigValue } from './config.js';
6
6
  import { APIError } from './errors.js';
7
7
  import { HTTPClient } from './http.js';
8
8
  import { GatewayClient, } from './openclaw/gateway.js';
9
+ import { normalizeSlackRelayBaseUrl } from './channels.js';
9
10
  const AGENTS_API_BASE = 'https://api.hypercli.com/agents';
10
11
  const DEV_AGENTS_API_BASE = 'https://api.dev.hypercli.com/agents';
11
12
  const DEPLOYMENTS_API_PREFIX = '/deployments';
12
13
  const AGENTS_WS_URL = 'wss://api.agents.hypercli.com/ws';
13
14
  const DEV_AGENTS_WS_URL = 'wss://api.agents.dev.hypercli.com/ws';
14
- export const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:prod';
15
- export const DEFAULT_OPENCLAW_PRO_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-prod';
15
+ export const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-latest';
16
+ export const DEFAULT_OPENCLAW_PRO_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-latest';
17
+ export const DEFAULT_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-opencode:latest';
18
+ export const DEFAULT_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-codex:latest';
19
+ export const DEFAULT_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-claude-code:latest';
20
+ export const DEFAULT_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-goose:latest';
21
+ export const DEFAULT_KIMI_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-kimi-code:latest';
22
+ export const DEFAULT_BUZZ_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-opencode:latest';
23
+ export const DEFAULT_BUZZ_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-codex:latest';
24
+ export const DEFAULT_BUZZ_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-claude:latest';
25
+ export const DEFAULT_BUZZ_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-goose:latest';
26
+ export const DEFAULT_BUZZ_KIMI_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-kimi-code:latest';
27
+ export const DEFAULT_CODING_AGENT_SYNC_ROOT = '/home/node';
28
+ export const DEFAULT_CODING_AGENT_IMAGES = {
29
+ opencode: DEFAULT_OPENCODE_IMAGE,
30
+ codex: DEFAULT_CODEX_IMAGE,
31
+ 'claude-code': DEFAULT_CLAUDE_CODE_IMAGE,
32
+ goose: DEFAULT_GOOSE_IMAGE,
33
+ 'kimi-code': DEFAULT_KIMI_CODE_IMAGE,
34
+ };
35
+ export const DEFAULT_BUZZ_CODING_AGENT_IMAGES = {
36
+ opencode: DEFAULT_BUZZ_OPENCODE_IMAGE,
37
+ codex: DEFAULT_BUZZ_CODEX_IMAGE,
38
+ 'claude-code': DEFAULT_BUZZ_CLAUDE_CODE_IMAGE,
39
+ goose: DEFAULT_BUZZ_GOOSE_IMAGE,
40
+ 'kimi-code': DEFAULT_BUZZ_KIMI_CODE_IMAGE,
41
+ };
42
+ const CODING_AGENT_RUNTIMES = new Set(['opencode', 'codex', 'claude-code', 'goose', 'kimi-code']);
43
+ const BUZZ_RUNTIME_COMMANDS = {
44
+ opencode: {
45
+ command: '/usr/local/bin/opencode',
46
+ args: ['acp'],
47
+ mcpCommand: '/usr/local/bin/buzz-dev-mcp',
48
+ },
49
+ codex: {
50
+ command: '/usr/local/bin/codex-acp',
51
+ args: [],
52
+ mcpCommand: '/usr/local/bin/buzz-dev-mcp',
53
+ },
54
+ 'claude-code': {
55
+ command: '/usr/local/bin/claude-agent-acp',
56
+ args: [],
57
+ mcpCommand: '',
58
+ },
59
+ goose: {
60
+ command: '/usr/local/bin/goose',
61
+ args: ['acp'],
62
+ mcpCommand: '',
63
+ },
64
+ 'kimi-code': {
65
+ command: '/usr/local/bin/kimi',
66
+ args: ['acp'],
67
+ mcpCommand: '',
68
+ },
69
+ };
70
+ export const DEFAULT_BUZZ_RUST_LOG = 'buzz_acp=info,pool::prompt=info,acp::stream=off';
71
+ const BUZZ_RESERVED_ENV_KEYS = new Set([
72
+ 'BUZZ_PRIVATE_KEY',
73
+ 'NOSTR_PRIVATE_KEY',
74
+ 'BUZZ_AUTH_TAG',
75
+ 'BUZZ_API_TOKEN',
76
+ 'BUZZ_ACP_PRIVATE_KEY',
77
+ 'BUZZ_ACP_API_TOKEN',
78
+ 'BUZZ_RELAY_URL',
79
+ 'BUZZ_ACP_AGENT_OWNER',
80
+ 'BUZZ_ACP_AGENT_COMMAND',
81
+ 'BUZZ_ACP_AGENT_ARGS',
82
+ 'BUZZ_ACP_MCP_COMMAND',
83
+ 'BUZZ_ACP_LAZY_POOL',
84
+ 'BUZZ_ACP_RELAY_OBSERVER',
85
+ 'BUZZ_ACP_SESSION_TITLE',
86
+ 'BUZZ_ACP_SYSTEM_PROMPT',
87
+ 'BUZZ_ACP_MODEL',
88
+ 'BUZZ_ACP_IDLE_TIMEOUT',
89
+ 'BUZZ_ACP_MAX_TURN_DURATION',
90
+ 'BUZZ_ACP_AGENTS',
91
+ 'BUZZ_ACP_RESPOND_TO',
92
+ 'BUZZ_ACP_RESPOND_TO_ALLOWLIST',
93
+ 'BUZZ_ACP_MULTIPLE_EVENT_HANDLING',
94
+ 'BUZZ_ACP_DEDUP',
95
+ 'BUZZ_ACP_SETUP_PAYLOAD',
96
+ 'BUZZ_MANAGED_AGENT',
97
+ 'BUZZ_MANAGED_AGENT_START_NONCE',
98
+ ]);
16
99
  export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
17
100
  OPENCLAW_MEMORY_SEARCH_ENABLED: '1',
18
101
  OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START: '0',
@@ -21,11 +104,306 @@ export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
21
104
  OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS: '30000',
22
105
  OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES: '0',
23
106
  };
24
- const LAUNCH_CONFIG_KEYS = new Set(['image', 'env', 'routes', 'ports', 'command', 'entrypoint', 'sync_root', 'sync_enabled', 'sync_uid', 'sync_gid', 'registry_url', 'registry_auth']);
107
+ export const OPENCLAW_WORKSPACES_SYNC_ENV_DEFAULTS = {
108
+ HYPER_WORKSPACES_BOOT_SYNC: '1',
109
+ HYPER_WORKSPACES_DIR: '/home/node/workspaces',
110
+ HYPER_WORKSPACES_SYNC_READY_ONLY: '1',
111
+ };
112
+ const LAUNCH_CONFIG_KEYS = new Set([
113
+ 'image',
114
+ 'env',
115
+ 'routes',
116
+ 'ports',
117
+ 'command',
118
+ 'entrypoint',
119
+ 'sync_root',
120
+ 'sync_enabled',
121
+ 'sync_uid',
122
+ 'sync_gid',
123
+ 'registry_url',
124
+ 'registry_auth',
125
+ 'restart',
126
+ ]);
25
127
  const DEFAULT_OPENCLAW_SYNC_ROOT = '/home/node';
26
- export const AGENT_FILE_MAX_BYTES = 50 * 1024 * 1024;
128
+ export const AGENT_FILE_MAX_BYTES = 250 * 1024 * 1024;
27
129
  export const AGENT_FILE_TRANSFER_CHUNK_BYTES = 64 * 1024;
28
130
  export const AGENT_FILE_OPERATION_TIMEOUT_MS = 300_000;
131
+ function shellAbortError() {
132
+ const error = new Error('Shell connection cancelled');
133
+ error.name = 'AbortError';
134
+ return error;
135
+ }
136
+ function runShellOperation(operation, signal, timeoutMs, timeoutMessage) {
137
+ return new Promise((resolve, reject) => {
138
+ const controller = new AbortController();
139
+ let settled = false;
140
+ const cleanup = () => {
141
+ clearTimeout(timer);
142
+ signal?.removeEventListener('abort', abortOperation);
143
+ };
144
+ const finish = (error, value) => {
145
+ if (settled)
146
+ return;
147
+ settled = true;
148
+ cleanup();
149
+ if (error)
150
+ reject(error);
151
+ else
152
+ resolve(value);
153
+ };
154
+ const abortOperation = () => {
155
+ controller.abort(signal?.reason);
156
+ finish(shellAbortError());
157
+ };
158
+ const timer = setTimeout(() => {
159
+ controller.abort();
160
+ finish(new Error(timeoutMessage));
161
+ }, timeoutMs);
162
+ if (signal?.aborted) {
163
+ abortOperation();
164
+ return;
165
+ }
166
+ signal?.addEventListener('abort', abortOperation, { once: true });
167
+ void operation(controller.signal).then((value) => finish(undefined, value), (error) => {
168
+ if (error instanceof Error && error.name === 'AbortError' && !signal?.aborted) {
169
+ finish(new Error(timeoutMessage));
170
+ }
171
+ else {
172
+ finish(error);
173
+ }
174
+ });
175
+ });
176
+ }
177
+ function buildBuzzLaunchEnv(runtime, buzz, defaultSessionTitle) {
178
+ if (!buzz.privateKeyNsec.trim())
179
+ throw new Error('buzz.privateKeyNsec is required');
180
+ if (!buzz.relayUrl.trim())
181
+ throw new Error('buzz.relayUrl is required');
182
+ const parallelism = buzz.parallelism ?? 1;
183
+ if (!Number.isInteger(parallelism) || parallelism < 1 || parallelism > 32) {
184
+ throw new Error('buzz.parallelism must be between 1 and 32');
185
+ }
186
+ const harness = BUZZ_RUNTIME_COMMANDS[runtime];
187
+ const env = {
188
+ BUZZ_PRIVATE_KEY: buzz.privateKeyNsec,
189
+ NOSTR_PRIVATE_KEY: buzz.privateKeyNsec,
190
+ BUZZ_RELAY_URL: buzz.relayUrl,
191
+ BUZZ_ACP_AGENT_COMMAND: harness.command,
192
+ BUZZ_ACP_AGENT_ARGS: harness.args.join(','),
193
+ BUZZ_ACP_MCP_COMMAND: harness.mcpCommand,
194
+ BUZZ_ACP_LAZY_POOL: 'true',
195
+ BUZZ_ACP_RELAY_OBSERVER: 'true',
196
+ BUZZ_ACP_AGENTS: String(parallelism),
197
+ BUZZ_ACP_MULTIPLE_EVENT_HANDLING: 'steer',
198
+ BUZZ_ACP_DEDUP: 'queue',
199
+ };
200
+ if (buzz.rustLog)
201
+ env.RUST_LOG = buzz.rustLog;
202
+ const optional = {
203
+ BUZZ_AUTH_TAG: buzz.authTag,
204
+ BUZZ_ACP_SESSION_TITLE: buzz.sessionTitle || defaultSessionTitle,
205
+ BUZZ_ACP_SYSTEM_PROMPT: buzz.systemPrompt,
206
+ BUZZ_ACP_MODEL: buzz.model,
207
+ BUZZ_ACP_IDLE_TIMEOUT: buzz.idleTimeoutSeconds == null
208
+ ? undefined
209
+ : String(buzz.idleTimeoutSeconds),
210
+ BUZZ_ACP_MAX_TURN_DURATION: buzz.maxTurnDurationSeconds == null
211
+ ? undefined
212
+ : String(buzz.maxTurnDurationSeconds),
213
+ BUZZ_ACP_RESPOND_TO: buzz.respondTo,
214
+ BUZZ_ACP_RESPOND_TO_ALLOWLIST: buzz.respondToAllowlist?.length
215
+ ? buzz.respondToAllowlist.join(',')
216
+ : undefined,
217
+ };
218
+ for (const [key, value] of Object.entries(optional)) {
219
+ if (value)
220
+ env[key] = value;
221
+ }
222
+ return env;
223
+ }
224
+ /** The agent's sync root and workspace, owned by the SDK so paths are unambiguous. */
225
+ export const OPENCLAW_SYNC_ROOT = '/home/node';
226
+ export const OPENCLAW_WORKSPACE_PREFIX = '.openclaw/workspace';
227
+ /** Map the friendly `agent`/`backup` names to their on-the-wire `pod`/`s3` values. */
228
+ export function toWireFileSource(source) {
229
+ switch (source) {
230
+ case 'agent': return 'pod';
231
+ case 'backup': return 's3';
232
+ default: return source; // 'auto' | 'pod' | 's3'
233
+ }
234
+ }
235
+ const GATEWAY_FILE_SOURCES = new Set(['gateway']);
236
+ const FULL_FS_FILE_SOURCES = new Set(['agent', 'pod']);
237
+ /**
238
+ * Resolve a caller path to the deployment HTTP files path (sync-root relative).
239
+ * Workspace-relative by default (prefixed with the workspace); an absolute `/…`
240
+ * path is read-only full-fs and only valid for the `agent` source.
241
+ */
242
+ function resolveBackendFilePath(path, source) {
243
+ if (path.startsWith('/')) {
244
+ if (!FULL_FS_FILE_SOURCES.has(source)) {
245
+ throw new Error(`absolute paths need the 'agent' source (full pod filesystem); ` +
246
+ `'${source}' is scoped to ${source === 'backup' || source === 's3' ? 'the sync root' : 'the workspace'}.`);
247
+ }
248
+ return path; // full-fs path, passed through to source=pod
249
+ }
250
+ const rel = stripRelPrefix(path);
251
+ return rel ? `${OPENCLAW_WORKSPACE_PREFIX}/${rel}` : OPENCLAW_WORKSPACE_PREFIX;
252
+ }
253
+ function normalizeWritableBackendFilePath(path) {
254
+ const normalized = path.replace(/\\/g, '/');
255
+ if (normalized.startsWith('/')) {
256
+ const parts = normalized.split('/');
257
+ const resolved = [];
258
+ for (const part of parts) {
259
+ if (!part || part === '.')
260
+ continue;
261
+ if (part === '..') {
262
+ resolved.pop();
263
+ continue;
264
+ }
265
+ resolved.push(part);
266
+ }
267
+ const absolute = `/${resolved.join('/')}`;
268
+ const prefix = `${OPENCLAW_SYNC_ROOT}/`;
269
+ if (!absolute.startsWith(prefix)) {
270
+ throw new Error(`absolute write paths must stay within the sync root (${OPENCLAW_SYNC_ROOT}).`);
271
+ }
272
+ return absolute.slice(prefix.length);
273
+ }
274
+ if (normalized.split('/').includes('..')) {
275
+ throw new Error("paths containing '..' are not writable; writes and deletes must stay within the sync root.");
276
+ }
277
+ return stripRelPrefix(normalized);
278
+ }
279
+ /** Strip leading `./` segments and slashes without eating a dotfile's dot. */
280
+ function stripRelPrefix(path) {
281
+ return path.replace(/^(?:\.\/)+/, '').replace(/^\/+/, '');
282
+ }
283
+ function isUuidRef(value) {
284
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value.trim());
285
+ }
286
+ function isDirectAgentIdRef(value) {
287
+ const raw = value.trim();
288
+ return isUuidRef(raw) || /^[0-9a-f]{6,}$/i.test(raw) || /^(agent|external)[-_:]/i.test(raw);
289
+ }
290
+ /** Strip a workspace prefix so gateway (name-addressed) sees the bare name. */
291
+ function resolveGatewayFileName(path) {
292
+ if (path.startsWith('/')) {
293
+ throw new Error("absolute paths are not valid for the 'gateway' source (scoped to the workspace).");
294
+ }
295
+ const rel = stripRelPrefix(path);
296
+ return rel.startsWith(`${OPENCLAW_WORKSPACE_PREFIX}/`)
297
+ ? rel.slice(OPENCLAW_WORKSPACE_PREFIX.length + 1)
298
+ : rel;
299
+ }
300
+ /**
301
+ * ONE client wrapping all three agent file-access paths behind a single `source`
302
+ * switch (`agent` | `backup` | `gateway`, plus `auto`). The SDK owns the roots,
303
+ * so a workspace-relative path is the same file on every source; `agent` also
304
+ * takes absolute `/…` paths for read-only full-filesystem browsing. The
305
+ * underlying APIs are left as-is — this only wraps them.
306
+ */
307
+ export class AgentFiles {
308
+ agent;
309
+ deployments;
310
+ gatewayHooks;
311
+ constructor(agent, deployments, gatewayHooks) {
312
+ this.agent = agent;
313
+ this.deployments = deployments;
314
+ this.gatewayHooks = gatewayHooks;
315
+ }
316
+ requireGateway() {
317
+ if (!this.gatewayHooks) {
318
+ throw new Error('gateway file source requires an OpenClaw agent with a gateway URL/token.');
319
+ }
320
+ return this.gatewayHooks;
321
+ }
322
+ async list(path = '', source = 'auto') {
323
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
324
+ return this.deployments.filesList(this.agent, resolveBackendFilePath(path, source), toWireFileSource(source));
325
+ }
326
+ const gw = this.requireGateway();
327
+ const name = resolveGatewayFileName(path);
328
+ const files = await gw.withGateway(async (c) => c.filesList(await gw.resolveAgentId(c)));
329
+ return (files ?? [])
330
+ .map((f) => ({ name: f.name, path: f.name, type: 'file', size: f.size }))
331
+ .filter((f) => !name || f.name === name || f.name.startsWith(name.endsWith('/') ? name : `${name}/`));
332
+ }
333
+ async readBytes(path, source = 'auto', options) {
334
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
335
+ const resolvedPath = resolveBackendFilePath(path, source);
336
+ const wireSource = toWireFileSource(source);
337
+ return options
338
+ ? this.deployments.fileReadBytes(this.agent, resolvedPath, wireSource, options)
339
+ : this.deployments.fileReadBytes(this.agent, resolvedPath, wireSource);
340
+ }
341
+ return (await this.readBytesWithMetadata(path, source, options)).content;
342
+ }
343
+ async readBytesWithMetadata(path, source = 'auto', options) {
344
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
345
+ return this.deployments.fileReadBytesWithMetadata(this.agent, resolveBackendFilePath(path, source), toWireFileSource(source), options);
346
+ }
347
+ if (options?.signal?.aborted) {
348
+ const error = new Error('File read cancelled');
349
+ error.name = 'AbortError';
350
+ throw error;
351
+ }
352
+ const content = encodeUtf8(await this.read(path, source));
353
+ if (options?.maxBytes !== undefined && content.byteLength > options.maxBytes) {
354
+ throw fileReadLimitError(path, options.maxBytes);
355
+ }
356
+ return { content, mimeType: 'text/plain' };
357
+ }
358
+ async read(path, source = 'auto', options) {
359
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
360
+ return decodeUtf8(await this.readBytes(path, source, options));
361
+ }
362
+ const gw = this.requireGateway();
363
+ return gw.withGateway(async (c) => c.fileGet(await gw.resolveAgentId(c), resolveGatewayFileName(path)));
364
+ }
365
+ async writeBytes(path, content, source = 'auto') {
366
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
367
+ const writablePath = normalizeWritableBackendFilePath(path);
368
+ const resolvedPath = path.startsWith('/')
369
+ ? writablePath
370
+ : resolveBackendFilePath(writablePath, source);
371
+ return this.deployments.fileWriteBytes(this.agent, resolvedPath, content, toWireFileSource(source));
372
+ }
373
+ return this.write(path, coerceToUtf8String(content), source);
374
+ }
375
+ async write(path, content, source = 'auto') {
376
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
377
+ const writablePath = normalizeWritableBackendFilePath(path);
378
+ const resolvedPath = path.startsWith('/')
379
+ ? writablePath
380
+ : resolveBackendFilePath(writablePath, source);
381
+ return this.deployments.fileWrite(this.agent, resolvedPath, content, toWireFileSource(source));
382
+ }
383
+ const gw = this.requireGateway();
384
+ const name = resolveGatewayFileName(path);
385
+ const agentId = await gw.withGateway(async (c) => {
386
+ const aid = await gw.resolveAgentId(c);
387
+ await c.fileSet(aid, name, content);
388
+ return aid;
389
+ });
390
+ return { name, source: 'gateway', agentId };
391
+ }
392
+ async delete(path, options = {}) {
393
+ const source = options.source ?? 'auto';
394
+ if (GATEWAY_FILE_SOURCES.has(source)) {
395
+ throw new Error('delete is not supported over the gateway file source; use the agent/backup source.');
396
+ }
397
+ const writablePath = normalizeWritableBackendFilePath(path);
398
+ const resolvedPath = path.startsWith('/')
399
+ ? writablePath
400
+ : resolveBackendFilePath(writablePath, source);
401
+ return this.deployments.fileDelete(this.agent, resolvedPath, {
402
+ recursive: options.recursive,
403
+ source: toWireFileSource(source),
404
+ });
405
+ }
406
+ }
29
407
  function parseDate(value) {
30
408
  if (typeof value !== 'string' || !value)
31
409
  return null;
@@ -52,6 +430,8 @@ function sleep(ms) {
52
430
  return new Promise((resolve) => setTimeout(resolve, ms));
53
431
  }
54
432
  function isOpenClawHydrationData(data) {
433
+ if (data.runtime === 'openclaw' || data.runtime === 'openclaw-pro')
434
+ return true;
55
435
  const routes = data.routes;
56
436
  if (routes && typeof routes === 'object' && !Array.isArray(routes) && routes.openclaw) {
57
437
  return true;
@@ -59,15 +439,128 @@ function isOpenClawHydrationData(data) {
59
439
  const launchRoutes = data.launch_config?.routes;
60
440
  return !!(launchRoutes && typeof launchRoutes === 'object' && !Array.isArray(launchRoutes) && launchRoutes.openclaw);
61
441
  }
442
+ function isCodingAgentRuntime(runtime) {
443
+ return typeof runtime === 'string' && CODING_AGENT_RUNTIMES.has(runtime);
444
+ }
62
445
  function isTruthyEnv(value) {
63
446
  return ['1', 'true', 'yes', 'on', 'enabled'].includes(String(value ?? '').trim().toLowerCase());
64
447
  }
448
+ function isPlainRecord(value) {
449
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
450
+ }
451
+ function flattenConfigValue(value, prefix, out) {
452
+ if (!prefix) {
453
+ if (isPlainRecord(value)) {
454
+ for (const [key, child] of Object.entries(value)) {
455
+ flattenConfigValue(child, key, out);
456
+ }
457
+ return;
458
+ }
459
+ out[''] = value;
460
+ return;
461
+ }
462
+ out[prefix] = value;
463
+ if (Array.isArray(value)) {
464
+ value.forEach((child, index) => flattenConfigValue(child, `${prefix}[${index}]`, out));
465
+ return;
466
+ }
467
+ if (isPlainRecord(value)) {
468
+ for (const [key, child] of Object.entries(value)) {
469
+ flattenConfigValue(child, `${prefix}.${key}`, out);
470
+ }
471
+ }
472
+ }
473
+ export function flattenLaunchConfig(launchConfig) {
474
+ const flat = {};
475
+ if (!isPlainRecord(launchConfig))
476
+ return flat;
477
+ flattenConfigValue(launchConfig, '', flat);
478
+ return flat;
479
+ }
480
+ function pathParts(path) {
481
+ if (Array.isArray(path))
482
+ return path;
483
+ const parts = [];
484
+ for (const part of path.replace(/\[(\d+)\]/g, '.$1').split('.')) {
485
+ if (!part)
486
+ continue;
487
+ parts.push(/^\d+$/.test(part) ? Number(part) : part);
488
+ }
489
+ return parts;
490
+ }
491
+ export function getLaunchConfigValue(launchConfig, path) {
492
+ let current = launchConfig;
493
+ for (const part of pathParts(path)) {
494
+ if (typeof part === 'number') {
495
+ if (!Array.isArray(current))
496
+ return undefined;
497
+ current = current[part];
498
+ continue;
499
+ }
500
+ if (!isPlainRecord(current))
501
+ return undefined;
502
+ current = current[part];
503
+ }
504
+ return current;
505
+ }
506
+ export function routesHaveDesktop(routes) {
507
+ if (!isPlainRecord(routes))
508
+ return false;
509
+ if (isPlainRecord(routes.desktop))
510
+ return true;
511
+ return Object.values(routes).some((route) => isPlainRecord(route) && route.prefix === 'desktop');
512
+ }
513
+ export function portsHaveDesktop(ports) {
514
+ return Array.isArray(ports) && ports.some((port) => isPlainRecord(port) && Number(port.port) === 3000);
515
+ }
516
+ export function launchConfigHasDesktop(launchConfig) {
517
+ if (!isPlainRecord(launchConfig))
518
+ return false;
519
+ if (isTruthyEnv(getLaunchConfigValue(launchConfig, 'env.OPENCLAW_DESKTOP_ENABLED')))
520
+ return true;
521
+ if (routesHaveDesktop(getLaunchConfigValue(launchConfig, 'routes')))
522
+ return true;
523
+ return portsHaveDesktop(getLaunchConfigValue(launchConfig, 'ports'));
524
+ }
525
+ export function agentConfigHasDesktop(source) {
526
+ if (!source)
527
+ return false;
528
+ return (launchConfigHasDesktop(source.launchConfig ?? source.launch_config) ||
529
+ routesHaveDesktop(source.routes) ||
530
+ portsHaveDesktop(source.ports));
531
+ }
532
+ function browserDesktopRedirectPath(options = {}) {
533
+ const redirect = (options.redirect ?? 'vnc.html').trim() || 'vnc.html';
534
+ if (redirect.includes('\\')) {
535
+ throw new Error('Desktop redirect must be a relative path');
536
+ }
537
+ const base = 'https://desktop.local';
538
+ const parsed = new URL(redirect, `${base}/`);
539
+ if (parsed.origin !== base) {
540
+ throw new Error('Desktop redirect must be a relative path');
541
+ }
542
+ if (options.resize !== null) {
543
+ const resize = options.resize ?? 'scale';
544
+ if (resize.trim())
545
+ parsed.searchParams.set('resize', resize);
546
+ }
547
+ const pathname = parsed.pathname.replace(/^\/+/, '') || 'vnc.html';
548
+ return `${pathname}${parsed.search}${parsed.hash}`;
549
+ }
550
+ export function buildBrowserDesktopUrl(desktopBaseUrl, token, options = {}) {
551
+ const jwt = token.trim();
552
+ if (!jwt)
553
+ throw new Error('Desktop token is required');
554
+ const url = new URL('/_jwt_auth', desktopBaseUrl);
555
+ url.searchParams.set('jwt', jwt);
556
+ url.searchParams.set('redirect', browserDesktopRedirectPath(options));
557
+ return url.toString();
558
+ }
65
559
  function isOpenClawProHydrationData(data) {
66
560
  const launchConfig = data.launch_config;
67
561
  if (!launchConfig || typeof launchConfig !== 'object' || Array.isArray(launchConfig))
68
562
  return false;
69
- const env = launchConfig.env;
70
- if (env && typeof env === 'object' && !Array.isArray(env) && isTruthyEnv(env.OPENCLAW_DESKTOP_ENABLED)) {
563
+ if (launchConfigHasDesktop(launchConfig)) {
71
564
  return true;
72
565
  }
73
566
  const image = String(launchConfig.image ?? '');
@@ -164,12 +657,78 @@ function encodeFilePath(path) {
164
657
  .map((part) => encodeURIComponent(part))
165
658
  .join('/');
166
659
  }
660
+ function deploymentFileReadRequest(path, source) {
661
+ const params = new URLSearchParams({ source });
662
+ if (path.startsWith('/')) {
663
+ if (source !== 'pod') {
664
+ throw new Error("absolute paths require source='pod'.");
665
+ }
666
+ params.set('absolute_path', path);
667
+ return { suffix: '', params };
668
+ }
669
+ const encodedPath = encodeFilePath(path);
670
+ return { suffix: encodedPath ? `/${encodedPath}` : '', params };
671
+ }
167
672
  function decodeUtf8(content) {
168
673
  return new TextDecoder().decode(content);
169
674
  }
170
675
  function encodeUtf8(content) {
171
676
  return new TextEncoder().encode(content);
172
677
  }
678
+ function fileReadLimitError(path, maxBytes) {
679
+ return new Error(`File ${path} exceeds the ${maxBytes / 1024 / 1024} MiB read limit`);
680
+ }
681
+ async function readResponseBytes(response, path, maxBytes) {
682
+ if (maxBytes === undefined)
683
+ return new Uint8Array(await response.arrayBuffer());
684
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
685
+ throw new RangeError('maxBytes must be a non-negative safe integer');
686
+ }
687
+ const contentLength = Number(response.headers.get('content-length'));
688
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
689
+ await response.body?.cancel();
690
+ throw fileReadLimitError(path, maxBytes);
691
+ }
692
+ if (!response.body) {
693
+ const bytes = new Uint8Array(await response.arrayBuffer());
694
+ if (bytes.byteLength > maxBytes)
695
+ throw fileReadLimitError(path, maxBytes);
696
+ return bytes;
697
+ }
698
+ const reader = response.body.getReader();
699
+ const chunks = [];
700
+ let totalBytes = 0;
701
+ try {
702
+ while (true) {
703
+ const { done, value } = await reader.read();
704
+ if (done)
705
+ break;
706
+ totalBytes += value.byteLength;
707
+ if (totalBytes > maxBytes) {
708
+ await reader.cancel();
709
+ throw fileReadLimitError(path, maxBytes);
710
+ }
711
+ chunks.push(value);
712
+ }
713
+ }
714
+ finally {
715
+ reader.releaseLock();
716
+ }
717
+ const bytes = new Uint8Array(totalBytes);
718
+ let offset = 0;
719
+ for (const chunk of chunks) {
720
+ bytes.set(chunk, offset);
721
+ offset += chunk.byteLength;
722
+ }
723
+ return bytes;
724
+ }
725
+ /** Coerce write content to a string for the gateway (text-only) file source. */
726
+ function coerceToUtf8String(content) {
727
+ if (typeof content === 'string')
728
+ return content;
729
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
730
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
731
+ }
173
732
  function toUint8Array(content) {
174
733
  if (typeof content === 'string')
175
734
  return encodeUtf8(content);
@@ -192,6 +751,16 @@ function agentStateFromDict(data) {
192
751
  podName: data.pod_name ?? '',
193
752
  state: data.state ?? 'unknown',
194
753
  name: data.name ?? null,
754
+ handle: data.handle ?? null,
755
+ displayName: data.display_name ?? data.name ?? null,
756
+ avatarUrl: data.avatar_url ?? null,
757
+ displayIdentity: data.display_identity ? structuredClone(data.display_identity) : null,
758
+ runtime: data.runtime ?? null,
759
+ managed: data.managed ?? null,
760
+ isLaunchable: data.is_launchable ?? data.managed !== false,
761
+ gatewayId: data.gateway_id ?? null,
762
+ runtimeKeyAlias: data.runtime_key_alias ?? null,
763
+ relayKey: data.relay_key ?? null,
195
764
  cpu: data.cpu ?? 0,
196
765
  memory: data.memory ?? 0,
197
766
  hostname: data.hostname ?? null,
@@ -233,15 +802,18 @@ export function buildAgentConfig(config = {}, options = {}) {
233
802
  preparedConfig.agents = agentsConfig;
234
803
  }
235
804
  const env = { ...(options.env ?? {}) };
236
- let gatewayToken = options.gatewayToken?.trim() || env.OPENCLAW_GATEWAY_TOKEN?.trim() || '';
237
- if (!gatewayToken) {
238
- gatewayToken = randomHexToken(32);
239
- }
240
- env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
241
- if (options.controlUiOriginLock !== false && !env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN?.trim()) {
242
- const controlUiOrigin = defaultControlUiAllowedOrigin();
243
- if (controlUiOrigin) {
244
- env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN = controlUiOrigin;
805
+ let gatewayToken = '';
806
+ if (options.injectGatewayToken !== false) {
807
+ gatewayToken = options.gatewayToken?.trim() || env.OPENCLAW_GATEWAY_TOKEN?.trim() || '';
808
+ if (!gatewayToken) {
809
+ gatewayToken = randomHexToken(32);
810
+ }
811
+ env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
812
+ if (options.controlUiOriginLock !== false && !env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN?.trim()) {
813
+ const controlUiOrigin = defaultControlUiAllowedOrigin();
814
+ if (controlUiOrigin) {
815
+ env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN = controlUiOrigin;
816
+ }
245
817
  }
246
818
  }
247
819
  const prepared = {};
@@ -271,6 +843,8 @@ export function buildAgentConfig(config = {}, options = {}) {
271
843
  prepared.registry_url = options.registryUrl;
272
844
  if (options.registryAuth !== undefined && options.registryAuth !== null)
273
845
  prepared.registry_auth = options.registryAuth;
846
+ if (options.restart !== undefined && options.restart !== null)
847
+ prepared.restart = options.restart;
274
848
  return { config: prepared, gatewayToken };
275
849
  }
276
850
  function defaultOpenClawImage(image) {
@@ -283,6 +857,203 @@ function defaultOpenClawProImage(image) {
283
857
  return image;
284
858
  return DEFAULT_OPENCLAW_PRO_IMAGE;
285
859
  }
860
+ function productApiBaseFromAgentsApiBase(apiBase) {
861
+ const normalized = apiBase.replace(/\/+$/, '');
862
+ const agentsSuffix = '/agents';
863
+ return normalized.endsWith(agentsSuffix) ? normalized.slice(0, -agentsSuffix.length) : normalized;
864
+ }
865
+ export async function startSlackOAuth(options) {
866
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
867
+ if (!relayBaseUrl)
868
+ throw new Error('Slack relay base URL is required');
869
+ if (!options.token)
870
+ throw new Error('Slack OAuth requires an app token');
871
+ const response = await fetch(`${relayBaseUrl}/slack/oauth/start`, {
872
+ method: 'GET',
873
+ headers: {
874
+ 'Authorization': `Bearer ${options.token}`,
875
+ },
876
+ });
877
+ if (!response.ok) {
878
+ let detail = response.statusText || 'Slack OAuth start failed';
879
+ try {
880
+ const payload = await response.json();
881
+ if (typeof payload.detail === 'string' && payload.detail)
882
+ detail = payload.detail;
883
+ }
884
+ catch {
885
+ // Keep the HTTP status text when the error body is not JSON.
886
+ }
887
+ throw new APIError(response.status, detail);
888
+ }
889
+ const payload = await response.json();
890
+ const authorizeUrl = typeof payload.authorize_url === 'string' ? payload.authorize_url : '';
891
+ if (!authorizeUrl)
892
+ throw new Error('Slack OAuth start did not return an authorization URL');
893
+ return {
894
+ authorizeUrl,
895
+ expiresAt: typeof payload.expires_at === 'string' ? payload.expires_at : null,
896
+ };
897
+ }
898
+ export async function getSlackInstallStatus(options) {
899
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
900
+ if (!relayBaseUrl)
901
+ throw new Error('Slack relay base URL is required');
902
+ if (!options.token)
903
+ throw new Error('Slack install status requires an app token');
904
+ const response = await fetch(`${relayBaseUrl}/slack/install`, {
905
+ method: 'GET',
906
+ headers: { 'Authorization': `Bearer ${options.token}` },
907
+ });
908
+ if (!response.ok) {
909
+ let detail = response.statusText || 'Slack install status failed';
910
+ try {
911
+ const payload = await response.json();
912
+ if (typeof payload.detail === 'string' && payload.detail)
913
+ detail = payload.detail;
914
+ }
915
+ catch {
916
+ // Keep the HTTP status text when the error body is not JSON.
917
+ }
918
+ throw new APIError(response.status, detail);
919
+ }
920
+ const payload = await response.json();
921
+ return {
922
+ connected: payload.connected === true,
923
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
924
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
925
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
926
+ installerUserId: typeof payload.installer_user_id === 'string' ? payload.installer_user_id : null,
927
+ updatedAt: typeof payload.updated_at === 'string' ? payload.updated_at : null,
928
+ };
929
+ }
930
+ function slackRelayAuthorizedUrl(path, options) {
931
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
932
+ if (!relayBaseUrl)
933
+ throw new Error('Slack relay base URL is required');
934
+ if (!options.token)
935
+ throw new Error('Slack directory lookup requires an app token');
936
+ const url = new URL(path, `${relayBaseUrl}/`);
937
+ if (options.cursor)
938
+ url.searchParams.set('cursor', options.cursor);
939
+ if (typeof options.limit === 'number' && Number.isFinite(options.limit)) {
940
+ url.searchParams.set('limit', String(Math.trunc(options.limit)));
941
+ }
942
+ return url;
943
+ }
944
+ export async function listSlackDirectoryConversations(options) {
945
+ const url = slackRelayAuthorizedUrl('/slack/directory/conversations', options);
946
+ if (options.types)
947
+ url.searchParams.set('types', options.types);
948
+ const response = await fetch(url.toString(), {
949
+ method: 'GET',
950
+ headers: { 'Authorization': `Bearer ${options.token}` },
951
+ });
952
+ if (!response.ok) {
953
+ let detail = response.statusText || 'Slack conversation lookup failed';
954
+ try {
955
+ const payload = await response.json();
956
+ if (typeof payload.detail === 'string' && payload.detail)
957
+ detail = payload.detail;
958
+ }
959
+ catch {
960
+ // Keep the HTTP status text when the error body is not JSON.
961
+ }
962
+ throw new APIError(response.status, detail);
963
+ }
964
+ const payload = await response.json();
965
+ const conversations = Array.isArray(payload.conversations) ? payload.conversations : [];
966
+ return {
967
+ conversations: conversations
968
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
969
+ .map((item) => ({
970
+ id: item.id,
971
+ name: typeof item.name === 'string' ? item.name : null,
972
+ isChannel: typeof item.is_channel === 'boolean' ? item.is_channel : null,
973
+ isGroup: typeof item.is_group === 'boolean' ? item.is_group : null,
974
+ isIm: typeof item.is_im === 'boolean' ? item.is_im : null,
975
+ isMpim: typeof item.is_mpim === 'boolean' ? item.is_mpim : null,
976
+ isMember: typeof item.is_member === 'boolean' ? item.is_member : null,
977
+ isPrivate: typeof item.is_private === 'boolean' ? item.is_private : null,
978
+ })),
979
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
980
+ };
981
+ }
982
+ export async function listSlackDirectoryUsers(options) {
983
+ const url = slackRelayAuthorizedUrl('/slack/directory/users', options);
984
+ const response = await fetch(url.toString(), {
985
+ method: 'GET',
986
+ headers: { 'Authorization': `Bearer ${options.token}` },
987
+ });
988
+ if (!response.ok) {
989
+ let detail = response.statusText || 'Slack user lookup failed';
990
+ try {
991
+ const payload = await response.json();
992
+ if (typeof payload.detail === 'string' && payload.detail)
993
+ detail = payload.detail;
994
+ }
995
+ catch {
996
+ // Keep the HTTP status text when the error body is not JSON.
997
+ }
998
+ throw new APIError(response.status, detail);
999
+ }
1000
+ const payload = await response.json();
1001
+ const users = Array.isArray(payload.users) ? payload.users : [];
1002
+ return {
1003
+ users: users
1004
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
1005
+ .map((item) => ({
1006
+ id: item.id,
1007
+ name: typeof item.name === 'string' ? item.name : null,
1008
+ realName: typeof item.real_name === 'string' ? item.real_name : null,
1009
+ teamId: typeof item.team_id === 'string' ? item.team_id : null,
1010
+ isBot: typeof item.is_bot === 'boolean' ? item.is_bot : null,
1011
+ deleted: typeof item.deleted === 'boolean' ? item.deleted : null,
1012
+ })),
1013
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
1014
+ };
1015
+ }
1016
+ export async function attachSlackRelayAgent(options) {
1017
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1018
+ const agentId = options.agentId.trim();
1019
+ if (!relayBaseUrl)
1020
+ throw new Error('Slack relay base URL is required');
1021
+ if (!options.token)
1022
+ throw new Error('Slack relay attach requires an app token');
1023
+ if (!agentId)
1024
+ throw new Error('Slack relay attach requires an agent id');
1025
+ const response = await fetch(`${relayBaseUrl}/slack/agents/${encodeURIComponent(agentId)}/relay`, {
1026
+ method: 'POST',
1027
+ headers: { 'Authorization': `Bearer ${options.token}` },
1028
+ });
1029
+ if (!response.ok) {
1030
+ let detail = response.statusText || 'Slack relay attach failed';
1031
+ try {
1032
+ const payload = await response.json();
1033
+ if (typeof payload.detail === 'string' && payload.detail)
1034
+ detail = payload.detail;
1035
+ }
1036
+ catch {
1037
+ // Keep the HTTP status text when the error body is not JSON.
1038
+ }
1039
+ throw new APIError(response.status, detail);
1040
+ }
1041
+ const payload = await response.json();
1042
+ const agentIdValue = typeof payload.agent_id === 'string' ? payload.agent_id : '';
1043
+ const gatewayIdValue = typeof payload.gateway_id === 'string' ? payload.gateway_id : '';
1044
+ if (!agentIdValue || !gatewayIdValue)
1045
+ throw new Error('Slack relay attach response is missing agent identity');
1046
+ return {
1047
+ connected: payload.connected === true,
1048
+ agentId: agentIdValue,
1049
+ gatewayId: gatewayIdValue,
1050
+ config: payload.config && typeof payload.config === 'object' && !Array.isArray(payload.config) ? payload.config : {},
1051
+ restartRequired: payload.restart_required !== false,
1052
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
1053
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
1054
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
1055
+ };
1056
+ }
286
1057
  export function buildOpenClawRoutes(options = {}) {
287
1058
  const routes = {};
288
1059
  if (options.includeGateway ?? true) {
@@ -292,7 +1063,7 @@ export function buildOpenClawRoutes(options = {}) {
292
1063
  prefix: options.gatewayPrefix ?? '',
293
1064
  };
294
1065
  }
295
- if (options.includeDesktop ?? true) {
1066
+ if (options.includeDesktop ?? false) {
296
1067
  routes.desktop = {
297
1068
  port: options.desktopPort ?? 3000,
298
1069
  auth: options.desktopAuth ?? true,
@@ -335,6 +1106,27 @@ export function buildOpenClawMemoryIndexEnv(memoryIndex = null) {
335
1106
  }
336
1107
  return env;
337
1108
  }
1109
+ export function buildOpenClawWorkspacesSyncEnv(workspacesSync = null) {
1110
+ if (workspacesSync === false)
1111
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1112
+ const options = typeof workspacesSync === 'object' && workspacesSync !== null ? workspacesSync : {};
1113
+ if (options.enabled === false)
1114
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1115
+ const env = { ...OPENCLAW_WORKSPACES_SYNC_ENV_DEFAULTS };
1116
+ if (options.enabled !== undefined && options.enabled !== null) {
1117
+ env.HYPER_WORKSPACES_BOOT_SYNC = envBool(options.enabled);
1118
+ }
1119
+ if (options.outputDir) {
1120
+ env.HYPER_WORKSPACES_DIR = options.outputDir;
1121
+ }
1122
+ if (options.readyOnly !== undefined && options.readyOnly !== null) {
1123
+ env.HYPER_WORKSPACES_SYNC_READY_ONLY = envBool(options.readyOnly);
1124
+ }
1125
+ if (options.workspace) {
1126
+ env.HYPER_WORKSPACES_SYNC_WORKSPACE = options.workspace;
1127
+ }
1128
+ return env;
1129
+ }
338
1130
  async function getFsPromises() {
339
1131
  return import('node:fs/promises');
340
1132
  }
@@ -349,6 +1141,16 @@ export class Agent {
349
1141
  podName;
350
1142
  state;
351
1143
  name;
1144
+ handle;
1145
+ displayName;
1146
+ avatarUrl;
1147
+ displayIdentity;
1148
+ runtime;
1149
+ managed;
1150
+ isLaunchable;
1151
+ gatewayId;
1152
+ runtimeKeyAlias;
1153
+ relayKey;
352
1154
  cpu;
353
1155
  memory;
354
1156
  hostname;
@@ -375,6 +1177,16 @@ export class Agent {
375
1177
  this.podName = fields.podName;
376
1178
  this.state = fields.state;
377
1179
  this.name = fields.name ?? null;
1180
+ this.handle = fields.handle ?? null;
1181
+ this.displayName = fields.displayName ?? this.name;
1182
+ this.avatarUrl = fields.avatarUrl ?? null;
1183
+ this.displayIdentity = fields.displayIdentity ? structuredClone(fields.displayIdentity) : null;
1184
+ this.runtime = fields.runtime ?? null;
1185
+ this.managed = fields.managed ?? null;
1186
+ this.isLaunchable = fields.isLaunchable ?? true;
1187
+ this.gatewayId = fields.gatewayId ?? null;
1188
+ this.runtimeKeyAlias = fields.runtimeKeyAlias ?? null;
1189
+ this.relayKey = fields.relayKey ? structuredClone(fields.relayKey) : null;
378
1190
  this.cpu = fields.cpu;
379
1191
  this.memory = fields.memory;
380
1192
  this.hostname = fields.hostname ?? null;
@@ -422,6 +1234,11 @@ export class Agent {
422
1234
  get vncUrl() {
423
1235
  return this.desktopUrl;
424
1236
  }
1237
+ browserDesktopUrl(token, options = {}) {
1238
+ if (!this.desktopUrl)
1239
+ return null;
1240
+ return buildBrowserDesktopUrl(this.desktopUrl, token, options);
1241
+ }
425
1242
  get shellUrl() {
426
1243
  return this.routeUrl('shell');
427
1244
  }
@@ -431,6 +1248,13 @@ export class Agent {
431
1248
  get isRunning() {
432
1249
  return this.state.toLowerCase() === 'running';
433
1250
  }
1251
+ get hasDesktop() {
1252
+ return agentConfigHasDesktop({
1253
+ launchConfig: this.launchConfig,
1254
+ routes: this.routes,
1255
+ ports: this.ports,
1256
+ });
1257
+ }
434
1258
  requireDeployments() {
435
1259
  if (!this._deployments) {
436
1260
  throw new Error('Agent is not bound to a Deployments client');
@@ -469,23 +1293,45 @@ export class Agent {
469
1293
  async health() {
470
1294
  return this.requireDeployments().health(this);
471
1295
  }
1296
+ /**
1297
+ * The gateway file writer a subclass supplies — the seam that enables the
1298
+ * `gateway` source. The base agent has none (gateway calls throw); an agent
1299
+ * type that speaks a gateway file protocol (e.g. OpenClaw's `agents.files.*`)
1300
+ * overloads this. Kept on the base so the *calling* is standardized through
1301
+ * the superclass even though each agent's gateway differs.
1302
+ */
1303
+ gatewayFileHooks() {
1304
+ return undefined;
1305
+ }
1306
+ /**
1307
+ * The single file client for this agent — one `source` switch (`agent` |
1308
+ * `backup` | `gateway`) routes to the three underlying APIs (pod sidecar / S3
1309
+ * backup / gateway), with the SDK owning the roots. One implementation; a
1310
+ * subclass only supplies its gateway via {@link gatewayFileHooks}.
1311
+ */
1312
+ get files() {
1313
+ return new AgentFiles(this, this.requireDeployments(), this.gatewayFileHooks());
1314
+ }
472
1315
  async filesList(path = '', source = 'auto') {
473
- return this.requireDeployments().filesList(this, path, source);
1316
+ return this.files.list(path, source);
474
1317
  }
475
- async fileReadBytes(path, source = 'auto') {
476
- return this.requireDeployments().fileReadBytes(this, path, source);
1318
+ async fileReadBytes(path, source = 'auto', options) {
1319
+ return this.files.readBytes(path, source, options);
477
1320
  }
478
- async fileRead(path, source = 'auto') {
479
- return decodeUtf8(await this.fileReadBytes(path, source));
1321
+ async fileReadBytesWithMetadata(path, source = 'auto', options) {
1322
+ return this.files.readBytesWithMetadata(path, source, options);
1323
+ }
1324
+ async fileRead(path, source = 'auto', options) {
1325
+ return this.files.read(path, source, options);
480
1326
  }
481
1327
  async fileWriteBytes(path, content, destination = 'auto') {
482
- return this.requireDeployments().fileWriteBytes(this, path, content, destination);
1328
+ return this.files.writeBytes(path, content, destination);
483
1329
  }
484
1330
  async fileWrite(path, content, destination = 'auto') {
485
- return this.requireDeployments().fileWrite(this, path, content, destination);
1331
+ return this.files.write(path, content, destination);
486
1332
  }
487
1333
  async fileDelete(path, options = {}) {
488
- return this.requireDeployments().fileDelete(this, path, options);
1334
+ return this.files.delete(path, options);
489
1335
  }
490
1336
  async cpTo(localPath, remotePath) {
491
1337
  return this.requireDeployments().cpTo(this, localPath, remotePath);
@@ -493,8 +1339,346 @@ export class Agent {
493
1339
  async cpFrom(remotePath, localPath) {
494
1340
  return this.requireDeployments().cpFrom(this, remotePath, localPath);
495
1341
  }
496
- async shellConnect(shell) {
497
- return this.requireDeployments().shellConnect(this.id, shell);
1342
+ async shellConnect(shell, options) {
1343
+ return this.requireDeployments().shellConnect(this.id, shell, options);
1344
+ }
1345
+ }
1346
+ const RUNTIME_AUTH_CONFIG = {
1347
+ opencode: {
1348
+ agentCommand: ['opencode', 'acp'],
1349
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'opencode', '--agent-args', 'acp', '--json'],
1350
+ logoutCommand: ['opencode', 'auth', 'logout'],
1351
+ nativeMethods: [],
1352
+ },
1353
+ codex: {
1354
+ agentCommand: ['codex-acp'],
1355
+ statusCommand: ['codex', 'login', 'status'],
1356
+ logoutCommand: ['codex', 'logout'],
1357
+ nativeMethods: [{
1358
+ id: 'device',
1359
+ name: 'Device authentication',
1360
+ description: 'Authenticate Codex with a device code.',
1361
+ kind: 'native',
1362
+ command: ['codex', 'login', '--device-auth'],
1363
+ metadata: {},
1364
+ }],
1365
+ },
1366
+ 'claude-code': {
1367
+ agentCommand: ['claude-agent-acp'],
1368
+ statusCommand: ['claude', 'auth', 'status', '--json'],
1369
+ logoutCommand: ['claude', 'auth', 'logout'],
1370
+ nativeMethods: [
1371
+ { id: 'claude-ai', name: 'Claude.ai', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--claudeai'], metadata: {} },
1372
+ { id: 'console', name: 'Anthropic Console', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--console'], metadata: {} },
1373
+ { id: 'sso', name: 'Enterprise SSO', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--sso'], metadata: {} },
1374
+ ],
1375
+ },
1376
+ goose: {
1377
+ agentCommand: ['goose', 'acp'],
1378
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'goose', '--agent-args', 'acp', '--json'],
1379
+ logoutCommand: null,
1380
+ nativeMethods: [],
1381
+ },
1382
+ 'kimi-code': {
1383
+ agentCommand: ['kimi', 'acp'],
1384
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'kimi', '--agent-args', 'acp', '--json'],
1385
+ logoutCommand: null,
1386
+ nativeMethods: [],
1387
+ },
1388
+ };
1389
+ function shellQuote(value) {
1390
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1391
+ }
1392
+ function commandString(command) {
1393
+ return command.map(shellQuote).join(' ');
1394
+ }
1395
+ // Terminal control-sequence matching intentionally includes ESC and BEL.
1396
+ /* eslint-disable no-control-regex */
1397
+ const TERMINAL_ESCAPE_PATTERN = new RegExp('\\x1B(?:\\[[0-?]*[ -/]*[@-~]|\\][^\\x07]*(?:\\x07|\\x1B\\\\))', 'g');
1398
+ /* eslint-enable no-control-regex */
1399
+ function stripTerminalCodes(value) {
1400
+ return value.replace(TERMINAL_ESCAPE_PATTERN, '').replace(/\r/g, '');
1401
+ }
1402
+ function authMethodFromPayload(value) {
1403
+ if (!isPlainRecord(value))
1404
+ return null;
1405
+ const id = typeof value.id === 'string' ? value.id : '';
1406
+ if (!id)
1407
+ return null;
1408
+ const metadata = isPlainRecord(value._meta) ? { ...value._meta } : {};
1409
+ let command = [];
1410
+ const terminal = isPlainRecord(metadata['terminal-auth']) ? metadata['terminal-auth'] : null;
1411
+ const source = terminal ?? value;
1412
+ if (Array.isArray(source.command) && source.command.every((part) => typeof part === 'string')) {
1413
+ command = [...source.command];
1414
+ }
1415
+ else if (typeof source.command === 'string') {
1416
+ const args = Array.isArray(source.args) ? source.args.filter((part) => typeof part === 'string') : [];
1417
+ command = [source.command, ...args];
1418
+ }
1419
+ if (id === 'claude-login' && command.length > 0 && !command.includes('login')) {
1420
+ command.push('auth', 'login');
1421
+ }
1422
+ return {
1423
+ id,
1424
+ name: typeof value.name === 'string' ? value.name : id,
1425
+ description: typeof value.description === 'string' ? value.description : '',
1426
+ kind: typeof value.kind === 'string' ? value.kind : 'acp',
1427
+ command,
1428
+ metadata,
1429
+ };
1430
+ }
1431
+ async function websocketMessageText(data) {
1432
+ if (typeof data === 'string')
1433
+ return data;
1434
+ if (data instanceof ArrayBuffer)
1435
+ return decodeUtf8(new Uint8Array(data));
1436
+ if (ArrayBuffer.isView(data))
1437
+ return decodeUtf8(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
1438
+ if (typeof Blob !== 'undefined' && data instanceof Blob)
1439
+ return data.text();
1440
+ return String(data ?? '');
1441
+ }
1442
+ export class RuntimeLoginSession {
1443
+ authClient;
1444
+ socket;
1445
+ output = '';
1446
+ verificationUrl = null;
1447
+ userCode = null;
1448
+ interactiveRequired = false;
1449
+ exitCode = null;
1450
+ marker;
1451
+ completion;
1452
+ ready;
1453
+ complete;
1454
+ markReady;
1455
+ constructor(authClient, socket, command) {
1456
+ this.authClient = authClient;
1457
+ this.socket = socket;
1458
+ this.marker = `__HYPERCLI_AUTH_EXIT_${randomHexToken(12)}__=`;
1459
+ this.completion = new Promise((resolve) => { this.complete = resolve; });
1460
+ this.ready = new Promise((resolve) => { this.markReady = resolve; });
1461
+ socket.onmessage = (event) => {
1462
+ void websocketMessageText(event.data).then((chunk) => this.consume(chunk));
1463
+ };
1464
+ socket.onclose = () => {
1465
+ this.markReady();
1466
+ this.complete();
1467
+ };
1468
+ socket.send(`${commandString(command)}; _hypercli_auth_rc=$?; printf '\\n${this.marker}%s\\n' "$_hypercli_auth_rc"\n`);
1469
+ }
1470
+ static async start(authClient, command, challengeTimeoutMs = 45_000) {
1471
+ const socket = await authClient.agent.shellConnect();
1472
+ const session = new RuntimeLoginSession(authClient, socket, command);
1473
+ let timer;
1474
+ try {
1475
+ await Promise.race([
1476
+ session.ready,
1477
+ new Promise((_, reject) => {
1478
+ timer = setTimeout(() => reject(new Error('Timed out waiting for runtime login instructions')), challengeTimeoutMs);
1479
+ }),
1480
+ ]);
1481
+ return session;
1482
+ }
1483
+ catch (error) {
1484
+ session.cancel();
1485
+ throw error;
1486
+ }
1487
+ finally {
1488
+ if (timer)
1489
+ clearTimeout(timer);
1490
+ }
1491
+ }
1492
+ consume(chunk) {
1493
+ this.output += stripTerminalCodes(chunk);
1494
+ const markerIndex = this.output.lastIndexOf(this.marker);
1495
+ if (markerIndex >= 0) {
1496
+ const match = this.output.slice(markerIndex + this.marker.length).match(/^(-?\d+)/);
1497
+ if (match) {
1498
+ this.exitCode = Number(match[1]);
1499
+ this.markReady();
1500
+ this.complete();
1501
+ }
1502
+ }
1503
+ this.verificationUrl ??= this.output.match(/https?:\/\/[^\s"'<>]+/)?.[0] ?? null;
1504
+ this.userCode ??= this.output.match(/(?:user|device|verification|one[- ]time)?\s*code\s*(?:is|:)?\s*([A-Z0-9][A-Z0-9-]{3,})/i)?.[1] ?? null;
1505
+ this.interactiveRequired ||= /\b(select|choose)\b.*\b(provider|login method)\b/i.test(this.output);
1506
+ if (this.verificationUrl || this.userCode || this.interactiveRequired)
1507
+ this.markReady();
1508
+ }
1509
+ send(text) {
1510
+ this.socket.send(text.endsWith('\n') ? text : `${text}\n`);
1511
+ }
1512
+ async wait(timeoutMs = 600_000) {
1513
+ let timer;
1514
+ try {
1515
+ await Promise.race([
1516
+ this.completion,
1517
+ new Promise((_, reject) => {
1518
+ timer = setTimeout(() => reject(new Error('Runtime authentication timed out')), timeoutMs);
1519
+ }),
1520
+ ]);
1521
+ }
1522
+ finally {
1523
+ if (timer)
1524
+ clearTimeout(timer);
1525
+ }
1526
+ if (this.exitCode !== null && this.exitCode !== 0) {
1527
+ throw new Error(`Runtime authentication failed (${this.exitCode}): ${this.output.trim()}`);
1528
+ }
1529
+ return this.authClient.status();
1530
+ }
1531
+ cancel() {
1532
+ if (this.socket.readyState === WebSocket.OPEN)
1533
+ this.socket.send('\x03');
1534
+ this.socket.close();
1535
+ }
1536
+ }
1537
+ export class RuntimeAuthClient {
1538
+ agent;
1539
+ config;
1540
+ constructor(agent) {
1541
+ this.agent = agent;
1542
+ this.config = RUNTIME_AUTH_CONFIG[agent.runtime];
1543
+ }
1544
+ async methods() {
1545
+ const [agentCommand, ...agentArgs] = this.config.agentCommand;
1546
+ const command = ['buzz-acp', 'auth-methods', '--agent-command', agentCommand];
1547
+ if (agentArgs.length)
1548
+ command.push('--agent-args', agentArgs.join(','));
1549
+ command.push('--json');
1550
+ const result = await this.agent.exec(commandString(command));
1551
+ const discovered = [];
1552
+ if (result.exitCode === 0) {
1553
+ try {
1554
+ const payload = JSON.parse(stripTerminalCodes(result.stdout));
1555
+ const values = isPlainRecord(payload) && Array.isArray(payload.methods) ? payload.methods : [];
1556
+ for (const value of values) {
1557
+ const method = authMethodFromPayload(value);
1558
+ if (method)
1559
+ discovered.push(method);
1560
+ }
1561
+ }
1562
+ catch {
1563
+ // Native fallbacks still make authentication available.
1564
+ }
1565
+ }
1566
+ if (this.agent.runtime === 'opencode' && discovered.length === 0) {
1567
+ discovered.push({
1568
+ id: 'provider',
1569
+ name: 'Provider login',
1570
+ description: '',
1571
+ kind: 'native',
1572
+ command: ['opencode', 'auth', 'login'],
1573
+ metadata: {},
1574
+ });
1575
+ }
1576
+ const seen = new Set(discovered.map((method) => method.id));
1577
+ for (const method of this.config.nativeMethods) {
1578
+ if (!seen.has(method.id))
1579
+ discovered.push({ ...method, command: [...method.command], metadata: { ...method.metadata } });
1580
+ }
1581
+ return discovered;
1582
+ }
1583
+ async status() {
1584
+ const result = await this.agent.exec(commandString(this.config.statusCommand));
1585
+ const output = stripTerminalCodes([result.stdout, result.stderr].filter(Boolean).join('\n')).trim();
1586
+ const detail = { exitCode: result.exitCode, output };
1587
+ if (this.agent.runtime === 'claude-code') {
1588
+ try {
1589
+ const payload = JSON.parse(stripTerminalCodes(result.stdout));
1590
+ Object.assign(detail, payload);
1591
+ const loginMethod = payload.loginMethod ?? payload.authMethod;
1592
+ return {
1593
+ authenticated: payload.loggedIn === true || payload.authenticated === true ||
1594
+ (typeof loginMethod === 'string' && loginMethod.toLowerCase() !== 'none'),
1595
+ provider: typeof (payload.subscriptionType ?? payload.provider ?? payload.apiProvider) === 'string'
1596
+ ? String(payload.subscriptionType ?? payload.provider ?? payload.apiProvider) : null,
1597
+ account: typeof payload.email === 'string' ? payload.email : null,
1598
+ method: typeof loginMethod === 'string' ? loginMethod : null,
1599
+ detail,
1600
+ };
1601
+ }
1602
+ catch {
1603
+ // Fall through to the generic status parser.
1604
+ }
1605
+ }
1606
+ const negative = /\b(not logged|not authenticated|unauthenticated|no credentials|0 credentials)\b/i.test(output);
1607
+ return { authenticated: result.exitCode === 0 && !negative, detail };
1608
+ }
1609
+ async login(options = {}) {
1610
+ const methods = await this.methods();
1611
+ const method = options.method
1612
+ ? methods.find((candidate) => candidate.id === options.method)
1613
+ : methods.find((candidate) => candidate.command.length > 0) ?? methods[0];
1614
+ if (!method) {
1615
+ throw new Error(options.method
1616
+ ? `Unknown authentication method: ${options.method}`
1617
+ : 'No authentication methods are available');
1618
+ }
1619
+ let command = [...method.command];
1620
+ if (!command.length) {
1621
+ const [agentCommand, ...agentArgs] = this.config.agentCommand;
1622
+ command = ['buzz-acp', 'authenticate', '--agent-command', agentCommand];
1623
+ if (agentArgs.length)
1624
+ command.push('--agent-args', agentArgs.join(','));
1625
+ command.push('--method-id', method.id);
1626
+ }
1627
+ if (this.agent.runtime === 'opencode') {
1628
+ if (options.provider)
1629
+ command.push('--provider', options.provider);
1630
+ if (options.providerMethod)
1631
+ command.push('--method', options.providerMethod);
1632
+ }
1633
+ if (this.agent.runtime === 'claude-code' && options.email)
1634
+ command.push('--email', options.email);
1635
+ return RuntimeLoginSession.start(this, command, options.challengeTimeoutMs);
1636
+ }
1637
+ async logout(provider) {
1638
+ if (this.config.logoutCommand === null) {
1639
+ const reason = this.agent.runtime === 'goose'
1640
+ ? 'uses its injected deployment credential'
1641
+ : 'does not expose a noninteractive logout command';
1642
+ throw new Error(`${this.agent.runtime} ${reason} and cannot log out`);
1643
+ }
1644
+ const command = [...this.config.logoutCommand];
1645
+ if (this.agent.runtime === 'opencode' && provider)
1646
+ command.push(provider);
1647
+ const result = await this.agent.exec(commandString(command));
1648
+ if (result.exitCode !== 0) {
1649
+ throw new Error(`Runtime logout failed (${result.exitCode}): ${stripTerminalCodes(result.stderr || result.stdout).trim()}`);
1650
+ }
1651
+ return this.status();
1652
+ }
1653
+ }
1654
+ export class CodingAgent extends Agent {
1655
+ get auth() {
1656
+ return new RuntimeAuthClient(this);
1657
+ }
1658
+ }
1659
+ export class OpenCodeAgent extends CodingAgent {
1660
+ static fromDict(data) {
1661
+ return new OpenCodeAgent(agentStateFromDict(data));
1662
+ }
1663
+ }
1664
+ export class CodexAgent extends CodingAgent {
1665
+ static fromDict(data) {
1666
+ return new CodexAgent(agentStateFromDict(data));
1667
+ }
1668
+ }
1669
+ export class ClaudeCodeAgent extends CodingAgent {
1670
+ static fromDict(data) {
1671
+ return new ClaudeCodeAgent(agentStateFromDict(data));
1672
+ }
1673
+ }
1674
+ export class GooseAgent extends CodingAgent {
1675
+ static fromDict(data) {
1676
+ return new GooseAgent(agentStateFromDict(data));
1677
+ }
1678
+ }
1679
+ export class KimiCodeAgent extends CodingAgent {
1680
+ static fromDict(data) {
1681
+ return new KimiCodeAgent(agentStateFromDict(data));
498
1682
  }
499
1683
  }
500
1684
  export class OpenClawAgent extends Agent {
@@ -508,7 +1692,7 @@ export class OpenClawAgent extends Agent {
508
1692
  static fromDict(data) {
509
1693
  return new OpenClawAgent({
510
1694
  ...agentStateFromDict(data),
511
- gatewayUrl: null,
1695
+ gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
512
1696
  gatewayToken: data.gateway_token ?? null,
513
1697
  });
514
1698
  }
@@ -537,6 +1721,15 @@ export class OpenClawAgent extends Agent {
537
1721
  * token from `OPENCLAW_GATEWAY_TOKEN` in the agent env route.
538
1722
  */
539
1723
  async waitForGatewayContext(options = {}) {
1724
+ const callerAbortError = () => {
1725
+ if (options.signal?.reason instanceof Error)
1726
+ return options.signal.reason;
1727
+ const error = new Error('OpenClaw gateway context wait cancelled');
1728
+ error.name = 'AbortError';
1729
+ return error;
1730
+ };
1731
+ if (options.signal?.aborted)
1732
+ throw callerAbortError();
540
1733
  if (this.gatewayToken && this.gatewayUrl) {
541
1734
  return {
542
1735
  agent_id: this.id,
@@ -549,29 +1742,98 @@ export class OpenClawAgent extends Agent {
549
1742
  const deployments = this.requireDeployments();
550
1743
  const deadline = Date.now() + timeoutMs;
551
1744
  let lastError = null;
552
- while (true) {
553
- try {
554
- const refreshed = await deployments.get(this.id);
555
- const hostname = refreshed.hostname ?? null;
556
- const gatewayUrl = OpenClawAgent.gatewayUrlFromHostname(hostname);
557
- const envData = await deployments.env(this.id);
558
- const gatewayToken = envData.env?.OPENCLAW_GATEWAY_TOKEN?.trim() || null;
559
- if (gatewayToken && gatewayUrl) {
560
- this.gatewayToken = gatewayToken;
561
- this.gatewayUrl = gatewayUrl;
562
- return { agent_id: this.id, gateway_token: gatewayToken, hostname };
1745
+ const timeoutError = () => {
1746
+ return lastError instanceof Error
1747
+ ? lastError
1748
+ : new Error('Timed out waiting for OpenClaw gateway context');
1749
+ };
1750
+ const controller = new AbortController();
1751
+ const forwardAbort = () => controller.abort(callerAbortError());
1752
+ options.signal?.addEventListener('abort', forwardAbort, { once: true });
1753
+ const timeoutId = setTimeout(() => controller.abort(timeoutError()), Math.max(0, timeoutMs));
1754
+ const waitAbortError = () => {
1755
+ return controller.signal.reason instanceof Error ? controller.signal.reason : callerAbortError();
1756
+ };
1757
+ const runWithAbort = (operation) => {
1758
+ if (controller.signal.aborted)
1759
+ return Promise.reject(waitAbortError());
1760
+ return new Promise((resolve, reject) => {
1761
+ const abortOperation = () => {
1762
+ cleanup();
1763
+ reject(waitAbortError());
1764
+ };
1765
+ const cleanup = () => controller.signal.removeEventListener('abort', abortOperation);
1766
+ controller.signal.addEventListener('abort', abortOperation, { once: true });
1767
+ operation.then((value) => {
1768
+ cleanup();
1769
+ resolve(value);
1770
+ }, (error) => {
1771
+ cleanup();
1772
+ reject(error);
1773
+ });
1774
+ });
1775
+ };
1776
+ const waitForRetry = (delayMs) => {
1777
+ if (controller.signal.aborted)
1778
+ return Promise.reject(waitAbortError());
1779
+ return new Promise((resolve, reject) => {
1780
+ const finish = () => {
1781
+ controller.signal.removeEventListener('abort', abortWait);
1782
+ resolve();
1783
+ };
1784
+ const timer = setTimeout(finish, delayMs);
1785
+ const abortWait = () => {
1786
+ clearTimeout(timer);
1787
+ controller.signal.removeEventListener('abort', abortWait);
1788
+ reject(waitAbortError());
1789
+ };
1790
+ controller.signal.addEventListener('abort', abortWait, { once: true });
1791
+ });
1792
+ };
1793
+ try {
1794
+ while (true) {
1795
+ const remainingMs = deadline - Date.now();
1796
+ if (remainingMs <= 0)
1797
+ throw timeoutError();
1798
+ const requestOptions = {
1799
+ signal: controller.signal,
1800
+ timeout: Math.max(1, remainingMs),
1801
+ };
1802
+ const requests = [];
1803
+ if (!this.gatewayUrl) {
1804
+ requests.push(deployments.get(this.id, requestOptions).then((refreshed) => {
1805
+ const gatewayUrl = OpenClawAgent.gatewayUrlFromHostname(refreshed.hostname);
1806
+ if (gatewayUrl)
1807
+ this.gatewayUrl = gatewayUrl;
1808
+ }));
563
1809
  }
564
- lastError = new Error('missing gateway context');
565
- }
566
- catch (error) {
567
- lastError = error;
568
- }
569
- if (Date.now() >= deadline) {
570
- if (lastError instanceof Error)
571
- throw lastError;
572
- throw new Error('Timed out waiting for OpenClaw gateway context');
1810
+ if (!this.gatewayToken) {
1811
+ requests.push(deployments.env(this.id, requestOptions).then((envData) => {
1812
+ const gatewayToken = envData.env?.OPENCLAW_GATEWAY_TOKEN?.trim() || null;
1813
+ if (gatewayToken)
1814
+ this.gatewayToken = gatewayToken;
1815
+ }));
1816
+ }
1817
+ const results = await runWithAbort(Promise.allSettled(requests));
1818
+ const rejected = results.find((result) => result.status === 'rejected');
1819
+ if (this.gatewayToken && this.gatewayUrl) {
1820
+ return {
1821
+ agent_id: this.id,
1822
+ hostname: this.currentGatewayHostname(),
1823
+ gateway_token: this.gatewayToken,
1824
+ };
1825
+ }
1826
+ lastError = rejected?.reason ?? new Error('missing gateway context');
1827
+ const remainingAfterRequestMs = deadline - Date.now();
1828
+ if (remainingAfterRequestMs <= 0)
1829
+ throw timeoutError();
1830
+ const retryDelayMs = Math.min(Math.max(0, retryIntervalMs), remainingAfterRequestMs);
1831
+ await waitForRetry(retryDelayMs);
573
1832
  }
574
- await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
1833
+ }
1834
+ finally {
1835
+ clearTimeout(timeoutId);
1836
+ options.signal?.removeEventListener('abort', forwardAbort);
575
1837
  }
576
1838
  }
577
1839
  async resolveGatewayToken() {
@@ -603,6 +1865,7 @@ export class OpenClawAgent extends Agent {
603
1865
  onHello: options.onHello,
604
1866
  onClose: options.onClose,
605
1867
  onGap: options.onGap,
1868
+ onProtocolError: options.onProtocolError,
606
1869
  onPairing: options.onPairing,
607
1870
  });
608
1871
  }
@@ -614,6 +1877,47 @@ export class OpenClawAgent extends Agent {
614
1877
  await client.connect();
615
1878
  return client;
616
1879
  }
1880
+ /** Run a fn against a connected gateway client, then close it. */
1881
+ async withGateway(fn) {
1882
+ const client = await this.connect();
1883
+ try {
1884
+ return await fn(client);
1885
+ }
1886
+ finally {
1887
+ client.close();
1888
+ }
1889
+ }
1890
+ _gatewayAgentId;
1891
+ /**
1892
+ * The in-gateway agent id for `agents.files.*` — NOT the deployment id. A
1893
+ * deployment's gateway hosts an agent named `main` (or a named agent);
1894
+ * matches the existing fileGet/fileSet/workspaceFiles convention.
1895
+ */
1896
+ async resolveGatewayAgentId(client) {
1897
+ if (this._gatewayAgentId)
1898
+ return this._gatewayAgentId;
1899
+ let agents = [];
1900
+ try {
1901
+ agents = await client.agentsList();
1902
+ }
1903
+ catch {
1904
+ // fall through to the canonical default
1905
+ }
1906
+ const resolved = agents[0]?.id ?? 'main';
1907
+ this._gatewayAgentId = resolved;
1908
+ return resolved;
1909
+ }
1910
+ /**
1911
+ * Overload the base gateway seam with OpenClaw's `agents.files.*` — the actual
1912
+ * OpenClaw gateway file methods. The base `files`/`fileX` calling surface is
1913
+ * inherited unchanged; only the gateway writer is supplied here.
1914
+ */
1915
+ gatewayFileHooks() {
1916
+ return {
1917
+ withGateway: (fn) => this.withGateway(fn),
1918
+ resolveAgentId: (c) => this.resolveGatewayAgentId(c),
1919
+ };
1920
+ }
617
1921
  async gatewayStatus(options = {}) {
618
1922
  const client = await this.connect(options);
619
1923
  try {
@@ -665,6 +1969,64 @@ export class OpenClawAgent extends Agent {
665
1969
  client.close();
666
1970
  }
667
1971
  }
1972
+ async configureSlackRelay(options, gatewayOptions = {}) {
1973
+ const { accountId, ...connectOptions } = gatewayOptions;
1974
+ const client = await this.connect(connectOptions);
1975
+ try {
1976
+ if ('relay' in options) {
1977
+ await client.configureSlackRelay(options, accountId);
1978
+ }
1979
+ else {
1980
+ await client.configureSlackRelay({
1981
+ ...options,
1982
+ gatewayId: options.gatewayId ?? this.gatewayId ?? `agent:${this.id}`,
1983
+ });
1984
+ }
1985
+ }
1986
+ finally {
1987
+ client.close();
1988
+ }
1989
+ }
1990
+ async configureSlackSocket(config, options = {}) {
1991
+ const { accountId, ...gatewayOptions } = options;
1992
+ const client = await this.connect(gatewayOptions);
1993
+ try {
1994
+ await client.configureSlackSocket(config, accountId);
1995
+ }
1996
+ finally {
1997
+ client.close();
1998
+ }
1999
+ }
2000
+ async configureSlackHttp(config, options = {}) {
2001
+ const { accountId, ...gatewayOptions } = options;
2002
+ const client = await this.connect(gatewayOptions);
2003
+ try {
2004
+ await client.configureSlackHttp(config, accountId);
2005
+ }
2006
+ finally {
2007
+ client.close();
2008
+ }
2009
+ }
2010
+ async configureTelegram(config, options = {}) {
2011
+ const { accountId, ...gatewayOptions } = options;
2012
+ const client = await this.connect(gatewayOptions);
2013
+ try {
2014
+ await client.configureTelegram(config, accountId);
2015
+ }
2016
+ finally {
2017
+ client.close();
2018
+ }
2019
+ }
2020
+ async configureWhatsapp(config, options = {}) {
2021
+ const { accountId, ...gatewayOptions } = options;
2022
+ const client = await this.connect(gatewayOptions);
2023
+ try {
2024
+ await client.configureWhatsapp(config, accountId);
2025
+ }
2026
+ finally {
2027
+ client.close();
2028
+ }
2029
+ }
668
2030
  async configApply(config, options = {}) {
669
2031
  const client = await this.connect(options);
670
2032
  try {
@@ -692,6 +2054,15 @@ export class OpenClawAgent extends Agent {
692
2054
  client.close();
693
2055
  }
694
2056
  }
2057
+ async sessionsListResult(options = {}) {
2058
+ const client = await this.connect(options);
2059
+ try {
2060
+ return await client.sessionsListResult();
2061
+ }
2062
+ finally {
2063
+ client.close();
2064
+ }
2065
+ }
695
2066
  async *chatSend(message, sessionKey, options = {}) {
696
2067
  const client = await this.connect(options);
697
2068
  try {
@@ -706,7 +2077,25 @@ export class OpenClawAgent extends Agent {
706
2077
  async channelsStatus(options = {}) {
707
2078
  const client = await this.connect(options);
708
2079
  try {
709
- return await client.channelsStatus(options.probe ?? false, options.timeoutMs);
2080
+ return await client.channelsStatus(options.probe ?? false, options.timeoutMs, options.channel);
2081
+ }
2082
+ finally {
2083
+ client.close();
2084
+ }
2085
+ }
2086
+ async channelsStart(channel, accountId, options = {}) {
2087
+ const client = await this.connect(options);
2088
+ try {
2089
+ return await client.channelsStart(channel, accountId);
2090
+ }
2091
+ finally {
2092
+ client.close();
2093
+ }
2094
+ }
2095
+ async channelsStop(channel, accountId, options = {}) {
2096
+ const client = await this.connect(options);
2097
+ try {
2098
+ return await client.channelsStop(channel, accountId);
710
2099
  }
711
2100
  finally {
712
2101
  client.close();
@@ -741,6 +2130,7 @@ export class OpenClawAgent extends Agent {
741
2130
  return await client.webLoginWait({
742
2131
  timeoutMs: options.timeoutMs,
743
2132
  accountId: options.accountId,
2133
+ currentQrDataUrl: options.currentQrDataUrl,
744
2134
  });
745
2135
  }
746
2136
  finally {
@@ -841,7 +2231,7 @@ export class OpenClawAgent extends Agent {
841
2231
  async chatSendMessage(message, options = {}) {
842
2232
  const client = await this.connect(options);
843
2233
  try {
844
- return await client.sendChat(message, options.sessionKey ?? 'main', options.agentId, options.attachments);
2234
+ return await client.sendChat(message, options.sessionKey, options.agentId, options.attachments);
845
2235
  }
846
2236
  finally {
847
2237
  client.close();
@@ -984,12 +2374,39 @@ export class OpenClawAgent extends Agent {
984
2374
  client.close();
985
2375
  }
986
2376
  }
2377
+ async cronAdd(job, options = {}) {
2378
+ const client = await this.connect(options);
2379
+ try {
2380
+ return await client.cronAdd(job);
2381
+ }
2382
+ finally {
2383
+ client.close();
2384
+ }
2385
+ }
2386
+ async cronRemove(jobId, options = {}) {
2387
+ const client = await this.connect(options);
2388
+ try {
2389
+ await client.cronRemove(jobId);
2390
+ }
2391
+ finally {
2392
+ client.close();
2393
+ }
2394
+ }
2395
+ async cronRun(jobId, options = {}) {
2396
+ const client = await this.connect(options);
2397
+ try {
2398
+ return await client.cronRun(jobId);
2399
+ }
2400
+ finally {
2401
+ client.close();
2402
+ }
2403
+ }
987
2404
  }
988
2405
  export class OpenClawProAgent extends OpenClawAgent {
989
2406
  static fromDict(data) {
990
2407
  return new OpenClawProAgent({
991
2408
  ...agentStateFromDict(data),
992
- gatewayUrl: null,
2409
+ gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
993
2410
  gatewayToken: data.gateway_token ?? null,
994
2411
  });
995
2412
  }
@@ -1000,12 +2417,13 @@ export class Deployments {
1000
2417
  apiBase;
1001
2418
  agentsWsUrl;
1002
2419
  agentHttp;
1003
- constructor(http, agentApiKey, agentApiBase, agentsWsUrl) {
2420
+ constructor(http, agentApiKey, agentApiBase, agentsWsUrl, requestTimeout) {
1004
2421
  this.http = http;
1005
2422
  this.apiKey = agentApiKey || http.apiKey;
1006
2423
  this.apiBase = resolveAgentsApiBase(agentApiBase || getAgentsApiBaseUrl());
1007
2424
  this.agentsWsUrl = normalizeAgentsWsUrl(agentsWsUrl || getConfigValue('AGENTS_WS_URL') || defaultAgentsWsUrl(this.apiBase));
1008
- this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey) : http;
2425
+ const agentTimeout = requestTimeout ?? (http instanceof HTTPClient ? http.timeout : undefined);
2426
+ this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey, agentTimeout) : http;
1009
2427
  }
1010
2428
  get agentApiKey() {
1011
2429
  return this.apiKey;
@@ -1015,7 +2433,22 @@ export class Deployments {
1015
2433
  }
1016
2434
  hydrateAgent(data) {
1017
2435
  let agent;
1018
- if (isOpenClawProHydrationData(data)) {
2436
+ if (data.runtime === 'opencode') {
2437
+ agent = OpenCodeAgent.fromDict(data);
2438
+ }
2439
+ else if (data.runtime === 'codex') {
2440
+ agent = CodexAgent.fromDict(data);
2441
+ }
2442
+ else if (data.runtime === 'claude-code') {
2443
+ agent = ClaudeCodeAgent.fromDict(data);
2444
+ }
2445
+ else if (data.runtime === 'goose') {
2446
+ agent = GooseAgent.fromDict(data);
2447
+ }
2448
+ else if (data.runtime === 'kimi-code') {
2449
+ agent = KimiCodeAgent.fromDict(data);
2450
+ }
2451
+ else if (data.runtime === 'openclaw-pro' || isOpenClawProHydrationData(data)) {
1019
2452
  agent = OpenClawProAgent.fromDict(data);
1020
2453
  }
1021
2454
  else if (isOpenClawHydrationData(data)) {
@@ -1026,8 +2459,52 @@ export class Deployments {
1026
2459
  }
1027
2460
  return bindAgent(agent, this);
1028
2461
  }
1029
- agentIdFor(target) {
1030
- return typeof target === 'string' ? target : target.id;
2462
+ async getById(agentId, requestOptions = {}) {
2463
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}`;
2464
+ const data = Object.keys(requestOptions).length === 0
2465
+ ? await this.agentHttp.get(path)
2466
+ : await this.agentHttp.get(path, undefined, requestOptions);
2467
+ return this.hydrateAgent(data);
2468
+ }
2469
+ async resolveAgent(agentIdOrName, requestOptions = {}) {
2470
+ const raw = String(agentIdOrName || '').trim();
2471
+ if (!raw) {
2472
+ throw new Error('agentIdOrName is required');
2473
+ }
2474
+ if (isUuidRef(raw)) {
2475
+ return this.getById(raw, requestOptions);
2476
+ }
2477
+ const matches = [];
2478
+ for (const agent of await this.list(requestOptions)) {
2479
+ const values = [agent.id, agent.name, agent.handle, agent.podName, agent.hostname];
2480
+ if (values.some((value) => String(value || '') === raw)) {
2481
+ matches.push(agent);
2482
+ continue;
2483
+ }
2484
+ if (values.some((value) => String(value || '').startsWith(raw))) {
2485
+ matches.push(agent);
2486
+ }
2487
+ }
2488
+ if (matches.length === 0) {
2489
+ throw new Error(`Agent not found: ${raw}`);
2490
+ }
2491
+ if (matches.length > 1) {
2492
+ throw new Error(`Agent reference is ambiguous: ${raw} (${matches.slice(0, 5).map((agent) => agent.id).join(', ')})`);
2493
+ }
2494
+ return this.getById(matches[0].id, requestOptions);
2495
+ }
2496
+ async resolveAgentId(agentIdOrName, requestOptions = {}) {
2497
+ const raw = String(agentIdOrName || '').trim();
2498
+ if (!raw) {
2499
+ throw new Error('agentIdOrName is required');
2500
+ }
2501
+ if (isDirectAgentIdRef(raw)) {
2502
+ return raw;
2503
+ }
2504
+ return (await this.resolveAgent(raw, requestOptions)).id;
2505
+ }
2506
+ async agentIdFor(target) {
2507
+ return typeof target === 'string' ? this.resolveAgentId(target) : target.id;
1031
2508
  }
1032
2509
  async fetchRaw(path, init = {}) {
1033
2510
  const headers = new Headers(init.headers ?? {});
@@ -1056,18 +2533,25 @@ export class Deployments {
1056
2533
  return response;
1057
2534
  }
1058
2535
  async create(options = {}) {
1059
- const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, options);
2536
+ const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, {
2537
+ ...options,
2538
+ injectGatewayToken: options.injectGatewayToken ?? !isCodingAgentRuntime(options.runtime),
2539
+ });
1060
2540
  const body = { ...config, start: options.start ?? true };
1061
2541
  if (options.dryRun)
1062
2542
  body.dry_run = true;
1063
2543
  if (options.name)
1064
2544
  body.name = options.name;
2545
+ if (options.handle !== undefined)
2546
+ body.handle = options.handle;
1065
2547
  if (options.size)
1066
2548
  body.size = options.size;
1067
2549
  if (options.meta?.ui)
1068
2550
  body.meta = { ui: structuredClone(options.meta.ui) };
1069
2551
  if (options.tags?.length)
1070
2552
  body.tags = [...options.tags];
2553
+ if (options.runtime)
2554
+ body.runtime = options.runtime;
1071
2555
  const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body);
1072
2556
  const agent = this.hydrateAgent(data);
1073
2557
  if (agent instanceof OpenClawAgent) {
@@ -1079,8 +2563,13 @@ export class Deployments {
1079
2563
  return agent;
1080
2564
  }
1081
2565
  async createOpenClaw(options = {}) {
1082
- const effectiveOptions = { ...options };
1083
- effectiveOptions.env = { ...buildOpenClawMemoryIndexEnv(options.memoryIndex), ...(options.env ?? {}) };
2566
+ const effectiveOptions = { ...options, runtime: options.runtime ?? 'openclaw' };
2567
+ effectiveOptions.env = {
2568
+ HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2569
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2570
+ ...buildOpenClawMemoryIndexEnv(options.memoryIndex),
2571
+ ...(options.env ?? {}),
2572
+ };
1084
2573
  if (options.routes === undefined) {
1085
2574
  effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
1086
2575
  }
@@ -1094,47 +2583,226 @@ export class Deployments {
1094
2583
  async createOpenClawPro(options = {}) {
1095
2584
  return this.createOpenClaw({
1096
2585
  ...options,
2586
+ runtime: 'openclaw-pro',
1097
2587
  env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
1098
2588
  image: defaultOpenClawProImage(options.image),
1099
2589
  openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
1100
2590
  });
1101
2591
  }
2592
+ async createCodingAgent(runtime, options) {
2593
+ if (options.buzzEnabled && options.buzz) {
2594
+ throw new Error('buzzEnabled cannot be combined with buzz');
2595
+ }
2596
+ if ((options.buzzEnabled || options.buzz) && options.command !== undefined && options.command !== null) {
2597
+ throw new Error('Buzz launch cannot be combined with an explicit command');
2598
+ }
2599
+ const buzzLaunch = options.buzzEnabled || options.buzz !== undefined && options.buzz !== null;
2600
+ if (buzzLaunch && options.size !== undefined && options.size !== 'large') {
2601
+ throw new Error("Buzz coding agents require size='large'");
2602
+ }
2603
+ const effectiveEnv = {
2604
+ HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2605
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2606
+ ...(options.env ?? {}),
2607
+ };
2608
+ if (options.buzz) {
2609
+ for (const key of BUZZ_RESERVED_ENV_KEYS)
2610
+ delete effectiveEnv[key];
2611
+ Object.assign(effectiveEnv, buildBuzzLaunchEnv(runtime, options.buzz, options.name));
2612
+ }
2613
+ if (buzzLaunch) {
2614
+ effectiveEnv.RUST_LOG ??= DEFAULT_BUZZ_RUST_LOG;
2615
+ }
2616
+ const effectiveOptions = {
2617
+ ...options,
2618
+ runtime,
2619
+ size: buzzLaunch ? 'large' : options.size,
2620
+ injectGatewayToken: false,
2621
+ env: effectiveEnv,
2622
+ routes: options.routes ?? {},
2623
+ image: options.image ?? (buzzLaunch
2624
+ ? DEFAULT_BUZZ_CODING_AGENT_IMAGES[runtime]
2625
+ : DEFAULT_CODING_AGENT_IMAGES[runtime]),
2626
+ command: options.buzzEnabled || options.buzz
2627
+ ? ['/usr/local/bin/buzz-acp']
2628
+ : options.command,
2629
+ syncRoot: options.syncRoot ?? DEFAULT_CODING_AGENT_SYNC_ROOT,
2630
+ syncEnabled: options.syncEnabled ?? true,
2631
+ syncUid: options.syncUid ?? 1000,
2632
+ syncGid: options.syncGid ?? 1000,
2633
+ restart: buzzLaunch
2634
+ ? options.restart ?? options.buzz?.restart ?? false
2635
+ : options.restart,
2636
+ };
2637
+ return await this.create(effectiveOptions);
2638
+ }
2639
+ async createOpenCode(options = {}) {
2640
+ return await this.createCodingAgent('opencode', options);
2641
+ }
2642
+ async createCodex(options = {}) {
2643
+ return await this.createCodingAgent('codex', options);
2644
+ }
2645
+ async createClaudeCode(options = {}) {
2646
+ return await this.createCodingAgent('claude-code', options);
2647
+ }
2648
+ async createGoose(options = {}) {
2649
+ return await this.createCodingAgent('goose', options);
2650
+ }
2651
+ async createKimiCode(options = {}) {
2652
+ return await this.createCodingAgent('kimi-code', options);
2653
+ }
1102
2654
  async budget() {
1103
2655
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/budget`);
1104
2656
  }
1105
- async metrics(agentId) {
2657
+ async bootstrapInference(messages, responseFormat = { type: 'json_object' }, requestOptions = {}) {
2658
+ return this.agentHttp.post('/bootstrap', {
2659
+ messages,
2660
+ response_format: responseFormat,
2661
+ }, requestOptions);
2662
+ }
2663
+ async metrics(agentIdOrName) {
2664
+ const agentId = await this.resolveAgentId(agentIdOrName);
1106
2665
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/metrics`);
1107
2666
  }
1108
- async list() {
1109
- const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX);
2667
+ async list(options = {}) {
2668
+ const params = {};
2669
+ if (options.state)
2670
+ params.state = options.state;
2671
+ if (options.handle)
2672
+ params.handle = options.handle;
2673
+ if (options.name)
2674
+ params.name = options.name;
2675
+ if (options.query)
2676
+ params.q = options.query;
2677
+ if (options.includeDeleted !== undefined && options.includeDeleted !== null) {
2678
+ params.include_deleted = options.includeDeleted ? 'true' : 'false';
2679
+ }
2680
+ const requestOptions = {
2681
+ retries: options.retries,
2682
+ backoff: options.backoff,
2683
+ timeout: options.timeout,
2684
+ signal: options.signal,
2685
+ retryStatuses: options.retryStatuses,
2686
+ };
2687
+ const cleanRequestOptions = {};
2688
+ for (const key of Object.keys(requestOptions)) {
2689
+ const value = requestOptions[key];
2690
+ if (value !== undefined) {
2691
+ cleanRequestOptions[key] = value;
2692
+ }
2693
+ }
2694
+ const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX, Object.keys(params).length ? params : undefined, Object.keys(cleanRequestOptions).length ? cleanRequestOptions : undefined);
1110
2695
  const items = Array.isArray(data) ? data : data.items ?? [];
1111
2696
  return items.map((item) => this.hydrateAgent(item));
1112
2697
  }
1113
- async get(agentId) {
1114
- const data = await this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
2698
+ async get(agentIdOrName, requestOptions = {}) {
2699
+ const raw = String(agentIdOrName || '').trim();
2700
+ if (!raw)
2701
+ throw new Error('agentIdOrName is required');
2702
+ if (!isDirectAgentIdRef(raw)) {
2703
+ return this.resolveAgent(raw, requestOptions);
2704
+ }
2705
+ try {
2706
+ return await this.getById(raw, requestOptions);
2707
+ }
2708
+ catch (error) {
2709
+ if (!(error instanceof APIError) || ![404, 422].includes(error.statusCode) || isUuidRef(raw)) {
2710
+ throw error;
2711
+ }
2712
+ return this.resolveAgent(raw, requestOptions);
2713
+ }
2714
+ }
2715
+ async createExternalAgent(options) {
2716
+ const body = {
2717
+ name: options.name,
2718
+ runtime: options.runtime ?? 'openclaw',
2719
+ status: options.status ?? 'active',
2720
+ };
2721
+ if (options.displayName !== undefined)
2722
+ body.display_name = options.displayName;
2723
+ if (options.handle !== undefined)
2724
+ body.handle = options.handle;
2725
+ if (options.meta !== undefined)
2726
+ body.meta = options.meta;
2727
+ const data = await this.agentHttp.post('/external-agents', body);
1115
2728
  return this.hydrateAgent(data);
1116
2729
  }
1117
- async waitRunning(agentId, timeoutMs = 300_000, pollIntervalMs = 5_000) {
2730
+ async updateExternalAgent(externalAgentId, options) {
2731
+ const body = {};
2732
+ if (options.name !== undefined)
2733
+ body.name = options.name;
2734
+ if (options.displayName !== undefined)
2735
+ body.display_name = options.displayName;
2736
+ if (options.handle !== undefined)
2737
+ body.handle = options.handle;
2738
+ if (options.runtime !== undefined)
2739
+ body.runtime = options.runtime;
2740
+ if (options.status !== undefined)
2741
+ body.status = options.status;
2742
+ if (options.meta !== undefined)
2743
+ body.meta = options.meta;
2744
+ const data = await this.agentHttp.patch(`/external-agents/${externalAgentId}`, body);
2745
+ return this.hydrateAgent(data);
2746
+ }
2747
+ async getExternalAgent(externalAgentId) {
2748
+ const data = await this.agentHttp.get(`/external-agents/${externalAgentId}`);
2749
+ return this.hydrateAgent(data);
2750
+ }
2751
+ async uploadExternalAgentProfileImage(externalAgentId, content, contentType) {
2752
+ const resolvedContentType = contentType || (content instanceof Blob ? content.type : '') || 'image/png';
2753
+ return this.agentHttp.postRaw(`/external-agents/${externalAgentId}/profile-image`, content, resolvedContentType);
2754
+ }
2755
+ async deleteExternalAgentProfileImage(externalAgentId) {
2756
+ return this.agentHttp.delete(`/external-agents/${externalAgentId}/profile-image`);
2757
+ }
2758
+ async rotateExternalAgentKey(agentIdOrName) {
2759
+ const agentId = await this.resolveAgentId(agentIdOrName);
2760
+ return this.agentHttp.post(`/external-agents/${agentId}/keys/rotate`);
2761
+ }
2762
+ async attachSlackRelayAgent(agentIdOrName, options) {
2763
+ const agentId = await this.resolveAgentId(agentIdOrName);
2764
+ return attachSlackRelayAgent({
2765
+ relayBaseUrl: options.relayBaseUrl,
2766
+ token: options.token || this.apiKey,
2767
+ agentId,
2768
+ });
2769
+ }
2770
+ async waitRunning(agentIdOrName, timeoutMs = 300_000, pollIntervalMs = 5_000) {
2771
+ const agentId = await this.resolveAgentId(agentIdOrName);
1118
2772
  const deadline = Date.now() + timeoutMs;
1119
2773
  let lastState = '';
2774
+ let lastAgent = null;
2775
+ const diagnostics = (agent) => {
2776
+ if (!agent)
2777
+ return '';
2778
+ const details = [];
2779
+ if (agent.lastError)
2780
+ details.push(`lastError=${JSON.stringify(agent.lastError)}`);
2781
+ if (agent.updatedAt && !Number.isNaN(agent.updatedAt.getTime())) {
2782
+ details.push(`updatedAt=${agent.updatedAt.toISOString()}`);
2783
+ }
2784
+ return details.length ? `, ${details.join(', ')}` : '';
2785
+ };
1120
2786
  while (Date.now() < deadline) {
1121
2787
  const agent = await this.get(agentId);
2788
+ lastAgent = agent;
1122
2789
  lastState = String(agent.state || '');
1123
2790
  if (lastState.toLowerCase() === 'running') {
1124
2791
  return agent;
1125
2792
  }
1126
- if (lastState.toLowerCase() === 'failed' || lastState.toLowerCase() === 'error') {
1127
- throw new Error(`Agent entered ${lastState} while waiting for RUNNING`);
2793
+ if (['failed', 'error', 'restore_failed', 'sync_failed'].includes(lastState.toLowerCase())) {
2794
+ throw new Error(`Agent entered ${lastState} while waiting for RUNNING${diagnostics(agent)}`);
1128
2795
  }
1129
2796
  await sleep(pollIntervalMs);
1130
2797
  }
1131
- throw new Error(`Timed out waiting for agent ${agentId} to reach RUNNING (last=${lastState})`);
2798
+ throw new Error(`Timed out waiting for agent ${agentId} to reach RUNNING (last=${lastState || 'unknown'}${diagnostics(lastAgent)})`);
1132
2799
  }
1133
- async start(agentId, options = {}) {
2800
+ async start(agentIdOrName, options = {}) {
1134
2801
  const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, options);
1135
2802
  const body = { ...config };
1136
2803
  if (options.dryRun)
1137
2804
  body.dry_run = true;
2805
+ const agentId = await this.resolveAgentId(agentIdOrName);
1138
2806
  const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, body);
1139
2807
  const agent = this.hydrateAgent(data);
1140
2808
  if (agent instanceof OpenClawAgent) {
@@ -1145,9 +2813,14 @@ export class Deployments {
1145
2813
  agent.entrypoint = [...(config.entrypoint ?? [])];
1146
2814
  return agent;
1147
2815
  }
1148
- async startOpenClaw(agentId, options = {}) {
2816
+ async startOpenClaw(agentIdOrName, options = {}) {
1149
2817
  const effectiveOptions = { ...options };
1150
- effectiveOptions.env = { ...buildOpenClawMemoryIndexEnv(options.memoryIndex), ...(options.env ?? {}) };
2818
+ effectiveOptions.env = {
2819
+ HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2820
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2821
+ ...buildOpenClawMemoryIndexEnv(options.memoryIndex),
2822
+ ...(options.env ?? {}),
2823
+ };
1151
2824
  if (options.routes === undefined) {
1152
2825
  effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
1153
2826
  }
@@ -1156,46 +2829,71 @@ export class Deployments {
1156
2829
  effectiveOptions.syncRoot = DEFAULT_OPENCLAW_SYNC_ROOT;
1157
2830
  if (effectiveOptions.syncEnabled === undefined)
1158
2831
  effectiveOptions.syncEnabled = true;
1159
- return this.start(agentId, effectiveOptions);
2832
+ return this.start(agentIdOrName, effectiveOptions);
1160
2833
  }
1161
- async startOpenClawPro(agentId, options = {}) {
1162
- return this.startOpenClaw(agentId, {
2834
+ async startOpenClawPro(agentIdOrName, options = {}) {
2835
+ return this.startOpenClaw(agentIdOrName, {
1163
2836
  ...options,
1164
2837
  env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
1165
2838
  image: defaultOpenClawProImage(options.image),
1166
2839
  openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
1167
2840
  });
1168
2841
  }
1169
- async update(agentId, options = {}) {
2842
+ async update(agentIdOrName, options = {}) {
1170
2843
  const body = {};
1171
2844
  if (options.name !== undefined)
1172
2845
  body.name = options.name;
2846
+ if (options.handle !== undefined)
2847
+ body.handle = options.handle;
1173
2848
  if (options.size !== undefined)
1174
2849
  body.size = options.size;
2850
+ if (options.launchConfig !== undefined)
2851
+ body.launch_config = options.launchConfig;
1175
2852
  if (options.refreshFromLagoon !== undefined)
1176
2853
  body.refresh_from_lagoon = options.refreshFromLagoon;
1177
2854
  if (options.lastError !== undefined)
1178
2855
  body.last_error = options.lastError;
2856
+ const agentId = await this.resolveAgentId(agentIdOrName);
1179
2857
  const data = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}`, body);
1180
2858
  return this.hydrateAgent(data);
1181
2859
  }
2860
+ async uploadProfileImage(agentId, content, contentType) {
2861
+ const resolvedAgentId = await this.resolveAgentId(agentId);
2862
+ const resolvedContentType = contentType || (content instanceof Blob ? content.type : '') || 'image/png';
2863
+ return this.agentHttp.postRaw(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`, content, resolvedContentType);
2864
+ }
2865
+ async deleteProfileImage(agentId) {
2866
+ const resolvedAgentId = await this.resolveAgentId(agentId);
2867
+ return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`);
2868
+ }
1182
2869
  async resize(agentId, options) {
1183
2870
  return this.update(agentId, options);
1184
2871
  }
1185
- async stop(agentId) {
2872
+ /**
2873
+ * Request an agent stop.
2874
+ *
2875
+ * The returned agent remains `STOPPING` while runtime cleanup is in
2876
+ * progress. Fetch it again until it becomes `STOPPED` before treating its
2877
+ * deployment slot as released.
2878
+ */
2879
+ async stop(agentIdOrName) {
2880
+ const agentId = await this.resolveAgentId(agentIdOrName);
1186
2881
  const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/stop`);
1187
2882
  return this.hydrateAgent(data);
1188
2883
  }
1189
- async delete(agentId) {
2884
+ async delete(agentIdOrName) {
2885
+ const agentId = await this.resolveAgentId(agentIdOrName);
1190
2886
  return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
1191
2887
  }
1192
- async refreshToken(agentId) {
2888
+ async refreshToken(agentIdOrName) {
2889
+ const agentId = await this.resolveAgentId(agentIdOrName);
1193
2890
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/token`);
1194
2891
  }
1195
- async createScopedKey(agentId, name) {
2892
+ async createScopedKey(agentIdOrName, name) {
1196
2893
  const payload = {};
1197
2894
  if (name)
1198
2895
  payload.name = name;
2896
+ const agentId = await this.resolveAgentId(agentIdOrName);
1199
2897
  return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/keys`, Object.keys(payload).length ? payload : undefined);
1200
2898
  }
1201
2899
  async webSearch(query, options = {}) {
@@ -1218,14 +2916,19 @@ export class Deployments {
1218
2916
  const response = await this.fetchRaw(`/brave/res/v1/web/search?${new URLSearchParams(Object.fromEntries(Object.entries(params).map(([key, value]) => [key, String(value)]))).toString()}`, { headers });
1219
2917
  return (await response.json());
1220
2918
  }
1221
- async logsToken(agentId) {
2919
+ async logsToken(agentIdOrName) {
2920
+ const agentId = await this.resolveAgentId(agentIdOrName);
1222
2921
  return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/logs/token`);
1223
2922
  }
1224
- async env(agentId) {
1225
- return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/env`);
2923
+ async env(agentIdOrName, requestOptions = {}) {
2924
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
2925
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/env`;
2926
+ return Object.keys(requestOptions).length === 0
2927
+ ? this.agentHttp.get(path)
2928
+ : this.agentHttp.get(path, undefined, requestOptions);
1226
2929
  }
1227
2930
  async exec(target, command, options = {}) {
1228
- const agentId = this.agentIdFor(target);
2931
+ const agentId = await this.agentIdFor(target);
1229
2932
  const payload = {
1230
2933
  command,
1231
2934
  timeout: options.timeout ?? 30,
@@ -1251,20 +2954,20 @@ export class Deployments {
1251
2954
  return (await response.json());
1252
2955
  }
1253
2956
  async filesList(target, path = '', source = 'auto') {
1254
- const encodedPath = encodeFilePath(path);
1255
- const suffix = encodedPath ? `/${encodedPath}` : '';
1256
- const params = new URLSearchParams({ source });
1257
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files${suffix}?${params.toString()}`);
2957
+ const { suffix, params } = deploymentFileReadRequest(path, source);
2958
+ const agentId = await this.agentIdFor(target);
2959
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files${suffix}?${params.toString()}`);
1258
2960
  const payload = (await response.json());
1259
2961
  return [...(payload.directories ?? []), ...(payload.files ?? [])];
1260
2962
  }
1261
- async fileReadBytes(target, path, source = 'auto') {
1262
- const encodedPath = encodeFilePath(path);
1263
- const params = new URLSearchParams({ source });
1264
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}?${params.toString()}`, {
2963
+ async fileReadBytesWithMetadata(target, path, source = 'auto', options) {
2964
+ const { suffix, params } = deploymentFileReadRequest(path, source);
2965
+ const agentId = await this.agentIdFor(target);
2966
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files${suffix}?${params.toString()}`, {
1265
2967
  redirect: 'follow',
2968
+ signal: options?.signal,
1266
2969
  });
1267
- const bytes = new Uint8Array(await response.arrayBuffer());
2970
+ const bytes = await readResponseBytes(response, path, options?.maxBytes);
1268
2971
  const contentType = response.headers.get('content-type') || '';
1269
2972
  if (contentType.includes('application/json')) {
1270
2973
  try {
@@ -1279,19 +2982,24 @@ export class Deployments {
1279
2982
  }
1280
2983
  }
1281
2984
  }
1282
- return bytes;
2985
+ return { content: bytes, mimeType: contentType || undefined };
2986
+ }
2987
+ async fileReadBytes(target, path, source = 'auto', options) {
2988
+ return (await this.fileReadBytesWithMetadata(target, path, source, options)).content;
1283
2989
  }
1284
- async fileRead(target, path, source = 'auto') {
1285
- return decodeUtf8(await this.fileReadBytes(target, path, source));
2990
+ async fileRead(target, path, source = 'auto', options) {
2991
+ return decodeUtf8(await this.fileReadBytes(target, path, source, options));
1286
2992
  }
1287
2993
  async fileWriteBytes(target, path, content, destination = 'auto') {
2994
+ path = normalizeWritableBackendFilePath(path);
1288
2995
  const encodedPath = encodeFilePath(path);
1289
2996
  const params = new URLSearchParams({ destination });
1290
2997
  const bytes = toUint8Array(content);
1291
2998
  if (bytes.byteLength > AGENT_FILE_MAX_BYTES) {
1292
2999
  throw new Error(`Agent file writes are limited to ${AGENT_FILE_MAX_BYTES / 1024 / 1024} MiB`);
1293
3000
  }
1294
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}?${params.toString()}`, {
3001
+ const agentId = await this.agentIdFor(target);
3002
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}?${params.toString()}`, {
1295
3003
  method: 'POST',
1296
3004
  headers: { 'Content-Type': 'application/octet-stream' },
1297
3005
  body: bytes,
@@ -1302,12 +3010,16 @@ export class Deployments {
1302
3010
  return this.fileWriteBytes(target, path, content, destination);
1303
3011
  }
1304
3012
  async fileDelete(target, path, options = {}) {
3013
+ path = normalizeWritableBackendFilePath(path);
1305
3014
  const encodedPath = encodeFilePath(path);
1306
3015
  const params = new URLSearchParams();
1307
3016
  if (options.recursive)
1308
3017
  params.set('recursive', 'true');
3018
+ if (options.source && options.source !== 'auto')
3019
+ params.set('source', options.source);
1309
3020
  const suffix = params.toString() ? `?${params.toString()}` : '';
1310
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}${suffix}`, { method: 'DELETE' });
3021
+ const agentId = await this.agentIdFor(target);
3022
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}${suffix}`, { method: 'DELETE' });
1311
3023
  return (await response.json());
1312
3024
  }
1313
3025
  async cpTo(target, localPath, remotePath) {
@@ -1326,7 +3038,8 @@ export class Deployments {
1326
3038
  await fs.writeFile(destination, content);
1327
3039
  return destination;
1328
3040
  }
1329
- async logsConnect(agentId, options = {}) {
3041
+ async logsConnect(agentIdOrName, options = {}) {
3042
+ const agentId = await this.resolveAgentId(agentIdOrName);
1330
3043
  const tokenData = await this.logsToken(agentId);
1331
3044
  const container = options.container ?? 'reef';
1332
3045
  const tailLines = options.tailLines ?? 100;
@@ -1348,31 +3061,91 @@ export class Deployments {
1348
3061
  };
1349
3062
  });
1350
3063
  }
1351
- async shellToken(agentId, shell, dryRun = false) {
3064
+ async shellToken(agentIdOrName, shell, dryRun = false, requestOptions = {}) {
1352
3065
  const selectedShell = shell ?? '/bin/bash';
1353
3066
  const payload = { shell: selectedShell };
1354
3067
  if (dryRun)
1355
3068
  payload.dry_run = true;
1356
- return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/shell/token`, payload);
1357
- }
1358
- async shellConnect(agentId, shell) {
3069
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3070
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/shell/token`;
3071
+ if (Object.keys(requestOptions).length === 0)
3072
+ return this.agentHttp.post(path, payload);
3073
+ return this.agentHttp.post(path, payload, requestOptions);
3074
+ }
3075
+ async shellConnect(agentIdOrName, shell, options = {}) {
3076
+ if (options.signal?.aborted)
3077
+ throw shellAbortError();
3078
+ const tokenTimeoutMs = options.tokenTimeoutMs ?? 15_000;
3079
+ const openTimeoutMs = options.openTimeoutMs ?? 10_000;
3080
+ const tokenDeadline = Date.now() + tokenTimeoutMs;
3081
+ const remainingTokenTime = () => Math.max(0, tokenDeadline - Date.now());
3082
+ const runBeforeTokenDeadline = (operation) => {
3083
+ const remaining = remainingTokenTime();
3084
+ if (remaining <= 0)
3085
+ return Promise.reject(new Error('Shell token request timed out'));
3086
+ return runShellOperation(operation, options.signal, remaining, 'Shell token request timed out');
3087
+ };
3088
+ const agentId = await runBeforeTokenDeadline((signal) => this.resolveAgentId(agentIdOrName, { signal }));
1359
3089
  const connectWithShell = async (requestedShell) => {
1360
- const tokenData = await this.shellToken(agentId, requestedShell);
3090
+ const remaining = remainingTokenTime();
3091
+ if (remaining <= 0)
3092
+ throw new Error('Shell token request timed out');
3093
+ const requestOptions = {
3094
+ retries: 3,
3095
+ timeout: Math.max(500, Math.floor((tokenTimeoutMs - 3_000) / 3)),
3096
+ retryStatuses: [429, 502, 503, 504],
3097
+ };
3098
+ const tokenData = await runBeforeTokenDeadline((signal) => {
3099
+ requestOptions.signal = signal;
3100
+ return this.shellToken(agentId, requestedShell, false, requestOptions);
3101
+ });
1361
3102
  const baseUrl = `${this.agentsWsUrl}/shell/${agentId}`;
1362
3103
  const separator = baseUrl.includes("?") ? "&" : "?";
1363
3104
  const wsUrl = `${baseUrl}${separator}jwt=${encodeURIComponent(tokenData.jwt)}` +
1364
3105
  `&shell=${encodeURIComponent(tokenData.shell || requestedShell)}`;
1365
3106
  const ws = new WebSocket(wsUrl);
3107
+ ws.binaryType = 'arraybuffer';
1366
3108
  return await new Promise((resolve, reject) => {
1367
3109
  let settled = false;
1368
- ws.onopen = () => {
1369
- settled = true;
1370
- resolve(ws);
3110
+ const abortConnection = () => finish(shellAbortError());
3111
+ const openTimer = setTimeout(() => finish(new Error('Shell connection timed out')), openTimeoutMs);
3112
+ const cleanup = () => {
3113
+ clearTimeout(openTimer);
3114
+ options.signal?.removeEventListener('abort', abortConnection);
1371
3115
  };
1372
- ws.onerror = () => {
1373
- if (!settled) {
1374
- reject(new Error('WebSocket connection failed'));
3116
+ const finish = (error) => {
3117
+ if (settled)
3118
+ return;
3119
+ settled = true;
3120
+ cleanup();
3121
+ if (error) {
3122
+ try {
3123
+ ws.close();
3124
+ }
3125
+ catch {
3126
+ // The browser may have already finalized a failed socket.
3127
+ }
3128
+ reject(error);
1375
3129
  }
3130
+ else {
3131
+ resolve(ws);
3132
+ }
3133
+ };
3134
+ if (options.signal?.aborted) {
3135
+ finish(shellAbortError());
3136
+ return;
3137
+ }
3138
+ options.signal?.addEventListener('abort', abortConnection, { once: true });
3139
+ ws.onopen = () => {
3140
+ finish();
3141
+ };
3142
+ ws.onerror = () => undefined;
3143
+ ws.onclose = (event) => {
3144
+ const reason = event.reason ? `: ${event.reason}` : '';
3145
+ const error = new Error(`WebSocket closed before opening${reason}`);
3146
+ error.closeCode = event.code;
3147
+ error.closeReason = event.reason;
3148
+ finish(error);
1376
3149
  };
1377
3150
  });
1378
3151
  };
@@ -1382,7 +3155,14 @@ export class Deployments {
1382
3155
  try {
1383
3156
  return await connectWithShell('/bin/bash');
1384
3157
  }
1385
- catch {
3158
+ catch (error) {
3159
+ if (options.signal?.aborted)
3160
+ throw error;
3161
+ const detail = error instanceof APIError ? error.detail : error instanceof Error ? error.message : '';
3162
+ const bashUnavailable = /(\/bin\/bash|\bbash\b)/i.test(detail)
3163
+ && /(not found|no such file|missing|unavailable|unsupported)/i.test(detail);
3164
+ if (!bashUnavailable)
3165
+ throw error;
1386
3166
  return connectWithShell('/bin/sh');
1387
3167
  }
1388
3168
  }