@hypercli.com/sdk 2026.7.31 → 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 (52) hide show
  1. package/README.md +113 -11
  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 +48 -10
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +196 -28
  9. package/dist/agent.js.map +1 -1
  10. package/dist/agents.d.ts +168 -51
  11. package/dist/agents.d.ts.map +1 -1
  12. package/dist/agents.js +801 -211
  13. package/dist/agents.js.map +1 -1
  14. package/dist/browser.d.ts +2 -2
  15. package/dist/browser.d.ts.map +1 -1
  16. package/dist/browser.js +1 -1
  17. package/dist/browser.js.map +1 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +5 -1
  20. package/dist/client.js.map +1 -1
  21. package/dist/config.d.ts +1 -1
  22. package/dist/config.js +2 -2
  23. package/dist/config.js.map +1 -1
  24. package/dist/errors.d.ts +4 -1
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +9 -2
  27. package/dist/errors.js.map +1 -1
  28. package/dist/hermes/gateway.d.ts +194 -0
  29. package/dist/hermes/gateway.d.ts.map +1 -0
  30. package/dist/hermes/gateway.js +209 -0
  31. package/dist/hermes/gateway.js.map +1 -0
  32. package/dist/http.d.ts +6 -0
  33. package/dist/http.d.ts.map +1 -1
  34. package/dist/http.js +84 -25
  35. package/dist/http.js.map +1 -1
  36. package/dist/index.d.ts +6 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +6 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/job/base.d.ts +6 -0
  41. package/dist/job/base.d.ts.map +1 -1
  42. package/dist/job/base.js +7 -1
  43. package/dist/job/base.js.map +1 -1
  44. package/dist/keys.d.ts +15 -4
  45. package/dist/keys.d.ts.map +1 -1
  46. package/dist/keys.js +55 -3
  47. package/dist/keys.js.map +1 -1
  48. package/dist/openclaw/gateway.d.ts +32 -1
  49. package/dist/openclaw/gateway.d.ts.map +1 -1
  50. package/dist/openclaw/gateway.js +101 -21
  51. package/dist/openclaw/gateway.js.map +1 -1
  52. package/package.json +6 -2
package/dist/agents.js CHANGED
@@ -2,11 +2,15 @@
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';
9
12
  import { normalizeSlackRelayBaseUrl } from './channels.js';
13
+ import { HermesApiClient } from './hermes/gateway.js';
10
14
  const AGENTS_API_BASE = 'https://api.hypercli.com/agents';
11
15
  const DEV_AGENTS_API_BASE = 'https://api.dev.hypercli.com/agents';
12
16
  const DEPLOYMENTS_API_PREFIX = '/deployments';
@@ -14,37 +18,68 @@ const AGENTS_WS_URL = 'wss://api.agents.hypercli.com/ws';
14
18
  const DEV_AGENTS_WS_URL = 'wss://api.agents.dev.hypercli.com/ws';
15
19
  export const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-latest';
16
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';
17
22
  export const DEFAULT_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-opencode:latest';
18
23
  export const DEFAULT_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-codex:latest';
19
24
  export const DEFAULT_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-claude-code:latest';
20
25
  export const DEFAULT_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-goose:latest';
21
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';
22
28
  export const DEFAULT_BUZZ_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-opencode:latest';
23
29
  export const DEFAULT_BUZZ_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-codex:latest';
24
30
  export const DEFAULT_BUZZ_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-claude:latest';
25
31
  export const DEFAULT_BUZZ_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-goose:latest';
26
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
+ ]);
27
42
  export const DEFAULT_CODING_AGENT_SYNC_ROOT = '/home/node';
28
43
  export const DEFAULT_CODING_AGENT_IMAGES = {
44
+ 'buzz-agent': DEFAULT_BUZZ_AGENT_IMAGE,
29
45
  opencode: DEFAULT_OPENCODE_IMAGE,
30
46
  codex: DEFAULT_CODEX_IMAGE,
31
47
  'claude-code': DEFAULT_CLAUDE_CODE_IMAGE,
32
48
  goose: DEFAULT_GOOSE_IMAGE,
33
49
  'kimi-code': DEFAULT_KIMI_CODE_IMAGE,
34
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
+ };
35
64
  export const DEFAULT_BUZZ_CODING_AGENT_IMAGES = {
65
+ 'buzz-agent': DEFAULT_BUZZ_AGENT_IMAGE,
36
66
  opencode: DEFAULT_BUZZ_OPENCODE_IMAGE,
37
67
  codex: DEFAULT_BUZZ_CODEX_IMAGE,
38
68
  'claude-code': DEFAULT_BUZZ_CLAUDE_CODE_IMAGE,
39
69
  goose: DEFAULT_BUZZ_GOOSE_IMAGE,
40
70
  'kimi-code': DEFAULT_BUZZ_KIMI_CODE_IMAGE,
41
71
  };
42
- const CODING_AGENT_RUNTIMES = new Set(['opencode', 'codex', 'claude-code', 'goose', 'kimi-code']);
72
+ const CODING_AGENT_RUNTIMES = new Set(['buzz-agent', 'opencode', 'codex', 'claude-code', 'goose', 'kimi-code']);
43
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
+ },
44
79
  opencode: {
45
80
  command: '/usr/local/bin/opencode',
46
81
  args: ['acp'],
47
- mcpCommand: '/usr/local/bin/buzz-dev-mcp',
82
+ mcpCommand: '',
48
83
  },
49
84
  codex: {
50
85
  command: '/usr/local/bin/codex-acp',
@@ -82,6 +117,10 @@ const BUZZ_RESERVED_ENV_KEYS = new Set([
82
117
  'BUZZ_ACP_MCP_COMMAND',
83
118
  'BUZZ_ACP_LAZY_POOL',
84
119
  'BUZZ_ACP_RELAY_OBSERVER',
120
+ 'BUZZ_ACP_DISPLAY_NAME',
121
+ 'BUZZ_ACP_TEXT_MENTIONS',
122
+ 'BUZZ_ACP_REQUIRE_REPLY',
123
+ 'CLAUDE_CODE_EXECUTABLE',
85
124
  'BUZZ_ACP_SESSION_TITLE',
86
125
  'BUZZ_ACP_SYSTEM_PROMPT',
87
126
  'BUZZ_ACP_MODEL',
@@ -106,28 +145,64 @@ export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
106
145
  };
107
146
  export const OPENCLAW_WORKSPACES_SYNC_ENV_DEFAULTS = {
108
147
  HYPER_WORKSPACES_BOOT_SYNC: '1',
109
- HYPER_WORKSPACES_DIR: '/home/node/workspaces',
148
+ HYPER_WORKSPACES_DIR: '/home/node/shared',
110
149
  HYPER_WORKSPACES_SYNC_READY_ONLY: '1',
111
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
+ ];
112
162
  const LAUNCH_CONFIG_KEYS = new Set([
113
163
  'image',
114
164
  'env',
165
+ 'secrets',
115
166
  'routes',
116
- 'ports',
117
167
  'command',
118
168
  'entrypoint',
119
169
  'sync_root',
120
- 'sync_enabled',
170
+ 'sync_include',
171
+ 'sync_exclude',
121
172
  'sync_uid',
122
173
  'sync_gid',
123
174
  'registry_url',
124
175
  'registry_auth',
125
176
  'restart',
177
+ 'runtime_scopes',
126
178
  ]);
127
179
  const DEFAULT_OPENCLAW_SYNC_ROOT = '/home/node';
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;
128
183
  export const AGENT_FILE_MAX_BYTES = 250 * 1024 * 1024;
129
184
  export const AGENT_FILE_TRANSFER_CHUNK_BYTES = 64 * 1024;
130
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
+ }
131
206
  function shellAbortError() {
132
207
  const error = new Error('Shell connection cancelled');
133
208
  error.name = 'AbortError';
@@ -185,9 +260,8 @@ function buildBuzzLaunchEnv(runtime, buzz, defaultSessionTitle) {
185
260
  }
186
261
  const harness = BUZZ_RUNTIME_COMMANDS[runtime];
187
262
  const env = {
188
- BUZZ_PRIVATE_KEY: buzz.privateKeyNsec,
189
- NOSTR_PRIVATE_KEY: buzz.privateKeyNsec,
190
263
  BUZZ_RELAY_URL: buzz.relayUrl,
264
+ BUZZ_MANAGED_AGENT_START_NONCE: randomHexToken(16),
191
265
  BUZZ_ACP_AGENT_COMMAND: harness.command,
192
266
  BUZZ_ACP_AGENT_ARGS: harness.args.join(','),
193
267
  BUZZ_ACP_MCP_COMMAND: harness.mcpCommand,
@@ -197,10 +271,14 @@ function buildBuzzLaunchEnv(runtime, buzz, defaultSessionTitle) {
197
271
  BUZZ_ACP_MULTIPLE_EVENT_HANDLING: 'steer',
198
272
  BUZZ_ACP_DEDUP: 'queue',
199
273
  };
274
+ if (runtime === 'claude-code') {
275
+ env.CLAUDE_CODE_EXECUTABLE = '/usr/local/bin/claude';
276
+ }
200
277
  if (buzz.rustLog)
201
278
  env.RUST_LOG = buzz.rustLog;
202
279
  const optional = {
203
280
  BUZZ_AUTH_TAG: buzz.authTag,
281
+ BUZZ_ACP_DISPLAY_NAME: buzz.displayName,
204
282
  BUZZ_ACP_SESSION_TITLE: buzz.sessionTitle || defaultSessionTitle,
205
283
  BUZZ_ACP_SYSTEM_PROMPT: buzz.systemPrompt,
206
284
  BUZZ_ACP_MODEL: buzz.model,
@@ -219,8 +297,20 @@ function buildBuzzLaunchEnv(runtime, buzz, defaultSessionTitle) {
219
297
  if (value)
220
298
  env[key] = value;
221
299
  }
300
+ if (buzz.textMentions)
301
+ env.BUZZ_ACP_TEXT_MENTIONS = 'true';
302
+ if (buzz.requireReply)
303
+ env.BUZZ_ACP_REQUIRE_REPLY = 'true';
222
304
  return env;
223
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
+ }
224
314
  /** The agent's sync root and workspace, owned by the SDK so paths are unambiguous. */
225
315
  export const OPENCLAW_SYNC_ROOT = '/home/node';
226
316
  export const OPENCLAW_WORKSPACE_PREFIX = '.openclaw/workspace';
@@ -287,6 +377,16 @@ function isDirectAgentIdRef(value) {
287
377
  const raw = value.trim();
288
378
  return isUuidRef(raw) || /^[0-9a-f]{6,}$/i.test(raw) || /^(agent|external)[-_:]/i.test(raw);
289
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
+ }
290
390
  /** Strip a workspace prefix so gateway (name-addressed) sees the bare name. */
291
391
  function resolveGatewayFileName(path) {
292
392
  if (path.startsWith('/')) {
@@ -404,6 +504,39 @@ export class AgentFiles {
404
504
  });
405
505
  }
406
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
+ }
407
540
  function parseDate(value) {
408
541
  if (typeof value !== 'string' || !value)
409
542
  return null;
@@ -510,24 +643,18 @@ export function routesHaveDesktop(routes) {
510
643
  return true;
511
644
  return Object.values(routes).some((route) => isPlainRecord(route) && route.prefix === 'desktop');
512
645
  }
513
- export function portsHaveDesktop(ports) {
514
- return Array.isArray(ports) && ports.some((port) => isPlainRecord(port) && Number(port.port) === 3000);
515
- }
516
646
  export function launchConfigHasDesktop(launchConfig) {
517
647
  if (!isPlainRecord(launchConfig))
518
648
  return false;
519
649
  if (isTruthyEnv(getLaunchConfigValue(launchConfig, 'env.OPENCLAW_DESKTOP_ENABLED')))
520
650
  return true;
521
- if (routesHaveDesktop(getLaunchConfigValue(launchConfig, 'routes')))
522
- return true;
523
- return portsHaveDesktop(getLaunchConfigValue(launchConfig, 'ports'));
651
+ return routesHaveDesktop(getLaunchConfigValue(launchConfig, 'routes'));
524
652
  }
525
653
  export function agentConfigHasDesktop(source) {
526
654
  if (!source)
527
655
  return false;
528
656
  return (launchConfigHasDesktop(source.launchConfig ?? source.launch_config) ||
529
- routesHaveDesktop(source.routes) ||
530
- portsHaveDesktop(source.ports));
657
+ routesHaveDesktop(source.routes));
531
658
  }
532
659
  function browserDesktopRedirectPath(options = {}) {
533
660
  const redirect = (options.redirect ?? 'vnc.html').trim() || 'vnc.html';
@@ -744,11 +871,13 @@ function execResultFromDict(data) {
744
871
  };
745
872
  }
746
873
  function agentStateFromDict(data) {
874
+ const launchConfig = isPlainRecord(data.launch_config) ? structuredClone(data.launch_config) : null;
875
+ if (launchConfig) {
876
+ delete launchConfig.secrets;
877
+ }
747
878
  return {
748
879
  id: data.id ?? '',
749
880
  userId: data.user_id ?? '',
750
- podId: data.pod_id ?? '',
751
- podName: data.pod_name ?? '',
752
881
  state: data.state ?? 'unknown',
753
882
  name: data.name ?? null,
754
883
  handle: data.handle ?? null,
@@ -769,15 +898,18 @@ function agentStateFromDict(data) {
769
898
  jwtExpiresAt: parseDate(data.jwt_expires_at),
770
899
  startedAt: parseDate(data.started_at),
771
900
  stoppedAt: parseDate(data.stopped_at),
772
- lastError: data.last_error ?? null,
901
+ archivedAt: parseDate(data.archived_at),
902
+ clusterId: data.cluster_id ?? null,
903
+ launchEpoch: data.launch_epoch ?? 0,
773
904
  createdAt: parseDate(data.created_at),
774
905
  updatedAt: parseDate(data.updated_at),
775
- launchConfig: data.launch_config ?? null,
906
+ launchConfig,
776
907
  meta: data.meta?.ui ? { ui: structuredClone(data.meta.ui) } : null,
777
- routes: data.routes ?? {},
778
- command: data.command ?? [],
779
- entrypoint: data.entrypoint ?? [],
780
- 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 : []),
781
913
  dryRun: Boolean(data.dry_run),
782
914
  };
783
915
  }
@@ -802,13 +934,20 @@ export function buildAgentConfig(config = {}, options = {}) {
802
934
  preparedConfig.agents = agentsConfig;
803
935
  }
804
936
  const env = { ...(options.env ?? {}) };
805
- let gatewayToken = '';
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;
806
942
  if (options.injectGatewayToken !== false) {
807
- gatewayToken = options.gatewayToken?.trim() || env.OPENCLAW_GATEWAY_TOKEN?.trim() || '';
808
- if (!gatewayToken) {
809
- gatewayToken = randomHexToken(32);
810
- }
811
- env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
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;
812
951
  if (options.controlUiOriginLock !== false && !env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN?.trim()) {
813
952
  const controlUiOrigin = defaultControlUiAllowedOrigin();
814
953
  if (controlUiOrigin) {
@@ -816,13 +955,27 @@ export function buildAgentConfig(config = {}, options = {}) {
816
955
  }
817
956
  }
818
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');
819
972
  const prepared = {};
820
973
  if (Object.keys(preparedConfig).length > 0)
821
974
  prepared.config = preparedConfig;
822
975
  if (Object.keys(env).length > 0)
823
976
  prepared.env = env;
824
- if (options.ports !== undefined && options.ports !== null)
825
- prepared.ports = options.ports;
977
+ if (Object.keys(secrets).length > 0)
978
+ prepared.secrets = secrets;
826
979
  if (options.routes !== undefined && options.routes !== null)
827
980
  prepared.routes = options.routes;
828
981
  if (options.command !== undefined && options.command !== null)
@@ -833,18 +986,25 @@ export function buildAgentConfig(config = {}, options = {}) {
833
986
  prepared.image = options.image;
834
987
  if (options.syncRoot !== undefined && options.syncRoot !== null)
835
988
  prepared.sync_root = options.syncRoot;
836
- if (options.syncEnabled !== undefined && options.syncEnabled !== null)
837
- prepared.sync_enabled = options.syncEnabled;
838
- if (options.syncUid !== undefined && options.syncUid !== null)
839
- prepared.sync_uid = options.syncUid;
840
- if (options.syncGid !== undefined && options.syncGid !== null)
841
- 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;
842
999
  if (options.registryUrl !== undefined && options.registryUrl !== null)
843
1000
  prepared.registry_url = options.registryUrl;
844
1001
  if (options.registryAuth !== undefined && options.registryAuth !== null)
845
1002
  prepared.registry_auth = options.registryAuth;
846
1003
  if (options.restart !== undefined && options.restart !== null)
847
1004
  prepared.restart = options.restart;
1005
+ if (options.runtimeScopes !== undefined && options.runtimeScopes !== null) {
1006
+ prepared.runtime_scopes = [...options.runtimeScopes];
1007
+ }
848
1008
  return { config: prepared, gatewayToken };
849
1009
  }
850
1010
  function defaultOpenClawImage(image) {
@@ -857,10 +1017,17 @@ function defaultOpenClawProImage(image) {
857
1017
  return image;
858
1018
  return DEFAULT_OPENCLAW_PRO_IMAGE;
859
1019
  }
860
- function productApiBaseFromAgentsApiBase(apiBase) {
861
- const normalized = apiBase.replace(/\/+$/, '');
862
- const agentsSuffix = '/agents';
863
- return normalized.endsWith(agentsSuffix) ? normalized.slice(0, -agentsSuffix.length) : normalized;
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;
864
1031
  }
865
1032
  export async function startSlackOAuth(options) {
866
1033
  const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
@@ -1072,6 +1239,15 @@ export function buildOpenClawRoutes(options = {}) {
1072
1239
  }
1073
1240
  return routes;
1074
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
+ }
1075
1251
  function envBool(value) {
1076
1252
  return value ? '1' : '0';
1077
1253
  }
@@ -1116,9 +1292,6 @@ export function buildOpenClawWorkspacesSyncEnv(workspacesSync = null) {
1116
1292
  if (options.enabled !== undefined && options.enabled !== null) {
1117
1293
  env.HYPER_WORKSPACES_BOOT_SYNC = envBool(options.enabled);
1118
1294
  }
1119
- if (options.outputDir) {
1120
- env.HYPER_WORKSPACES_DIR = options.outputDir;
1121
- }
1122
1295
  if (options.readyOnly !== undefined && options.readyOnly !== null) {
1123
1296
  env.HYPER_WORKSPACES_SYNC_READY_ONLY = envBool(options.readyOnly);
1124
1297
  }
@@ -1137,8 +1310,6 @@ function bindAgent(agent, deployments) {
1137
1310
  export class Agent {
1138
1311
  id;
1139
1312
  userId;
1140
- podId;
1141
- podName;
1142
1313
  state;
1143
1314
  name;
1144
1315
  handle;
@@ -1159,7 +1330,9 @@ export class Agent {
1159
1330
  jwtExpiresAt;
1160
1331
  startedAt;
1161
1332
  stoppedAt;
1162
- lastError;
1333
+ archivedAt;
1334
+ clusterId;
1335
+ launchEpoch;
1163
1336
  createdAt;
1164
1337
  updatedAt;
1165
1338
  launchConfig;
@@ -1167,14 +1340,11 @@ export class Agent {
1167
1340
  routes;
1168
1341
  command;
1169
1342
  entrypoint;
1170
- ports;
1171
1343
  dryRun;
1172
1344
  _deployments = null;
1173
1345
  constructor(fields) {
1174
1346
  this.id = fields.id;
1175
1347
  this.userId = fields.userId;
1176
- this.podId = fields.podId;
1177
- this.podName = fields.podName;
1178
1348
  this.state = fields.state;
1179
1349
  this.name = fields.name ?? null;
1180
1350
  this.handle = fields.handle ?? null;
@@ -1195,7 +1365,9 @@ export class Agent {
1195
1365
  this.jwtExpiresAt = fields.jwtExpiresAt ?? null;
1196
1366
  this.startedAt = fields.startedAt ?? null;
1197
1367
  this.stoppedAt = fields.stoppedAt ?? null;
1198
- this.lastError = fields.lastError ?? null;
1368
+ this.archivedAt = fields.archivedAt ?? null;
1369
+ this.clusterId = fields.clusterId ?? null;
1370
+ this.launchEpoch = fields.launchEpoch ?? 0;
1199
1371
  this.createdAt = fields.createdAt ?? null;
1200
1372
  this.updatedAt = fields.updatedAt ?? null;
1201
1373
  this.launchConfig = fields.launchConfig ?? null;
@@ -1203,7 +1375,6 @@ export class Agent {
1203
1375
  this.routes = { ...fields.routes };
1204
1376
  this.command = [...fields.command];
1205
1377
  this.entrypoint = [...fields.entrypoint];
1206
- this.ports = [...fields.ports];
1207
1378
  this.dryRun = fields.dryRun;
1208
1379
  }
1209
1380
  static fromDict(data) {
@@ -1242,17 +1413,24 @@ export class Agent {
1242
1413
  get shellUrl() {
1243
1414
  return this.routeUrl('shell');
1244
1415
  }
1245
- get executorUrl() {
1246
- return this.shellUrl;
1247
- }
1248
1416
  get isRunning() {
1249
1417
  return this.state.toLowerCase() === 'running';
1250
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
+ }
1251
1430
  get hasDesktop() {
1252
1431
  return agentConfigHasDesktop({
1253
1432
  launchConfig: this.launchConfig,
1254
1433
  routes: this.routes,
1255
- ports: this.ports,
1256
1434
  });
1257
1435
  }
1258
1436
  requireDeployments() {
@@ -1275,7 +1453,7 @@ export class Agent {
1275
1453
  return data;
1276
1454
  }
1277
1455
  async waitRunning(timeoutMs = 300_000, pollIntervalMs = 5_000) {
1278
- return this.requireDeployments().waitRunning(this.id, timeoutMs, pollIntervalMs);
1456
+ return this.requireDeployments().waitRunning(this.id, timeoutMs, pollIntervalMs, this.launchEpoch > 0 ? this.launchEpoch : undefined);
1279
1457
  }
1280
1458
  async update(options) {
1281
1459
  return this.requireDeployments().update(this.id, options);
@@ -1284,15 +1462,29 @@ export class Agent {
1284
1462
  return this.requireDeployments().resize(this.id, options);
1285
1463
  }
1286
1464
  async env() {
1287
- const data = await this.requireDeployments().env(this.id);
1288
- 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;
1289
1484
  }
1290
1485
  async exec(command, options = {}) {
1291
1486
  return this.requireDeployments().exec(this, command, options);
1292
1487
  }
1293
- async health() {
1294
- return this.requireDeployments().health(this);
1295
- }
1296
1488
  /**
1297
1489
  * The gateway file writer a subclass supplies — the seam that enables the
1298
1490
  * `gateway` source. The base agent has none (gateway calls throw); an agent
@@ -1344,6 +1536,12 @@ export class Agent {
1344
1536
  }
1345
1537
  }
1346
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
+ },
1347
1545
  opencode: {
1348
1546
  agentCommand: ['opencode', 'acp'],
1349
1547
  statusCommand: ['buzz-acp', 'models', '--agent-command', 'opencode', '--agent-args', 'acp', '--json'],
@@ -1442,19 +1640,22 @@ async function websocketMessageText(data) {
1442
1640
  export class RuntimeLoginSession {
1443
1641
  authClient;
1444
1642
  socket;
1643
+ requiresDeviceChallenge;
1445
1644
  output = '';
1446
1645
  verificationUrl = null;
1447
1646
  userCode = null;
1448
1647
  interactiveRequired = false;
1648
+ rawOutput = '';
1449
1649
  exitCode = null;
1450
1650
  marker;
1451
1651
  completion;
1452
1652
  ready;
1453
1653
  complete;
1454
1654
  markReady;
1455
- constructor(authClient, socket, command) {
1655
+ constructor(authClient, socket, command, requiresDeviceChallenge) {
1456
1656
  this.authClient = authClient;
1457
1657
  this.socket = socket;
1658
+ this.requiresDeviceChallenge = requiresDeviceChallenge;
1458
1659
  this.marker = `__HYPERCLI_AUTH_EXIT_${randomHexToken(12)}__=`;
1459
1660
  this.completion = new Promise((resolve) => { this.complete = resolve; });
1460
1661
  this.ready = new Promise((resolve) => { this.markReady = resolve; });
@@ -1467,9 +1668,9 @@ export class RuntimeLoginSession {
1467
1668
  };
1468
1669
  socket.send(`${commandString(command)}; _hypercli_auth_rc=$?; printf '\\n${this.marker}%s\\n' "$_hypercli_auth_rc"\n`);
1469
1670
  }
1470
- static async start(authClient, command, challengeTimeoutMs = 45_000) {
1671
+ static async start(authClient, command, challengeTimeoutMs = 45_000, requiresDeviceChallenge = false) {
1471
1672
  const socket = await authClient.agent.shellConnect();
1472
- const session = new RuntimeLoginSession(authClient, socket, command);
1673
+ const session = new RuntimeLoginSession(authClient, socket, command, requiresDeviceChallenge);
1473
1674
  let timer;
1474
1675
  try {
1475
1676
  await Promise.race([
@@ -1490,7 +1691,8 @@ export class RuntimeLoginSession {
1490
1691
  }
1491
1692
  }
1492
1693
  consume(chunk) {
1493
- this.output += stripTerminalCodes(chunk);
1694
+ this.rawOutput += chunk;
1695
+ this.output = stripTerminalCodes(this.rawOutput);
1494
1696
  const markerIndex = this.output.lastIndexOf(this.marker);
1495
1697
  if (markerIndex >= 0) {
1496
1698
  const match = this.output.slice(markerIndex + this.marker.length).match(/^(-?\d+)/);
@@ -1500,10 +1702,13 @@ export class RuntimeLoginSession {
1500
1702
  this.complete();
1501
1703
  }
1502
1704
  }
1503
- this.verificationUrl ??= this.output.match(/https?:\/\/[^\s"'<>]+/)?.[0] ?? null;
1504
- this.userCode ??= this.output.match(/(?:user|device|verification|one[- ]time)?\s*code\s*(?:is|:)?\s*([A-Z0-9][A-Z0-9-]{3,})/i)?.[1] ?? null;
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;
1505
1707
  this.interactiveRequired ||= /\b(select|choose)\b.*\b(provider|login method)\b/i.test(this.output);
1506
- if (this.verificationUrl || this.userCode || this.interactiveRequired)
1708
+ const challengeReady = this.requiresDeviceChallenge
1709
+ ? Boolean(this.verificationUrl && this.userCode)
1710
+ : Boolean(this.verificationUrl || this.userCode);
1711
+ if (challengeReady || this.interactiveRequired)
1507
1712
  this.markReady();
1508
1713
  }
1509
1714
  send(text) {
@@ -1519,6 +1724,10 @@ export class RuntimeLoginSession {
1519
1724
  }),
1520
1725
  ]);
1521
1726
  }
1727
+ catch (error) {
1728
+ this.cancel();
1729
+ throw error;
1730
+ }
1522
1731
  finally {
1523
1732
  if (timer)
1524
1733
  clearTimeout(timer);
@@ -1529,7 +1738,9 @@ export class RuntimeLoginSession {
1529
1738
  return this.authClient.status();
1530
1739
  }
1531
1740
  cancel() {
1532
- if (this.socket.readyState === WebSocket.OPEN)
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)
1533
1744
  this.socket.send('\x03');
1534
1745
  this.socket.close();
1535
1746
  }
@@ -1632,7 +1843,10 @@ export class RuntimeAuthClient {
1632
1843
  }
1633
1844
  if (this.agent.runtime === 'claude-code' && options.email)
1634
1845
  command.push('--email', options.email);
1635
- return RuntimeLoginSession.start(this, command, options.challengeTimeoutMs);
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);
1636
1850
  }
1637
1851
  async logout(provider) {
1638
1852
  if (this.config.logoutCommand === null) {
@@ -1652,35 +1866,101 @@ export class RuntimeAuthClient {
1652
1866
  }
1653
1867
  }
1654
1868
  export class CodingAgent extends Agent {
1869
+ static defaultSyncInclude = null;
1655
1870
  get auth() {
1656
1871
  return new RuntimeAuthClient(this);
1657
1872
  }
1658
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
+ }
1659
1880
  export class OpenCodeAgent extends CodingAgent {
1881
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES.opencode;
1660
1882
  static fromDict(data) {
1661
1883
  return new OpenCodeAgent(agentStateFromDict(data));
1662
1884
  }
1663
1885
  }
1664
1886
  export class CodexAgent extends CodingAgent {
1887
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES.codex;
1665
1888
  static fromDict(data) {
1666
1889
  return new CodexAgent(agentStateFromDict(data));
1667
1890
  }
1668
1891
  }
1669
1892
  export class ClaudeCodeAgent extends CodingAgent {
1893
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES['claude-code'];
1670
1894
  static fromDict(data) {
1671
1895
  return new ClaudeCodeAgent(agentStateFromDict(data));
1672
1896
  }
1673
1897
  }
1674
1898
  export class GooseAgent extends CodingAgent {
1899
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES.goose;
1675
1900
  static fromDict(data) {
1676
1901
  return new GooseAgent(agentStateFromDict(data));
1677
1902
  }
1678
1903
  }
1679
1904
  export class KimiCodeAgent extends CodingAgent {
1905
+ static defaultSyncInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES['kimi-code'];
1680
1906
  static fromDict(data) {
1681
1907
  return new KimiCodeAgent(agentStateFromDict(data));
1682
1908
  }
1683
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;
1962
+ }
1963
+ }
1684
1964
  export class OpenClawAgent extends Agent {
1685
1965
  gatewayUrl;
1686
1966
  gatewayToken;
@@ -1693,33 +1973,14 @@ export class OpenClawAgent extends Agent {
1693
1973
  return new OpenClawAgent({
1694
1974
  ...agentStateFromDict(data),
1695
1975
  gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
1696
- gatewayToken: data.gateway_token ?? null,
1976
+ gatewayToken: null,
1697
1977
  });
1698
1978
  }
1699
1979
  static gatewayUrlFromHostname(hostname) {
1700
1980
  const trimmed = String(hostname ?? '').trim();
1701
1981
  return trimmed ? `wss://${trimmed}` : null;
1702
1982
  }
1703
- currentGatewayHostname() {
1704
- if (this.hostname)
1705
- return this.hostname;
1706
- if (!this.gatewayUrl)
1707
- return null;
1708
- try {
1709
- return new URL(this.gatewayUrl).hostname || null;
1710
- }
1711
- catch {
1712
- return this.gatewayUrl.replace(/^wss?:\/\//, '').split('/')[0] || null;
1713
- }
1714
- }
1715
- /**
1716
- * Resolve gateway context through the deployment record plus `/env`.
1717
- *
1718
- * Agent startup is eventually consistent: the deployment record may lag
1719
- * behind hostname attachment, and runtime env can lag behind both. The SDK
1720
- * derives the gateway URL from the attached hostname and reads the gateway
1721
- * token from `OPENCLAW_GATEWAY_TOKEN` in the agent env route.
1722
- */
1983
+ /** Resolve OpenClaw connection context from the generic Agent and Secret APIs. */
1723
1984
  async waitForGatewayContext(options = {}) {
1724
1985
  const callerAbortError = () => {
1725
1986
  if (options.signal?.reason instanceof Error)
@@ -1730,13 +1991,6 @@ export class OpenClawAgent extends Agent {
1730
1991
  };
1731
1992
  if (options.signal?.aborted)
1732
1993
  throw callerAbortError();
1733
- if (this.gatewayToken && this.gatewayUrl) {
1734
- return {
1735
- agent_id: this.id,
1736
- hostname: this.currentGatewayHostname(),
1737
- gateway_token: this.gatewayToken,
1738
- };
1739
- }
1740
1994
  const timeoutMs = options.timeoutMs ?? 30_000;
1741
1995
  const retryIntervalMs = options.retryIntervalMs ?? 1_000;
1742
1996
  const deployments = this.requireDeployments();
@@ -1799,31 +2053,41 @@ export class OpenClawAgent extends Agent {
1799
2053
  signal: controller.signal,
1800
2054
  timeout: Math.max(1, remainingMs),
1801
2055
  };
1802
- const requests = [];
1803
- if (!this.gatewayUrl) {
1804
- requests.push(deployments.get(this.id, requestOptions).then((refreshed) => {
1805
- const gatewayUrl = OpenClawAgent.gatewayUrlFromHostname(refreshed.hostname);
1806
- if (gatewayUrl)
1807
- this.gatewayUrl = gatewayUrl;
1808
- }));
1809
- }
1810
- if (!this.gatewayToken) {
1811
- requests.push(deployments.env(this.id, requestOptions).then((envData) => {
1812
- const gatewayToken = envData.env?.OPENCLAW_GATEWAY_TOKEN?.trim() || null;
1813
- if (gatewayToken)
1814
- this.gatewayToken = gatewayToken;
1815
- }));
1816
- }
1817
- const results = await runWithAbort(Promise.allSettled(requests));
1818
- const rejected = results.find((result) => result.status === 'rejected');
1819
- if (this.gatewayToken && this.gatewayUrl) {
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');
2079
+ this.gatewayUrl = gatewayUrl;
2080
+ this.gatewayToken = gatewayToken;
1820
2081
  return {
1821
2082
  agent_id: this.id,
1822
- hostname: this.currentGatewayHostname(),
1823
- gateway_token: this.gatewayToken,
2083
+ gateway_url: gatewayUrl,
2084
+ gateway_token: gatewayToken,
2085
+ launch_epoch: refreshed.launchEpoch,
1824
2086
  };
1825
2087
  }
1826
- lastError = rejected?.reason ?? new Error('missing gateway context');
2088
+ catch (error) {
2089
+ lastError = error;
2090
+ }
1827
2091
  const remainingAfterRequestMs = deadline - Date.now();
1828
2092
  if (remainingAfterRequestMs <= 0)
1829
2093
  throw timeoutError();
@@ -2063,6 +2327,35 @@ export class OpenClawAgent extends Agent {
2063
2327
  client.close();
2064
2328
  }
2065
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
+ }
2066
2359
  async *chatSend(message, sessionKey, options = {}) {
2067
2360
  const client = await this.connect(options);
2068
2361
  try {
@@ -2407,7 +2700,7 @@ export class OpenClawProAgent extends OpenClawAgent {
2407
2700
  return new OpenClawProAgent({
2408
2701
  ...agentStateFromDict(data),
2409
2702
  gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
2410
- gatewayToken: data.gateway_token ?? null,
2703
+ gatewayToken: null,
2411
2704
  });
2412
2705
  }
2413
2706
  }
@@ -2433,7 +2726,13 @@ export class Deployments {
2433
2726
  }
2434
2727
  hydrateAgent(data) {
2435
2728
  let agent;
2436
- if (data.runtime === 'opencode') {
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') {
2437
2736
  agent = OpenCodeAgent.fromDict(data);
2438
2737
  }
2439
2738
  else if (data.runtime === 'codex') {
@@ -2476,7 +2775,7 @@ export class Deployments {
2476
2775
  }
2477
2776
  const matches = [];
2478
2777
  for (const agent of await this.list(requestOptions)) {
2479
- const values = [agent.id, agent.name, agent.handle, agent.podName, agent.hostname];
2778
+ const values = [agent.id, agent.name, agent.handle, agent.hostname];
2480
2779
  if (values.some((value) => String(value || '') === raw)) {
2481
2780
  matches.push(agent);
2482
2781
  continue;
@@ -2493,11 +2792,16 @@ export class Deployments {
2493
2792
  }
2494
2793
  return this.getById(matches[0].id, requestOptions);
2495
2794
  }
2496
- async resolveAgentId(agentIdOrName, requestOptions = {}) {
2795
+ async resolveAgentId(agentIdOrName, requestOptions = {}, allowSelf = false) {
2497
2796
  const raw = String(agentIdOrName || '').trim();
2498
2797
  if (!raw) {
2499
2798
  throw new Error('agentIdOrName is required');
2500
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
+ }
2501
2805
  if (isDirectAgentIdRef(raw)) {
2502
2806
  return raw;
2503
2807
  }
@@ -2533,7 +2837,7 @@ export class Deployments {
2533
2837
  return response;
2534
2838
  }
2535
2839
  async create(options = {}) {
2536
- const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, {
2840
+ const { config } = buildAgentConfig(options.config ?? {}, {
2537
2841
  ...options,
2538
2842
  injectGatewayToken: options.injectGatewayToken ?? !isCodingAgentRuntime(options.runtime),
2539
2843
  });
@@ -2552,40 +2856,71 @@ export class Deployments {
2552
2856
  body.tags = [...options.tags];
2553
2857
  if (options.runtime)
2554
2858
  body.runtime = options.runtime;
2555
- const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body);
2556
- const agent = this.hydrateAgent(data);
2557
- if (agent instanceof OpenClawAgent) {
2558
- agent.gatewayToken = gatewayToken;
2559
- }
2560
- agent.launchConfig = config;
2561
- agent.command = [...(config.command ?? [])];
2562
- agent.entrypoint = [...(config.entrypoint ?? [])];
2563
- return agent;
2859
+ const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body, { retries: 1 });
2860
+ return this.hydrateAgent(data);
2564
2861
  }
2565
2862
  async createOpenClaw(options = {}) {
2566
2863
  const effectiveOptions = { ...options, runtime: options.runtime ?? 'openclaw' };
2567
2864
  effectiveOptions.env = {
2568
- HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2569
2865
  ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2570
2866
  ...buildOpenClawMemoryIndexEnv(options.memoryIndex),
2571
2867
  ...(options.env ?? {}),
2572
2868
  };
2869
+ if (effectiveOptions.env.HYPER_WORKSPACES_BOOT_SYNC !== '0') {
2870
+ effectiveOptions.env.HYPER_WORKSPACES_DIR = '/home/node/shared';
2871
+ }
2573
2872
  if (options.routes === undefined) {
2574
2873
  effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
2575
2874
  }
2576
2875
  effectiveOptions.image = defaultOpenClawImage(options.image);
2577
2876
  if (effectiveOptions.syncRoot === undefined)
2578
2877
  effectiveOptions.syncRoot = DEFAULT_OPENCLAW_SYNC_ROOT;
2579
- if (effectiveOptions.syncEnabled === undefined)
2580
- effectiveOptions.syncEnabled = true;
2878
+ if (options.syncInclude === undefined && options.syncExclude === undefined) {
2879
+ effectiveOptions.syncExclude = DEFAULT_OPENCLAW_SYNC_EXCLUDE;
2880
+ }
2581
2881
  return this.create(effectiveOptions);
2582
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
+ }
2583
2917
  async createOpenClawPro(options = {}) {
2584
2918
  return this.createOpenClaw({
2585
2919
  ...options,
2586
2920
  runtime: 'openclaw-pro',
2587
2921
  env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
2588
2922
  image: defaultOpenClawProImage(options.image),
2923
+ runtimeScopes: options.runtimeScopes ?? DEFAULT_AGENT_RUNTIME_SCOPES,
2589
2924
  openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
2590
2925
  });
2591
2926
  }
@@ -2601,24 +2936,57 @@ export class Deployments {
2601
2936
  throw new Error("Buzz coding agents require size='large'");
2602
2937
  }
2603
2938
  const effectiveEnv = {
2604
- HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2605
2939
  ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2606
2940
  ...(options.env ?? {}),
2607
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
+ }
2608
2956
  if (options.buzz) {
2609
2957
  for (const key of BUZZ_RESERVED_ENV_KEYS)
2610
2958
  delete effectiveEnv[key];
2611
2959
  Object.assign(effectiveEnv, buildBuzzLaunchEnv(runtime, options.buzz, options.name));
2960
+ Object.assign(effectiveSecrets, buildBuzzLaunchSecrets(options.buzz));
2612
2961
  }
2613
2962
  if (buzzLaunch) {
2614
2963
  effectiveEnv.RUST_LOG ??= DEFAULT_BUZZ_RUST_LOG;
2615
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
+ }
2616
2983
  const effectiveOptions = {
2617
2984
  ...options,
2618
2985
  runtime,
2619
2986
  size: buzzLaunch ? 'large' : options.size,
2620
2987
  injectGatewayToken: false,
2621
2988
  env: effectiveEnv,
2989
+ secrets: effectiveSecrets,
2622
2990
  routes: options.routes ?? {},
2623
2991
  image: options.image ?? (buzzLaunch
2624
2992
  ? DEFAULT_BUZZ_CODING_AGENT_IMAGES[runtime]
@@ -2627,18 +2995,23 @@ export class Deployments {
2627
2995
  ? ['/usr/local/bin/buzz-acp']
2628
2996
  : options.command,
2629
2997
  syncRoot: options.syncRoot ?? DEFAULT_CODING_AGENT_SYNC_ROOT,
2630
- syncEnabled: options.syncEnabled ?? true,
2998
+ syncInclude,
2999
+ syncExclude,
2631
3000
  syncUid: options.syncUid ?? 1000,
2632
3001
  syncGid: options.syncGid ?? 1000,
2633
- restart: buzzLaunch
2634
- ? options.restart ?? options.buzz?.restart ?? false
2635
- : options.restart,
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,
2636
3006
  };
2637
3007
  return await this.create(effectiveOptions);
2638
3008
  }
2639
3009
  async createOpenCode(options = {}) {
2640
3010
  return await this.createCodingAgent('opencode', options);
2641
3011
  }
3012
+ async createBuzzAgent(options = {}) {
3013
+ return await this.createCodingAgent('buzz-agent', options);
3014
+ }
2642
3015
  async createCodex(options = {}) {
2643
3016
  return await this.createCodingAgent('codex', options);
2644
3017
  }
@@ -2665,6 +3038,9 @@ export class Deployments {
2665
3038
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/metrics`);
2666
3039
  }
2667
3040
  async list(options = {}) {
3041
+ return (await this.listWithCapacity(options)).items;
3042
+ }
3043
+ async listWithCapacity(options = {}) {
2668
3044
  const params = {};
2669
3045
  if (options.state)
2670
3046
  params.state = options.state;
@@ -2692,13 +3068,33 @@ export class Deployments {
2692
3068
  }
2693
3069
  }
2694
3070
  const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX, Object.keys(params).length ? params : undefined, Object.keys(cleanRequestOptions).length ? cleanRequestOptions : undefined);
2695
- const items = Array.isArray(data) ? data : data.items ?? [];
2696
- return items.map((item) => this.hydrateAgent(item));
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
+ };
2697
3090
  }
2698
3091
  async get(agentIdOrName, requestOptions = {}) {
2699
3092
  const raw = String(agentIdOrName || '').trim();
2700
3093
  if (!raw)
2701
3094
  throw new Error('agentIdOrName is required');
3095
+ if (isSelfAgentRef(raw)) {
3096
+ return this.getById('self', requestOptions);
3097
+ }
2702
3098
  if (!isDirectAgentIdRef(raw)) {
2703
3099
  return this.resolveAgent(raw, requestOptions);
2704
3100
  }
@@ -2767,77 +3163,240 @@ export class Deployments {
2767
3163
  agentId,
2768
3164
  });
2769
3165
  }
2770
- async waitRunning(agentIdOrName, timeoutMs = 300_000, pollIntervalMs = 5_000) {
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
+ }
2771
3257
  const agentId = await this.resolveAgentId(agentIdOrName);
2772
3258
  const deadline = Date.now() + timeoutMs;
2773
3259
  let lastState = '';
2774
- let lastAgent = null;
2775
- const diagnostics = (agent) => {
2776
- if (!agent)
2777
- return '';
2778
- const details = [];
2779
- if (agent.lastError)
2780
- details.push(`lastError=${JSON.stringify(agent.lastError)}`);
2781
- if (agent.updatedAt && !Number.isNaN(agent.updatedAt.getTime())) {
2782
- details.push(`updatedAt=${agent.updatedAt.toISOString()}`);
2783
- }
2784
- return details.length ? `, ${details.join(', ')}` : '';
2785
- };
2786
- 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 () => {
2787
3268
  const agent = await this.get(agentId);
2788
- lastAgent = agent;
2789
3269
  lastState = String(agent.state || '');
2790
- if (lastState.toLowerCase() === 'running') {
3270
+ if (minimumLaunchEpoch !== undefined
3271
+ && agent.launchEpoch < minimumLaunchEpoch)
3272
+ return null;
3273
+ if (desired.has(lastState.toLowerCase()))
2791
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?.();
2792
3284
  }
2793
- if (['failed', 'error', 'restore_failed', 'sync_failed'].includes(lastState.toLowerCase())) {
2794
- throw new Error(`Agent entered ${lastState} while waiting for RUNNING${diagnostics(agent)}`);
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;
2795
3315
  }
2796
- await sleep(pollIntervalMs);
3316
+ const finalAgent = await refresh();
3317
+ if (finalAgent)
3318
+ return finalAgent;
2797
3319
  }
2798
- throw new Error(`Timed out waiting for agent ${agentId} to reach RUNNING (last=${lastState || 'unknown'}${diagnostics(lastAgent)})`);
3320
+ finally {
3321
+ controller.abort();
3322
+ await subscription;
3323
+ }
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);
2799
3328
  }
2800
3329
  async start(agentIdOrName, options = {}) {
2801
- const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, 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);
2802
3345
  const body = { ...config };
2803
3346
  if (options.dryRun)
2804
3347
  body.dry_run = true;
2805
- const agentId = await this.resolveAgentId(agentIdOrName);
2806
- const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, body);
2807
- const agent = this.hydrateAgent(data);
2808
- if (agent instanceof OpenClawAgent) {
2809
- agent.gatewayToken = gatewayToken;
2810
- }
2811
- agent.launchConfig = config;
2812
- agent.command = [...(config.command ?? [])];
2813
- agent.entrypoint = [...(config.entrypoint ?? [])];
2814
- return agent;
3348
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, body, { retries: 1 });
3349
+ return this.hydrateAgent(data);
2815
3350
  }
2816
3351
  async startOpenClaw(agentIdOrName, options = {}) {
2817
3352
  const effectiveOptions = { ...options };
2818
- effectiveOptions.env = {
2819
- HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2820
- ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2821
- ...buildOpenClawMemoryIndexEnv(options.memoryIndex),
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
+ };
3363
+ }
3364
+ if (options.routes === undefined && options.openClawRoutes !== undefined) {
3365
+ effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? undefined);
3366
+ }
3367
+ return this.start(agentIdOrName, effectiveOptions);
3368
+ }
3369
+ async startHermesAgent(agentIdOrName, options = {}) {
3370
+ const apiServerKey = resolveHermesApiServerKey(options.apiServerKey, options.env);
3371
+ const env = {
2822
3372
  ...(options.env ?? {}),
3373
+ API_SERVER_ENABLED: 'true',
3374
+ API_SERVER_HOST: '0.0.0.0',
3375
+ API_SERVER_KEY: apiServerKey,
2823
3376
  };
2824
- if (options.routes === undefined) {
2825
- effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
3377
+ delete env.OPENCLAW_GATEWAY_TOKEN;
3378
+ const effectiveOptions = {
3379
+ ...options,
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'");
2826
3394
  }
2827
- effectiveOptions.image = defaultOpenClawImage(options.image);
2828
- if (effectiveOptions.syncRoot === undefined)
2829
- effectiveOptions.syncRoot = DEFAULT_OPENCLAW_SYNC_ROOT;
2830
- if (effectiveOptions.syncEnabled === undefined)
2831
- effectiveOptions.syncEnabled = true;
2832
- return this.start(agentIdOrName, effectiveOptions);
3395
+ agent.apiServerKey = apiServerKey;
3396
+ return agent;
2833
3397
  }
2834
3398
  async startOpenClawPro(agentIdOrName, options = {}) {
2835
- return this.startOpenClaw(agentIdOrName, {
2836
- ...options,
2837
- env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
2838
- image: defaultOpenClawProImage(options.image),
2839
- openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
2840
- });
3399
+ return this.startOpenClaw(agentIdOrName, options);
2841
3400
  }
2842
3401
  async update(agentIdOrName, options = {}) {
2843
3402
  const body = {};
@@ -2851,8 +3410,8 @@ export class Deployments {
2851
3410
  body.launch_config = options.launchConfig;
2852
3411
  if (options.refreshFromLagoon !== undefined)
2853
3412
  body.refresh_from_lagoon = options.refreshFromLagoon;
2854
- if (options.lastError !== undefined)
2855
- body.last_error = options.lastError;
3413
+ if (options.error !== undefined)
3414
+ body.error = options.error;
2856
3415
  const agentId = await this.resolveAgentId(agentIdOrName);
2857
3416
  const data = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}`, body);
2858
3417
  return this.hydrateAgent(data);
@@ -2877,10 +3436,42 @@ export class Deployments {
2877
3436
  * deployment slot as released.
2878
3437
  */
2879
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) {
2880
3445
  const agentId = await this.resolveAgentId(agentIdOrName);
2881
- const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/stop`);
3446
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/restore`, undefined, { retries: 1 });
2882
3447
  return this.hydrateAgent(data);
2883
3448
  }
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
+ }
2884
3475
  async delete(agentIdOrName) {
2885
3476
  const agentId = await this.resolveAgentId(agentIdOrName);
2886
3477
  return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
@@ -2927,6 +3518,20 @@ export class Deployments {
2927
3518
  ? this.agentHttp.get(path)
2928
3519
  : this.agentHttp.get(path, undefined, requestOptions);
2929
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);
3534
+ }
2930
3535
  async exec(target, command, options = {}) {
2931
3536
  const agentId = await this.agentIdFor(target);
2932
3537
  const payload = {
@@ -2938,21 +3543,6 @@ export class Deployments {
2938
3543
  const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/exec`, payload);
2939
3544
  return execResultFromDict(data);
2940
3545
  }
2941
- async health(target) {
2942
- if (!target.executorUrl) {
2943
- throw new Error('Agent has no executor URL');
2944
- }
2945
- const headers = {};
2946
- if (target.jwtToken) {
2947
- headers.Authorization = `Bearer ${target.jwtToken}`;
2948
- headers.Cookie = `${target.podName}-token=${target.jwtToken}`;
2949
- }
2950
- const response = await fetch(`${target.executorUrl}/health`, { headers });
2951
- if (!response.ok) {
2952
- throw new Error(`Agent health failed: ${response.status} ${response.statusText}`);
2953
- }
2954
- return (await response.json());
2955
- }
2956
3546
  async filesList(target, path = '', source = 'auto') {
2957
3547
  const { suffix, params } = deploymentFileReadRequest(path, source);
2958
3548
  const agentId = await this.agentIdFor(target);