@hypercli.com/sdk 2026.6.26 → 2026.8.11

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 (106) hide show
  1. package/README.md +181 -1
  2. package/dist/agent-slots.d.ts +17 -0
  3. package/dist/agent-slots.d.ts.map +1 -0
  4. package/dist/agent-slots.js +18 -0
  5. package/dist/agent-slots.js.map +1 -0
  6. package/dist/agent.d.ts +67 -12
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +220 -28
  9. package/dist/agent.js.map +1 -1
  10. package/dist/agents.d.ts +690 -82
  11. package/dist/agents.d.ts.map +1 -1
  12. package/dist/agents.js +2595 -225
  13. package/dist/agents.js.map +1 -1
  14. package/dist/browser.d.ts +4 -2
  15. package/dist/browser.d.ts.map +1 -1
  16. package/dist/browser.js +10 -3
  17. package/dist/browser.js.map +1 -1
  18. package/dist/channels.d.ts +156 -0
  19. package/dist/channels.d.ts.map +1 -0
  20. package/dist/channels.js +110 -0
  21. package/dist/channels.js.map +1 -0
  22. package/dist/client.d.ts +2 -0
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +14 -4
  25. package/dist/client.js.map +1 -1
  26. package/dist/config.d.ts +1 -1
  27. package/dist/config.js +2 -2
  28. package/dist/config.js.map +1 -1
  29. package/dist/connectors.d.ts +79 -0
  30. package/dist/connectors.d.ts.map +1 -0
  31. package/dist/connectors.js +2 -0
  32. package/dist/connectors.js.map +1 -0
  33. package/dist/errors.d.ts +4 -1
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +9 -2
  36. package/dist/errors.js.map +1 -1
  37. package/dist/hermes/gateway.d.ts +194 -0
  38. package/dist/hermes/gateway.d.ts.map +1 -0
  39. package/dist/hermes/gateway.js +209 -0
  40. package/dist/hermes/gateway.js.map +1 -0
  41. package/dist/http.d.ts +16 -3
  42. package/dist/http.d.ts.map +1 -1
  43. package/dist/http.js +196 -63
  44. package/dist/http.js.map +1 -1
  45. package/dist/index.d.ts +15 -5
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +13 -4
  48. package/dist/index.js.map +1 -1
  49. package/dist/job/base.d.ts +6 -0
  50. package/dist/job/base.d.ts.map +1 -1
  51. package/dist/job/base.js +7 -1
  52. package/dist/job/base.js.map +1 -1
  53. package/dist/keys.d.ts +21 -5
  54. package/dist/keys.d.ts.map +1 -1
  55. package/dist/keys.js +62 -3
  56. package/dist/keys.js.map +1 -1
  57. package/dist/openclaw/channels.d.ts +86 -0
  58. package/dist/openclaw/channels.d.ts.map +1 -0
  59. package/dist/openclaw/channels.js +290 -0
  60. package/dist/openclaw/channels.js.map +1 -0
  61. package/dist/openclaw/connectors.d.ts +61 -0
  62. package/dist/openclaw/connectors.d.ts.map +1 -0
  63. package/dist/openclaw/connectors.js +174 -0
  64. package/dist/openclaw/connectors.js.map +1 -0
  65. package/dist/openclaw/gateway.d.ts +682 -16
  66. package/dist/openclaw/gateway.d.ts.map +1 -1
  67. package/dist/openclaw/gateway.js +1794 -187
  68. package/dist/openclaw/gateway.js.map +1 -1
  69. package/dist/openclaw/skills.d.ts +106 -0
  70. package/dist/openclaw/skills.d.ts.map +1 -0
  71. package/dist/openclaw/skills.js +575 -0
  72. package/dist/openclaw/skills.js.map +1 -0
  73. package/dist/openclaw/slack.d.ts +818 -0
  74. package/dist/openclaw/slack.d.ts.map +1 -0
  75. package/dist/openclaw/slack.js +536 -0
  76. package/dist/openclaw/slack.js.map +1 -0
  77. package/dist/openclaw/whatsapp.d.ts +63 -0
  78. package/dist/openclaw/whatsapp.d.ts.map +1 -0
  79. package/dist/openclaw/whatsapp.js +264 -0
  80. package/dist/openclaw/whatsapp.js.map +1 -0
  81. package/dist/renders.js +11 -11
  82. package/dist/renders.js.map +1 -1
  83. package/dist/skills.d.ts +126 -0
  84. package/dist/skills.d.ts.map +1 -0
  85. package/dist/skills.js +2 -0
  86. package/dist/skills.js.map +1 -0
  87. package/dist/user.d.ts +15 -2
  88. package/dist/user.d.ts.map +1 -1
  89. package/dist/user.js +51 -4
  90. package/dist/user.js.map +1 -1
  91. package/dist/voice-session.d.ts +74 -0
  92. package/dist/voice-session.d.ts.map +1 -0
  93. package/dist/voice-session.js +255 -0
  94. package/dist/voice-session.js.map +1 -0
  95. package/dist/voice.d.ts +27 -3
  96. package/dist/voice.d.ts.map +1 -1
  97. package/dist/voice.js +89 -6
  98. package/dist/voice.js.map +1 -1
  99. package/dist/workspaces.d.ts +181 -0
  100. package/dist/workspaces.d.ts.map +1 -0
  101. package/dist/workspaces.js +495 -0
  102. package/dist/workspaces.js.map +1 -0
  103. package/dist/x402.d.ts.map +1 -1
  104. package/dist/x402.js +1 -2
  105. package/dist/x402.js.map +1 -1
  106. package/package.json +42 -2
package/dist/agents.js CHANGED
@@ -2,17 +2,139 @@
2
2
  * HyperClaw agents API - typed agent lifecycle, files, exec, and OpenClaw access.
3
3
  */
4
4
  import { randomFillSync } from 'node:crypto';
5
+ import NodeWebSocket from 'ws';
6
+ import { agentSlotFromDict, } from './agent-slots.js';
7
+ export { agentSlotFromDict, } from './agent-slots.js';
5
8
  import { getAgentsApiBaseUrl, getConfigValue } from './config.js';
6
9
  import { APIError } from './errors.js';
7
10
  import { HTTPClient } from './http.js';
8
11
  import { GatewayClient, } from './openclaw/gateway.js';
12
+ import { normalizeSlackRelayBaseUrl } from './channels.js';
13
+ import { HermesApiClient } from './hermes/gateway.js';
9
14
  const AGENTS_API_BASE = 'https://api.hypercli.com/agents';
10
15
  const DEV_AGENTS_API_BASE = 'https://api.dev.hypercli.com/agents';
11
16
  const DEPLOYMENTS_API_PREFIX = '/deployments';
12
17
  const AGENTS_WS_URL = 'wss://api.agents.hypercli.com/ws';
13
18
  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';
19
+ export const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-latest';
20
+ export const DEFAULT_OPENCLAW_PRO_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-latest';
21
+ export const DEFAULT_HERMES_AGENT_IMAGE = 'ghcr.io/hypercli/hypercli-hermes-agent:latest';
22
+ export const DEFAULT_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-opencode:latest';
23
+ export const DEFAULT_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-codex:latest';
24
+ export const DEFAULT_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-claude-code:latest';
25
+ export const DEFAULT_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-goose:latest';
26
+ export const DEFAULT_KIMI_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-kimi-code:latest';
27
+ export const DEFAULT_BUZZ_AGENT_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-agent:latest';
28
+ export const DEFAULT_BUZZ_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-opencode:latest';
29
+ export const DEFAULT_BUZZ_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-codex:latest';
30
+ export const DEFAULT_BUZZ_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-claude:latest';
31
+ export const DEFAULT_BUZZ_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-goose:latest';
32
+ export const DEFAULT_BUZZ_KIMI_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-kimi-code:latest';
33
+ export const DEFAULT_AGENT_RUNTIME_SCOPES = Object.freeze([
34
+ 'agents:none',
35
+ 'files:*',
36
+ 'flows:*',
37
+ 'models:*',
38
+ 'voice:*',
39
+ 'web:*',
40
+ 'workspaces:*',
41
+ ]);
42
+ export const DEFAULT_CODING_AGENT_SYNC_ROOT = '/home/node';
43
+ export const DEFAULT_CODING_AGENT_IMAGES = {
44
+ 'buzz-agent': DEFAULT_BUZZ_AGENT_IMAGE,
45
+ opencode: DEFAULT_OPENCODE_IMAGE,
46
+ codex: DEFAULT_CODEX_IMAGE,
47
+ 'claude-code': DEFAULT_CLAUDE_CODE_IMAGE,
48
+ goose: DEFAULT_GOOSE_IMAGE,
49
+ 'kimi-code': DEFAULT_KIMI_CODE_IMAGE,
50
+ };
51
+ export const DEFAULT_CODING_AGENT_SYNC_INCLUDES = {
52
+ 'buzz-agent': [],
53
+ opencode: [
54
+ '.config/opencode',
55
+ '.local/share/opencode',
56
+ '.local/state/opencode',
57
+ '.cache/opencode',
58
+ ],
59
+ codex: ['.codex'],
60
+ 'claude-code': ['.claude', '.claude.json'],
61
+ goose: ['.goose'],
62
+ 'kimi-code': ['.kimi-code'],
63
+ };
64
+ export const DEFAULT_BUZZ_CODING_AGENT_IMAGES = {
65
+ 'buzz-agent': DEFAULT_BUZZ_AGENT_IMAGE,
66
+ opencode: DEFAULT_BUZZ_OPENCODE_IMAGE,
67
+ codex: DEFAULT_BUZZ_CODEX_IMAGE,
68
+ 'claude-code': DEFAULT_BUZZ_CLAUDE_CODE_IMAGE,
69
+ goose: DEFAULT_BUZZ_GOOSE_IMAGE,
70
+ 'kimi-code': DEFAULT_BUZZ_KIMI_CODE_IMAGE,
71
+ };
72
+ const CODING_AGENT_RUNTIMES = new Set(['buzz-agent', 'opencode', 'codex', 'claude-code', 'goose', 'kimi-code']);
73
+ const BUZZ_RUNTIME_COMMANDS = {
74
+ 'buzz-agent': {
75
+ command: '/usr/local/bin/buzz-agent',
76
+ args: [],
77
+ mcpCommand: '/usr/local/bin/buzz-dev-mcp',
78
+ },
79
+ opencode: {
80
+ command: '/usr/local/bin/opencode',
81
+ args: ['acp'],
82
+ mcpCommand: '',
83
+ },
84
+ codex: {
85
+ command: '/usr/local/bin/codex-acp',
86
+ args: [],
87
+ mcpCommand: '/usr/local/bin/buzz-dev-mcp',
88
+ },
89
+ 'claude-code': {
90
+ command: '/usr/local/bin/claude-agent-acp',
91
+ args: [],
92
+ mcpCommand: '',
93
+ },
94
+ goose: {
95
+ command: '/usr/local/bin/goose',
96
+ args: ['acp'],
97
+ mcpCommand: '',
98
+ },
99
+ 'kimi-code': {
100
+ command: '/usr/local/bin/kimi',
101
+ args: ['acp'],
102
+ mcpCommand: '',
103
+ },
104
+ };
105
+ export const DEFAULT_BUZZ_RUST_LOG = 'buzz_acp=info,pool::prompt=info,acp::stream=off';
106
+ const BUZZ_RESERVED_ENV_KEYS = new Set([
107
+ 'BUZZ_PRIVATE_KEY',
108
+ 'NOSTR_PRIVATE_KEY',
109
+ 'BUZZ_AUTH_TAG',
110
+ 'BUZZ_API_TOKEN',
111
+ 'BUZZ_ACP_PRIVATE_KEY',
112
+ 'BUZZ_ACP_API_TOKEN',
113
+ 'BUZZ_RELAY_URL',
114
+ 'BUZZ_ACP_AGENT_OWNER',
115
+ 'BUZZ_ACP_AGENT_COMMAND',
116
+ 'BUZZ_ACP_AGENT_ARGS',
117
+ 'BUZZ_ACP_MCP_COMMAND',
118
+ 'BUZZ_ACP_LAZY_POOL',
119
+ 'BUZZ_ACP_RELAY_OBSERVER',
120
+ 'BUZZ_ACP_DISPLAY_NAME',
121
+ 'BUZZ_ACP_TEXT_MENTIONS',
122
+ 'BUZZ_ACP_REQUIRE_REPLY',
123
+ 'CLAUDE_CODE_EXECUTABLE',
124
+ 'BUZZ_ACP_SESSION_TITLE',
125
+ 'BUZZ_ACP_SYSTEM_PROMPT',
126
+ 'BUZZ_ACP_MODEL',
127
+ 'BUZZ_ACP_IDLE_TIMEOUT',
128
+ 'BUZZ_ACP_MAX_TURN_DURATION',
129
+ 'BUZZ_ACP_AGENTS',
130
+ 'BUZZ_ACP_RESPOND_TO',
131
+ 'BUZZ_ACP_RESPOND_TO_ALLOWLIST',
132
+ 'BUZZ_ACP_MULTIPLE_EVENT_HANDLING',
133
+ 'BUZZ_ACP_DEDUP',
134
+ 'BUZZ_ACP_SETUP_PAYLOAD',
135
+ 'BUZZ_MANAGED_AGENT',
136
+ 'BUZZ_MANAGED_AGENT_START_NONCE',
137
+ ]);
16
138
  export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
17
139
  OPENCLAW_MEMORY_SEARCH_ENABLED: '1',
18
140
  OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START: '0',
@@ -21,11 +143,400 @@ export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
21
143
  OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS: '30000',
22
144
  OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES: '0',
23
145
  };
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']);
146
+ export const OPENCLAW_WORKSPACES_SYNC_ENV_DEFAULTS = {
147
+ HYPER_WORKSPACES_BOOT_SYNC: '1',
148
+ HYPER_WORKSPACES_DIR: '/home/node/shared',
149
+ HYPER_WORKSPACES_SYNC_READY_ONLY: '1',
150
+ };
151
+ const DEFAULT_OPENCLAW_SYNC_EXCLUDE = [
152
+ 'shared/**',
153
+ '.openclaw/npm/**/node_modules/**',
154
+ '.openclaw/agents/**/agent/*.sqlite.memory-reindex-*',
155
+ '.openclaw/agents/**/agent/*.sqlite.reindex-lock.sqlite*',
156
+ '.openclaw/browser/**/Code Cache/**',
157
+ '.openclaw/browser/**/GPUCache/**',
158
+ '.openclaw/browser/**/ShaderCache/**',
159
+ '.openclaw/browser/**/GrShaderCache/**',
160
+ '.openclaw/browser/**/optimization_guide_model_store/**',
161
+ ];
162
+ const LAUNCH_CONFIG_KEYS = new Set([
163
+ 'image',
164
+ 'env',
165
+ 'secrets',
166
+ 'routes',
167
+ 'command',
168
+ 'entrypoint',
169
+ 'sync_root',
170
+ 'sync_include',
171
+ 'sync_exclude',
172
+ 'sync_uid',
173
+ 'sync_gid',
174
+ 'registry_url',
175
+ 'registry_auth',
176
+ 'restart',
177
+ 'runtime_scopes',
178
+ ]);
25
179
  const DEFAULT_OPENCLAW_SYNC_ROOT = '/home/node';
26
- export const AGENT_FILE_MAX_BYTES = 50 * 1024 * 1024;
180
+ export const DEFAULT_HERMES_AGENT_SYNC_ROOT = '/opt/data';
181
+ export const DEFAULT_HERMES_AGENT_SYNC_UID = 10000;
182
+ export const DEFAULT_HERMES_AGENT_SYNC_GID = 10000;
183
+ export const AGENT_FILE_MAX_BYTES = 250 * 1024 * 1024;
27
184
  export const AGENT_FILE_TRANSFER_CHUNK_BYTES = 64 * 1024;
28
185
  export const AGENT_FILE_OPERATION_TIMEOUT_MS = 300_000;
186
+ function stringifyOpenClawOperationsFailure(reason) {
187
+ if (reason instanceof Error)
188
+ return reason.message || reason.name || 'Error';
189
+ if (typeof reason === 'string')
190
+ return reason;
191
+ try {
192
+ const serialized = JSON.stringify(reason);
193
+ if (serialized !== undefined)
194
+ return serialized;
195
+ }
196
+ catch {
197
+ // Fall back to String for non-serializable rejection values.
198
+ }
199
+ try {
200
+ return String(reason);
201
+ }
202
+ catch {
203
+ return 'Unknown error';
204
+ }
205
+ }
206
+ function shellAbortError() {
207
+ const error = new Error('Shell connection cancelled');
208
+ error.name = 'AbortError';
209
+ return error;
210
+ }
211
+ function runShellOperation(operation, signal, timeoutMs, timeoutMessage) {
212
+ return new Promise((resolve, reject) => {
213
+ const controller = new AbortController();
214
+ let settled = false;
215
+ const cleanup = () => {
216
+ clearTimeout(timer);
217
+ signal?.removeEventListener('abort', abortOperation);
218
+ };
219
+ const finish = (error, value) => {
220
+ if (settled)
221
+ return;
222
+ settled = true;
223
+ cleanup();
224
+ if (error)
225
+ reject(error);
226
+ else
227
+ resolve(value);
228
+ };
229
+ const abortOperation = () => {
230
+ controller.abort(signal?.reason);
231
+ finish(shellAbortError());
232
+ };
233
+ const timer = setTimeout(() => {
234
+ controller.abort();
235
+ finish(new Error(timeoutMessage));
236
+ }, timeoutMs);
237
+ if (signal?.aborted) {
238
+ abortOperation();
239
+ return;
240
+ }
241
+ signal?.addEventListener('abort', abortOperation, { once: true });
242
+ void operation(controller.signal).then((value) => finish(undefined, value), (error) => {
243
+ if (error instanceof Error && error.name === 'AbortError' && !signal?.aborted) {
244
+ finish(new Error(timeoutMessage));
245
+ }
246
+ else {
247
+ finish(error);
248
+ }
249
+ });
250
+ });
251
+ }
252
+ function buildBuzzLaunchEnv(runtime, buzz, defaultSessionTitle) {
253
+ if (!buzz.privateKeyNsec.trim())
254
+ throw new Error('buzz.privateKeyNsec is required');
255
+ if (!buzz.relayUrl.trim())
256
+ throw new Error('buzz.relayUrl is required');
257
+ const parallelism = buzz.parallelism ?? 1;
258
+ if (!Number.isInteger(parallelism) || parallelism < 1 || parallelism > 32) {
259
+ throw new Error('buzz.parallelism must be between 1 and 32');
260
+ }
261
+ const harness = BUZZ_RUNTIME_COMMANDS[runtime];
262
+ const env = {
263
+ BUZZ_RELAY_URL: buzz.relayUrl,
264
+ BUZZ_MANAGED_AGENT_START_NONCE: randomHexToken(16),
265
+ BUZZ_ACP_AGENT_COMMAND: harness.command,
266
+ BUZZ_ACP_AGENT_ARGS: harness.args.join(','),
267
+ BUZZ_ACP_MCP_COMMAND: harness.mcpCommand,
268
+ BUZZ_ACP_LAZY_POOL: 'true',
269
+ BUZZ_ACP_RELAY_OBSERVER: 'true',
270
+ BUZZ_ACP_AGENTS: String(parallelism),
271
+ BUZZ_ACP_MULTIPLE_EVENT_HANDLING: 'steer',
272
+ BUZZ_ACP_DEDUP: 'queue',
273
+ };
274
+ if (runtime === 'claude-code') {
275
+ env.CLAUDE_CODE_EXECUTABLE = '/usr/local/bin/claude';
276
+ }
277
+ if (buzz.rustLog)
278
+ env.RUST_LOG = buzz.rustLog;
279
+ const optional = {
280
+ BUZZ_AUTH_TAG: buzz.authTag,
281
+ BUZZ_ACP_DISPLAY_NAME: buzz.displayName,
282
+ BUZZ_ACP_SESSION_TITLE: buzz.sessionTitle || defaultSessionTitle,
283
+ BUZZ_ACP_SYSTEM_PROMPT: buzz.systemPrompt,
284
+ BUZZ_ACP_MODEL: buzz.model,
285
+ BUZZ_ACP_IDLE_TIMEOUT: buzz.idleTimeoutSeconds == null
286
+ ? undefined
287
+ : String(buzz.idleTimeoutSeconds),
288
+ BUZZ_ACP_MAX_TURN_DURATION: buzz.maxTurnDurationSeconds == null
289
+ ? undefined
290
+ : String(buzz.maxTurnDurationSeconds),
291
+ BUZZ_ACP_RESPOND_TO: buzz.respondTo,
292
+ BUZZ_ACP_RESPOND_TO_ALLOWLIST: buzz.respondToAllowlist?.length
293
+ ? buzz.respondToAllowlist.join(',')
294
+ : undefined,
295
+ };
296
+ for (const [key, value] of Object.entries(optional)) {
297
+ if (value)
298
+ env[key] = value;
299
+ }
300
+ if (buzz.textMentions)
301
+ env.BUZZ_ACP_TEXT_MENTIONS = 'true';
302
+ if (buzz.requireReply)
303
+ env.BUZZ_ACP_REQUIRE_REPLY = 'true';
304
+ return env;
305
+ }
306
+ function buildBuzzLaunchSecrets(buzz) {
307
+ if (!buzz.privateKeyNsec.trim())
308
+ throw new Error('buzz.privateKeyNsec is required');
309
+ return {
310
+ BUZZ_PRIVATE_KEY: buzz.privateKeyNsec,
311
+ NOSTR_PRIVATE_KEY: buzz.privateKeyNsec,
312
+ };
313
+ }
314
+ /** The agent's sync root and workspace, owned by the SDK so paths are unambiguous. */
315
+ export const OPENCLAW_SYNC_ROOT = '/home/node';
316
+ export const OPENCLAW_WORKSPACE_PREFIX = '.openclaw/workspace';
317
+ /** Map the friendly `agent`/`backup` names to their on-the-wire `pod`/`s3` values. */
318
+ export function toWireFileSource(source) {
319
+ switch (source) {
320
+ case 'agent': return 'pod';
321
+ case 'backup': return 's3';
322
+ default: return source; // 'auto' | 'pod' | 's3'
323
+ }
324
+ }
325
+ const GATEWAY_FILE_SOURCES = new Set(['gateway']);
326
+ const FULL_FS_FILE_SOURCES = new Set(['agent', 'pod']);
327
+ /**
328
+ * Resolve a caller path to the deployment HTTP files path (sync-root relative).
329
+ * Workspace-relative by default (prefixed with the workspace); an absolute `/…`
330
+ * path is read-only full-fs and only valid for the `agent` source.
331
+ */
332
+ function resolveBackendFilePath(path, source) {
333
+ if (path.startsWith('/')) {
334
+ if (!FULL_FS_FILE_SOURCES.has(source)) {
335
+ throw new Error(`absolute paths need the 'agent' source (full pod filesystem); ` +
336
+ `'${source}' is scoped to ${source === 'backup' || source === 's3' ? 'the sync root' : 'the workspace'}.`);
337
+ }
338
+ return path; // full-fs path, passed through to source=pod
339
+ }
340
+ const rel = stripRelPrefix(path);
341
+ return rel ? `${OPENCLAW_WORKSPACE_PREFIX}/${rel}` : OPENCLAW_WORKSPACE_PREFIX;
342
+ }
343
+ function normalizeWritableBackendFilePath(path) {
344
+ const normalized = path.replace(/\\/g, '/');
345
+ if (normalized.startsWith('/')) {
346
+ const parts = normalized.split('/');
347
+ const resolved = [];
348
+ for (const part of parts) {
349
+ if (!part || part === '.')
350
+ continue;
351
+ if (part === '..') {
352
+ resolved.pop();
353
+ continue;
354
+ }
355
+ resolved.push(part);
356
+ }
357
+ const absolute = `/${resolved.join('/')}`;
358
+ const prefix = `${OPENCLAW_SYNC_ROOT}/`;
359
+ if (!absolute.startsWith(prefix)) {
360
+ throw new Error(`absolute write paths must stay within the sync root (${OPENCLAW_SYNC_ROOT}).`);
361
+ }
362
+ return absolute.slice(prefix.length);
363
+ }
364
+ if (normalized.split('/').includes('..')) {
365
+ throw new Error("paths containing '..' are not writable; writes and deletes must stay within the sync root.");
366
+ }
367
+ return stripRelPrefix(normalized);
368
+ }
369
+ /** Strip leading `./` segments and slashes without eating a dotfile's dot. */
370
+ function stripRelPrefix(path) {
371
+ return path.replace(/^(?:\.\/)+/, '').replace(/^\/+/, '');
372
+ }
373
+ function isUuidRef(value) {
374
+ 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());
375
+ }
376
+ function isDirectAgentIdRef(value) {
377
+ const raw = value.trim();
378
+ return isUuidRef(raw) || /^[0-9a-f]{6,}$/i.test(raw) || /^(agent|external)[-_:]/i.test(raw);
379
+ }
380
+ function isSelfAgentRef(value) {
381
+ return value.trim().toLowerCase() === 'self';
382
+ }
383
+ function agentRoutesStateFromData(data) {
384
+ return {
385
+ agentId: String(data.agent_id ?? ''),
386
+ routes: structuredClone(data.routes ?? {}),
387
+ routeStatuses: structuredClone(data.route_statuses ?? {}),
388
+ };
389
+ }
390
+ /** Strip a workspace prefix so gateway (name-addressed) sees the bare name. */
391
+ function resolveGatewayFileName(path) {
392
+ if (path.startsWith('/')) {
393
+ throw new Error("absolute paths are not valid for the 'gateway' source (scoped to the workspace).");
394
+ }
395
+ const rel = stripRelPrefix(path);
396
+ return rel.startsWith(`${OPENCLAW_WORKSPACE_PREFIX}/`)
397
+ ? rel.slice(OPENCLAW_WORKSPACE_PREFIX.length + 1)
398
+ : rel;
399
+ }
400
+ /**
401
+ * ONE client wrapping all three agent file-access paths behind a single `source`
402
+ * switch (`agent` | `backup` | `gateway`, plus `auto`). The SDK owns the roots,
403
+ * so a workspace-relative path is the same file on every source; `agent` also
404
+ * takes absolute `/…` paths for read-only full-filesystem browsing. The
405
+ * underlying APIs are left as-is — this only wraps them.
406
+ */
407
+ export class AgentFiles {
408
+ agent;
409
+ deployments;
410
+ gatewayHooks;
411
+ constructor(agent, deployments, gatewayHooks) {
412
+ this.agent = agent;
413
+ this.deployments = deployments;
414
+ this.gatewayHooks = gatewayHooks;
415
+ }
416
+ requireGateway() {
417
+ if (!this.gatewayHooks) {
418
+ throw new Error('gateway file source requires an OpenClaw agent with a gateway URL/token.');
419
+ }
420
+ return this.gatewayHooks;
421
+ }
422
+ async list(path = '', source = 'auto') {
423
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
424
+ return this.deployments.filesList(this.agent, resolveBackendFilePath(path, source), toWireFileSource(source));
425
+ }
426
+ const gw = this.requireGateway();
427
+ const name = resolveGatewayFileName(path);
428
+ const files = await gw.withGateway(async (c) => c.filesList(await gw.resolveAgentId(c)));
429
+ return (files ?? [])
430
+ .map((f) => ({ name: f.name, path: f.name, type: 'file', size: f.size }))
431
+ .filter((f) => !name || f.name === name || f.name.startsWith(name.endsWith('/') ? name : `${name}/`));
432
+ }
433
+ async readBytes(path, source = 'auto', options) {
434
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
435
+ const resolvedPath = resolveBackendFilePath(path, source);
436
+ const wireSource = toWireFileSource(source);
437
+ return options
438
+ ? this.deployments.fileReadBytes(this.agent, resolvedPath, wireSource, options)
439
+ : this.deployments.fileReadBytes(this.agent, resolvedPath, wireSource);
440
+ }
441
+ return (await this.readBytesWithMetadata(path, source, options)).content;
442
+ }
443
+ async readBytesWithMetadata(path, source = 'auto', options) {
444
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
445
+ return this.deployments.fileReadBytesWithMetadata(this.agent, resolveBackendFilePath(path, source), toWireFileSource(source), options);
446
+ }
447
+ if (options?.signal?.aborted) {
448
+ const error = new Error('File read cancelled');
449
+ error.name = 'AbortError';
450
+ throw error;
451
+ }
452
+ const content = encodeUtf8(await this.read(path, source));
453
+ if (options?.maxBytes !== undefined && content.byteLength > options.maxBytes) {
454
+ throw fileReadLimitError(path, options.maxBytes);
455
+ }
456
+ return { content, mimeType: 'text/plain' };
457
+ }
458
+ async read(path, source = 'auto', options) {
459
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
460
+ return decodeUtf8(await this.readBytes(path, source, options));
461
+ }
462
+ const gw = this.requireGateway();
463
+ return gw.withGateway(async (c) => c.fileGet(await gw.resolveAgentId(c), resolveGatewayFileName(path)));
464
+ }
465
+ async writeBytes(path, content, source = 'auto') {
466
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
467
+ const writablePath = normalizeWritableBackendFilePath(path);
468
+ const resolvedPath = path.startsWith('/')
469
+ ? writablePath
470
+ : resolveBackendFilePath(writablePath, source);
471
+ return this.deployments.fileWriteBytes(this.agent, resolvedPath, content, toWireFileSource(source));
472
+ }
473
+ return this.write(path, coerceToUtf8String(content), source);
474
+ }
475
+ async write(path, content, source = 'auto') {
476
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
477
+ const writablePath = normalizeWritableBackendFilePath(path);
478
+ const resolvedPath = path.startsWith('/')
479
+ ? writablePath
480
+ : resolveBackendFilePath(writablePath, source);
481
+ return this.deployments.fileWrite(this.agent, resolvedPath, content, toWireFileSource(source));
482
+ }
483
+ const gw = this.requireGateway();
484
+ const name = resolveGatewayFileName(path);
485
+ const agentId = await gw.withGateway(async (c) => {
486
+ const aid = await gw.resolveAgentId(c);
487
+ await c.fileSet(aid, name, content);
488
+ return aid;
489
+ });
490
+ return { name, source: 'gateway', agentId };
491
+ }
492
+ async delete(path, options = {}) {
493
+ const source = options.source ?? 'auto';
494
+ if (GATEWAY_FILE_SOURCES.has(source)) {
495
+ throw new Error('delete is not supported over the gateway file source; use the agent/backup source.');
496
+ }
497
+ const writablePath = normalizeWritableBackendFilePath(path);
498
+ const resolvedPath = path.startsWith('/')
499
+ ? writablePath
500
+ : resolveBackendFilePath(writablePath, source);
501
+ return this.deployments.fileDelete(this.agent, resolvedPath, {
502
+ recursive: options.recursive,
503
+ source: toWireFileSource(source),
504
+ });
505
+ }
506
+ }
507
+ /** Canonical states understood by this SDK. AgentState remains forward-open. */
508
+ export const CANONICAL_AGENT_STATES = [
509
+ 'CREATING',
510
+ 'STARTING',
511
+ 'RESTORING',
512
+ 'RUNNING',
513
+ 'STOPPING',
514
+ 'STOPPED',
515
+ 'ARCHIVING',
516
+ 'ARCHIVED',
517
+ 'FAILED',
518
+ 'DELETED',
519
+ ];
520
+ export const AGENT_TRANSITIONAL_STATES = new Set([
521
+ 'CREATING',
522
+ 'STARTING',
523
+ 'RESTORING',
524
+ 'STOPPING',
525
+ 'ARCHIVING',
526
+ ]);
527
+ export const AGENT_RUNTIME_INACTIVE_STATES = new Set([
528
+ 'STOPPED',
529
+ 'ARCHIVING',
530
+ 'ARCHIVED',
531
+ 'FAILED',
532
+ 'DELETED',
533
+ ]);
534
+ export function isAgentTransitionalState(state) {
535
+ return AGENT_TRANSITIONAL_STATES.has(state.toUpperCase());
536
+ }
537
+ export function isAgentRuntimeInactiveState(state) {
538
+ return AGENT_RUNTIME_INACTIVE_STATES.has(state.toUpperCase());
539
+ }
29
540
  function parseDate(value) {
30
541
  if (typeof value !== 'string' || !value)
31
542
  return null;
@@ -52,6 +563,8 @@ function sleep(ms) {
52
563
  return new Promise((resolve) => setTimeout(resolve, ms));
53
564
  }
54
565
  function isOpenClawHydrationData(data) {
566
+ if (data.runtime === 'openclaw' || data.runtime === 'openclaw-pro')
567
+ return true;
55
568
  const routes = data.routes;
56
569
  if (routes && typeof routes === 'object' && !Array.isArray(routes) && routes.openclaw) {
57
570
  return true;
@@ -59,15 +572,122 @@ function isOpenClawHydrationData(data) {
59
572
  const launchRoutes = data.launch_config?.routes;
60
573
  return !!(launchRoutes && typeof launchRoutes === 'object' && !Array.isArray(launchRoutes) && launchRoutes.openclaw);
61
574
  }
575
+ function isCodingAgentRuntime(runtime) {
576
+ return typeof runtime === 'string' && CODING_AGENT_RUNTIMES.has(runtime);
577
+ }
62
578
  function isTruthyEnv(value) {
63
579
  return ['1', 'true', 'yes', 'on', 'enabled'].includes(String(value ?? '').trim().toLowerCase());
64
580
  }
581
+ function isPlainRecord(value) {
582
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
583
+ }
584
+ function flattenConfigValue(value, prefix, out) {
585
+ if (!prefix) {
586
+ if (isPlainRecord(value)) {
587
+ for (const [key, child] of Object.entries(value)) {
588
+ flattenConfigValue(child, key, out);
589
+ }
590
+ return;
591
+ }
592
+ out[''] = value;
593
+ return;
594
+ }
595
+ out[prefix] = value;
596
+ if (Array.isArray(value)) {
597
+ value.forEach((child, index) => flattenConfigValue(child, `${prefix}[${index}]`, out));
598
+ return;
599
+ }
600
+ if (isPlainRecord(value)) {
601
+ for (const [key, child] of Object.entries(value)) {
602
+ flattenConfigValue(child, `${prefix}.${key}`, out);
603
+ }
604
+ }
605
+ }
606
+ export function flattenLaunchConfig(launchConfig) {
607
+ const flat = {};
608
+ if (!isPlainRecord(launchConfig))
609
+ return flat;
610
+ flattenConfigValue(launchConfig, '', flat);
611
+ return flat;
612
+ }
613
+ function pathParts(path) {
614
+ if (Array.isArray(path))
615
+ return path;
616
+ const parts = [];
617
+ for (const part of path.replace(/\[(\d+)\]/g, '.$1').split('.')) {
618
+ if (!part)
619
+ continue;
620
+ parts.push(/^\d+$/.test(part) ? Number(part) : part);
621
+ }
622
+ return parts;
623
+ }
624
+ export function getLaunchConfigValue(launchConfig, path) {
625
+ let current = launchConfig;
626
+ for (const part of pathParts(path)) {
627
+ if (typeof part === 'number') {
628
+ if (!Array.isArray(current))
629
+ return undefined;
630
+ current = current[part];
631
+ continue;
632
+ }
633
+ if (!isPlainRecord(current))
634
+ return undefined;
635
+ current = current[part];
636
+ }
637
+ return current;
638
+ }
639
+ export function routesHaveDesktop(routes) {
640
+ if (!isPlainRecord(routes))
641
+ return false;
642
+ if (isPlainRecord(routes.desktop))
643
+ return true;
644
+ return Object.values(routes).some((route) => isPlainRecord(route) && route.prefix === 'desktop');
645
+ }
646
+ export function launchConfigHasDesktop(launchConfig) {
647
+ if (!isPlainRecord(launchConfig))
648
+ return false;
649
+ if (isTruthyEnv(getLaunchConfigValue(launchConfig, 'env.OPENCLAW_DESKTOP_ENABLED')))
650
+ return true;
651
+ return routesHaveDesktop(getLaunchConfigValue(launchConfig, 'routes'));
652
+ }
653
+ export function agentConfigHasDesktop(source) {
654
+ if (!source)
655
+ return false;
656
+ return (launchConfigHasDesktop(source.launchConfig ?? source.launch_config) ||
657
+ routesHaveDesktop(source.routes));
658
+ }
659
+ function browserDesktopRedirectPath(options = {}) {
660
+ const redirect = (options.redirect ?? 'vnc.html').trim() || 'vnc.html';
661
+ if (redirect.includes('\\')) {
662
+ throw new Error('Desktop redirect must be a relative path');
663
+ }
664
+ const base = 'https://desktop.local';
665
+ const parsed = new URL(redirect, `${base}/`);
666
+ if (parsed.origin !== base) {
667
+ throw new Error('Desktop redirect must be a relative path');
668
+ }
669
+ if (options.resize !== null) {
670
+ const resize = options.resize ?? 'scale';
671
+ if (resize.trim())
672
+ parsed.searchParams.set('resize', resize);
673
+ }
674
+ const pathname = parsed.pathname.replace(/^\/+/, '') || 'vnc.html';
675
+ return `${pathname}${parsed.search}${parsed.hash}`;
676
+ }
677
+ export function buildBrowserDesktopUrl(desktopBaseUrl, token, options = {}) {
678
+ const jwt = token.trim();
679
+ if (!jwt)
680
+ throw new Error('Desktop token is required');
681
+ const url = new URL('/_jwt_auth', desktopBaseUrl);
682
+ url.searchParams.set('jwt', jwt);
683
+ url.searchParams.set('redirect', browserDesktopRedirectPath(options));
684
+ return url.toString();
685
+ }
65
686
  function isOpenClawProHydrationData(data) {
66
687
  const launchConfig = data.launch_config;
67
688
  if (!launchConfig || typeof launchConfig !== 'object' || Array.isArray(launchConfig))
68
689
  return false;
69
- const env = launchConfig.env;
70
- if (env && typeof env === 'object' && !Array.isArray(env) && isTruthyEnv(env.OPENCLAW_DESKTOP_ENABLED)) {
690
+ if (launchConfigHasDesktop(launchConfig)) {
71
691
  return true;
72
692
  }
73
693
  const image = String(launchConfig.image ?? '');
@@ -164,12 +784,78 @@ function encodeFilePath(path) {
164
784
  .map((part) => encodeURIComponent(part))
165
785
  .join('/');
166
786
  }
787
+ function deploymentFileReadRequest(path, source) {
788
+ const params = new URLSearchParams({ source });
789
+ if (path.startsWith('/')) {
790
+ if (source !== 'pod') {
791
+ throw new Error("absolute paths require source='pod'.");
792
+ }
793
+ params.set('absolute_path', path);
794
+ return { suffix: '', params };
795
+ }
796
+ const encodedPath = encodeFilePath(path);
797
+ return { suffix: encodedPath ? `/${encodedPath}` : '', params };
798
+ }
167
799
  function decodeUtf8(content) {
168
800
  return new TextDecoder().decode(content);
169
801
  }
170
802
  function encodeUtf8(content) {
171
803
  return new TextEncoder().encode(content);
172
804
  }
805
+ function fileReadLimitError(path, maxBytes) {
806
+ return new Error(`File ${path} exceeds the ${maxBytes / 1024 / 1024} MiB read limit`);
807
+ }
808
+ async function readResponseBytes(response, path, maxBytes) {
809
+ if (maxBytes === undefined)
810
+ return new Uint8Array(await response.arrayBuffer());
811
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
812
+ throw new RangeError('maxBytes must be a non-negative safe integer');
813
+ }
814
+ const contentLength = Number(response.headers.get('content-length'));
815
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
816
+ await response.body?.cancel();
817
+ throw fileReadLimitError(path, maxBytes);
818
+ }
819
+ if (!response.body) {
820
+ const bytes = new Uint8Array(await response.arrayBuffer());
821
+ if (bytes.byteLength > maxBytes)
822
+ throw fileReadLimitError(path, maxBytes);
823
+ return bytes;
824
+ }
825
+ const reader = response.body.getReader();
826
+ const chunks = [];
827
+ let totalBytes = 0;
828
+ try {
829
+ while (true) {
830
+ const { done, value } = await reader.read();
831
+ if (done)
832
+ break;
833
+ totalBytes += value.byteLength;
834
+ if (totalBytes > maxBytes) {
835
+ await reader.cancel();
836
+ throw fileReadLimitError(path, maxBytes);
837
+ }
838
+ chunks.push(value);
839
+ }
840
+ }
841
+ finally {
842
+ reader.releaseLock();
843
+ }
844
+ const bytes = new Uint8Array(totalBytes);
845
+ let offset = 0;
846
+ for (const chunk of chunks) {
847
+ bytes.set(chunk, offset);
848
+ offset += chunk.byteLength;
849
+ }
850
+ return bytes;
851
+ }
852
+ /** Coerce write content to a string for the gateway (text-only) file source. */
853
+ function coerceToUtf8String(content) {
854
+ if (typeof content === 'string')
855
+ return content;
856
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
857
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
858
+ }
173
859
  function toUint8Array(content) {
174
860
  if (typeof content === 'string')
175
861
  return encodeUtf8(content);
@@ -185,13 +871,25 @@ function execResultFromDict(data) {
185
871
  };
186
872
  }
187
873
  function agentStateFromDict(data) {
874
+ const launchConfig = isPlainRecord(data.launch_config) ? structuredClone(data.launch_config) : null;
875
+ if (launchConfig) {
876
+ delete launchConfig.secrets;
877
+ }
188
878
  return {
189
879
  id: data.id ?? '',
190
880
  userId: data.user_id ?? '',
191
- podId: data.pod_id ?? '',
192
- podName: data.pod_name ?? '',
193
881
  state: data.state ?? 'unknown',
194
882
  name: data.name ?? null,
883
+ handle: data.handle ?? null,
884
+ displayName: data.display_name ?? data.name ?? null,
885
+ avatarUrl: data.avatar_url ?? null,
886
+ displayIdentity: data.display_identity ? structuredClone(data.display_identity) : null,
887
+ runtime: data.runtime ?? null,
888
+ managed: data.managed ?? null,
889
+ isLaunchable: data.is_launchable ?? data.managed !== false,
890
+ gatewayId: data.gateway_id ?? null,
891
+ runtimeKeyAlias: data.runtime_key_alias ?? null,
892
+ relayKey: data.relay_key ?? null,
195
893
  cpu: data.cpu ?? 0,
196
894
  memory: data.memory ?? 0,
197
895
  hostname: data.hostname ?? null,
@@ -200,15 +898,18 @@ function agentStateFromDict(data) {
200
898
  jwtExpiresAt: parseDate(data.jwt_expires_at),
201
899
  startedAt: parseDate(data.started_at),
202
900
  stoppedAt: parseDate(data.stopped_at),
203
- lastError: data.last_error ?? null,
901
+ archivedAt: parseDate(data.archived_at),
902
+ clusterId: data.cluster_id ?? null,
903
+ launchEpoch: data.launch_epoch ?? 0,
204
904
  createdAt: parseDate(data.created_at),
205
905
  updatedAt: parseDate(data.updated_at),
206
- launchConfig: data.launch_config ?? null,
906
+ launchConfig,
207
907
  meta: data.meta?.ui ? { ui: structuredClone(data.meta.ui) } : null,
208
- routes: data.routes ?? {},
209
- command: data.command ?? [],
210
- entrypoint: data.entrypoint ?? [],
211
- ports: data.ports ?? [],
908
+ routes: data.routes ?? (isPlainRecord(launchConfig?.routes)
909
+ ? launchConfig.routes
910
+ : {}),
911
+ command: data.command ?? (Array.isArray(launchConfig?.command) ? launchConfig.command : []),
912
+ entrypoint: data.entrypoint ?? (Array.isArray(launchConfig?.entrypoint) ? launchConfig.entrypoint : []),
212
913
  dryRun: Boolean(data.dry_run),
213
914
  };
214
915
  }
@@ -233,24 +934,48 @@ export function buildAgentConfig(config = {}, options = {}) {
233
934
  preparedConfig.agents = agentsConfig;
234
935
  }
235
936
  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;
937
+ const secrets = { ...(options.secrets ?? {}) };
938
+ if (Object.prototype.hasOwnProperty.call(env, 'OPENCLAW_GATEWAY_TOKEN')) {
939
+ throw new Error('OPENCLAW_GATEWAY_TOKEN is a Secret; pass it through secrets or gatewayToken, not env');
940
+ }
941
+ let gatewayToken = null;
942
+ if (options.injectGatewayToken !== false) {
943
+ const explicitGatewayToken = options.gatewayToken?.trim() || null;
944
+ const secretGatewayToken = secrets.OPENCLAW_GATEWAY_TOKEN?.trim() || null;
945
+ if (explicitGatewayToken && secretGatewayToken && explicitGatewayToken !== secretGatewayToken) {
946
+ throw new Error('gatewayToken conflicts with secrets.OPENCLAW_GATEWAY_TOKEN');
947
+ }
948
+ gatewayToken = explicitGatewayToken ?? secretGatewayToken;
949
+ if (gatewayToken)
950
+ secrets.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
951
+ if (options.controlUiOriginLock !== false && !env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN?.trim()) {
952
+ const controlUiOrigin = defaultControlUiAllowedOrigin();
953
+ if (controlUiOrigin) {
954
+ env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN = controlUiOrigin;
955
+ }
245
956
  }
246
957
  }
958
+ const collidingKeys = Object.keys(env).filter((key) => Object.prototype.hasOwnProperty.call(secrets, key));
959
+ if (collidingKeys.length > 0) {
960
+ throw new Error(`Launch keys cannot appear in both env and secrets: ${collidingKeys.join(', ')}`);
961
+ }
962
+ const normalizeSyncOwner = (value, field) => {
963
+ if (value === undefined || value === null)
964
+ return undefined;
965
+ if (!Number.isSafeInteger(value) || value < 0 || value > 4_294_967_294) {
966
+ throw new Error(`${field} must be an integer between 0 and 4294967294`);
967
+ }
968
+ return value;
969
+ };
970
+ const syncUid = normalizeSyncOwner(options.syncUid, 'syncUid');
971
+ const syncGid = normalizeSyncOwner(options.syncGid, 'syncGid');
247
972
  const prepared = {};
248
973
  if (Object.keys(preparedConfig).length > 0)
249
974
  prepared.config = preparedConfig;
250
975
  if (Object.keys(env).length > 0)
251
976
  prepared.env = env;
252
- if (options.ports !== undefined && options.ports !== null)
253
- prepared.ports = options.ports;
977
+ if (Object.keys(secrets).length > 0)
978
+ prepared.secrets = secrets;
254
979
  if (options.routes !== undefined && options.routes !== null)
255
980
  prepared.routes = options.routes;
256
981
  if (options.command !== undefined && options.command !== null)
@@ -261,16 +986,25 @@ export function buildAgentConfig(config = {}, options = {}) {
261
986
  prepared.image = options.image;
262
987
  if (options.syncRoot !== undefined && options.syncRoot !== null)
263
988
  prepared.sync_root = options.syncRoot;
264
- if (options.syncEnabled !== undefined && options.syncEnabled !== null)
265
- prepared.sync_enabled = options.syncEnabled;
266
- if (options.syncUid !== undefined && options.syncUid !== null)
267
- prepared.sync_uid = options.syncUid;
268
- if (options.syncGid !== undefined && options.syncGid !== null)
269
- prepared.sync_gid = options.syncGid;
989
+ if (options.syncInclude !== undefined) {
990
+ prepared.sync_include = options.syncInclude === null ? null : [...options.syncInclude];
991
+ }
992
+ if (options.syncInclude === undefined && options.syncExclude !== undefined) {
993
+ prepared.sync_exclude = options.syncExclude === null ? null : [...options.syncExclude];
994
+ }
995
+ if (syncUid !== undefined)
996
+ prepared.sync_uid = syncUid;
997
+ if (syncGid !== undefined)
998
+ prepared.sync_gid = syncGid;
270
999
  if (options.registryUrl !== undefined && options.registryUrl !== null)
271
1000
  prepared.registry_url = options.registryUrl;
272
1001
  if (options.registryAuth !== undefined && options.registryAuth !== null)
273
1002
  prepared.registry_auth = options.registryAuth;
1003
+ if (options.restart !== undefined && options.restart !== null)
1004
+ prepared.restart = options.restart;
1005
+ if (options.runtimeScopes !== undefined && options.runtimeScopes !== null) {
1006
+ prepared.runtime_scopes = [...options.runtimeScopes];
1007
+ }
274
1008
  return { config: prepared, gatewayToken };
275
1009
  }
276
1010
  function defaultOpenClawImage(image) {
@@ -283,6 +1017,210 @@ function defaultOpenClawProImage(image) {
283
1017
  return image;
284
1018
  return DEFAULT_OPENCLAW_PRO_IMAGE;
285
1019
  }
1020
+ function defaultHermesAgentImage(image) {
1021
+ if (image !== undefined && image !== null)
1022
+ return image;
1023
+ return DEFAULT_HERMES_AGENT_IMAGE;
1024
+ }
1025
+ function resolveHermesApiServerKey(explicit, env) {
1026
+ const supplied = explicit?.trim() || env?.API_SERVER_KEY?.trim() || '';
1027
+ const key = supplied || randomHexToken(32);
1028
+ if (key.length < 32)
1029
+ throw new Error('Hermes API_SERVER_KEY must be at least 32 characters');
1030
+ return key;
1031
+ }
1032
+ export async function startSlackOAuth(options) {
1033
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1034
+ if (!relayBaseUrl)
1035
+ throw new Error('Slack relay base URL is required');
1036
+ if (!options.token)
1037
+ throw new Error('Slack OAuth requires an app token');
1038
+ const response = await fetch(`${relayBaseUrl}/slack/oauth/start`, {
1039
+ method: 'GET',
1040
+ headers: {
1041
+ 'Authorization': `Bearer ${options.token}`,
1042
+ },
1043
+ });
1044
+ if (!response.ok) {
1045
+ let detail = response.statusText || 'Slack OAuth start failed';
1046
+ try {
1047
+ const payload = await response.json();
1048
+ if (typeof payload.detail === 'string' && payload.detail)
1049
+ detail = payload.detail;
1050
+ }
1051
+ catch {
1052
+ // Keep the HTTP status text when the error body is not JSON.
1053
+ }
1054
+ throw new APIError(response.status, detail);
1055
+ }
1056
+ const payload = await response.json();
1057
+ const authorizeUrl = typeof payload.authorize_url === 'string' ? payload.authorize_url : '';
1058
+ if (!authorizeUrl)
1059
+ throw new Error('Slack OAuth start did not return an authorization URL');
1060
+ return {
1061
+ authorizeUrl,
1062
+ expiresAt: typeof payload.expires_at === 'string' ? payload.expires_at : null,
1063
+ };
1064
+ }
1065
+ export async function getSlackInstallStatus(options) {
1066
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1067
+ if (!relayBaseUrl)
1068
+ throw new Error('Slack relay base URL is required');
1069
+ if (!options.token)
1070
+ throw new Error('Slack install status requires an app token');
1071
+ const response = await fetch(`${relayBaseUrl}/slack/install`, {
1072
+ method: 'GET',
1073
+ headers: { 'Authorization': `Bearer ${options.token}` },
1074
+ });
1075
+ if (!response.ok) {
1076
+ let detail = response.statusText || 'Slack install status failed';
1077
+ try {
1078
+ const payload = await response.json();
1079
+ if (typeof payload.detail === 'string' && payload.detail)
1080
+ detail = payload.detail;
1081
+ }
1082
+ catch {
1083
+ // Keep the HTTP status text when the error body is not JSON.
1084
+ }
1085
+ throw new APIError(response.status, detail);
1086
+ }
1087
+ const payload = await response.json();
1088
+ return {
1089
+ connected: payload.connected === true,
1090
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
1091
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
1092
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
1093
+ installerUserId: typeof payload.installer_user_id === 'string' ? payload.installer_user_id : null,
1094
+ updatedAt: typeof payload.updated_at === 'string' ? payload.updated_at : null,
1095
+ };
1096
+ }
1097
+ function slackRelayAuthorizedUrl(path, options) {
1098
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1099
+ if (!relayBaseUrl)
1100
+ throw new Error('Slack relay base URL is required');
1101
+ if (!options.token)
1102
+ throw new Error('Slack directory lookup requires an app token');
1103
+ const url = new URL(path, `${relayBaseUrl}/`);
1104
+ if (options.cursor)
1105
+ url.searchParams.set('cursor', options.cursor);
1106
+ if (typeof options.limit === 'number' && Number.isFinite(options.limit)) {
1107
+ url.searchParams.set('limit', String(Math.trunc(options.limit)));
1108
+ }
1109
+ return url;
1110
+ }
1111
+ export async function listSlackDirectoryConversations(options) {
1112
+ const url = slackRelayAuthorizedUrl('/slack/directory/conversations', options);
1113
+ if (options.types)
1114
+ url.searchParams.set('types', options.types);
1115
+ const response = await fetch(url.toString(), {
1116
+ method: 'GET',
1117
+ headers: { 'Authorization': `Bearer ${options.token}` },
1118
+ });
1119
+ if (!response.ok) {
1120
+ let detail = response.statusText || 'Slack conversation lookup failed';
1121
+ try {
1122
+ const payload = await response.json();
1123
+ if (typeof payload.detail === 'string' && payload.detail)
1124
+ detail = payload.detail;
1125
+ }
1126
+ catch {
1127
+ // Keep the HTTP status text when the error body is not JSON.
1128
+ }
1129
+ throw new APIError(response.status, detail);
1130
+ }
1131
+ const payload = await response.json();
1132
+ const conversations = Array.isArray(payload.conversations) ? payload.conversations : [];
1133
+ return {
1134
+ conversations: conversations
1135
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
1136
+ .map((item) => ({
1137
+ id: item.id,
1138
+ name: typeof item.name === 'string' ? item.name : null,
1139
+ isChannel: typeof item.is_channel === 'boolean' ? item.is_channel : null,
1140
+ isGroup: typeof item.is_group === 'boolean' ? item.is_group : null,
1141
+ isIm: typeof item.is_im === 'boolean' ? item.is_im : null,
1142
+ isMpim: typeof item.is_mpim === 'boolean' ? item.is_mpim : null,
1143
+ isMember: typeof item.is_member === 'boolean' ? item.is_member : null,
1144
+ isPrivate: typeof item.is_private === 'boolean' ? item.is_private : null,
1145
+ })),
1146
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
1147
+ };
1148
+ }
1149
+ export async function listSlackDirectoryUsers(options) {
1150
+ const url = slackRelayAuthorizedUrl('/slack/directory/users', options);
1151
+ const response = await fetch(url.toString(), {
1152
+ method: 'GET',
1153
+ headers: { 'Authorization': `Bearer ${options.token}` },
1154
+ });
1155
+ if (!response.ok) {
1156
+ let detail = response.statusText || 'Slack user lookup failed';
1157
+ try {
1158
+ const payload = await response.json();
1159
+ if (typeof payload.detail === 'string' && payload.detail)
1160
+ detail = payload.detail;
1161
+ }
1162
+ catch {
1163
+ // Keep the HTTP status text when the error body is not JSON.
1164
+ }
1165
+ throw new APIError(response.status, detail);
1166
+ }
1167
+ const payload = await response.json();
1168
+ const users = Array.isArray(payload.users) ? payload.users : [];
1169
+ return {
1170
+ users: users
1171
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
1172
+ .map((item) => ({
1173
+ id: item.id,
1174
+ name: typeof item.name === 'string' ? item.name : null,
1175
+ realName: typeof item.real_name === 'string' ? item.real_name : null,
1176
+ teamId: typeof item.team_id === 'string' ? item.team_id : null,
1177
+ isBot: typeof item.is_bot === 'boolean' ? item.is_bot : null,
1178
+ deleted: typeof item.deleted === 'boolean' ? item.deleted : null,
1179
+ })),
1180
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
1181
+ };
1182
+ }
1183
+ export async function attachSlackRelayAgent(options) {
1184
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1185
+ const agentId = options.agentId.trim();
1186
+ if (!relayBaseUrl)
1187
+ throw new Error('Slack relay base URL is required');
1188
+ if (!options.token)
1189
+ throw new Error('Slack relay attach requires an app token');
1190
+ if (!agentId)
1191
+ throw new Error('Slack relay attach requires an agent id');
1192
+ const response = await fetch(`${relayBaseUrl}/slack/agents/${encodeURIComponent(agentId)}/relay`, {
1193
+ method: 'POST',
1194
+ headers: { 'Authorization': `Bearer ${options.token}` },
1195
+ });
1196
+ if (!response.ok) {
1197
+ let detail = response.statusText || 'Slack relay attach failed';
1198
+ try {
1199
+ const payload = await response.json();
1200
+ if (typeof payload.detail === 'string' && payload.detail)
1201
+ detail = payload.detail;
1202
+ }
1203
+ catch {
1204
+ // Keep the HTTP status text when the error body is not JSON.
1205
+ }
1206
+ throw new APIError(response.status, detail);
1207
+ }
1208
+ const payload = await response.json();
1209
+ const agentIdValue = typeof payload.agent_id === 'string' ? payload.agent_id : '';
1210
+ const gatewayIdValue = typeof payload.gateway_id === 'string' ? payload.gateway_id : '';
1211
+ if (!agentIdValue || !gatewayIdValue)
1212
+ throw new Error('Slack relay attach response is missing agent identity');
1213
+ return {
1214
+ connected: payload.connected === true,
1215
+ agentId: agentIdValue,
1216
+ gatewayId: gatewayIdValue,
1217
+ config: payload.config && typeof payload.config === 'object' && !Array.isArray(payload.config) ? payload.config : {},
1218
+ restartRequired: payload.restart_required !== false,
1219
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
1220
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
1221
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
1222
+ };
1223
+ }
286
1224
  export function buildOpenClawRoutes(options = {}) {
287
1225
  const routes = {};
288
1226
  if (options.includeGateway ?? true) {
@@ -292,7 +1230,7 @@ export function buildOpenClawRoutes(options = {}) {
292
1230
  prefix: options.gatewayPrefix ?? '',
293
1231
  };
294
1232
  }
295
- if (options.includeDesktop ?? true) {
1233
+ if (options.includeDesktop ?? false) {
296
1234
  routes.desktop = {
297
1235
  port: options.desktopPort ?? 3000,
298
1236
  auth: options.desktopAuth ?? true,
@@ -301,6 +1239,15 @@ export function buildOpenClawRoutes(options = {}) {
301
1239
  }
302
1240
  return routes;
303
1241
  }
1242
+ export function buildHermesAgentRoutes(options = {}) {
1243
+ return {
1244
+ hermes: {
1245
+ port: options.port ?? 8642,
1246
+ auth: options.auth ?? false,
1247
+ prefix: options.prefix ?? '',
1248
+ },
1249
+ };
1250
+ }
304
1251
  function envBool(value) {
305
1252
  return value ? '1' : '0';
306
1253
  }
@@ -335,6 +1282,24 @@ export function buildOpenClawMemoryIndexEnv(memoryIndex = null) {
335
1282
  }
336
1283
  return env;
337
1284
  }
1285
+ export function buildOpenClawWorkspacesSyncEnv(workspacesSync = null) {
1286
+ if (workspacesSync === false)
1287
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1288
+ const options = typeof workspacesSync === 'object' && workspacesSync !== null ? workspacesSync : {};
1289
+ if (options.enabled === false)
1290
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1291
+ const env = { ...OPENCLAW_WORKSPACES_SYNC_ENV_DEFAULTS };
1292
+ if (options.enabled !== undefined && options.enabled !== null) {
1293
+ env.HYPER_WORKSPACES_BOOT_SYNC = envBool(options.enabled);
1294
+ }
1295
+ if (options.readyOnly !== undefined && options.readyOnly !== null) {
1296
+ env.HYPER_WORKSPACES_SYNC_READY_ONLY = envBool(options.readyOnly);
1297
+ }
1298
+ if (options.workspace) {
1299
+ env.HYPER_WORKSPACES_SYNC_WORKSPACE = options.workspace;
1300
+ }
1301
+ return env;
1302
+ }
338
1303
  async function getFsPromises() {
339
1304
  return import('node:fs/promises');
340
1305
  }
@@ -345,10 +1310,18 @@ function bindAgent(agent, deployments) {
345
1310
  export class Agent {
346
1311
  id;
347
1312
  userId;
348
- podId;
349
- podName;
350
1313
  state;
351
1314
  name;
1315
+ handle;
1316
+ displayName;
1317
+ avatarUrl;
1318
+ displayIdentity;
1319
+ runtime;
1320
+ managed;
1321
+ isLaunchable;
1322
+ gatewayId;
1323
+ runtimeKeyAlias;
1324
+ relayKey;
352
1325
  cpu;
353
1326
  memory;
354
1327
  hostname;
@@ -357,7 +1330,9 @@ export class Agent {
357
1330
  jwtExpiresAt;
358
1331
  startedAt;
359
1332
  stoppedAt;
360
- lastError;
1333
+ archivedAt;
1334
+ clusterId;
1335
+ launchEpoch;
361
1336
  createdAt;
362
1337
  updatedAt;
363
1338
  launchConfig;
@@ -365,16 +1340,23 @@ export class Agent {
365
1340
  routes;
366
1341
  command;
367
1342
  entrypoint;
368
- ports;
369
1343
  dryRun;
370
1344
  _deployments = null;
371
1345
  constructor(fields) {
372
1346
  this.id = fields.id;
373
1347
  this.userId = fields.userId;
374
- this.podId = fields.podId;
375
- this.podName = fields.podName;
376
1348
  this.state = fields.state;
377
1349
  this.name = fields.name ?? null;
1350
+ this.handle = fields.handle ?? null;
1351
+ this.displayName = fields.displayName ?? this.name;
1352
+ this.avatarUrl = fields.avatarUrl ?? null;
1353
+ this.displayIdentity = fields.displayIdentity ? structuredClone(fields.displayIdentity) : null;
1354
+ this.runtime = fields.runtime ?? null;
1355
+ this.managed = fields.managed ?? null;
1356
+ this.isLaunchable = fields.isLaunchable ?? true;
1357
+ this.gatewayId = fields.gatewayId ?? null;
1358
+ this.runtimeKeyAlias = fields.runtimeKeyAlias ?? null;
1359
+ this.relayKey = fields.relayKey ? structuredClone(fields.relayKey) : null;
378
1360
  this.cpu = fields.cpu;
379
1361
  this.memory = fields.memory;
380
1362
  this.hostname = fields.hostname ?? null;
@@ -383,7 +1365,9 @@ export class Agent {
383
1365
  this.jwtExpiresAt = fields.jwtExpiresAt ?? null;
384
1366
  this.startedAt = fields.startedAt ?? null;
385
1367
  this.stoppedAt = fields.stoppedAt ?? null;
386
- this.lastError = fields.lastError ?? null;
1368
+ this.archivedAt = fields.archivedAt ?? null;
1369
+ this.clusterId = fields.clusterId ?? null;
1370
+ this.launchEpoch = fields.launchEpoch ?? 0;
387
1371
  this.createdAt = fields.createdAt ?? null;
388
1372
  this.updatedAt = fields.updatedAt ?? null;
389
1373
  this.launchConfig = fields.launchConfig ?? null;
@@ -391,7 +1375,6 @@ export class Agent {
391
1375
  this.routes = { ...fields.routes };
392
1376
  this.command = [...fields.command];
393
1377
  this.entrypoint = [...fields.entrypoint];
394
- this.ports = [...fields.ports];
395
1378
  this.dryRun = fields.dryRun;
396
1379
  }
397
1380
  static fromDict(data) {
@@ -422,15 +1405,34 @@ export class Agent {
422
1405
  get vncUrl() {
423
1406
  return this.desktopUrl;
424
1407
  }
1408
+ browserDesktopUrl(token, options = {}) {
1409
+ if (!this.desktopUrl)
1410
+ return null;
1411
+ return buildBrowserDesktopUrl(this.desktopUrl, token, options);
1412
+ }
425
1413
  get shellUrl() {
426
1414
  return this.routeUrl('shell');
427
1415
  }
428
- get executorUrl() {
429
- return this.shellUrl;
430
- }
431
1416
  get isRunning() {
432
1417
  return this.state.toLowerCase() === 'running';
433
1418
  }
1419
+ get isTransitioning() {
1420
+ return isAgentTransitionalState(this.state);
1421
+ }
1422
+ /** True when the agent is cold-restorable from its verified archive. */
1423
+ get isArchived() {
1424
+ return this.state.toUpperCase() === 'ARCHIVED';
1425
+ }
1426
+ /** True only for an explicitly included deletion tombstone. */
1427
+ get isDeleted() {
1428
+ return this.state.toUpperCase() === 'DELETED';
1429
+ }
1430
+ get hasDesktop() {
1431
+ return agentConfigHasDesktop({
1432
+ launchConfig: this.launchConfig,
1433
+ routes: this.routes,
1434
+ });
1435
+ }
434
1436
  requireDeployments() {
435
1437
  if (!this._deployments) {
436
1438
  throw new Error('Agent is not bound to a Deployments client');
@@ -451,7 +1453,7 @@ export class Agent {
451
1453
  return data;
452
1454
  }
453
1455
  async waitRunning(timeoutMs = 300_000, pollIntervalMs = 5_000) {
454
- return this.requireDeployments().waitRunning(this.id, timeoutMs, pollIntervalMs);
1456
+ return this.requireDeployments().waitRunning(this.id, timeoutMs, pollIntervalMs, this.launchEpoch > 0 ? this.launchEpoch : undefined);
455
1457
  }
456
1458
  async update(options) {
457
1459
  return this.requireDeployments().update(this.id, options);
@@ -460,32 +1462,68 @@ export class Agent {
460
1462
  return this.requireDeployments().resize(this.id, options);
461
1463
  }
462
1464
  async env() {
463
- const data = await this.requireDeployments().env(this.id);
464
- return data.env ?? {};
1465
+ const response = await this.requireDeployments().env(this.id);
1466
+ if (response.launch_epoch < this.launchEpoch) {
1467
+ throw new Error('agent env belongs to an older launch epoch');
1468
+ }
1469
+ return response.env;
1470
+ }
1471
+ async secretNames() {
1472
+ const response = await this.requireDeployments().secretNames(this.id);
1473
+ if (response.launch_epoch < this.launchEpoch) {
1474
+ throw new Error('agent Secret names belong to an older launch epoch');
1475
+ }
1476
+ return response.names;
1477
+ }
1478
+ async secret(key) {
1479
+ const response = await this.requireDeployments().secret(this.id, key);
1480
+ if (response.launch_epoch < this.launchEpoch) {
1481
+ throw new Error('agent Secret belongs to an older launch epoch');
1482
+ }
1483
+ return response.value;
465
1484
  }
466
1485
  async exec(command, options = {}) {
467
1486
  return this.requireDeployments().exec(this, command, options);
468
1487
  }
469
- async health() {
470
- return this.requireDeployments().health(this);
1488
+ /**
1489
+ * The gateway file writer a subclass supplies — the seam that enables the
1490
+ * `gateway` source. The base agent has none (gateway calls throw); an agent
1491
+ * type that speaks a gateway file protocol (e.g. OpenClaw's `agents.files.*`)
1492
+ * overloads this. Kept on the base so the *calling* is standardized through
1493
+ * the superclass even though each agent's gateway differs.
1494
+ */
1495
+ gatewayFileHooks() {
1496
+ return undefined;
1497
+ }
1498
+ /**
1499
+ * The single file client for this agent — one `source` switch (`agent` |
1500
+ * `backup` | `gateway`) routes to the three underlying APIs (pod sidecar / S3
1501
+ * backup / gateway), with the SDK owning the roots. One implementation; a
1502
+ * subclass only supplies its gateway via {@link gatewayFileHooks}.
1503
+ */
1504
+ get files() {
1505
+ return new AgentFiles(this, this.requireDeployments(), this.gatewayFileHooks());
471
1506
  }
472
1507
  async filesList(path = '', source = 'auto') {
473
- return this.requireDeployments().filesList(this, path, source);
1508
+ return this.files.list(path, source);
1509
+ }
1510
+ async fileReadBytes(path, source = 'auto', options) {
1511
+ return this.files.readBytes(path, source, options);
474
1512
  }
475
- async fileReadBytes(path, source = 'auto') {
476
- return this.requireDeployments().fileReadBytes(this, path, source);
1513
+ async fileReadBytesWithMetadata(path, source = 'auto', options) {
1514
+ return this.files.readBytesWithMetadata(path, source, options);
477
1515
  }
478
- async fileRead(path, source = 'auto') {
479
- return decodeUtf8(await this.fileReadBytes(path, source));
1516
+ async fileRead(path, source = 'auto', options) {
1517
+ return this.files.read(path, source, options);
480
1518
  }
481
1519
  async fileWriteBytes(path, content, destination = 'auto') {
482
- return this.requireDeployments().fileWriteBytes(this, path, content, destination);
1520
+ return this.files.writeBytes(path, content, destination);
483
1521
  }
484
1522
  async fileWrite(path, content, destination = 'auto') {
485
- return this.requireDeployments().fileWrite(this, path, content, destination);
1523
+ return this.files.write(path, content, destination);
486
1524
  }
487
1525
  async fileDelete(path, options = {}) {
488
- return this.requireDeployments().fileDelete(this, path, options);
1526
+ return this.files.delete(path, options);
489
1527
  }
490
1528
  async cpTo(localPath, remotePath) {
491
1529
  return this.requireDeployments().cpTo(this, localPath, remotePath);
@@ -493,8 +1531,434 @@ export class Agent {
493
1531
  async cpFrom(remotePath, localPath) {
494
1532
  return this.requireDeployments().cpFrom(this, remotePath, localPath);
495
1533
  }
496
- async shellConnect(shell) {
497
- return this.requireDeployments().shellConnect(this.id, shell);
1534
+ async shellConnect(shell, options) {
1535
+ return this.requireDeployments().shellConnect(this.id, shell, options);
1536
+ }
1537
+ }
1538
+ const RUNTIME_AUTH_CONFIG = {
1539
+ 'buzz-agent': {
1540
+ agentCommand: ['buzz-agent'],
1541
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'buzz-agent', '--json'],
1542
+ logoutCommand: null,
1543
+ nativeMethods: [],
1544
+ },
1545
+ opencode: {
1546
+ agentCommand: ['opencode', 'acp'],
1547
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'opencode', '--agent-args', 'acp', '--json'],
1548
+ logoutCommand: ['opencode', 'auth', 'logout'],
1549
+ nativeMethods: [],
1550
+ },
1551
+ codex: {
1552
+ agentCommand: ['codex-acp'],
1553
+ statusCommand: ['codex', 'login', 'status'],
1554
+ logoutCommand: ['codex', 'logout'],
1555
+ nativeMethods: [{
1556
+ id: 'device',
1557
+ name: 'Device authentication',
1558
+ description: 'Authenticate Codex with a device code.',
1559
+ kind: 'native',
1560
+ command: ['codex', 'login', '--device-auth'],
1561
+ metadata: {},
1562
+ }],
1563
+ },
1564
+ 'claude-code': {
1565
+ agentCommand: ['claude-agent-acp'],
1566
+ statusCommand: ['claude', 'auth', 'status', '--json'],
1567
+ logoutCommand: ['claude', 'auth', 'logout'],
1568
+ nativeMethods: [
1569
+ { id: 'claude-ai', name: 'Claude.ai', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--claudeai'], metadata: {} },
1570
+ { id: 'console', name: 'Anthropic Console', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--console'], metadata: {} },
1571
+ { id: 'sso', name: 'Enterprise SSO', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--sso'], metadata: {} },
1572
+ ],
1573
+ },
1574
+ goose: {
1575
+ agentCommand: ['goose', 'acp'],
1576
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'goose', '--agent-args', 'acp', '--json'],
1577
+ logoutCommand: null,
1578
+ nativeMethods: [],
1579
+ },
1580
+ 'kimi-code': {
1581
+ agentCommand: ['kimi', 'acp'],
1582
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'kimi', '--agent-args', 'acp', '--json'],
1583
+ logoutCommand: null,
1584
+ nativeMethods: [],
1585
+ },
1586
+ };
1587
+ function shellQuote(value) {
1588
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1589
+ }
1590
+ function commandString(command) {
1591
+ return command.map(shellQuote).join(' ');
1592
+ }
1593
+ // Terminal control-sequence matching intentionally includes ESC and BEL.
1594
+ /* eslint-disable no-control-regex */
1595
+ const TERMINAL_ESCAPE_PATTERN = new RegExp('\\x1B(?:\\[[0-?]*[ -/]*[@-~]|\\][^\\x07]*(?:\\x07|\\x1B\\\\))', 'g');
1596
+ /* eslint-enable no-control-regex */
1597
+ function stripTerminalCodes(value) {
1598
+ return value.replace(TERMINAL_ESCAPE_PATTERN, '').replace(/\r/g, '');
1599
+ }
1600
+ function authMethodFromPayload(value) {
1601
+ if (!isPlainRecord(value))
1602
+ return null;
1603
+ const id = typeof value.id === 'string' ? value.id : '';
1604
+ if (!id)
1605
+ return null;
1606
+ const metadata = isPlainRecord(value._meta) ? { ...value._meta } : {};
1607
+ let command = [];
1608
+ const terminal = isPlainRecord(metadata['terminal-auth']) ? metadata['terminal-auth'] : null;
1609
+ const source = terminal ?? value;
1610
+ if (Array.isArray(source.command) && source.command.every((part) => typeof part === 'string')) {
1611
+ command = [...source.command];
1612
+ }
1613
+ else if (typeof source.command === 'string') {
1614
+ const args = Array.isArray(source.args) ? source.args.filter((part) => typeof part === 'string') : [];
1615
+ command = [source.command, ...args];
1616
+ }
1617
+ if (id === 'claude-login' && command.length > 0 && !command.includes('login')) {
1618
+ command.push('auth', 'login');
1619
+ }
1620
+ return {
1621
+ id,
1622
+ name: typeof value.name === 'string' ? value.name : id,
1623
+ description: typeof value.description === 'string' ? value.description : '',
1624
+ kind: typeof value.kind === 'string' ? value.kind : 'acp',
1625
+ command,
1626
+ metadata,
1627
+ };
1628
+ }
1629
+ async function websocketMessageText(data) {
1630
+ if (typeof data === 'string')
1631
+ return data;
1632
+ if (data instanceof ArrayBuffer)
1633
+ return decodeUtf8(new Uint8Array(data));
1634
+ if (ArrayBuffer.isView(data))
1635
+ return decodeUtf8(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
1636
+ if (typeof Blob !== 'undefined' && data instanceof Blob)
1637
+ return data.text();
1638
+ return String(data ?? '');
1639
+ }
1640
+ export class RuntimeLoginSession {
1641
+ authClient;
1642
+ socket;
1643
+ requiresDeviceChallenge;
1644
+ output = '';
1645
+ verificationUrl = null;
1646
+ userCode = null;
1647
+ interactiveRequired = false;
1648
+ rawOutput = '';
1649
+ exitCode = null;
1650
+ marker;
1651
+ completion;
1652
+ ready;
1653
+ complete;
1654
+ markReady;
1655
+ constructor(authClient, socket, command, requiresDeviceChallenge) {
1656
+ this.authClient = authClient;
1657
+ this.socket = socket;
1658
+ this.requiresDeviceChallenge = requiresDeviceChallenge;
1659
+ this.marker = `__HYPERCLI_AUTH_EXIT_${randomHexToken(12)}__=`;
1660
+ this.completion = new Promise((resolve) => { this.complete = resolve; });
1661
+ this.ready = new Promise((resolve) => { this.markReady = resolve; });
1662
+ socket.onmessage = (event) => {
1663
+ void websocketMessageText(event.data).then((chunk) => this.consume(chunk));
1664
+ };
1665
+ socket.onclose = () => {
1666
+ this.markReady();
1667
+ this.complete();
1668
+ };
1669
+ socket.send(`${commandString(command)}; _hypercli_auth_rc=$?; printf '\\n${this.marker}%s\\n' "$_hypercli_auth_rc"\n`);
1670
+ }
1671
+ static async start(authClient, command, challengeTimeoutMs = 45_000, requiresDeviceChallenge = false) {
1672
+ const socket = await authClient.agent.shellConnect();
1673
+ const session = new RuntimeLoginSession(authClient, socket, command, requiresDeviceChallenge);
1674
+ let timer;
1675
+ try {
1676
+ await Promise.race([
1677
+ session.ready,
1678
+ new Promise((_, reject) => {
1679
+ timer = setTimeout(() => reject(new Error('Timed out waiting for runtime login instructions')), challengeTimeoutMs);
1680
+ }),
1681
+ ]);
1682
+ return session;
1683
+ }
1684
+ catch (error) {
1685
+ session.cancel();
1686
+ throw error;
1687
+ }
1688
+ finally {
1689
+ if (timer)
1690
+ clearTimeout(timer);
1691
+ }
1692
+ }
1693
+ consume(chunk) {
1694
+ this.rawOutput += chunk;
1695
+ this.output = stripTerminalCodes(this.rawOutput);
1696
+ const markerIndex = this.output.lastIndexOf(this.marker);
1697
+ if (markerIndex >= 0) {
1698
+ const match = this.output.slice(markerIndex + this.marker.length).match(/^(-?\d+)/);
1699
+ if (match) {
1700
+ this.exitCode = Number(match[1]);
1701
+ this.markReady();
1702
+ this.complete();
1703
+ }
1704
+ }
1705
+ this.verificationUrl ??= this.output.match(/https?:\/\/[^\s"'<>]+(?=[\s"'<>])/)?.[0] ?? null;
1706
+ this.userCode ??= this.output.match(/\b(?:user|device|verification|one[- ]time)\s+code\b\s*(?:is|:)?\s*(?:\([^\r\n)]*\)\s*)*((?!authorization\b)[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?)(?=[\s.,;:)\]])/i)?.[1] ?? null;
1707
+ this.interactiveRequired ||= /\b(select|choose)\b.*\b(provider|login method)\b/i.test(this.output);
1708
+ const challengeReady = this.requiresDeviceChallenge
1709
+ ? Boolean(this.verificationUrl && this.userCode)
1710
+ : Boolean(this.verificationUrl || this.userCode);
1711
+ if (challengeReady || this.interactiveRequired)
1712
+ this.markReady();
1713
+ }
1714
+ send(text) {
1715
+ this.socket.send(text.endsWith('\n') ? text : `${text}\n`);
1716
+ }
1717
+ async wait(timeoutMs = 600_000) {
1718
+ let timer;
1719
+ try {
1720
+ await Promise.race([
1721
+ this.completion,
1722
+ new Promise((_, reject) => {
1723
+ timer = setTimeout(() => reject(new Error('Runtime authentication timed out')), timeoutMs);
1724
+ }),
1725
+ ]);
1726
+ }
1727
+ catch (error) {
1728
+ this.cancel();
1729
+ throw error;
1730
+ }
1731
+ finally {
1732
+ if (timer)
1733
+ clearTimeout(timer);
1734
+ }
1735
+ if (this.exitCode !== null && this.exitCode !== 0) {
1736
+ throw new Error(`Runtime authentication failed (${this.exitCode}): ${this.output.trim()}`);
1737
+ }
1738
+ return this.authClient.status();
1739
+ }
1740
+ cancel() {
1741
+ // The SDK supports Node runtimes where the socket is supplied by `ws` and
1742
+ // no global WebSocket constructor exists. OPEN is the protocol state 1.
1743
+ if (this.socket.readyState === 1)
1744
+ this.socket.send('\x03');
1745
+ this.socket.close();
1746
+ }
1747
+ }
1748
+ export class RuntimeAuthClient {
1749
+ agent;
1750
+ config;
1751
+ constructor(agent) {
1752
+ this.agent = agent;
1753
+ this.config = RUNTIME_AUTH_CONFIG[agent.runtime];
1754
+ }
1755
+ async methods() {
1756
+ const [agentCommand, ...agentArgs] = this.config.agentCommand;
1757
+ const command = ['buzz-acp', 'auth-methods', '--agent-command', agentCommand];
1758
+ if (agentArgs.length)
1759
+ command.push('--agent-args', agentArgs.join(','));
1760
+ command.push('--json');
1761
+ const result = await this.agent.exec(commandString(command));
1762
+ const discovered = [];
1763
+ if (result.exitCode === 0) {
1764
+ try {
1765
+ const payload = JSON.parse(stripTerminalCodes(result.stdout));
1766
+ const values = isPlainRecord(payload) && Array.isArray(payload.methods) ? payload.methods : [];
1767
+ for (const value of values) {
1768
+ const method = authMethodFromPayload(value);
1769
+ if (method)
1770
+ discovered.push(method);
1771
+ }
1772
+ }
1773
+ catch {
1774
+ // Native fallbacks still make authentication available.
1775
+ }
1776
+ }
1777
+ if (this.agent.runtime === 'opencode' && discovered.length === 0) {
1778
+ discovered.push({
1779
+ id: 'provider',
1780
+ name: 'Provider login',
1781
+ description: '',
1782
+ kind: 'native',
1783
+ command: ['opencode', 'auth', 'login'],
1784
+ metadata: {},
1785
+ });
1786
+ }
1787
+ const seen = new Set(discovered.map((method) => method.id));
1788
+ for (const method of this.config.nativeMethods) {
1789
+ if (!seen.has(method.id))
1790
+ discovered.push({ ...method, command: [...method.command], metadata: { ...method.metadata } });
1791
+ }
1792
+ return discovered;
1793
+ }
1794
+ async status() {
1795
+ const result = await this.agent.exec(commandString(this.config.statusCommand));
1796
+ const output = stripTerminalCodes([result.stdout, result.stderr].filter(Boolean).join('\n')).trim();
1797
+ const detail = { exitCode: result.exitCode, output };
1798
+ if (this.agent.runtime === 'claude-code') {
1799
+ try {
1800
+ const payload = JSON.parse(stripTerminalCodes(result.stdout));
1801
+ Object.assign(detail, payload);
1802
+ const loginMethod = payload.loginMethod ?? payload.authMethod;
1803
+ return {
1804
+ authenticated: payload.loggedIn === true || payload.authenticated === true ||
1805
+ (typeof loginMethod === 'string' && loginMethod.toLowerCase() !== 'none'),
1806
+ provider: typeof (payload.subscriptionType ?? payload.provider ?? payload.apiProvider) === 'string'
1807
+ ? String(payload.subscriptionType ?? payload.provider ?? payload.apiProvider) : null,
1808
+ account: typeof payload.email === 'string' ? payload.email : null,
1809
+ method: typeof loginMethod === 'string' ? loginMethod : null,
1810
+ detail,
1811
+ };
1812
+ }
1813
+ catch {
1814
+ // Fall through to the generic status parser.
1815
+ }
1816
+ }
1817
+ const negative = /\b(not logged|not authenticated|unauthenticated|no credentials|0 credentials)\b/i.test(output);
1818
+ return { authenticated: result.exitCode === 0 && !negative, detail };
1819
+ }
1820
+ async login(options = {}) {
1821
+ const methods = await this.methods();
1822
+ const method = options.method
1823
+ ? methods.find((candidate) => candidate.id === options.method)
1824
+ : methods.find((candidate) => candidate.command.length > 0) ?? methods[0];
1825
+ if (!method) {
1826
+ throw new Error(options.method
1827
+ ? `Unknown authentication method: ${options.method}`
1828
+ : 'No authentication methods are available');
1829
+ }
1830
+ let command = [...method.command];
1831
+ if (!command.length) {
1832
+ const [agentCommand, ...agentArgs] = this.config.agentCommand;
1833
+ command = ['buzz-acp', 'authenticate', '--agent-command', agentCommand];
1834
+ if (agentArgs.length)
1835
+ command.push('--agent-args', agentArgs.join(','));
1836
+ command.push('--method-id', method.id);
1837
+ }
1838
+ if (this.agent.runtime === 'opencode') {
1839
+ if (options.provider)
1840
+ command.push('--provider', options.provider);
1841
+ if (options.providerMethod)
1842
+ command.push('--method', options.providerMethod);
1843
+ }
1844
+ if (this.agent.runtime === 'claude-code' && options.email)
1845
+ command.push('--email', options.email);
1846
+ const requiresDeviceChallenge = method.kind === 'device'
1847
+ || method.id === 'device'
1848
+ || command.some((part) => part.toLowerCase().includes('device-auth'));
1849
+ return RuntimeLoginSession.start(this, command, options.challengeTimeoutMs, requiresDeviceChallenge);
1850
+ }
1851
+ async logout(provider) {
1852
+ if (this.config.logoutCommand === null) {
1853
+ const reason = this.agent.runtime === 'goose'
1854
+ ? 'uses its injected deployment credential'
1855
+ : 'does not expose a noninteractive logout command';
1856
+ throw new Error(`${this.agent.runtime} ${reason} and cannot log out`);
1857
+ }
1858
+ const command = [...this.config.logoutCommand];
1859
+ if (this.agent.runtime === 'opencode' && provider)
1860
+ command.push(provider);
1861
+ const result = await this.agent.exec(commandString(command));
1862
+ if (result.exitCode !== 0) {
1863
+ throw new Error(`Runtime logout failed (${result.exitCode}): ${stripTerminalCodes(result.stderr || result.stdout).trim()}`);
1864
+ }
1865
+ return this.status();
1866
+ }
1867
+ }
1868
+ export class CodingAgent extends Agent {
1869
+ static defaultSyncInclude = null;
1870
+ get auth() {
1871
+ return new RuntimeAuthClient(this);
1872
+ }
1873
+ }
1874
+ export class BuzzAgent extends CodingAgent {
1875
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES['buzz-agent'];
1876
+ static fromDict(data) {
1877
+ return new BuzzAgent(agentStateFromDict(data));
1878
+ }
1879
+ }
1880
+ export class OpenCodeAgent extends CodingAgent {
1881
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES.opencode;
1882
+ static fromDict(data) {
1883
+ return new OpenCodeAgent(agentStateFromDict(data));
1884
+ }
1885
+ }
1886
+ export class CodexAgent extends CodingAgent {
1887
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES.codex;
1888
+ static fromDict(data) {
1889
+ return new CodexAgent(agentStateFromDict(data));
1890
+ }
1891
+ }
1892
+ export class ClaudeCodeAgent extends CodingAgent {
1893
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES['claude-code'];
1894
+ static fromDict(data) {
1895
+ return new ClaudeCodeAgent(agentStateFromDict(data));
1896
+ }
1897
+ }
1898
+ export class GooseAgent extends CodingAgent {
1899
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES.goose;
1900
+ static fromDict(data) {
1901
+ return new GooseAgent(agentStateFromDict(data));
1902
+ }
1903
+ }
1904
+ export class KimiCodeAgent extends CodingAgent {
1905
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES['kimi-code'];
1906
+ static fromDict(data) {
1907
+ return new KimiCodeAgent(agentStateFromDict(data));
1908
+ }
1909
+ }
1910
+ const CODING_AGENT_CLASSES = {
1911
+ 'buzz-agent': BuzzAgent,
1912
+ opencode: OpenCodeAgent,
1913
+ codex: CodexAgent,
1914
+ 'claude-code': ClaudeCodeAgent,
1915
+ goose: GooseAgent,
1916
+ 'kimi-code': KimiCodeAgent,
1917
+ };
1918
+ export class HermesAgent extends Agent {
1919
+ apiUrl;
1920
+ openaiBaseUrl;
1921
+ /** Available only on the instance returned by createHermesAgent/startHermesAgent. */
1922
+ apiServerKey;
1923
+ constructor(fields) {
1924
+ super(fields);
1925
+ this.apiUrl = fields.apiUrl ?? this.routeUrl('hermes', '');
1926
+ this.openaiBaseUrl = this.apiUrl ? `${this.apiUrl.replace(/\/+$/, '')}/v1` : null;
1927
+ this.apiServerKey = fields.apiServerKey ?? null;
1928
+ }
1929
+ static fromDict(data) {
1930
+ const fields = agentStateFromDict(data);
1931
+ if (fields.launchConfig?.env && typeof fields.launchConfig.env === 'object') {
1932
+ fields.launchConfig = structuredClone(fields.launchConfig);
1933
+ delete fields.launchConfig.env.API_SERVER_KEY;
1934
+ }
1935
+ return new HermesAgent({
1936
+ ...fields,
1937
+ // Never recover API_SERVER_KEY from launch_config or process env.
1938
+ apiServerKey: null,
1939
+ });
1940
+ }
1941
+ /** Create a client for this agent's authenticated Hermes API. */
1942
+ get api() {
1943
+ if (!this.apiUrl || !this.apiServerKey)
1944
+ return null;
1945
+ return new HermesApiClient(this.apiUrl, { apiKey: this.apiServerKey });
1946
+ }
1947
+ /** Require a ready URL and the one-time SDK-retained API server key. */
1948
+ apiClient() {
1949
+ const client = this.api;
1950
+ if (!client) {
1951
+ throw new Error('Hermes API context is unavailable; use the HermesAgent returned by createHermesAgent/startHermesAgent after its hostname is attached');
1952
+ }
1953
+ return client;
1954
+ }
1955
+ async waitRunning(timeoutMs = 300_000, pollIntervalMs = 5_000) {
1956
+ const ready = await super.waitRunning(timeoutMs, pollIntervalMs);
1957
+ if (!(ready instanceof HermesAgent)) {
1958
+ throw new Error("Running deployment did not identify runtime 'hermes-agent'");
1959
+ }
1960
+ ready.apiServerKey = this.apiServerKey;
1961
+ return ready;
498
1962
  }
499
1963
  }
500
1964
  export class OpenClawAgent extends Agent {
@@ -508,70 +1972,132 @@ export class OpenClawAgent extends Agent {
508
1972
  static fromDict(data) {
509
1973
  return new OpenClawAgent({
510
1974
  ...agentStateFromDict(data),
511
- gatewayUrl: null,
512
- gatewayToken: data.gateway_token ?? null,
1975
+ gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
1976
+ gatewayToken: null,
513
1977
  });
514
1978
  }
515
1979
  static gatewayUrlFromHostname(hostname) {
516
1980
  const trimmed = String(hostname ?? '').trim();
517
1981
  return trimmed ? `wss://${trimmed}` : null;
518
1982
  }
519
- currentGatewayHostname() {
520
- if (this.hostname)
521
- return this.hostname;
522
- if (!this.gatewayUrl)
523
- return null;
524
- try {
525
- return new URL(this.gatewayUrl).hostname || null;
526
- }
527
- catch {
528
- return this.gatewayUrl.replace(/^wss?:\/\//, '').split('/')[0] || null;
529
- }
530
- }
531
- /**
532
- * Resolve gateway context through the deployment record plus `/env`.
533
- *
534
- * Agent startup is eventually consistent: the deployment record may lag
535
- * behind hostname attachment, and runtime env can lag behind both. The SDK
536
- * derives the gateway URL from the attached hostname and reads the gateway
537
- * token from `OPENCLAW_GATEWAY_TOKEN` in the agent env route.
538
- */
1983
+ /** Resolve OpenClaw connection context from the generic Agent and Secret APIs. */
539
1984
  async waitForGatewayContext(options = {}) {
540
- if (this.gatewayToken && this.gatewayUrl) {
541
- return {
542
- agent_id: this.id,
543
- hostname: this.currentGatewayHostname(),
544
- gateway_token: this.gatewayToken,
545
- };
546
- }
1985
+ const callerAbortError = () => {
1986
+ if (options.signal?.reason instanceof Error)
1987
+ return options.signal.reason;
1988
+ const error = new Error('OpenClaw gateway context wait cancelled');
1989
+ error.name = 'AbortError';
1990
+ return error;
1991
+ };
1992
+ if (options.signal?.aborted)
1993
+ throw callerAbortError();
547
1994
  const timeoutMs = options.timeoutMs ?? 30_000;
548
1995
  const retryIntervalMs = options.retryIntervalMs ?? 1_000;
549
1996
  const deployments = this.requireDeployments();
550
1997
  const deadline = Date.now() + timeoutMs;
551
1998
  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;
1999
+ const timeoutError = () => {
2000
+ return lastError instanceof Error
2001
+ ? lastError
2002
+ : new Error('Timed out waiting for OpenClaw gateway context');
2003
+ };
2004
+ const controller = new AbortController();
2005
+ const forwardAbort = () => controller.abort(callerAbortError());
2006
+ options.signal?.addEventListener('abort', forwardAbort, { once: true });
2007
+ const timeoutId = setTimeout(() => controller.abort(timeoutError()), Math.max(0, timeoutMs));
2008
+ const waitAbortError = () => {
2009
+ return controller.signal.reason instanceof Error ? controller.signal.reason : callerAbortError();
2010
+ };
2011
+ const runWithAbort = (operation) => {
2012
+ if (controller.signal.aborted)
2013
+ return Promise.reject(waitAbortError());
2014
+ return new Promise((resolve, reject) => {
2015
+ const abortOperation = () => {
2016
+ cleanup();
2017
+ reject(waitAbortError());
2018
+ };
2019
+ const cleanup = () => controller.signal.removeEventListener('abort', abortOperation);
2020
+ controller.signal.addEventListener('abort', abortOperation, { once: true });
2021
+ operation.then((value) => {
2022
+ cleanup();
2023
+ resolve(value);
2024
+ }, (error) => {
2025
+ cleanup();
2026
+ reject(error);
2027
+ });
2028
+ });
2029
+ };
2030
+ const waitForRetry = (delayMs) => {
2031
+ if (controller.signal.aborted)
2032
+ return Promise.reject(waitAbortError());
2033
+ return new Promise((resolve, reject) => {
2034
+ const finish = () => {
2035
+ controller.signal.removeEventListener('abort', abortWait);
2036
+ resolve();
2037
+ };
2038
+ const timer = setTimeout(finish, delayMs);
2039
+ const abortWait = () => {
2040
+ clearTimeout(timer);
2041
+ controller.signal.removeEventListener('abort', abortWait);
2042
+ reject(waitAbortError());
2043
+ };
2044
+ controller.signal.addEventListener('abort', abortWait, { once: true });
2045
+ });
2046
+ };
2047
+ try {
2048
+ while (true) {
2049
+ const remainingMs = deadline - Date.now();
2050
+ if (remainingMs <= 0)
2051
+ throw timeoutError();
2052
+ const requestOptions = {
2053
+ signal: controller.signal,
2054
+ timeout: Math.max(1, remainingMs),
2055
+ };
2056
+ try {
2057
+ const refreshed = await runWithAbort(deployments.get(this.id, requestOptions));
2058
+ if (refreshed.launchEpoch < this.launchEpoch) {
2059
+ throw new Error('gateway Agent belongs to an older launch epoch');
2060
+ }
2061
+ if (refreshed.state.toUpperCase() !== 'RUNNING') {
2062
+ throw new Error(`gateway Agent is ${refreshed.state}, not RUNNING`);
2063
+ }
2064
+ const secret = await runWithAbort(deployments.secret(this.id, 'OPENCLAW_GATEWAY_TOKEN', requestOptions));
2065
+ if (secret.launch_epoch !== refreshed.launchEpoch) {
2066
+ throw new Error('gateway Secret and Agent belong to different launch epochs');
2067
+ }
2068
+ const gatewayToken = secret.value.trim();
2069
+ if (!gatewayToken)
2070
+ throw new Error('OPENCLAW_GATEWAY_TOKEN is empty');
2071
+ const confirmed = await runWithAbort(deployments.get(this.id, requestOptions));
2072
+ if (confirmed.launchEpoch !== refreshed.launchEpoch
2073
+ || confirmed.state.toUpperCase() !== 'RUNNING') {
2074
+ throw new Error('gateway Agent changed while its Secret was resolved');
2075
+ }
2076
+ const gatewayUrl = OpenClawAgent.gatewayUrlFromHostname(confirmed.hostname);
2077
+ if (!gatewayUrl)
2078
+ throw new Error('gateway Agent has no hostname');
561
2079
  this.gatewayUrl = gatewayUrl;
562
- return { agent_id: this.id, gateway_token: gatewayToken, hostname };
2080
+ this.gatewayToken = gatewayToken;
2081
+ return {
2082
+ agent_id: this.id,
2083
+ gateway_url: gatewayUrl,
2084
+ gateway_token: gatewayToken,
2085
+ launch_epoch: refreshed.launchEpoch,
2086
+ };
563
2087
  }
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');
2088
+ catch (error) {
2089
+ lastError = error;
2090
+ }
2091
+ const remainingAfterRequestMs = deadline - Date.now();
2092
+ if (remainingAfterRequestMs <= 0)
2093
+ throw timeoutError();
2094
+ const retryDelayMs = Math.min(Math.max(0, retryIntervalMs), remainingAfterRequestMs);
2095
+ await waitForRetry(retryDelayMs);
573
2096
  }
574
- await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
2097
+ }
2098
+ finally {
2099
+ clearTimeout(timeoutId);
2100
+ options.signal?.removeEventListener('abort', forwardAbort);
575
2101
  }
576
2102
  }
577
2103
  async resolveGatewayToken() {
@@ -603,6 +2129,7 @@ export class OpenClawAgent extends Agent {
603
2129
  onHello: options.onHello,
604
2130
  onClose: options.onClose,
605
2131
  onGap: options.onGap,
2132
+ onProtocolError: options.onProtocolError,
606
2133
  onPairing: options.onPairing,
607
2134
  });
608
2135
  }
@@ -614,6 +2141,47 @@ export class OpenClawAgent extends Agent {
614
2141
  await client.connect();
615
2142
  return client;
616
2143
  }
2144
+ /** Run a fn against a connected gateway client, then close it. */
2145
+ async withGateway(fn) {
2146
+ const client = await this.connect();
2147
+ try {
2148
+ return await fn(client);
2149
+ }
2150
+ finally {
2151
+ client.close();
2152
+ }
2153
+ }
2154
+ _gatewayAgentId;
2155
+ /**
2156
+ * The in-gateway agent id for `agents.files.*` — NOT the deployment id. A
2157
+ * deployment's gateway hosts an agent named `main` (or a named agent);
2158
+ * matches the existing fileGet/fileSet/workspaceFiles convention.
2159
+ */
2160
+ async resolveGatewayAgentId(client) {
2161
+ if (this._gatewayAgentId)
2162
+ return this._gatewayAgentId;
2163
+ let agents = [];
2164
+ try {
2165
+ agents = await client.agentsList();
2166
+ }
2167
+ catch {
2168
+ // fall through to the canonical default
2169
+ }
2170
+ const resolved = agents[0]?.id ?? 'main';
2171
+ this._gatewayAgentId = resolved;
2172
+ return resolved;
2173
+ }
2174
+ /**
2175
+ * Overload the base gateway seam with OpenClaw's `agents.files.*` — the actual
2176
+ * OpenClaw gateway file methods. The base `files`/`fileX` calling surface is
2177
+ * inherited unchanged; only the gateway writer is supplied here.
2178
+ */
2179
+ gatewayFileHooks() {
2180
+ return {
2181
+ withGateway: (fn) => this.withGateway(fn),
2182
+ resolveAgentId: (c) => this.resolveGatewayAgentId(c),
2183
+ };
2184
+ }
617
2185
  async gatewayStatus(options = {}) {
618
2186
  const client = await this.connect(options);
619
2187
  try {
@@ -665,6 +2233,64 @@ export class OpenClawAgent extends Agent {
665
2233
  client.close();
666
2234
  }
667
2235
  }
2236
+ async configureSlackRelay(options, gatewayOptions = {}) {
2237
+ const { accountId, ...connectOptions } = gatewayOptions;
2238
+ const client = await this.connect(connectOptions);
2239
+ try {
2240
+ if ('relay' in options) {
2241
+ await client.configureSlackRelay(options, accountId);
2242
+ }
2243
+ else {
2244
+ await client.configureSlackRelay({
2245
+ ...options,
2246
+ gatewayId: options.gatewayId ?? this.gatewayId ?? `agent:${this.id}`,
2247
+ });
2248
+ }
2249
+ }
2250
+ finally {
2251
+ client.close();
2252
+ }
2253
+ }
2254
+ async configureSlackSocket(config, options = {}) {
2255
+ const { accountId, ...gatewayOptions } = options;
2256
+ const client = await this.connect(gatewayOptions);
2257
+ try {
2258
+ await client.configureSlackSocket(config, accountId);
2259
+ }
2260
+ finally {
2261
+ client.close();
2262
+ }
2263
+ }
2264
+ async configureSlackHttp(config, options = {}) {
2265
+ const { accountId, ...gatewayOptions } = options;
2266
+ const client = await this.connect(gatewayOptions);
2267
+ try {
2268
+ await client.configureSlackHttp(config, accountId);
2269
+ }
2270
+ finally {
2271
+ client.close();
2272
+ }
2273
+ }
2274
+ async configureTelegram(config, options = {}) {
2275
+ const { accountId, ...gatewayOptions } = options;
2276
+ const client = await this.connect(gatewayOptions);
2277
+ try {
2278
+ await client.configureTelegram(config, accountId);
2279
+ }
2280
+ finally {
2281
+ client.close();
2282
+ }
2283
+ }
2284
+ async configureWhatsapp(config, options = {}) {
2285
+ const { accountId, ...gatewayOptions } = options;
2286
+ const client = await this.connect(gatewayOptions);
2287
+ try {
2288
+ await client.configureWhatsapp(config, accountId);
2289
+ }
2290
+ finally {
2291
+ client.close();
2292
+ }
2293
+ }
668
2294
  async configApply(config, options = {}) {
669
2295
  const client = await this.connect(options);
670
2296
  try {
@@ -692,6 +2318,44 @@ export class OpenClawAgent extends Agent {
692
2318
  client.close();
693
2319
  }
694
2320
  }
2321
+ async sessionsListResult(options = {}) {
2322
+ const client = await this.connect(options);
2323
+ try {
2324
+ return await client.sessionsListResult();
2325
+ }
2326
+ finally {
2327
+ client.close();
2328
+ }
2329
+ }
2330
+ async operationsSnapshot(options = {}) {
2331
+ if (!this.gatewayUrl || (!this.gatewayToken && !options.gatewayToken)) {
2332
+ await this.waitForGatewayContext({ timeoutMs: options.timeout });
2333
+ }
2334
+ const client = this.gateway(options);
2335
+ try {
2336
+ await client.connect();
2337
+ const [sessionsResult, cronResult] = await Promise.allSettled([
2338
+ client.sessionsListResult(),
2339
+ client.cronList(),
2340
+ ]);
2341
+ const failures = {};
2342
+ if (sessionsResult.status === 'rejected') {
2343
+ failures.sessions = stringifyOpenClawOperationsFailure(sessionsResult.reason);
2344
+ }
2345
+ if (cronResult.status === 'rejected') {
2346
+ failures.cron = stringifyOpenClawOperationsFailure(cronResult.reason);
2347
+ }
2348
+ return {
2349
+ sessions: sessionsResult.status === 'fulfilled' ? sessionsResult.value : null,
2350
+ cronJobs: cronResult.status === 'fulfilled' ? cronResult.value : null,
2351
+ failures,
2352
+ capturedAt: Date.now(),
2353
+ };
2354
+ }
2355
+ finally {
2356
+ client.close();
2357
+ }
2358
+ }
695
2359
  async *chatSend(message, sessionKey, options = {}) {
696
2360
  const client = await this.connect(options);
697
2361
  try {
@@ -706,7 +2370,25 @@ export class OpenClawAgent extends Agent {
706
2370
  async channelsStatus(options = {}) {
707
2371
  const client = await this.connect(options);
708
2372
  try {
709
- return await client.channelsStatus(options.probe ?? false, options.timeoutMs);
2373
+ return await client.channelsStatus(options.probe ?? false, options.timeoutMs, options.channel);
2374
+ }
2375
+ finally {
2376
+ client.close();
2377
+ }
2378
+ }
2379
+ async channelsStart(channel, accountId, options = {}) {
2380
+ const client = await this.connect(options);
2381
+ try {
2382
+ return await client.channelsStart(channel, accountId);
2383
+ }
2384
+ finally {
2385
+ client.close();
2386
+ }
2387
+ }
2388
+ async channelsStop(channel, accountId, options = {}) {
2389
+ const client = await this.connect(options);
2390
+ try {
2391
+ return await client.channelsStop(channel, accountId);
710
2392
  }
711
2393
  finally {
712
2394
  client.close();
@@ -741,6 +2423,7 @@ export class OpenClawAgent extends Agent {
741
2423
  return await client.webLoginWait({
742
2424
  timeoutMs: options.timeoutMs,
743
2425
  accountId: options.accountId,
2426
+ currentQrDataUrl: options.currentQrDataUrl,
744
2427
  });
745
2428
  }
746
2429
  finally {
@@ -841,7 +2524,7 @@ export class OpenClawAgent extends Agent {
841
2524
  async chatSendMessage(message, options = {}) {
842
2525
  const client = await this.connect(options);
843
2526
  try {
844
- return await client.sendChat(message, options.sessionKey ?? 'main', options.agentId, options.attachments);
2527
+ return await client.sendChat(message, options.sessionKey, options.agentId, options.attachments);
845
2528
  }
846
2529
  finally {
847
2530
  client.close();
@@ -984,13 +2667,40 @@ export class OpenClawAgent extends Agent {
984
2667
  client.close();
985
2668
  }
986
2669
  }
2670
+ async cronAdd(job, options = {}) {
2671
+ const client = await this.connect(options);
2672
+ try {
2673
+ return await client.cronAdd(job);
2674
+ }
2675
+ finally {
2676
+ client.close();
2677
+ }
2678
+ }
2679
+ async cronRemove(jobId, options = {}) {
2680
+ const client = await this.connect(options);
2681
+ try {
2682
+ await client.cronRemove(jobId);
2683
+ }
2684
+ finally {
2685
+ client.close();
2686
+ }
2687
+ }
2688
+ async cronRun(jobId, options = {}) {
2689
+ const client = await this.connect(options);
2690
+ try {
2691
+ return await client.cronRun(jobId);
2692
+ }
2693
+ finally {
2694
+ client.close();
2695
+ }
2696
+ }
987
2697
  }
988
2698
  export class OpenClawProAgent extends OpenClawAgent {
989
2699
  static fromDict(data) {
990
2700
  return new OpenClawProAgent({
991
2701
  ...agentStateFromDict(data),
992
- gatewayUrl: null,
993
- gatewayToken: data.gateway_token ?? null,
2702
+ gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
2703
+ gatewayToken: null,
994
2704
  });
995
2705
  }
996
2706
  }
@@ -1000,12 +2710,13 @@ export class Deployments {
1000
2710
  apiBase;
1001
2711
  agentsWsUrl;
1002
2712
  agentHttp;
1003
- constructor(http, agentApiKey, agentApiBase, agentsWsUrl) {
2713
+ constructor(http, agentApiKey, agentApiBase, agentsWsUrl, requestTimeout) {
1004
2714
  this.http = http;
1005
2715
  this.apiKey = agentApiKey || http.apiKey;
1006
2716
  this.apiBase = resolveAgentsApiBase(agentApiBase || getAgentsApiBaseUrl());
1007
2717
  this.agentsWsUrl = normalizeAgentsWsUrl(agentsWsUrl || getConfigValue('AGENTS_WS_URL') || defaultAgentsWsUrl(this.apiBase));
1008
- this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey) : http;
2718
+ const agentTimeout = requestTimeout ?? (http instanceof HTTPClient ? http.timeout : undefined);
2719
+ this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey, agentTimeout) : http;
1009
2720
  }
1010
2721
  get agentApiKey() {
1011
2722
  return this.apiKey;
@@ -1015,7 +2726,28 @@ export class Deployments {
1015
2726
  }
1016
2727
  hydrateAgent(data) {
1017
2728
  let agent;
1018
- if (isOpenClawProHydrationData(data)) {
2729
+ if (data.runtime === 'hermes-agent') {
2730
+ agent = HermesAgent.fromDict(data);
2731
+ }
2732
+ else if (data.runtime === 'buzz-agent') {
2733
+ agent = BuzzAgent.fromDict(data);
2734
+ }
2735
+ else if (data.runtime === 'opencode') {
2736
+ agent = OpenCodeAgent.fromDict(data);
2737
+ }
2738
+ else if (data.runtime === 'codex') {
2739
+ agent = CodexAgent.fromDict(data);
2740
+ }
2741
+ else if (data.runtime === 'claude-code') {
2742
+ agent = ClaudeCodeAgent.fromDict(data);
2743
+ }
2744
+ else if (data.runtime === 'goose') {
2745
+ agent = GooseAgent.fromDict(data);
2746
+ }
2747
+ else if (data.runtime === 'kimi-code') {
2748
+ agent = KimiCodeAgent.fromDict(data);
2749
+ }
2750
+ else if (data.runtime === 'openclaw-pro' || isOpenClawProHydrationData(data)) {
1019
2751
  agent = OpenClawProAgent.fromDict(data);
1020
2752
  }
1021
2753
  else if (isOpenClawHydrationData(data)) {
@@ -1026,8 +2758,57 @@ export class Deployments {
1026
2758
  }
1027
2759
  return bindAgent(agent, this);
1028
2760
  }
1029
- agentIdFor(target) {
1030
- return typeof target === 'string' ? target : target.id;
2761
+ async getById(agentId, requestOptions = {}) {
2762
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}`;
2763
+ const data = Object.keys(requestOptions).length === 0
2764
+ ? await this.agentHttp.get(path)
2765
+ : await this.agentHttp.get(path, undefined, requestOptions);
2766
+ return this.hydrateAgent(data);
2767
+ }
2768
+ async resolveAgent(agentIdOrName, requestOptions = {}) {
2769
+ const raw = String(agentIdOrName || '').trim();
2770
+ if (!raw) {
2771
+ throw new Error('agentIdOrName is required');
2772
+ }
2773
+ if (isUuidRef(raw)) {
2774
+ return this.getById(raw, requestOptions);
2775
+ }
2776
+ const matches = [];
2777
+ for (const agent of await this.list(requestOptions)) {
2778
+ const values = [agent.id, agent.name, agent.handle, agent.hostname];
2779
+ if (values.some((value) => String(value || '') === raw)) {
2780
+ matches.push(agent);
2781
+ continue;
2782
+ }
2783
+ if (values.some((value) => String(value || '').startsWith(raw))) {
2784
+ matches.push(agent);
2785
+ }
2786
+ }
2787
+ if (matches.length === 0) {
2788
+ throw new Error(`Agent not found: ${raw}`);
2789
+ }
2790
+ if (matches.length > 1) {
2791
+ throw new Error(`Agent reference is ambiguous: ${raw} (${matches.slice(0, 5).map((agent) => agent.id).join(', ')})`);
2792
+ }
2793
+ return this.getById(matches[0].id, requestOptions);
2794
+ }
2795
+ async resolveAgentId(agentIdOrName, requestOptions = {}, allowSelf = false) {
2796
+ const raw = String(agentIdOrName || '').trim();
2797
+ if (!raw) {
2798
+ throw new Error('agentIdOrName is required');
2799
+ }
2800
+ if (isSelfAgentRef(raw)) {
2801
+ if (allowSelf)
2802
+ return 'self';
2803
+ throw new Error('self is only supported for status, start, stop, and routes');
2804
+ }
2805
+ if (isDirectAgentIdRef(raw)) {
2806
+ return raw;
2807
+ }
2808
+ return (await this.resolveAgent(raw, requestOptions)).id;
2809
+ }
2810
+ async agentIdFor(target) {
2811
+ return typeof target === 'string' ? this.resolveAgentId(target) : target.id;
1031
2812
  }
1032
2813
  async fetchRaw(path, init = {}) {
1033
2814
  const headers = new Headers(init.headers ?? {});
@@ -1056,146 +2837,654 @@ export class Deployments {
1056
2837
  return response;
1057
2838
  }
1058
2839
  async create(options = {}) {
1059
- const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, options);
2840
+ const { config } = buildAgentConfig(options.config ?? {}, {
2841
+ ...options,
2842
+ injectGatewayToken: options.injectGatewayToken ?? !isCodingAgentRuntime(options.runtime),
2843
+ });
1060
2844
  const body = { ...config, start: options.start ?? true };
1061
2845
  if (options.dryRun)
1062
2846
  body.dry_run = true;
1063
2847
  if (options.name)
1064
2848
  body.name = options.name;
2849
+ if (options.handle !== undefined)
2850
+ body.handle = options.handle;
1065
2851
  if (options.size)
1066
2852
  body.size = options.size;
1067
2853
  if (options.meta?.ui)
1068
2854
  body.meta = { ui: structuredClone(options.meta.ui) };
1069
2855
  if (options.tags?.length)
1070
2856
  body.tags = [...options.tags];
1071
- const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body);
1072
- const agent = this.hydrateAgent(data);
1073
- if (agent instanceof OpenClawAgent) {
1074
- agent.gatewayToken = gatewayToken;
1075
- }
1076
- agent.launchConfig = config;
1077
- agent.command = [...(config.command ?? [])];
1078
- agent.entrypoint = [...(config.entrypoint ?? [])];
1079
- return agent;
2857
+ if (options.runtime)
2858
+ body.runtime = options.runtime;
2859
+ const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body, { retries: 1 });
2860
+ return this.hydrateAgent(data);
1080
2861
  }
1081
2862
  async createOpenClaw(options = {}) {
1082
- const effectiveOptions = { ...options };
1083
- effectiveOptions.env = { ...buildOpenClawMemoryIndexEnv(options.memoryIndex), ...(options.env ?? {}) };
2863
+ const effectiveOptions = { ...options, runtime: options.runtime ?? 'openclaw' };
2864
+ effectiveOptions.env = {
2865
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2866
+ ...buildOpenClawMemoryIndexEnv(options.memoryIndex),
2867
+ ...(options.env ?? {}),
2868
+ };
2869
+ if (effectiveOptions.env.HYPER_WORKSPACES_BOOT_SYNC !== '0') {
2870
+ effectiveOptions.env.HYPER_WORKSPACES_DIR = '/home/node/shared';
2871
+ }
1084
2872
  if (options.routes === undefined) {
1085
2873
  effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
1086
2874
  }
1087
2875
  effectiveOptions.image = defaultOpenClawImage(options.image);
1088
2876
  if (effectiveOptions.syncRoot === undefined)
1089
2877
  effectiveOptions.syncRoot = DEFAULT_OPENCLAW_SYNC_ROOT;
1090
- if (effectiveOptions.syncEnabled === undefined)
1091
- effectiveOptions.syncEnabled = true;
2878
+ if (options.syncInclude === undefined && options.syncExclude === undefined) {
2879
+ effectiveOptions.syncExclude = DEFAULT_OPENCLAW_SYNC_EXCLUDE;
2880
+ }
1092
2881
  return this.create(effectiveOptions);
1093
2882
  }
2883
+ async createHermesAgent(options = {}) {
2884
+ const apiServerKey = resolveHermesApiServerKey(options.apiServerKey, options.env);
2885
+ const env = {
2886
+ ...(options.env ?? {}),
2887
+ API_SERVER_ENABLED: 'true',
2888
+ API_SERVER_HOST: '0.0.0.0',
2889
+ API_SERVER_KEY: apiServerKey,
2890
+ };
2891
+ delete env.OPENCLAW_GATEWAY_TOKEN;
2892
+ const effectiveOptions = {
2893
+ ...options,
2894
+ runtime: 'hermes-agent',
2895
+ env,
2896
+ image: defaultHermesAgentImage(options.image),
2897
+ runtimeScopes: options.runtimeScopes ?? DEFAULT_AGENT_RUNTIME_SCOPES,
2898
+ injectGatewayToken: false,
2899
+ syncRoot: options.syncRoot ?? DEFAULT_HERMES_AGENT_SYNC_ROOT,
2900
+ syncUid: options.syncUid ?? DEFAULT_HERMES_AGENT_SYNC_UID,
2901
+ syncGid: options.syncGid ?? DEFAULT_HERMES_AGENT_SYNC_GID,
2902
+ routes: options.routes === undefined
2903
+ ? buildHermesAgentRoutes(options.hermesRoute ?? {})
2904
+ : options.routes,
2905
+ };
2906
+ const agent = await this.create(effectiveOptions);
2907
+ if (!(agent instanceof HermesAgent)) {
2908
+ throw new Error("Hermes deployment response did not identify runtime 'hermes-agent'");
2909
+ }
2910
+ agent.apiServerKey = apiServerKey;
2911
+ if (agent.launchConfig?.env && typeof agent.launchConfig.env === 'object') {
2912
+ agent.launchConfig = structuredClone(agent.launchConfig);
2913
+ delete agent.launchConfig.env.API_SERVER_KEY;
2914
+ }
2915
+ return agent;
2916
+ }
1094
2917
  async createOpenClawPro(options = {}) {
1095
2918
  return this.createOpenClaw({
1096
2919
  ...options,
2920
+ runtime: 'openclaw-pro',
1097
2921
  env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
1098
2922
  image: defaultOpenClawProImage(options.image),
2923
+ runtimeScopes: options.runtimeScopes ?? DEFAULT_AGENT_RUNTIME_SCOPES,
1099
2924
  openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
1100
2925
  });
1101
2926
  }
2927
+ async createCodingAgent(runtime, options) {
2928
+ if (options.buzzEnabled && options.buzz) {
2929
+ throw new Error('buzzEnabled cannot be combined with buzz');
2930
+ }
2931
+ if ((options.buzzEnabled || options.buzz) && options.command !== undefined && options.command !== null) {
2932
+ throw new Error('Buzz launch cannot be combined with an explicit command');
2933
+ }
2934
+ const buzzLaunch = options.buzzEnabled || options.buzz !== undefined && options.buzz !== null;
2935
+ if (buzzLaunch && options.size !== undefined && options.size !== 'large') {
2936
+ throw new Error("Buzz coding agents require size='large'");
2937
+ }
2938
+ const effectiveEnv = {
2939
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2940
+ ...(options.env ?? {}),
2941
+ };
2942
+ const effectiveSecrets = { ...(options.secrets ?? {}) };
2943
+ for (const key of ['BUZZ_PRIVATE_KEY', 'NOSTR_PRIVATE_KEY']) {
2944
+ const value = effectiveEnv[key];
2945
+ if (value === undefined)
2946
+ continue;
2947
+ delete effectiveEnv[key];
2948
+ if (effectiveSecrets[key] !== undefined && effectiveSecrets[key] !== value) {
2949
+ throw new Error(`${key} conflicts between env and secrets`);
2950
+ }
2951
+ effectiveSecrets[key] = value;
2952
+ }
2953
+ if (effectiveEnv.HYPER_WORKSPACES_BOOT_SYNC !== '0') {
2954
+ effectiveEnv.HYPER_WORKSPACES_DIR = '/home/node/shared';
2955
+ }
2956
+ if (options.buzz) {
2957
+ for (const key of BUZZ_RESERVED_ENV_KEYS)
2958
+ delete effectiveEnv[key];
2959
+ Object.assign(effectiveEnv, buildBuzzLaunchEnv(runtime, options.buzz, options.name));
2960
+ Object.assign(effectiveSecrets, buildBuzzLaunchSecrets(options.buzz));
2961
+ }
2962
+ if (buzzLaunch) {
2963
+ effectiveEnv.RUST_LOG ??= DEFAULT_BUZZ_RUST_LOG;
2964
+ }
2965
+ let syncInclude;
2966
+ let syncExclude;
2967
+ if (options.syncInclude !== undefined && options.syncInclude !== null) {
2968
+ syncInclude = options.syncInclude;
2969
+ syncExclude = undefined;
2970
+ }
2971
+ else if (options.syncExclude !== undefined) {
2972
+ syncInclude = undefined;
2973
+ syncExclude = options.syncExclude ?? undefined;
2974
+ }
2975
+ else if (options.syncInclude === null) {
2976
+ syncInclude = undefined;
2977
+ syncExclude = undefined;
2978
+ }
2979
+ else {
2980
+ syncInclude = CODING_AGENT_CLASSES[runtime].defaultSyncInclude;
2981
+ syncExclude = undefined;
2982
+ }
2983
+ const effectiveOptions = {
2984
+ ...options,
2985
+ runtime,
2986
+ size: buzzLaunch ? 'large' : options.size,
2987
+ injectGatewayToken: false,
2988
+ env: effectiveEnv,
2989
+ secrets: effectiveSecrets,
2990
+ routes: options.routes ?? {},
2991
+ image: options.image ?? (buzzLaunch
2992
+ ? DEFAULT_BUZZ_CODING_AGENT_IMAGES[runtime]
2993
+ : DEFAULT_CODING_AGENT_IMAGES[runtime]),
2994
+ command: options.buzzEnabled || options.buzz
2995
+ ? ['/usr/local/bin/buzz-acp']
2996
+ : options.command,
2997
+ syncRoot: options.syncRoot ?? DEFAULT_CODING_AGENT_SYNC_ROOT,
2998
+ syncInclude,
2999
+ syncExclude,
3000
+ syncUid: options.syncUid ?? 1000,
3001
+ syncGid: options.syncGid ?? 1000,
3002
+ // Hosted Buzz shutdown is process-driven; generic launch options cannot
3003
+ // opt it back into automatic restart.
3004
+ restart: buzzLaunch ? false : options.restart,
3005
+ runtimeScopes: options.runtimeScopes ?? DEFAULT_AGENT_RUNTIME_SCOPES,
3006
+ };
3007
+ return await this.create(effectiveOptions);
3008
+ }
3009
+ async createOpenCode(options = {}) {
3010
+ return await this.createCodingAgent('opencode', options);
3011
+ }
3012
+ async createBuzzAgent(options = {}) {
3013
+ return await this.createCodingAgent('buzz-agent', options);
3014
+ }
3015
+ async createCodex(options = {}) {
3016
+ return await this.createCodingAgent('codex', options);
3017
+ }
3018
+ async createClaudeCode(options = {}) {
3019
+ return await this.createCodingAgent('claude-code', options);
3020
+ }
3021
+ async createGoose(options = {}) {
3022
+ return await this.createCodingAgent('goose', options);
3023
+ }
3024
+ async createKimiCode(options = {}) {
3025
+ return await this.createCodingAgent('kimi-code', options);
3026
+ }
1102
3027
  async budget() {
1103
3028
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/budget`);
1104
3029
  }
1105
- async metrics(agentId) {
3030
+ async bootstrapInference(messages, responseFormat = { type: 'json_object' }, requestOptions = {}) {
3031
+ return this.agentHttp.post('/bootstrap', {
3032
+ messages,
3033
+ response_format: responseFormat,
3034
+ }, requestOptions);
3035
+ }
3036
+ async metrics(agentIdOrName) {
3037
+ const agentId = await this.resolveAgentId(agentIdOrName);
1106
3038
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/metrics`);
1107
3039
  }
1108
- async list() {
1109
- const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX);
1110
- const items = Array.isArray(data) ? data : data.items ?? [];
1111
- return items.map((item) => this.hydrateAgent(item));
3040
+ async list(options = {}) {
3041
+ return (await this.listWithCapacity(options)).items;
3042
+ }
3043
+ async listWithCapacity(options = {}) {
3044
+ const params = {};
3045
+ if (options.state)
3046
+ params.state = options.state;
3047
+ if (options.handle)
3048
+ params.handle = options.handle;
3049
+ if (options.name)
3050
+ params.name = options.name;
3051
+ if (options.query)
3052
+ params.q = options.query;
3053
+ if (options.includeDeleted !== undefined && options.includeDeleted !== null) {
3054
+ params.include_deleted = options.includeDeleted ? 'true' : 'false';
3055
+ }
3056
+ const requestOptions = {
3057
+ retries: options.retries,
3058
+ backoff: options.backoff,
3059
+ timeout: options.timeout,
3060
+ signal: options.signal,
3061
+ retryStatuses: options.retryStatuses,
3062
+ };
3063
+ const cleanRequestOptions = {};
3064
+ for (const key of Object.keys(requestOptions)) {
3065
+ const value = requestOptions[key];
3066
+ if (value !== undefined) {
3067
+ cleanRequestOptions[key] = value;
3068
+ }
3069
+ }
3070
+ const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX, Object.keys(params).length ? params : undefined, Object.keys(cleanRequestOptions).length ? cleanRequestOptions : undefined);
3071
+ const payload = Array.isArray(data) ? { items: data } : data;
3072
+ const items = (payload.items ?? []).map((item) => this.hydrateAgent(item));
3073
+ const runningFallback = items.filter((agent) => !isAgentRuntimeInactiveState(agent.state)).length;
3074
+ return {
3075
+ items,
3076
+ totalAgents: Number(payload.total_agents ?? items.length),
3077
+ maxAgentsPerAccount: Number(payload.max_agents_per_account ?? 0),
3078
+ runningAgents: Number(payload.running_agents ?? runningFallback),
3079
+ slots: Object.fromEntries(Object.entries(payload.slots ?? {}).map(([size, raw]) => {
3080
+ const inventory = raw;
3081
+ return [size, {
3082
+ granted: Number(inventory.granted ?? 0),
3083
+ used: Number(inventory.used ?? inventory.occupied ?? 0),
3084
+ available: Number(inventory.available ?? 0),
3085
+ }];
3086
+ })),
3087
+ agentSlots: (payload.agent_slots ?? []).map(agentSlotFromDict),
3088
+ pooledTpd: Number(payload.pooled_tpd ?? 0),
3089
+ };
3090
+ }
3091
+ async get(agentIdOrName, requestOptions = {}) {
3092
+ const raw = String(agentIdOrName || '').trim();
3093
+ if (!raw)
3094
+ throw new Error('agentIdOrName is required');
3095
+ if (isSelfAgentRef(raw)) {
3096
+ return this.getById('self', requestOptions);
3097
+ }
3098
+ if (!isDirectAgentIdRef(raw)) {
3099
+ return this.resolveAgent(raw, requestOptions);
3100
+ }
3101
+ try {
3102
+ return await this.getById(raw, requestOptions);
3103
+ }
3104
+ catch (error) {
3105
+ if (!(error instanceof APIError) || ![404, 422].includes(error.statusCode) || isUuidRef(raw)) {
3106
+ throw error;
3107
+ }
3108
+ return this.resolveAgent(raw, requestOptions);
3109
+ }
3110
+ }
3111
+ async createExternalAgent(options) {
3112
+ const body = {
3113
+ name: options.name,
3114
+ runtime: options.runtime ?? 'openclaw',
3115
+ status: options.status ?? 'active',
3116
+ };
3117
+ if (options.displayName !== undefined)
3118
+ body.display_name = options.displayName;
3119
+ if (options.handle !== undefined)
3120
+ body.handle = options.handle;
3121
+ if (options.meta !== undefined)
3122
+ body.meta = options.meta;
3123
+ const data = await this.agentHttp.post('/external-agents', body);
3124
+ return this.hydrateAgent(data);
3125
+ }
3126
+ async updateExternalAgent(externalAgentId, options) {
3127
+ const body = {};
3128
+ if (options.name !== undefined)
3129
+ body.name = options.name;
3130
+ if (options.displayName !== undefined)
3131
+ body.display_name = options.displayName;
3132
+ if (options.handle !== undefined)
3133
+ body.handle = options.handle;
3134
+ if (options.runtime !== undefined)
3135
+ body.runtime = options.runtime;
3136
+ if (options.status !== undefined)
3137
+ body.status = options.status;
3138
+ if (options.meta !== undefined)
3139
+ body.meta = options.meta;
3140
+ const data = await this.agentHttp.patch(`/external-agents/${externalAgentId}`, body);
3141
+ return this.hydrateAgent(data);
1112
3142
  }
1113
- async get(agentId) {
1114
- const data = await this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
3143
+ async getExternalAgent(externalAgentId) {
3144
+ const data = await this.agentHttp.get(`/external-agents/${externalAgentId}`);
1115
3145
  return this.hydrateAgent(data);
1116
3146
  }
1117
- async waitRunning(agentId, timeoutMs = 300_000, pollIntervalMs = 5_000) {
3147
+ async uploadExternalAgentProfileImage(externalAgentId, content, contentType) {
3148
+ const resolvedContentType = contentType || (content instanceof Blob ? content.type : '') || 'image/png';
3149
+ return this.agentHttp.postRaw(`/external-agents/${externalAgentId}/profile-image`, content, resolvedContentType);
3150
+ }
3151
+ async deleteExternalAgentProfileImage(externalAgentId) {
3152
+ return this.agentHttp.delete(`/external-agents/${externalAgentId}/profile-image`);
3153
+ }
3154
+ async rotateExternalAgentKey(agentIdOrName) {
3155
+ const agentId = await this.resolveAgentId(agentIdOrName);
3156
+ return this.agentHttp.post(`/external-agents/${agentId}/keys/rotate`);
3157
+ }
3158
+ async attachSlackRelayAgent(agentIdOrName, options) {
3159
+ const agentId = await this.resolveAgentId(agentIdOrName);
3160
+ return attachSlackRelayAgent({
3161
+ relayBaseUrl: options.relayBaseUrl,
3162
+ token: options.token || this.apiKey,
3163
+ agentId,
3164
+ });
3165
+ }
3166
+ async subscribe(handler, options = {}) {
3167
+ let retryDelay = 250;
3168
+ const waitBeforeReconnect = async () => {
3169
+ let abortRetry = () => { };
3170
+ const aborted = new Promise((resolve) => {
3171
+ if (options.signal?.aborted)
3172
+ resolve();
3173
+ else {
3174
+ abortRetry = resolve;
3175
+ options.signal?.addEventListener('abort', abortRetry, { once: true });
3176
+ }
3177
+ });
3178
+ try {
3179
+ await Promise.race([sleep(retryDelay), aborted]);
3180
+ }
3181
+ finally {
3182
+ options.signal?.removeEventListener('abort', abortRetry);
3183
+ }
3184
+ retryDelay = Math.min(retryDelay * 2, 5_000);
3185
+ };
3186
+ while (!options.signal?.aborted) {
3187
+ try {
3188
+ const token = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/events/token`, undefined, { signal: options.signal });
3189
+ const WebSocketImpl = globalThis.WebSocket ?? NodeWebSocket;
3190
+ const ws = new WebSocketImpl(token.ws_url);
3191
+ await new Promise((resolve, reject) => {
3192
+ let opened = false;
3193
+ let ready = false;
3194
+ let processing = Promise.resolve();
3195
+ const readyTimer = setTimeout(() => {
3196
+ ws.close(4002, 'Deployment event ready timed out');
3197
+ reject(new Error('Deployment event ready timed out'));
3198
+ }, 10_000);
3199
+ const abort = () => ws.close(1000, 'Subscription cancelled');
3200
+ options.signal?.addEventListener('abort', abort, { once: true });
3201
+ ws.addEventListener('open', () => {
3202
+ opened = true;
3203
+ ws.send(JSON.stringify({ type: 'auth', token: token.token }));
3204
+ });
3205
+ ws.addEventListener('message', (message) => {
3206
+ processing = processing.then(async () => {
3207
+ const frame = JSON.parse(await websocketMessageText(message.data));
3208
+ if (!ready) {
3209
+ if (frame.type !== 'ready') {
3210
+ throw new Error('Deployment event socket did not send ready');
3211
+ }
3212
+ ready = true;
3213
+ clearTimeout(readyTimer);
3214
+ await options.onReady?.();
3215
+ retryDelay = 250;
3216
+ return;
3217
+ }
3218
+ if (frame.type === 'deployment.transition'
3219
+ && typeof frame.agent_id === 'string'
3220
+ && frame.agent_id.length > 0) {
3221
+ await handler(frame);
3222
+ }
3223
+ }).catch((error) => {
3224
+ ws.close(4002, 'Invalid deployment event');
3225
+ reject(error);
3226
+ });
3227
+ });
3228
+ ws.addEventListener('error', () => reject(new Error('Deployment event WebSocket failed')));
3229
+ ws.addEventListener('close', () => {
3230
+ clearTimeout(readyTimer);
3231
+ options.signal?.removeEventListener('abort', abort);
3232
+ processing.then(resolve, reject);
3233
+ });
3234
+ if (options.signal?.aborted)
3235
+ abort();
3236
+ void opened;
3237
+ });
3238
+ if (!options.signal?.aborted)
3239
+ await waitBeforeReconnect();
3240
+ }
3241
+ catch (error) {
3242
+ if (options.signal?.aborted)
3243
+ break;
3244
+ if (error instanceof APIError && [401, 403].includes(error.statusCode))
3245
+ throw error;
3246
+ await waitBeforeReconnect();
3247
+ void error;
3248
+ }
3249
+ }
3250
+ }
3251
+ async waitForState(agentIdOrName, states, timeoutMs = 300_000, failureStates = [], minimumLaunchEpoch, pollIntervalMs = 5_000) {
3252
+ if (!states.length)
3253
+ throw new Error('states must not be empty');
3254
+ if (minimumLaunchEpoch !== undefined && minimumLaunchEpoch < 0) {
3255
+ throw new Error('minimumLaunchEpoch must be non-negative');
3256
+ }
3257
+ const agentId = await this.resolveAgentId(agentIdOrName);
1118
3258
  const deadline = Date.now() + timeoutMs;
1119
3259
  let lastState = '';
1120
- while (Date.now() < deadline) {
3260
+ let wakePending = true;
3261
+ let wake = null;
3262
+ const controller = new AbortController();
3263
+ const desired = new Set(states.map((state) => state.toLowerCase()));
3264
+ const failures = new Set(failureStates.map((state) => state.toLowerCase()));
3265
+ const effectivePollIntervalMs = Math.max(1, pollIntervalMs);
3266
+ const stateLabel = states.join(', ');
3267
+ const refresh = async () => {
1121
3268
  const agent = await this.get(agentId);
1122
3269
  lastState = String(agent.state || '');
1123
- if (lastState.toLowerCase() === 'running') {
3270
+ if (minimumLaunchEpoch !== undefined
3271
+ && agent.launchEpoch < minimumLaunchEpoch)
3272
+ return null;
3273
+ if (desired.has(lastState.toLowerCase()))
1124
3274
  return agent;
3275
+ if (failures.has(lastState.toLowerCase())) {
3276
+ throw new Error(`Agent entered ${lastState} while waiting for ${stateLabel}`);
3277
+ }
3278
+ return null;
3279
+ };
3280
+ const subscription = this.subscribe((event) => {
3281
+ if (event.type === 'deployment.transition' && event.agent_id === agentId) {
3282
+ wakePending = true;
3283
+ wake?.();
1125
3284
  }
1126
- if (lastState.toLowerCase() === 'failed' || lastState.toLowerCase() === 'error') {
1127
- throw new Error(`Agent entered ${lastState} while waiting for RUNNING`);
3285
+ }, {
3286
+ signal: controller.signal,
3287
+ onReady: () => {
3288
+ wakePending = true;
3289
+ wake?.();
3290
+ },
3291
+ }).catch(() => {
3292
+ // Lifecycle events reduce latency, but REST polling remains authoritative.
3293
+ wakePending = true;
3294
+ wake?.();
3295
+ });
3296
+ try {
3297
+ while (Date.now() < deadline) {
3298
+ if (wakePending) {
3299
+ wakePending = false;
3300
+ const agent = await refresh();
3301
+ if (agent)
3302
+ return agent;
3303
+ continue;
3304
+ }
3305
+ const remaining = deadline - Date.now();
3306
+ if (remaining <= 0)
3307
+ break;
3308
+ await Promise.race([
3309
+ new Promise((resolve) => { wake = resolve; }),
3310
+ sleep(Math.min(remaining, effectivePollIntervalMs)).then(() => {
3311
+ wakePending = true;
3312
+ }),
3313
+ ]);
3314
+ wake = null;
1128
3315
  }
1129
- await sleep(pollIntervalMs);
3316
+ const finalAgent = await refresh();
3317
+ if (finalAgent)
3318
+ return finalAgent;
3319
+ }
3320
+ finally {
3321
+ controller.abort();
3322
+ await subscription;
1130
3323
  }
1131
- throw new Error(`Timed out waiting for agent ${agentId} to reach RUNNING (last=${lastState})`);
3324
+ throw new Error(`Timed out waiting for agent ${agentId} to reach ${stateLabel} (last=${lastState || 'unknown'})`);
3325
+ }
3326
+ async waitRunning(agentIdOrName, timeoutMs = 300_000, pollIntervalMs = 5_000, minimumLaunchEpoch) {
3327
+ return this.waitForState(agentIdOrName, ['RUNNING'], timeoutMs, ['STOPPED', 'ARCHIVED', 'DELETED', 'FAILED'], minimumLaunchEpoch, pollIntervalMs);
1132
3328
  }
1133
- async start(agentId, options = {}) {
1134
- const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, options);
3329
+ async start(agentIdOrName, options = {}) {
3330
+ if (isSelfAgentRef(agentIdOrName)) {
3331
+ const provided = Object.keys(options).filter((key) => options[key] !== undefined);
3332
+ if (provided.length > 0) {
3333
+ throw new Error(`start self uses the backend-stored launch configuration and does not accept launch overrides: ${provided.join(', ')}`);
3334
+ }
3335
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/self/start`, {}, { retries: 1 });
3336
+ return this.hydrateAgent(data);
3337
+ }
3338
+ const provided = Object.keys(options).filter((key) => options[key] !== undefined);
3339
+ const agentId = await this.resolveAgentId(agentIdOrName, {}, true);
3340
+ if (provided.length === 0) {
3341
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, {}, { retries: 1 });
3342
+ return this.hydrateAgent(data);
3343
+ }
3344
+ const { config } = buildAgentConfig(options.config ?? {}, options);
1135
3345
  const body = { ...config };
1136
3346
  if (options.dryRun)
1137
3347
  body.dry_run = true;
1138
- const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, body);
1139
- const agent = this.hydrateAgent(data);
1140
- if (agent instanceof OpenClawAgent) {
1141
- agent.gatewayToken = gatewayToken;
1142
- }
1143
- agent.launchConfig = config;
1144
- agent.command = [...(config.command ?? [])];
1145
- agent.entrypoint = [...(config.entrypoint ?? [])];
1146
- return agent;
3348
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, body, { retries: 1 });
3349
+ return this.hydrateAgent(data);
1147
3350
  }
1148
- async startOpenClaw(agentId, options = {}) {
3351
+ async startOpenClaw(agentIdOrName, options = {}) {
1149
3352
  const effectiveOptions = { ...options };
1150
- effectiveOptions.env = { ...buildOpenClawMemoryIndexEnv(options.memoryIndex), ...(options.env ?? {}) };
1151
- if (options.routes === undefined) {
1152
- effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
3353
+ if (options.workspacesSync !== undefined || options.memoryIndex !== undefined) {
3354
+ effectiveOptions.env = {
3355
+ ...(options.workspacesSync !== undefined
3356
+ ? buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null)
3357
+ : {}),
3358
+ ...(options.memoryIndex !== undefined
3359
+ ? buildOpenClawMemoryIndexEnv(options.memoryIndex)
3360
+ : {}),
3361
+ ...(options.env ?? {}),
3362
+ };
1153
3363
  }
1154
- effectiveOptions.image = defaultOpenClawImage(options.image);
1155
- if (effectiveOptions.syncRoot === undefined)
1156
- effectiveOptions.syncRoot = DEFAULT_OPENCLAW_SYNC_ROOT;
1157
- if (effectiveOptions.syncEnabled === undefined)
1158
- effectiveOptions.syncEnabled = true;
1159
- return this.start(agentId, effectiveOptions);
3364
+ if (options.routes === undefined && options.openClawRoutes !== undefined) {
3365
+ effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? undefined);
3366
+ }
3367
+ return this.start(agentIdOrName, effectiveOptions);
1160
3368
  }
1161
- async startOpenClawPro(agentId, options = {}) {
1162
- return this.startOpenClaw(agentId, {
3369
+ async startHermesAgent(agentIdOrName, options = {}) {
3370
+ const apiServerKey = resolveHermesApiServerKey(options.apiServerKey, options.env);
3371
+ const env = {
3372
+ ...(options.env ?? {}),
3373
+ API_SERVER_ENABLED: 'true',
3374
+ API_SERVER_HOST: '0.0.0.0',
3375
+ API_SERVER_KEY: apiServerKey,
3376
+ };
3377
+ delete env.OPENCLAW_GATEWAY_TOKEN;
3378
+ const effectiveOptions = {
1163
3379
  ...options,
1164
- env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
1165
- image: defaultOpenClawProImage(options.image),
1166
- openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
1167
- });
3380
+ env,
3381
+ image: defaultHermesAgentImage(options.image),
3382
+ runtimeScopes: options.runtimeScopes ?? DEFAULT_AGENT_RUNTIME_SCOPES,
3383
+ injectGatewayToken: false,
3384
+ syncRoot: options.syncRoot ?? DEFAULT_HERMES_AGENT_SYNC_ROOT,
3385
+ syncUid: options.syncUid ?? DEFAULT_HERMES_AGENT_SYNC_UID,
3386
+ syncGid: options.syncGid ?? DEFAULT_HERMES_AGENT_SYNC_GID,
3387
+ routes: options.routes === undefined
3388
+ ? buildHermesAgentRoutes(options.hermesRoute ?? {})
3389
+ : options.routes,
3390
+ };
3391
+ const agent = await this.start(agentIdOrName, effectiveOptions);
3392
+ if (!(agent instanceof HermesAgent)) {
3393
+ throw new Error("Hermes deployment response did not identify runtime 'hermes-agent'");
3394
+ }
3395
+ agent.apiServerKey = apiServerKey;
3396
+ return agent;
1168
3397
  }
1169
- async update(agentId, options = {}) {
3398
+ async startOpenClawPro(agentIdOrName, options = {}) {
3399
+ return this.startOpenClaw(agentIdOrName, options);
3400
+ }
3401
+ async update(agentIdOrName, options = {}) {
1170
3402
  const body = {};
1171
3403
  if (options.name !== undefined)
1172
3404
  body.name = options.name;
3405
+ if (options.handle !== undefined)
3406
+ body.handle = options.handle;
1173
3407
  if (options.size !== undefined)
1174
3408
  body.size = options.size;
3409
+ if (options.launchConfig !== undefined)
3410
+ body.launch_config = options.launchConfig;
1175
3411
  if (options.refreshFromLagoon !== undefined)
1176
3412
  body.refresh_from_lagoon = options.refreshFromLagoon;
1177
- if (options.lastError !== undefined)
1178
- body.last_error = options.lastError;
3413
+ if (options.error !== undefined)
3414
+ body.error = options.error;
3415
+ const agentId = await this.resolveAgentId(agentIdOrName);
1179
3416
  const data = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}`, body);
1180
3417
  return this.hydrateAgent(data);
1181
3418
  }
3419
+ async uploadProfileImage(agentId, content, contentType) {
3420
+ const resolvedAgentId = await this.resolveAgentId(agentId);
3421
+ const resolvedContentType = contentType || (content instanceof Blob ? content.type : '') || 'image/png';
3422
+ return this.agentHttp.postRaw(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`, content, resolvedContentType);
3423
+ }
3424
+ async deleteProfileImage(agentId) {
3425
+ const resolvedAgentId = await this.resolveAgentId(agentId);
3426
+ return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`);
3427
+ }
1182
3428
  async resize(agentId, options) {
1183
3429
  return this.update(agentId, options);
1184
3430
  }
1185
- async stop(agentId) {
1186
- const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/stop`);
3431
+ /**
3432
+ * Request an agent stop.
3433
+ *
3434
+ * The returned agent remains `STOPPING` while runtime cleanup is in
3435
+ * progress. Fetch it again until it becomes `STOPPED` before treating its
3436
+ * deployment slot as released.
3437
+ */
3438
+ async stop(agentIdOrName) {
3439
+ const agentId = await this.resolveAgentId(agentIdOrName, {}, true);
3440
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/stop`, undefined, { retries: 1 });
3441
+ return this.hydrateAgent(data);
3442
+ }
3443
+ /** Restore durable storage. The accepted snapshot is `RESTORING`; completion is `STOPPED`. */
3444
+ async restore(agentIdOrName) {
3445
+ const agentId = await this.resolveAgentId(agentIdOrName);
3446
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/restore`, undefined, { retries: 1 });
1187
3447
  return this.hydrateAgent(data);
1188
3448
  }
1189
- async delete(agentId) {
3449
+ async getRoutes(agentIdOrName) {
3450
+ const agentId = await this.resolveAgentId(agentIdOrName, {}, true);
3451
+ const data = await this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/routes`);
3452
+ return agentRoutesStateFromData(data);
3453
+ }
3454
+ async setRoutes(agentIdOrName, routes) {
3455
+ const agentId = await this.resolveAgentId(agentIdOrName, {}, true);
3456
+ const body = { routes: structuredClone(routes) };
3457
+ const data = await this.agentHttp.put(`${DEPLOYMENTS_API_PREFIX}/${agentId}/routes`, body);
3458
+ return agentRoutesStateFromData(data);
3459
+ }
3460
+ async setRoute(agentIdOrName, name, route) {
3461
+ const agentId = await this.resolveAgentId(agentIdOrName, {}, true);
3462
+ const body = { port: route.port };
3463
+ if (route.auth !== undefined)
3464
+ body.auth = route.auth;
3465
+ if (route.prefix !== undefined)
3466
+ body.prefix = route.prefix;
3467
+ const data = await this.agentHttp.put(`${DEPLOYMENTS_API_PREFIX}/${agentId}/routes/${encodeURIComponent(name)}`, body);
3468
+ return agentRoutesStateFromData(data);
3469
+ }
3470
+ async removeRoute(agentIdOrName, name) {
3471
+ const agentId = await this.resolveAgentId(agentIdOrName, {}, true);
3472
+ const data = await this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}/routes/${encodeURIComponent(name)}`);
3473
+ return agentRoutesStateFromData(data);
3474
+ }
3475
+ async delete(agentIdOrName) {
3476
+ const agentId = await this.resolveAgentId(agentIdOrName);
1190
3477
  return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
1191
3478
  }
1192
- async refreshToken(agentId) {
3479
+ async refreshToken(agentIdOrName) {
3480
+ const agentId = await this.resolveAgentId(agentIdOrName);
1193
3481
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/token`);
1194
3482
  }
1195
- async createScopedKey(agentId, name) {
3483
+ async createScopedKey(agentIdOrName, name) {
1196
3484
  const payload = {};
1197
3485
  if (name)
1198
3486
  payload.name = name;
3487
+ const agentId = await this.resolveAgentId(agentIdOrName);
1199
3488
  return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/keys`, Object.keys(payload).length ? payload : undefined);
1200
3489
  }
1201
3490
  async webSearch(query, options = {}) {
@@ -1218,14 +3507,33 @@ export class Deployments {
1218
3507
  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
3508
  return (await response.json());
1220
3509
  }
1221
- async logsToken(agentId) {
3510
+ async logsToken(agentIdOrName) {
3511
+ const agentId = await this.resolveAgentId(agentIdOrName);
1222
3512
  return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/logs/token`);
1223
3513
  }
1224
- async env(agentId) {
1225
- return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/env`);
3514
+ async env(agentIdOrName, requestOptions = {}) {
3515
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3516
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/env`;
3517
+ return Object.keys(requestOptions).length === 0
3518
+ ? this.agentHttp.get(path)
3519
+ : this.agentHttp.get(path, undefined, requestOptions);
3520
+ }
3521
+ async secretNames(agentIdOrName, requestOptions = {}) {
3522
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3523
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/secrets`;
3524
+ return Object.keys(requestOptions).length === 0
3525
+ ? this.agentHttp.get(path)
3526
+ : this.agentHttp.get(path, undefined, requestOptions);
3527
+ }
3528
+ async secret(agentIdOrName, key, requestOptions = {}) {
3529
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3530
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/secrets/${encodeURIComponent(key)}`;
3531
+ return Object.keys(requestOptions).length === 0
3532
+ ? this.agentHttp.get(path)
3533
+ : this.agentHttp.get(path, undefined, requestOptions);
1226
3534
  }
1227
3535
  async exec(target, command, options = {}) {
1228
- const agentId = this.agentIdFor(target);
3536
+ const agentId = await this.agentIdFor(target);
1229
3537
  const payload = {
1230
3538
  command,
1231
3539
  timeout: options.timeout ?? 30,
@@ -1235,36 +3543,21 @@ export class Deployments {
1235
3543
  const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/exec`, payload);
1236
3544
  return execResultFromDict(data);
1237
3545
  }
1238
- async health(target) {
1239
- if (!target.executorUrl) {
1240
- throw new Error('Agent has no executor URL');
1241
- }
1242
- const headers = {};
1243
- if (target.jwtToken) {
1244
- headers.Authorization = `Bearer ${target.jwtToken}`;
1245
- headers.Cookie = `${target.podName}-token=${target.jwtToken}`;
1246
- }
1247
- const response = await fetch(`${target.executorUrl}/health`, { headers });
1248
- if (!response.ok) {
1249
- throw new Error(`Agent health failed: ${response.status} ${response.statusText}`);
1250
- }
1251
- return (await response.json());
1252
- }
1253
3546
  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()}`);
3547
+ const { suffix, params } = deploymentFileReadRequest(path, source);
3548
+ const agentId = await this.agentIdFor(target);
3549
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files${suffix}?${params.toString()}`);
1258
3550
  const payload = (await response.json());
1259
3551
  return [...(payload.directories ?? []), ...(payload.files ?? [])];
1260
3552
  }
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()}`, {
3553
+ async fileReadBytesWithMetadata(target, path, source = 'auto', options) {
3554
+ const { suffix, params } = deploymentFileReadRequest(path, source);
3555
+ const agentId = await this.agentIdFor(target);
3556
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files${suffix}?${params.toString()}`, {
1265
3557
  redirect: 'follow',
3558
+ signal: options?.signal,
1266
3559
  });
1267
- const bytes = new Uint8Array(await response.arrayBuffer());
3560
+ const bytes = await readResponseBytes(response, path, options?.maxBytes);
1268
3561
  const contentType = response.headers.get('content-type') || '';
1269
3562
  if (contentType.includes('application/json')) {
1270
3563
  try {
@@ -1279,19 +3572,24 @@ export class Deployments {
1279
3572
  }
1280
3573
  }
1281
3574
  }
1282
- return bytes;
3575
+ return { content: bytes, mimeType: contentType || undefined };
1283
3576
  }
1284
- async fileRead(target, path, source = 'auto') {
1285
- return decodeUtf8(await this.fileReadBytes(target, path, source));
3577
+ async fileReadBytes(target, path, source = 'auto', options) {
3578
+ return (await this.fileReadBytesWithMetadata(target, path, source, options)).content;
3579
+ }
3580
+ async fileRead(target, path, source = 'auto', options) {
3581
+ return decodeUtf8(await this.fileReadBytes(target, path, source, options));
1286
3582
  }
1287
3583
  async fileWriteBytes(target, path, content, destination = 'auto') {
3584
+ path = normalizeWritableBackendFilePath(path);
1288
3585
  const encodedPath = encodeFilePath(path);
1289
3586
  const params = new URLSearchParams({ destination });
1290
3587
  const bytes = toUint8Array(content);
1291
3588
  if (bytes.byteLength > AGENT_FILE_MAX_BYTES) {
1292
3589
  throw new Error(`Agent file writes are limited to ${AGENT_FILE_MAX_BYTES / 1024 / 1024} MiB`);
1293
3590
  }
1294
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}?${params.toString()}`, {
3591
+ const agentId = await this.agentIdFor(target);
3592
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}?${params.toString()}`, {
1295
3593
  method: 'POST',
1296
3594
  headers: { 'Content-Type': 'application/octet-stream' },
1297
3595
  body: bytes,
@@ -1302,12 +3600,16 @@ export class Deployments {
1302
3600
  return this.fileWriteBytes(target, path, content, destination);
1303
3601
  }
1304
3602
  async fileDelete(target, path, options = {}) {
3603
+ path = normalizeWritableBackendFilePath(path);
1305
3604
  const encodedPath = encodeFilePath(path);
1306
3605
  const params = new URLSearchParams();
1307
3606
  if (options.recursive)
1308
3607
  params.set('recursive', 'true');
3608
+ if (options.source && options.source !== 'auto')
3609
+ params.set('source', options.source);
1309
3610
  const suffix = params.toString() ? `?${params.toString()}` : '';
1310
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}${suffix}`, { method: 'DELETE' });
3611
+ const agentId = await this.agentIdFor(target);
3612
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}${suffix}`, { method: 'DELETE' });
1311
3613
  return (await response.json());
1312
3614
  }
1313
3615
  async cpTo(target, localPath, remotePath) {
@@ -1326,7 +3628,8 @@ export class Deployments {
1326
3628
  await fs.writeFile(destination, content);
1327
3629
  return destination;
1328
3630
  }
1329
- async logsConnect(agentId, options = {}) {
3631
+ async logsConnect(agentIdOrName, options = {}) {
3632
+ const agentId = await this.resolveAgentId(agentIdOrName);
1330
3633
  const tokenData = await this.logsToken(agentId);
1331
3634
  const container = options.container ?? 'reef';
1332
3635
  const tailLines = options.tailLines ?? 100;
@@ -1348,31 +3651,91 @@ export class Deployments {
1348
3651
  };
1349
3652
  });
1350
3653
  }
1351
- async shellToken(agentId, shell, dryRun = false) {
3654
+ async shellToken(agentIdOrName, shell, dryRun = false, requestOptions = {}) {
1352
3655
  const selectedShell = shell ?? '/bin/bash';
1353
3656
  const payload = { shell: selectedShell };
1354
3657
  if (dryRun)
1355
3658
  payload.dry_run = true;
1356
- return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/shell/token`, payload);
1357
- }
1358
- async shellConnect(agentId, shell) {
3659
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3660
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/shell/token`;
3661
+ if (Object.keys(requestOptions).length === 0)
3662
+ return this.agentHttp.post(path, payload);
3663
+ return this.agentHttp.post(path, payload, requestOptions);
3664
+ }
3665
+ async shellConnect(agentIdOrName, shell, options = {}) {
3666
+ if (options.signal?.aborted)
3667
+ throw shellAbortError();
3668
+ const tokenTimeoutMs = options.tokenTimeoutMs ?? 15_000;
3669
+ const openTimeoutMs = options.openTimeoutMs ?? 10_000;
3670
+ const tokenDeadline = Date.now() + tokenTimeoutMs;
3671
+ const remainingTokenTime = () => Math.max(0, tokenDeadline - Date.now());
3672
+ const runBeforeTokenDeadline = (operation) => {
3673
+ const remaining = remainingTokenTime();
3674
+ if (remaining <= 0)
3675
+ return Promise.reject(new Error('Shell token request timed out'));
3676
+ return runShellOperation(operation, options.signal, remaining, 'Shell token request timed out');
3677
+ };
3678
+ const agentId = await runBeforeTokenDeadline((signal) => this.resolveAgentId(agentIdOrName, { signal }));
1359
3679
  const connectWithShell = async (requestedShell) => {
1360
- const tokenData = await this.shellToken(agentId, requestedShell);
3680
+ const remaining = remainingTokenTime();
3681
+ if (remaining <= 0)
3682
+ throw new Error('Shell token request timed out');
3683
+ const requestOptions = {
3684
+ retries: 3,
3685
+ timeout: Math.max(500, Math.floor((tokenTimeoutMs - 3_000) / 3)),
3686
+ retryStatuses: [429, 502, 503, 504],
3687
+ };
3688
+ const tokenData = await runBeforeTokenDeadline((signal) => {
3689
+ requestOptions.signal = signal;
3690
+ return this.shellToken(agentId, requestedShell, false, requestOptions);
3691
+ });
1361
3692
  const baseUrl = `${this.agentsWsUrl}/shell/${agentId}`;
1362
3693
  const separator = baseUrl.includes("?") ? "&" : "?";
1363
3694
  const wsUrl = `${baseUrl}${separator}jwt=${encodeURIComponent(tokenData.jwt)}` +
1364
3695
  `&shell=${encodeURIComponent(tokenData.shell || requestedShell)}`;
1365
3696
  const ws = new WebSocket(wsUrl);
3697
+ ws.binaryType = 'arraybuffer';
1366
3698
  return await new Promise((resolve, reject) => {
1367
3699
  let settled = false;
1368
- ws.onopen = () => {
1369
- settled = true;
1370
- resolve(ws);
3700
+ const abortConnection = () => finish(shellAbortError());
3701
+ const openTimer = setTimeout(() => finish(new Error('Shell connection timed out')), openTimeoutMs);
3702
+ const cleanup = () => {
3703
+ clearTimeout(openTimer);
3704
+ options.signal?.removeEventListener('abort', abortConnection);
1371
3705
  };
1372
- ws.onerror = () => {
1373
- if (!settled) {
1374
- reject(new Error('WebSocket connection failed'));
3706
+ const finish = (error) => {
3707
+ if (settled)
3708
+ return;
3709
+ settled = true;
3710
+ cleanup();
3711
+ if (error) {
3712
+ try {
3713
+ ws.close();
3714
+ }
3715
+ catch {
3716
+ // The browser may have already finalized a failed socket.
3717
+ }
3718
+ reject(error);
1375
3719
  }
3720
+ else {
3721
+ resolve(ws);
3722
+ }
3723
+ };
3724
+ if (options.signal?.aborted) {
3725
+ finish(shellAbortError());
3726
+ return;
3727
+ }
3728
+ options.signal?.addEventListener('abort', abortConnection, { once: true });
3729
+ ws.onopen = () => {
3730
+ finish();
3731
+ };
3732
+ ws.onerror = () => undefined;
3733
+ ws.onclose = (event) => {
3734
+ const reason = event.reason ? `: ${event.reason}` : '';
3735
+ const error = new Error(`WebSocket closed before opening${reason}`);
3736
+ error.closeCode = event.code;
3737
+ error.closeReason = event.reason;
3738
+ finish(error);
1376
3739
  };
1377
3740
  });
1378
3741
  };
@@ -1382,7 +3745,14 @@ export class Deployments {
1382
3745
  try {
1383
3746
  return await connectWithShell('/bin/bash');
1384
3747
  }
1385
- catch {
3748
+ catch (error) {
3749
+ if (options.signal?.aborted)
3750
+ throw error;
3751
+ const detail = error instanceof APIError ? error.detail : error instanceof Error ? error.message : '';
3752
+ const bashUnavailable = /(\/bin\/bash|\bbash\b)/i.test(detail)
3753
+ && /(not found|no such file|missing|unavailable|unsupported)/i.test(detail);
3754
+ if (!bashUnavailable)
3755
+ throw error;
1386
3756
  return connectWithShell('/bin/sh');
1387
3757
  }
1388
3758
  }