@hypercli.com/sdk 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +629 -0
  2. package/dist/acp-driver.d.ts +72 -0
  3. package/dist/acp-driver.d.ts.map +1 -0
  4. package/dist/acp-driver.js +147 -0
  5. package/dist/acp-driver.js.map +1 -0
  6. package/dist/acp-pool.d.ts +70 -0
  7. package/dist/acp-pool.d.ts.map +1 -0
  8. package/dist/acp-pool.js +181 -0
  9. package/dist/acp-pool.js.map +1 -0
  10. package/dist/acp.d.ts +179 -0
  11. package/dist/acp.d.ts.map +1 -0
  12. package/dist/acp.js +487 -0
  13. package/dist/acp.js.map +1 -0
  14. package/dist/agent-persona.d.ts +68 -0
  15. package/dist/agent-persona.d.ts.map +1 -0
  16. package/dist/agent-persona.js +78 -0
  17. package/dist/agent-persona.js.map +1 -0
  18. package/dist/agent-slots.d.ts +18 -0
  19. package/dist/agent-slots.d.ts.map +1 -0
  20. package/dist/agent-slots.js +24 -0
  21. package/dist/agent-slots.js.map +1 -0
  22. package/dist/agent-urls.d.ts +47 -0
  23. package/dist/agent-urls.d.ts.map +1 -0
  24. package/dist/agent-urls.js +178 -0
  25. package/dist/agent-urls.js.map +1 -0
  26. package/dist/agent.d.ts +482 -0
  27. package/dist/agent.d.ts.map +1 -0
  28. package/dist/agent.js +926 -0
  29. package/dist/agent.js.map +1 -0
  30. package/dist/agents.d.ts +1452 -0
  31. package/dist/agents.d.ts.map +1 -0
  32. package/dist/agents.js +4022 -0
  33. package/dist/agents.js.map +1 -0
  34. package/dist/billing.d.ts +72 -0
  35. package/dist/billing.d.ts.map +1 -0
  36. package/dist/billing.js +93 -0
  37. package/dist/billing.js.map +1 -0
  38. package/dist/browser-jobs.d.ts +24 -0
  39. package/dist/browser-jobs.d.ts.map +1 -0
  40. package/dist/browser-jobs.js +209 -0
  41. package/dist/browser-jobs.js.map +1 -0
  42. package/dist/browser.d.ts +41 -0
  43. package/dist/browser.d.ts.map +1 -0
  44. package/dist/browser.js +57 -0
  45. package/dist/browser.js.map +1 -0
  46. package/dist/buzz-activity.d.ts +169 -0
  47. package/dist/buzz-activity.d.ts.map +1 -0
  48. package/dist/buzz-activity.js +844 -0
  49. package/dist/buzz-activity.js.map +1 -0
  50. package/dist/channels.d.ts +99 -0
  51. package/dist/channels.d.ts.map +1 -0
  52. package/dist/channels.js +27 -0
  53. package/dist/channels.js.map +1 -0
  54. package/dist/chat-integrations.d.ts +44 -0
  55. package/dist/chat-integrations.d.ts.map +1 -0
  56. package/dist/chat-integrations.js +138 -0
  57. package/dist/chat-integrations.js.map +1 -0
  58. package/dist/client.d.ts +88 -0
  59. package/dist/client.d.ts.map +1 -0
  60. package/dist/client.js +144 -0
  61. package/dist/client.js.map +1 -0
  62. package/dist/config.d.ts +44 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +192 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/connectors.d.ts +80 -0
  67. package/dist/connectors.d.ts.map +1 -0
  68. package/dist/connectors.js +2 -0
  69. package/dist/connectors.js.map +1 -0
  70. package/dist/errors.d.ts +16 -0
  71. package/dist/errors.d.ts.map +1 -0
  72. package/dist/errors.js +26 -0
  73. package/dist/errors.js.map +1 -0
  74. package/dist/files.d.ts +60 -0
  75. package/dist/files.d.ts.map +1 -0
  76. package/dist/files.js +128 -0
  77. package/dist/files.js.map +1 -0
  78. package/dist/http.d.ts +56 -0
  79. package/dist/http.d.ts.map +1 -0
  80. package/dist/http.js +362 -0
  81. package/dist/http.js.map +1 -0
  82. package/dist/index.d.ts +49 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +64 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/instances.d.ts +94 -0
  87. package/dist/instances.d.ts.map +1 -0
  88. package/dist/instances.js +197 -0
  89. package/dist/instances.js.map +1 -0
  90. package/dist/integrations.d.ts +111 -0
  91. package/dist/integrations.d.ts.map +1 -0
  92. package/dist/integrations.js +206 -0
  93. package/dist/integrations.js.map +1 -0
  94. package/dist/job/base.d.ts +86 -0
  95. package/dist/job/base.d.ts.map +1 -0
  96. package/dist/job/base.js +206 -0
  97. package/dist/job/base.js.map +1 -0
  98. package/dist/job/comfyui.d.ts +259 -0
  99. package/dist/job/comfyui.d.ts.map +1 -0
  100. package/dist/job/comfyui.js +1563 -0
  101. package/dist/job/comfyui.js.map +1 -0
  102. package/dist/job/gradio.d.ts +48 -0
  103. package/dist/job/gradio.d.ts.map +1 -0
  104. package/dist/job/gradio.js +90 -0
  105. package/dist/job/gradio.js.map +1 -0
  106. package/dist/job-codecs.d.ts +66 -0
  107. package/dist/job-codecs.d.ts.map +1 -0
  108. package/dist/job-codecs.js +132 -0
  109. package/dist/job-codecs.js.map +1 -0
  110. package/dist/jobs.d.ts +148 -0
  111. package/dist/jobs.d.ts.map +1 -0
  112. package/dist/jobs.js +480 -0
  113. package/dist/jobs.js.map +1 -0
  114. package/dist/keys.d.ts +99 -0
  115. package/dist/keys.d.ts.map +1 -0
  116. package/dist/keys.js +145 -0
  117. package/dist/keys.js.map +1 -0
  118. package/dist/logs.d.ts +54 -0
  119. package/dist/logs.d.ts.map +1 -0
  120. package/dist/logs.js +256 -0
  121. package/dist/logs.js.map +1 -0
  122. package/dist/managed-context.d.ts +20 -0
  123. package/dist/managed-context.d.ts.map +1 -0
  124. package/dist/managed-context.js +95 -0
  125. package/dist/managed-context.js.map +1 -0
  126. package/dist/mcp.d.ts +7 -0
  127. package/dist/mcp.d.ts.map +1 -0
  128. package/dist/mcp.js +8 -0
  129. package/dist/mcp.js.map +1 -0
  130. package/dist/memory.d.ts +87 -0
  131. package/dist/memory.d.ts.map +1 -0
  132. package/dist/memory.js +79 -0
  133. package/dist/memory.js.map +1 -0
  134. package/dist/models.d.ts +15 -0
  135. package/dist/models.d.ts.map +1 -0
  136. package/dist/models.js +19 -0
  137. package/dist/models.js.map +1 -0
  138. package/dist/openclaw-control-ui-origin.d.ts +23 -0
  139. package/dist/openclaw-control-ui-origin.d.ts.map +1 -0
  140. package/dist/openclaw-control-ui-origin.js +53 -0
  141. package/dist/openclaw-control-ui-origin.js.map +1 -0
  142. package/dist/renders.d.ts +187 -0
  143. package/dist/renders.d.ts.map +1 -0
  144. package/dist/renders.js +360 -0
  145. package/dist/renders.js.map +1 -0
  146. package/dist/routines.d.ts +59 -0
  147. package/dist/routines.d.ts.map +1 -0
  148. package/dist/routines.js +135 -0
  149. package/dist/routines.js.map +1 -0
  150. package/dist/runners.d.ts +61 -0
  151. package/dist/runners.d.ts.map +1 -0
  152. package/dist/runners.js +127 -0
  153. package/dist/runners.js.map +1 -0
  154. package/dist/sessions.d.ts +280 -0
  155. package/dist/sessions.d.ts.map +1 -0
  156. package/dist/sessions.js +292 -0
  157. package/dist/sessions.js.map +1 -0
  158. package/dist/skills.d.ts +170 -0
  159. package/dist/skills.d.ts.map +1 -0
  160. package/dist/skills.js +2 -0
  161. package/dist/skills.js.map +1 -0
  162. package/dist/user.d.ts +78 -0
  163. package/dist/user.d.ts.map +1 -0
  164. package/dist/user.js +143 -0
  165. package/dist/user.js.map +1 -0
  166. package/dist/version.d.ts +2 -0
  167. package/dist/version.d.ts.map +1 -0
  168. package/dist/version.js +3 -0
  169. package/dist/version.js.map +1 -0
  170. package/dist/voice-session.d.ts +66 -0
  171. package/dist/voice-session.d.ts.map +1 -0
  172. package/dist/voice-session.js +366 -0
  173. package/dist/voice-session.js.map +1 -0
  174. package/dist/voice-transcription-session.d.ts +64 -0
  175. package/dist/voice-transcription-session.d.ts.map +1 -0
  176. package/dist/voice-transcription-session.js +228 -0
  177. package/dist/voice-transcription-session.js.map +1 -0
  178. package/dist/voice.d.ts +62 -0
  179. package/dist/voice.d.ts.map +1 -0
  180. package/dist/voice.js +134 -0
  181. package/dist/voice.js.map +1 -0
  182. package/dist/workspace-tomd.d.ts +61 -0
  183. package/dist/workspace-tomd.d.ts.map +1 -0
  184. package/dist/workspace-tomd.js +177 -0
  185. package/dist/workspace-tomd.js.map +1 -0
  186. package/dist/workspaces.d.ts +255 -0
  187. package/dist/workspaces.d.ts.map +1 -0
  188. package/dist/workspaces.js +653 -0
  189. package/dist/workspaces.js.map +1 -0
  190. package/package.json +115 -0
  191. package/runtime/config-require.browser.js +4 -0
  192. package/runtime/config-require.d.ts +2 -0
  193. package/runtime/config-require.node.js +8 -0
package/dist/agents.js ADDED
@@ -0,0 +1,4022 @@
1
+ /**
2
+ * HyperClaw agents API - typed agent lifecycle, files, and exec. Agent chat
3
+ * and sessions are driven over ACP (see acp.ts) through the backend /ws
4
+ * bridge; the SDK no longer talks to runtime-internal gateway processes.
5
+ */
6
+ import { randomFillSync } from 'node:crypto';
7
+ import NodeWebSocket from 'ws';
8
+ import { sessionMessageMetaFromWire } from './sessions.js';
9
+ import { DEFAULT_PERSONA_SOUL_PATH, DEFAULT_PERSONA_USER_PATH, } from './agent-persona.js';
10
+ // undici's native WebSocket (Node 18+) drops a permessage-deflate frame when
11
+ // the peer's close frame and FIN land in the same TCP chunk, surfacing as a
12
+ // 1006 with the final message lost. In Node always use the `ws` package;
13
+ // everywhere else use the native implementation. Resolved per connection (not
14
+ // module-load), and an overridden globalThis.WebSocket (e.g. a test stub) is
15
+ // honored in every runtime.
16
+ function preferredWebSocket() {
17
+ const global = globalThis.WebSocket;
18
+ if (typeof process !== 'undefined' && typeof process.versions?.node === 'string' && global !== undefined) {
19
+ // Node's built-in WebSocket is `class _WebSocket extends EventTarget`
20
+ // (undici); anything else is a deliberate override.
21
+ if (Function.prototype.toString.call(global).startsWith('class _WebSocket')) {
22
+ return NodeWebSocket;
23
+ }
24
+ return global;
25
+ }
26
+ return global ?? NodeWebSocket;
27
+ }
28
+ export { preferredWebSocket };
29
+ import { agentSlotFromDict, parseAgentSlotSize, } from './agent-slots.js';
30
+ export { agentSlotFromDict, parseAgentSlotSize, } from './agent-slots.js';
31
+ import { defaultAcpProxyWsUrl, defaultAgentsWsUrl, defaultHyperAcpWsUrl, resolveAgentsApiBase, } from './agent-urls.js';
32
+ import { getAgentsApiBaseUrl } from './config.js';
33
+ export { parseControlUiAllowedOrigins, } from './openclaw-control-ui-origin.js';
34
+ import { subscribeBuzzActivity, subscribeBuzzActivityRoute, } from './buzz-activity.js';
35
+ import { CodingAgentAcpClient, } from './acp.js';
36
+ import { CodingAgentAcpPool } from './acp-pool.js';
37
+ import { AcpTurnDriver } from './acp-driver.js';
38
+ export { CodingAgentAcpClient, CodingAgentAcpConnectionError, CodingAgentAcpRequestError, CodingAgentAcpAttachmentError, CodingAgentAcpUnavailableError, ACP_RECONNECT_DELAYS_MS, } from './acp.js';
39
+ export { AcpTurnDriver, } from './acp-driver.js';
40
+ export { CodingAgentAcpPool } from './acp-pool.js';
41
+ // Activity-transport error classes, re-exported here so consumers can classify
42
+ // failures without pulling the SDK root entry into their bundle.
43
+ export { BuzzActivityGapError, BuzzActivityRouteUnavailableError, } from './buzz-activity.js';
44
+ import { APIError } from './errors.js';
45
+ import { HTTPClient } from './http.js';
46
+ import { normalizeSlackRelayBaseUrl } from './channels.js';
47
+ const DEPLOYMENTS_API_PREFIX = '/deployments';
48
+ export const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:prod';
49
+ export const DEFAULT_HERMES_AGENT_IMAGE = 'ghcr.io/hypercli/hypercli-hermes:latest';
50
+ export const DEFAULT_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-opencode:latest';
51
+ export const DEFAULT_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-codex:latest';
52
+ export const DEFAULT_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-claude:latest';
53
+ export const DEFAULT_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-goose:latest';
54
+ export const DEFAULT_KIMI_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-kimi-code:latest';
55
+ export const DEFAULT_PI_IMAGE = 'ghcr.io/hypercli/hypercli-pi:latest';
56
+ export const DEFAULT_BUZZ_AGENT_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-agent:latest';
57
+ export const DEFAULT_BUZZ_OPENCODE_IMAGE = DEFAULT_OPENCODE_IMAGE;
58
+ export const DEFAULT_BUZZ_CODEX_IMAGE = DEFAULT_CODEX_IMAGE;
59
+ export const DEFAULT_BUZZ_CLAUDE_CODE_IMAGE = DEFAULT_CLAUDE_CODE_IMAGE;
60
+ export const DEFAULT_BUZZ_GOOSE_IMAGE = DEFAULT_GOOSE_IMAGE;
61
+ export const DEFAULT_BUZZ_KIMI_CODE_IMAGE = DEFAULT_KIMI_CODE_IMAGE;
62
+ export const DEFAULT_AGENT_RUNTIME_SCOPES = Object.freeze([
63
+ 'agents:none',
64
+ 'files:*',
65
+ 'flows:*',
66
+ 'models:*',
67
+ 'voice:*',
68
+ 'web:*',
69
+ 'workspaces:*',
70
+ ]);
71
+ export const DEFAULT_CODING_AGENT_SYNC_ROOT = '/home/node';
72
+ // Runner-docker bind-mount cap; matches the Backend wire model
73
+ // (AssignmentDockerOptions.volumes max_length).
74
+ export const MAX_DOCKER_VOLUMES = 64;
75
+ // Compatibility export: runtime configuration roots are selected on the host.
76
+ export const DEFAULT_PI_ENV = Object.freeze({});
77
+ export const DEFAULT_CODING_AGENT_IMAGES = {
78
+ 'buzz-agent': DEFAULT_BUZZ_AGENT_IMAGE,
79
+ opencode: DEFAULT_OPENCODE_IMAGE,
80
+ codex: DEFAULT_CODEX_IMAGE,
81
+ 'claude-code': DEFAULT_CLAUDE_CODE_IMAGE,
82
+ goose: DEFAULT_GOOSE_IMAGE,
83
+ 'kimi-code': DEFAULT_KIMI_CODE_IMAGE,
84
+ pi: DEFAULT_PI_IMAGE,
85
+ };
86
+ export const DEFAULT_CODING_AGENT_SYNC_INCLUDES = {
87
+ 'buzz-agent': null,
88
+ opencode: [
89
+ DEFAULT_PERSONA_USER_PATH, DEFAULT_PERSONA_SOUL_PATH,
90
+ '.config/opencode',
91
+ '.local/share/opencode',
92
+ '.local/state/opencode',
93
+ '.cache/opencode',
94
+ ],
95
+ codex: ['.codex', DEFAULT_PERSONA_USER_PATH, DEFAULT_PERSONA_SOUL_PATH],
96
+ 'claude-code': ['.claude', '.claude.json', DEFAULT_PERSONA_USER_PATH, DEFAULT_PERSONA_SOUL_PATH],
97
+ goose: ['.goose', DEFAULT_PERSONA_USER_PATH, DEFAULT_PERSONA_SOUL_PATH],
98
+ 'kimi-code': ['.kimi-code', DEFAULT_PERSONA_USER_PATH, DEFAULT_PERSONA_SOUL_PATH],
99
+ // Native agent state and pi-acp metadata (~/.pi/pi-acp).
100
+ pi: ['.pi', DEFAULT_PERSONA_USER_PATH, DEFAULT_PERSONA_SOUL_PATH],
101
+ };
102
+ export const DEFAULT_BUZZ_CODING_AGENT_IMAGES = {
103
+ 'buzz-agent': DEFAULT_BUZZ_AGENT_IMAGE,
104
+ opencode: DEFAULT_BUZZ_OPENCODE_IMAGE,
105
+ codex: DEFAULT_BUZZ_CODEX_IMAGE,
106
+ 'claude-code': DEFAULT_BUZZ_CLAUDE_CODE_IMAGE,
107
+ goose: DEFAULT_BUZZ_GOOSE_IMAGE,
108
+ 'kimi-code': DEFAULT_BUZZ_KIMI_CODE_IMAGE,
109
+ pi: DEFAULT_PI_IMAGE,
110
+ };
111
+ const BUZZ_RUNTIME_COMMANDS = {
112
+ 'buzz-agent': {
113
+ command: '/usr/local/bin/buzz-agent',
114
+ args: [],
115
+ },
116
+ opencode: {
117
+ command: '/opt/hypercli/bin/opencode',
118
+ args: ['acp'],
119
+ },
120
+ codex: {
121
+ command: '/opt/hypercli/bin/codex-acp',
122
+ args: [],
123
+ },
124
+ 'claude-code': {
125
+ command: '/opt/hypercli/bin/claude-agent-acp',
126
+ args: [],
127
+ },
128
+ goose: {
129
+ command: '/usr/local/bin/goose',
130
+ args: ['acp'],
131
+ },
132
+ 'kimi-code': {
133
+ command: '/opt/hypercli/bin/kimi',
134
+ args: ['acp'],
135
+ },
136
+ pi: {
137
+ command: '/opt/hypercli/bin/pi-acp',
138
+ args: [],
139
+ },
140
+ };
141
+ export const DEFAULT_BUZZ_RUST_LOG = 'hyper_acp=info,buzz_acp=info,pool::prompt=info,acp::stream=off';
142
+ // Sync direction: this set mirrors the provider's AUTHORITATIVE_ENV_KEYS
143
+ // (acp/buzz-backend-provider) — the boundary enforcer — and rs-sdk
144
+ // BUZZ_RESERVED_ENV; a provider-side parity test asserts the three key sets
145
+ // are identical. Keys the provider deliberately passes through as caller
146
+ // policy (BUZZ_ACP_MODEL
147
+ // — typed buzz config mints overwrite caller
148
+ // values) are excluded on purpose: they remain caller-settable launch policy
149
+ // at the boundary.
150
+ const BUZZ_RESERVED_ENV_KEYS = new Set([
151
+ 'HYPER_AGENTS_API_KEY',
152
+ 'BUZZ_PRIVATE_KEY',
153
+ 'NOSTR_PRIVATE_KEY',
154
+ 'BUZZ_AUTH_TAG',
155
+ 'BUZZ_API_TOKEN',
156
+ 'BUZZ_ACP_PRIVATE_KEY',
157
+ 'BUZZ_ACP_API_TOKEN',
158
+ 'BUZZ_RELAY_URL',
159
+ 'BUZZ_ACP_AGENT_OWNER',
160
+ 'BUZZ_ACP_AGENT_COMMAND',
161
+ 'BUZZ_ACP_AGENT_ARGS',
162
+ 'BUZZ_ACP_MCP_COMMAND',
163
+ 'BUZZ_ACP_MULTIPLE_EVENT_HANDLING',
164
+ 'BUZZ_ACP_RELAY_OBSERVER',
165
+ 'BUZZ_ACP_RESPOND_TO',
166
+ 'BUZZ_ACP_RESPOND_TO_ALLOWLIST',
167
+ 'BUZZ_ACP_SETUP_PAYLOAD',
168
+ 'BUZZ_MANAGED_AGENT',
169
+ 'BUZZ_MANAGED_AGENT_START_NONCE',
170
+ // No longer minted anywhere (dead: nothing reads them); kept listed so
171
+ // caller-supplied values are stripped.
172
+ 'BUZZ_ACP_DISPLAY_NAME',
173
+ 'BUZZ_ACP_TEXT_MENTIONS',
174
+ 'BUZZ_ACP_REQUIRE_REPLY',
175
+ 'BUZZ_AGENT_REQUIRE_REPLY',
176
+ 'BUZZ_ACP_SYSTEM_PROMPT',
177
+ 'BUZZ_ACP_SYSTEM_PROMPT_FILE',
178
+ 'HYPER_ACP_WS_LISTEN',
179
+ 'HYPER_ACP_LOG',
180
+ 'HYPER_ACP_WS_TOKEN',
181
+ 'HYPER_ACP_WS_URL',
182
+ 'HYPER_ACP_AGENT_COMMAND',
183
+ 'HYPER_ACP_AGENT_ARGS',
184
+ 'HYPER_ACP_PERMISSIONS',
185
+ 'HYPER_ACP_PERMISSION_MODE',
186
+ 'HYPER_ACP_AUTO_APPROVE_PERMISSION',
187
+ 'HYPER_ACP_TRACE_DB',
188
+ 'HYPER_ACP_CORS_ORIGIN',
189
+ 'CLAUDE_CODE_EXECUTABLE',
190
+ 'HYPER_WORKSPACES_BOOT_SYNC',
191
+ 'HYPER_WORKSPACES_SYNC_READY_ONLY',
192
+ 'HYPER_WORKSPACES_SYNC_WORKSPACE',
193
+ ]);
194
+ export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
195
+ OPENCLAW_MEMORY_SEARCH_ENABLED: '1',
196
+ OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START: '0',
197
+ OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH: '0',
198
+ OPENCLAW_MEMORY_SEARCH_SYNC_WATCH: '0',
199
+ OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS: '30000',
200
+ OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES: '0',
201
+ };
202
+ export const WORKSPACES_SYNC_ENV_DEFAULTS = {
203
+ HYPER_WORKSPACES_BOOT_SYNC: '1',
204
+ HYPER_WORKSPACES_DIR: '/home/node/shared',
205
+ HYPER_WORKSPACES_SYNC_READY_ONLY: '1',
206
+ };
207
+ export const OPENCLAW_CRON_ENV_DEFAULTS = {
208
+ OPENCLAW_CRON_ENABLED: '1',
209
+ };
210
+ export const HERMES_CRON_ENV_DEFAULTS = {
211
+ HERMES_CRON_ENABLED: '1',
212
+ };
213
+ const DEFAULT_OPENCLAW_SYNC_EXCLUDE = [
214
+ 'shared/**',
215
+ '.openclaw/npm/**/node_modules/**',
216
+ '.openclaw/agents/**/agent/*.sqlite.memory-reindex-*',
217
+ '.openclaw/agents/**/agent/*.sqlite.reindex-lock.sqlite*',
218
+ '.openclaw/browser/**/Code Cache/**',
219
+ '.openclaw/browser/**/GPUCache/**',
220
+ '.openclaw/browser/**/ShaderCache/**',
221
+ '.openclaw/browser/**/GrShaderCache/**',
222
+ '.openclaw/browser/**/optimization_guide_model_store/**',
223
+ ];
224
+ const LAUNCH_CONFIG_KEYS = new Set([
225
+ 'image',
226
+ 'env',
227
+ 'secrets',
228
+ 'routes',
229
+ 'command',
230
+ 'entrypoint',
231
+ 'sync_root',
232
+ 'sync_include',
233
+ 'sync_exclude',
234
+ 'sync_uid',
235
+ 'sync_gid',
236
+ 'registry_url',
237
+ 'registry_auth',
238
+ 'restart',
239
+ 'runtime_scopes',
240
+ 'executor',
241
+ 'cwd',
242
+ 'docker',
243
+ ]);
244
+ const DEFAULT_OPENCLAW_SYNC_ROOT = '/home/node';
245
+ export const DEFAULT_HERMES_AGENT_SYNC_ROOT = '/home/hermes';
246
+ export const DEFAULT_HERMES_AGENT_SYNC_EXCLUDE = ['shared/**'];
247
+ export const DEFAULT_HERMES_AGENT_SYNC_UID = 10000;
248
+ export const DEFAULT_HERMES_AGENT_SYNC_GID = 10000;
249
+ export const AGENT_FILE_MAX_BYTES = 250 * 1024 * 1024;
250
+ // Reef file writes traverse the Cloudflare-proxied agent hostname
251
+ // (https://<agent>.hypercli.app/_reef/...), whose edge rejects request bodies
252
+ // above 100 MB. Enforced client-side so oversized writes fail fast with a
253
+ // clear error instead of an opaque edge `413 Payload Too Large`.
254
+ export const AGENT_FILE_WRITE_MAX_BYTES = 100 * 1024 * 1024;
255
+ export const AGENT_FILE_TRANSFER_CHUNK_BYTES = 64 * 1024;
256
+ export const AGENT_FILE_OPERATION_TIMEOUT_MS = 300_000;
257
+ /** Buffered exec stdin cap, mirrored by the backend exec protocol. */
258
+ export const AGENT_EXEC_STDIN_MAX_BYTES = 262_144;
259
+ function shellAbortError() {
260
+ const error = new Error('Shell connection cancelled');
261
+ error.name = 'AbortError';
262
+ return error;
263
+ }
264
+ function runShellOperation(operation, signal, timeoutMs, timeoutMessage) {
265
+ return new Promise((resolve, reject) => {
266
+ const controller = new AbortController();
267
+ let settled = false;
268
+ const cleanup = () => {
269
+ clearTimeout(timer);
270
+ signal?.removeEventListener('abort', abortOperation);
271
+ };
272
+ const finish = (error, value) => {
273
+ if (settled)
274
+ return;
275
+ settled = true;
276
+ cleanup();
277
+ if (error)
278
+ reject(error);
279
+ else
280
+ resolve(value);
281
+ };
282
+ const abortOperation = () => {
283
+ controller.abort(signal?.reason);
284
+ finish(shellAbortError());
285
+ };
286
+ const timer = setTimeout(() => {
287
+ controller.abort();
288
+ finish(new Error(timeoutMessage));
289
+ }, timeoutMs);
290
+ if (signal?.aborted) {
291
+ abortOperation();
292
+ return;
293
+ }
294
+ signal?.addEventListener('abort', abortOperation, { once: true });
295
+ void operation(controller.signal).then((value) => finish(undefined, value), (error) => {
296
+ if (error instanceof Error && error.name === 'AbortError' && !signal?.aborted) {
297
+ finish(new Error(timeoutMessage));
298
+ }
299
+ else {
300
+ finish(error);
301
+ }
302
+ });
303
+ });
304
+ }
305
+ export class AgentCapturedLogsError extends APIError {
306
+ snapshot;
307
+ constructor(error, snapshot) {
308
+ super(error.statusCode, error.detail, error.method, error.url, error.responseText, error.retryAfterSeconds);
309
+ this.snapshot = snapshot;
310
+ Object.setPrototypeOf(this, new.target.prototype);
311
+ this.name = 'AgentCapturedLogsError';
312
+ this.cause = error;
313
+ }
314
+ }
315
+ function isCapturedLogsSnapshot(value) {
316
+ if (!value || typeof value !== 'object')
317
+ return false;
318
+ const snapshot = value;
319
+ return snapshot.transport === 'runner' && typeof snapshot.agent_id === 'string'
320
+ && (typeof snapshot.logs === 'string' || snapshot.logs === null)
321
+ && (snapshot.unavailable === null || typeof snapshot.unavailable === 'string')
322
+ && Array.isArray(snapshot.captures);
323
+ }
324
+ function agentLogQuery(options) {
325
+ return {
326
+ ...(options.source !== undefined ? { source: options.source } : {}),
327
+ ...(options.generation !== undefined ? { generation: options.generation } : {}),
328
+ ...(options.maxBytes !== undefined ? { max_bytes: options.maxBytes } : {}),
329
+ ...(options.maxLines !== undefined ? { max_lines: options.maxLines } : {}),
330
+ };
331
+ }
332
+ export function parseAgentLogFrame(raw) {
333
+ let payload;
334
+ try {
335
+ payload = JSON.parse(raw);
336
+ }
337
+ catch {
338
+ return { kind: 'log', line: raw };
339
+ }
340
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
341
+ return { kind: 'log', line: raw };
342
+ }
343
+ const frame = payload;
344
+ if (typeof frame.event !== 'string')
345
+ return { kind: 'log', line: raw };
346
+ switch (frame.event) {
347
+ case 'captured_logs':
348
+ return isCapturedLogsSnapshot(payload)
349
+ ? { kind: 'capturedLogs', snapshot: payload }
350
+ : { kind: 'error', detail: 'Invalid captured logs snapshot' };
351
+ case 'log':
352
+ return {
353
+ kind: 'log',
354
+ line: typeof frame.log === 'string' ? frame.log : String(frame.log ?? ''),
355
+ };
356
+ case 'history_end':
357
+ return { kind: 'historyEnd' };
358
+ case 'error':
359
+ return {
360
+ kind: 'error',
361
+ detail: typeof frame.detail === 'string' && frame.detail
362
+ ? frame.detail
363
+ : 'Log stream failed',
364
+ };
365
+ default:
366
+ return { kind: 'ignore' };
367
+ }
368
+ }
369
+ function routeConfigBody(route) {
370
+ const body = { port: route.port };
371
+ if (route.auth !== undefined)
372
+ body.auth = route.auth;
373
+ if (route.prefix !== undefined)
374
+ body.prefix = route.prefix;
375
+ if (route.remove_headers?.length)
376
+ body.remove_headers = route.remove_headers;
377
+ return body;
378
+ }
379
+ function routesConfigBody(routes) {
380
+ return Object.fromEntries(Object.entries(routes ?? {}).map(([name, route]) => [name, routeConfigBody(route)]));
381
+ }
382
+ const REQUIRED_START_LAUNCH_CONFIG_KEYS = [
383
+ 'image',
384
+ 'env',
385
+ 'secrets',
386
+ 'routes',
387
+ 'command',
388
+ 'entrypoint',
389
+ 'restart',
390
+ 'sync_root',
391
+ 'sync_uid',
392
+ 'sync_gid',
393
+ 'registry_url',
394
+ 'registry_auth',
395
+ 'runtime_scopes',
396
+ ];
397
+ const OPENCLAW_SECRET_ONLY_ENV_KEYS = [
398
+ 'SLACK_BOT_TOKEN',
399
+ 'SLACK_APP_TOKEN',
400
+ 'SLACK_USER_TOKEN',
401
+ 'SLACK_SIGNING_SECRET',
402
+ 'SLACK_RELAY_AUTH_TOKEN',
403
+ ];
404
+ function rejectOpenClawSecretOnlyEnv(env, noun = 'env') {
405
+ const matches = OPENCLAW_SECRET_ONLY_ENV_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(env, key));
406
+ if (matches.length > 0) {
407
+ throw new Error(`${matches.join(', ')} must be supplied through secrets, not ${noun}`);
408
+ }
409
+ }
410
+ function cloneCompleteLaunchConfig(value) {
411
+ if (!isPlainRecord(value))
412
+ throw new Error('launchConfig must be a complete object');
413
+ const missing = REQUIRED_START_LAUNCH_CONFIG_KEYS.filter((key) => !Object.prototype.hasOwnProperty.call(value, key));
414
+ if (missing.length > 0) {
415
+ throw new Error(`launchConfig is incomplete; missing: ${missing.join(', ')}`);
416
+ }
417
+ if (Object.prototype.hasOwnProperty.call(value, 'sync_include')
418
+ && Object.prototype.hasOwnProperty.call(value, 'sync_exclude')) {
419
+ throw new Error('launchConfig cannot carry both sync policies');
420
+ }
421
+ if (Array.isArray(value.sync_include) && value.sync_include.length === 0) {
422
+ throw new Error('syncInclude must contain at least one path; omit it to sync all');
423
+ }
424
+ if (Array.isArray(value.sync_exclude) && (value.sync_exclude.includes('*') || value.sync_exclude.includes('**'))) {
425
+ throw new Error('syncExclude cannot exclude the entire sync root; omit it to sync all');
426
+ }
427
+ if (typeof value.restart !== 'boolean') {
428
+ throw new Error('launchConfig restart must be a boolean');
429
+ }
430
+ const executor = value.executor;
431
+ if (executor !== undefined && executor !== null && executor !== 'process' && executor !== 'docker') {
432
+ throw new Error("launchConfig executor must be 'process' or 'docker'");
433
+ }
434
+ if (executor === 'process' && value.docker != null) {
435
+ throw new Error('docker launch options require the docker executor');
436
+ }
437
+ return structuredClone(value);
438
+ }
439
+ export const OPENCLAW_TRUSTED_PROXIES_ENV = 'OPENCLAW_TRUSTED_PROXIES';
440
+ const DEFAULT_OPENCLAW_MODEL_ENV = Object.freeze({
441
+ HYPER_MODELS: 'default-anthropic',
442
+ HYPER_EMBEDDING_MODELS: 'qwen3-embedding-4b',
443
+ });
444
+ const DEFAULT_HERMES_MODEL_ENV = DEFAULT_OPENCLAW_MODEL_ENV;
445
+ export function buildOpenClawTrustedProxiesEnv(trustedProxies) {
446
+ const proxies = (trustedProxies ?? []).map((proxy) => proxy.trim()).filter(Boolean);
447
+ return proxies.length > 0 ? { [OPENCLAW_TRUSTED_PROXIES_ENV]: proxies.join(',') } : {};
448
+ }
449
+ function droppedLaunchConfigKeys(data) {
450
+ const warnings = Array.isArray(data.warnings) ? data.warnings : [];
451
+ return warnings
452
+ .filter((warning) => (Boolean(warning) && typeof warning === 'object'))
453
+ .filter((warning) => warning.code === 'unsupported_launch_config_keys_dropped')
454
+ .flatMap((warning) => (Array.isArray(warning.dropped_keys) ? warning.dropped_keys.map(String) : []));
455
+ }
456
+ function buildBuzzLaunchEnv(runtime, buzz) {
457
+ if (!buzz.privateKeyNsec.trim())
458
+ throw new Error('buzz.privateKeyNsec is required');
459
+ if (!buzz.relayUrl.trim())
460
+ throw new Error('buzz.relayUrl is required');
461
+ const harness = BUZZ_RUNTIME_COMMANDS[runtime];
462
+ const env = {
463
+ BUZZ_RELAY_URL: buzz.relayUrl,
464
+ BUZZ_ACP_AGENT_COMMAND: harness.command,
465
+ BUZZ_ACP_AGENT_ARGS: harness.args.join(','),
466
+ };
467
+ if (runtime === 'claude-code') {
468
+ env.CLAUDE_CODE_EXECUTABLE = '/opt/hypercli/bin/claude';
469
+ }
470
+ if (buzz.rustLog)
471
+ env.RUST_LOG = buzz.rustLog;
472
+ const optional = {
473
+ BUZZ_ACP_MODEL: buzz.model,
474
+ BUZZ_ACP_RESPOND_TO: buzz.respondTo,
475
+ BUZZ_ACP_RESPOND_TO_ALLOWLIST: buzz.respondToAllowlist?.length
476
+ ? buzz.respondToAllowlist.join(',')
477
+ : undefined,
478
+ };
479
+ for (const [key, value] of Object.entries(optional)) {
480
+ if (value)
481
+ env[key] = value;
482
+ }
483
+ return env;
484
+ }
485
+ function buildBuzzLaunchSecrets(buzz) {
486
+ if (!buzz.privateKeyNsec.trim())
487
+ throw new Error('buzz.privateKeyNsec is required');
488
+ const secrets = {
489
+ BUZZ_PRIVATE_KEY: buzz.privateKeyNsec,
490
+ };
491
+ // The NIP-OA attestation is a bearer credential: keep it in the k8s-backed
492
+ // secrets projection (unrolled to env at launch) rather than plaintext env.
493
+ if (buzz.authTag)
494
+ secrets.BUZZ_AUTH_TAG = buzz.authTag;
495
+ return secrets;
496
+ }
497
+ /** Construct the Buzz-owned launch fields for create or a stopped deployment update.
498
+ * Callers retain all other stored launch fields. No network or identity generation.
499
+ */
500
+ export function buildBuzzLaunchConfig(runtime, buzz, apiBase, stored = {}) {
501
+ if (!Object.prototype.hasOwnProperty.call(BUZZ_RUNTIME_COMMANDS, runtime)) {
502
+ throw new Error('Buzz requires a coding runtime');
503
+ }
504
+ const env = { ...(stored.env ?? {}) };
505
+ const secrets = { ...(stored.secrets ?? {}) };
506
+ if (buzz) {
507
+ for (const key of BUZZ_RESERVED_ENV_KEYS) {
508
+ delete env[key];
509
+ delete secrets[key];
510
+ }
511
+ Object.assign(env, buildBuzzLaunchEnv(runtime, buzz));
512
+ Object.assign(secrets, buildBuzzLaunchSecrets(buzz));
513
+ }
514
+ env.RUST_LOG ??= DEFAULT_BUZZ_RUST_LOG;
515
+ for (const key of [
516
+ 'HYPER_ACP_WS_LISTEN', 'HYPER_ACP_LOG', 'HYPER_ACP_WS_TOKEN',
517
+ 'BUZZ_ACP_RELAY_OBSERVER', 'HYPER_ACP_AGENT_COMMAND', 'HYPER_ACP_AGENT_ARGS',
518
+ 'HYPER_ACP_AUTO_APPROVE_PERMISSION', 'HYPER_ACP_PERMISSIONS', 'HYPER_ACP_PERMISSION_MODE',
519
+ ]) {
520
+ delete env[key];
521
+ delete secrets[key];
522
+ }
523
+ env.HYPER_ACP_WS_URL = defaultHyperAcpWsUrl(apiBase);
524
+ return { ...stored, env, secrets, routes: {}, restart: false,
525
+ command: [stored.executor === 'process' ? 'hyper-acp' : '/usr/local/bin/hyper-acp'] };
526
+ }
527
+ /** Whether the runtime has a Buzz coding harness launch mapping. */
528
+ export function supportsBuzzRuntime(runtime) {
529
+ return typeof runtime === 'string' && Object.prototype.hasOwnProperty.call(BUZZ_RUNTIME_COMMANDS, runtime);
530
+ }
531
+ /** Maximum decoded bytes for the native runner file transport. */
532
+ export const RUNNER_FILE_MAX_BYTES = 50 * 1024 * 1024;
533
+ function nativeFilePath(path) {
534
+ if (!path || encodeUtf8(path).byteLength > 4096 || /[\\:\0]/.test(path)
535
+ || path.split('/').some((part) => !part || part === '.' || part === '..' || /[ .]$/.test(part))) {
536
+ throw new Error('Runner file paths must be portable paths relative to the assignment root');
537
+ }
538
+ return path;
539
+ }
540
+ function resolveSyncRootFilePath(path) {
541
+ const normalized = path.replace(/\\/g, '/');
542
+ if (normalized.startsWith('/') || /^[a-z]:/i.test(normalized) || normalized.includes('\0')) {
543
+ throw new Error('agent file paths must be relative to the sync root');
544
+ }
545
+ const parts = normalized.split('/');
546
+ if (parts.includes('..')) {
547
+ throw new Error('agent file paths must stay within the sync root');
548
+ }
549
+ return parts.filter((part) => part && part !== '.').join('/');
550
+ }
551
+ function normalizeWritableBackendFilePath(path) {
552
+ return resolveSyncRootFilePath(path);
553
+ }
554
+ function isUuidRef(value) {
555
+ 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());
556
+ }
557
+ function isDirectAgentIdRef(value) {
558
+ const raw = value.trim();
559
+ return isUuidRef(raw) || /^[0-9a-f]{6,}$/i.test(raw) || /^(agent|external)[-_:]/i.test(raw);
560
+ }
561
+ function isSelfAgentRef(value) {
562
+ return value.trim().toLowerCase() === 'self';
563
+ }
564
+ function agentRoutesStateFromData(data) {
565
+ return {
566
+ agentId: String(data.agent_id ?? ''),
567
+ routes: structuredClone(data.routes ?? {}),
568
+ cors: data.cors === null || data.cors === undefined ? null : structuredClone(data.cors),
569
+ routeStatuses: structuredClone(data.route_statuses ?? {}),
570
+ };
571
+ }
572
+ function agentAccessIdentityFromData(data) {
573
+ const payload = data ?? {};
574
+ const agentId = payload.agent_id ? String(payload.agent_id) : null;
575
+ return {
576
+ userId: String(payload.user_id ?? ''),
577
+ authType: String(payload.auth_type ?? ''),
578
+ agentId,
579
+ tags: (payload.tags ?? []).map((tag) => String(tag)),
580
+ capabilities: (payload.capabilities ?? []).map((item) => String(item)),
581
+ keyId: payload.key_id ? String(payload.key_id) : null,
582
+ keyName: payload.key_name ? String(payload.key_name) : null,
583
+ teamId: payload.team_id ? String(payload.team_id) : null,
584
+ planId: payload.plan_id ? String(payload.plan_id) : null,
585
+ isAgentRuntimeKey: Boolean(agentId),
586
+ };
587
+ }
588
+ /** Backend-discovered file access scoped to an agent's retained storage root. */
589
+ export class AgentFiles {
590
+ agent;
591
+ deployments;
592
+ constructor(agent, deployments) {
593
+ this.agent = agent;
594
+ this.deployments = deployments;
595
+ }
596
+ async list(path = '') {
597
+ return this.deployments.filesList(this.agent, path);
598
+ }
599
+ async readBytes(path, options) {
600
+ return this.deployments.fileReadBytes(this.agent, path, options);
601
+ }
602
+ async readBytesWithMetadata(path, options) {
603
+ return this.deployments.fileReadBytesWithMetadata(this.agent, path, options);
604
+ }
605
+ async read(path, options) {
606
+ return this.deployments.fileRead(this.agent, path, options);
607
+ }
608
+ async writeBytes(path, content, options = {}) {
609
+ return this.deployments.fileWriteBytes(this.agent, path, content, options);
610
+ }
611
+ async write(path, content) {
612
+ return this.deployments.fileWrite(this.agent, path, content);
613
+ }
614
+ async delete(path, options = {}) {
615
+ return this.deployments.fileDelete(this.agent, path, options);
616
+ }
617
+ }
618
+ /** Canonical states understood by this SDK. AgentState remains forward-open. */
619
+ export const CANONICAL_AGENT_STATES = [
620
+ 'CREATING',
621
+ 'STARTING',
622
+ 'RESTORING',
623
+ 'RUNNING',
624
+ 'STOPPING',
625
+ 'STOPPED',
626
+ 'ARCHIVING',
627
+ 'ARCHIVED',
628
+ 'FAILED',
629
+ 'NO_NAMESPACE',
630
+ 'DELETED',
631
+ ];
632
+ export const AGENT_TRANSITIONAL_STATES = new Set([
633
+ 'CREATING',
634
+ 'STARTING',
635
+ 'RESTORING',
636
+ 'STOPPING',
637
+ 'ARCHIVING',
638
+ ]);
639
+ export const AGENT_RUNTIME_INACTIVE_STATES = new Set([
640
+ 'STOPPED',
641
+ 'ARCHIVING',
642
+ 'ARCHIVED',
643
+ 'FAILED',
644
+ 'NO_NAMESPACE',
645
+ 'DELETED',
646
+ ]);
647
+ export function isAgentTransitionalState(state) {
648
+ return AGENT_TRANSITIONAL_STATES.has(state.toUpperCase());
649
+ }
650
+ export function isAgentRuntimeInactiveState(state) {
651
+ return AGENT_RUNTIME_INACTIVE_STATES.has(state.toUpperCase());
652
+ }
653
+ function parseDate(value) {
654
+ if (typeof value !== 'string' || !value)
655
+ return null;
656
+ return new Date(value.replace('Z', '+00:00'));
657
+ }
658
+ function metaStatusFromDict(data) {
659
+ if (!isPlainRecord(data))
660
+ return null;
661
+ const observed = data.observed_state === 'RUNNING' || data.observed_state === 'STOPPED'
662
+ ? data.observed_state
663
+ : null;
664
+ return {
665
+ status: typeof data.status === 'string' ? data.status : '',
666
+ clusterId: typeof data.cluster_id === 'string' ? data.cluster_id : null,
667
+ namespace: typeof data.namespace === 'string' ? data.namespace : null,
668
+ observedState: observed,
669
+ reason: typeof data.reason === 'string' ? data.reason : null,
670
+ message: typeof data.message === 'string' ? data.message : null,
671
+ observedAt: typeof data.observed_at === 'string' ? data.observed_at : null,
672
+ };
673
+ }
674
+ function agentMetaFromDict(data) {
675
+ if (!isPlainRecord(data))
676
+ return null;
677
+ const meta = structuredClone(data);
678
+ if (Object.prototype.hasOwnProperty.call(data, 'status')) {
679
+ meta.status = metaStatusFromDict(data.status);
680
+ }
681
+ return meta;
682
+ }
683
+ function sleep(ms) {
684
+ return new Promise((resolve) => setTimeout(resolve, ms));
685
+ }
686
+ function isTruthyEnv(value) {
687
+ return ['1', 'true', 'yes', 'on', 'enabled'].includes(String(value ?? '').trim().toLowerCase());
688
+ }
689
+ function isFalseyEnv(value) {
690
+ return ['0', 'false', 'no', 'off', 'disabled'].includes(String(value ?? '').trim().toLowerCase());
691
+ }
692
+ function isPlainRecord(value) {
693
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
694
+ }
695
+ function flattenConfigValue(value, prefix, out) {
696
+ if (!prefix) {
697
+ if (isPlainRecord(value)) {
698
+ for (const [key, child] of Object.entries(value)) {
699
+ flattenConfigValue(child, key, out);
700
+ }
701
+ return;
702
+ }
703
+ out[''] = value;
704
+ return;
705
+ }
706
+ out[prefix] = value;
707
+ if (Array.isArray(value)) {
708
+ value.forEach((child, index) => flattenConfigValue(child, `${prefix}[${index}]`, out));
709
+ return;
710
+ }
711
+ if (isPlainRecord(value)) {
712
+ for (const [key, child] of Object.entries(value)) {
713
+ flattenConfigValue(child, `${prefix}.${key}`, out);
714
+ }
715
+ }
716
+ }
717
+ export function flattenLaunchConfig(launchConfig) {
718
+ const flat = {};
719
+ if (!isPlainRecord(launchConfig))
720
+ return flat;
721
+ flattenConfigValue(launchConfig, '', flat);
722
+ return flat;
723
+ }
724
+ function pathParts(path) {
725
+ if (Array.isArray(path))
726
+ return path;
727
+ const parts = [];
728
+ for (const part of path.replace(/\[(\d+)\]/g, '.$1').split('.')) {
729
+ if (!part)
730
+ continue;
731
+ parts.push(/^\d+$/.test(part) ? Number(part) : part);
732
+ }
733
+ return parts;
734
+ }
735
+ export function getLaunchConfigValue(launchConfig, path) {
736
+ let current = launchConfig;
737
+ for (const part of pathParts(path)) {
738
+ if (typeof part === 'number') {
739
+ if (!Array.isArray(current))
740
+ return undefined;
741
+ current = current[part];
742
+ continue;
743
+ }
744
+ if (!isPlainRecord(current))
745
+ return undefined;
746
+ current = current[part];
747
+ }
748
+ return current;
749
+ }
750
+ export function routesHaveDesktop(routes) {
751
+ if (!isPlainRecord(routes))
752
+ return false;
753
+ if (isPlainRecord(routes.desktop))
754
+ return true;
755
+ return Object.values(routes).some((route) => isPlainRecord(route) && route.prefix === 'desktop');
756
+ }
757
+ export function launchConfigHasDesktop(launchConfig) {
758
+ if (!isPlainRecord(launchConfig))
759
+ return false;
760
+ const desktopEnabled = getLaunchConfigValue(launchConfig, 'env.HYPER_DESKTOP_ENABLED');
761
+ if (isFalseyEnv(desktopEnabled))
762
+ return false;
763
+ if (isTruthyEnv(desktopEnabled))
764
+ return true;
765
+ return routesHaveDesktop(getLaunchConfigValue(launchConfig, 'routes'));
766
+ }
767
+ export function agentConfigHasDesktop(source) {
768
+ if (!source)
769
+ return false;
770
+ const launchConfig = source.launchConfig ?? source.launch_config;
771
+ const desktopEnabled = getLaunchConfigValue(launchConfig, 'env.HYPER_DESKTOP_ENABLED');
772
+ if (isFalseyEnv(desktopEnabled))
773
+ return false;
774
+ return (launchConfigHasDesktop(launchConfig) ||
775
+ routesHaveDesktop(source.routes));
776
+ }
777
+ function browserDesktopRedirectPath(options = {}) {
778
+ const redirect = (options.redirect ?? 'vnc_lite.html').trim() || 'vnc_lite.html';
779
+ if (redirect.includes('\\')) {
780
+ throw new Error('Desktop redirect must be a relative path');
781
+ }
782
+ const base = 'https://desktop.local';
783
+ const parsed = new URL(redirect, `${base}/`);
784
+ if (parsed.origin !== base) {
785
+ throw new Error('Desktop redirect must be a relative path');
786
+ }
787
+ if (options.resize !== null) {
788
+ const resize = options.resize ?? 'scale';
789
+ if (resize.trim()) {
790
+ // vnc_lite.html (the default viewer) takes `scale=true`; the full vnc.html
791
+ // UI takes `resize=scale`. Emit the parameter for whichever page the
792
+ // redirect targets.
793
+ const targetsLite = (parsed.pathname.endsWith('vnc_lite.html') || parsed.pathname.endsWith('vnc_auto.html'));
794
+ if (targetsLite)
795
+ parsed.searchParams.set('scale', resize === 'scale' ? 'true' : resize);
796
+ else
797
+ parsed.searchParams.set('resize', resize);
798
+ }
799
+ }
800
+ const pathname = parsed.pathname.replace(/^\/+/, '') || 'vnc_lite.html';
801
+ return `${pathname}${parsed.search}${parsed.hash}`;
802
+ }
803
+ export function buildBrowserDesktopUrl(desktopBaseUrl, token, options = {}) {
804
+ const jwt = token.trim();
805
+ if (!jwt)
806
+ throw new Error('Desktop token is required');
807
+ const url = new URL('/_jwt_auth', desktopBaseUrl);
808
+ url.searchParams.set('jwt', jwt);
809
+ url.searchParams.set('redirect', browserDesktopRedirectPath(options));
810
+ return url.toString();
811
+ }
812
+ function isDirectoryListingPayload(value) {
813
+ if (!value || typeof value !== 'object')
814
+ return false;
815
+ const payload = value;
816
+ return (payload.type === 'directory' &&
817
+ Array.isArray(payload.directories) &&
818
+ Array.isArray(payload.files));
819
+ }
820
+ export { defaultAcpProxyWsUrl, defaultHyperAcpWsUrl, resolveAgentsApiBase } from './agent-urls.js';
821
+ function randomHexToken(bytes) {
822
+ const buffer = new Uint8Array(bytes);
823
+ if (globalThis.crypto?.getRandomValues) {
824
+ globalThis.crypto.getRandomValues(buffer);
825
+ }
826
+ else {
827
+ randomFillSync(bytes ? buffer.subarray(0, bytes) : buffer);
828
+ }
829
+ return Array.from(buffer, (value) => value.toString(16).padStart(2, '0')).join('');
830
+ }
831
+ function encodeFilePath(path) {
832
+ return path
833
+ .replace(/^\/+/, '')
834
+ .split('/')
835
+ .filter(Boolean)
836
+ .map((part) => encodeURIComponent(part))
837
+ .join('/');
838
+ }
839
+ function decodeUtf8(content) {
840
+ return new TextDecoder().decode(content);
841
+ }
842
+ function encodeUtf8(content) {
843
+ return new TextEncoder().encode(content);
844
+ }
845
+ function encodeBase64(data) {
846
+ if (typeof Buffer !== 'undefined') {
847
+ return Buffer.from(data).toString('base64');
848
+ }
849
+ // Browser fallback: btoa over chunked binary string (avoids arg-limit blowups)
850
+ let binary = '';
851
+ const CHUNK = 0x8000;
852
+ for (let i = 0; i < data.length; i += CHUNK) {
853
+ binary += String.fromCharCode(...data.subarray(i, i + CHUNK));
854
+ }
855
+ return btoa(binary);
856
+ }
857
+ function fileReadLimitError(path, maxBytes) {
858
+ return new Error(`File ${path} exceeds the ${maxBytes / 1024 / 1024} MiB read limit`);
859
+ }
860
+ async function readResponseBytes(response, path, maxBytes) {
861
+ if (maxBytes === undefined)
862
+ return new Uint8Array(await response.arrayBuffer());
863
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
864
+ throw new RangeError('maxBytes must be a non-negative safe integer');
865
+ }
866
+ const contentLength = Number(response.headers.get('content-length'));
867
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
868
+ await response.body?.cancel();
869
+ throw fileReadLimitError(path, maxBytes);
870
+ }
871
+ if (!response.body) {
872
+ const bytes = new Uint8Array(await response.arrayBuffer());
873
+ if (bytes.byteLength > maxBytes)
874
+ throw fileReadLimitError(path, maxBytes);
875
+ return bytes;
876
+ }
877
+ const reader = response.body.getReader();
878
+ const chunks = [];
879
+ let totalBytes = 0;
880
+ try {
881
+ while (true) {
882
+ const { done, value } = await reader.read();
883
+ if (done)
884
+ break;
885
+ totalBytes += value.byteLength;
886
+ if (totalBytes > maxBytes) {
887
+ await reader.cancel();
888
+ throw fileReadLimitError(path, maxBytes);
889
+ }
890
+ chunks.push(value);
891
+ }
892
+ }
893
+ finally {
894
+ reader.releaseLock();
895
+ }
896
+ const bytes = new Uint8Array(totalBytes);
897
+ let offset = 0;
898
+ for (const chunk of chunks) {
899
+ bytes.set(chunk, offset);
900
+ offset += chunk.byteLength;
901
+ }
902
+ return bytes;
903
+ }
904
+ function toUint8Array(content) {
905
+ if (typeof content === 'string')
906
+ return encodeUtf8(content);
907
+ if (content instanceof Uint8Array)
908
+ return content;
909
+ return new Uint8Array(content);
910
+ }
911
+ function execResultFromDict(data) {
912
+ return {
913
+ exitCode: data.exit_code ?? -1,
914
+ stdout: data.stdout || '',
915
+ stderr: data.stderr || '',
916
+ };
917
+ }
918
+ function ownKeysEqual(value, expected) {
919
+ const keys = Object.keys(value).sort();
920
+ return keys.length === expected.length
921
+ && keys.every((key, index) => key === [...expected].sort()[index]);
922
+ }
923
+ function validateAgentWsToken(value, agentId, purpose, shell) {
924
+ const base = purpose === 'shell'
925
+ ? ['agent_id', 'expires_at', 'shell', 'ws_url']
926
+ : ['agent_id', 'expires_at', 'ws_url'];
927
+ const invalid = () => new Error(`Backend returned an invalid Agent ${purpose} token response`);
928
+ if (!isPlainRecord(value))
929
+ throw invalid();
930
+ if (!ownKeysEqual(value, [...base, 'token']) || typeof value.token !== 'string' || !value.token) {
931
+ throw invalid();
932
+ }
933
+ if (value.agent_id !== agentId
934
+ || typeof value.expires_at !== 'string'
935
+ || !value.expires_at
936
+ || typeof value.ws_url !== 'string'
937
+ || !value.ws_url
938
+ || (purpose === 'shell' && value.shell !== shell)) {
939
+ throw invalid();
940
+ }
941
+ let parsed;
942
+ try {
943
+ parsed = new URL(value.ws_url);
944
+ }
945
+ catch {
946
+ throw invalid();
947
+ }
948
+ if (!['ws:', 'wss:'].includes(parsed.protocol)
949
+ || !parsed.hostname
950
+ || parsed.username
951
+ || parsed.password
952
+ || parsed.search
953
+ || parsed.hash
954
+ || !parsed.pathname.endsWith(`/ws/${purpose}/${agentId}`)) {
955
+ throw invalid();
956
+ }
957
+ return value;
958
+ }
959
+ function validateDeploymentEventToken(value) {
960
+ const invalid = () => new Error('Backend returned an invalid deployment event token response');
961
+ if (!isPlainRecord(value) || !ownKeysEqual(value, ['token', 'ws_url']))
962
+ throw invalid();
963
+ if (typeof value.token !== 'string' || !value.token || typeof value.ws_url !== 'string' || !value.ws_url) {
964
+ throw invalid();
965
+ }
966
+ let parsed;
967
+ try {
968
+ parsed = new URL(value.ws_url);
969
+ }
970
+ catch {
971
+ throw invalid();
972
+ }
973
+ if (!['ws:', 'wss:'].includes(parsed.protocol) || !parsed.hostname || parsed.search || parsed.hash) {
974
+ throw invalid();
975
+ }
976
+ return { token: value.token, ws_url: value.ws_url };
977
+ }
978
+ function validateAgentLogsToken(value) {
979
+ const invalid = () => new Error('Backend returned an invalid Agent logs token response');
980
+ if (!isPlainRecord(value))
981
+ throw invalid();
982
+ if (!ownKeysEqual(value, ['agent_id', 'expires_at', 'token', 'ws_url']))
983
+ throw invalid();
984
+ if (typeof value.token !== 'string' || !value.token)
985
+ throw invalid();
986
+ if (typeof value.agent_id !== 'string' || !value.agent_id)
987
+ throw invalid();
988
+ if (typeof value.expires_at !== 'string' || !value.expires_at)
989
+ throw invalid();
990
+ if (typeof value.ws_url !== 'string' || !value.ws_url)
991
+ throw invalid();
992
+ if (typeof value.ws_url === 'string') {
993
+ let parsed;
994
+ try {
995
+ parsed = new URL(value.ws_url);
996
+ }
997
+ catch {
998
+ throw invalid();
999
+ }
1000
+ if (!['ws:', 'wss:'].includes(parsed.protocol) || !parsed.hostname || parsed.search || parsed.hash) {
1001
+ throw invalid();
1002
+ }
1003
+ }
1004
+ return value;
1005
+ }
1006
+ function validateAgentMetricsResult(value) {
1007
+ if (!isPlainRecord(value) || value.event !== 'agent_metrics_result') {
1008
+ throw new Error('Agent metrics WebSocket returned an invalid result frame');
1009
+ }
1010
+ if (value.ok === false && ownKeysEqual(value, ['error', 'event', 'ok'])
1011
+ && typeof value.error === 'string' && value.error) {
1012
+ throw new Error(value.error);
1013
+ }
1014
+ if (value.ok !== true
1015
+ || !ownKeysEqual(value, ['cpu', 'event', 'memory', 'ok', 'timestamp'])
1016
+ || typeof value.cpu !== 'string'
1017
+ || typeof value.memory !== 'string'
1018
+ || !Number.isInteger(value.timestamp)) {
1019
+ throw new Error('Agent metrics WebSocket returned an invalid result frame');
1020
+ }
1021
+ return value;
1022
+ }
1023
+ function validateAgentExecResult(value) {
1024
+ if (!isPlainRecord(value) || value.event !== 'agent_exec_result') {
1025
+ throw new Error('Agent exec WebSocket returned an invalid result frame');
1026
+ }
1027
+ if (value.ok === false && ownKeysEqual(value, ['error', 'event', 'ok'])
1028
+ && typeof value.error === 'string' && value.error) {
1029
+ throw new Error(value.error);
1030
+ }
1031
+ if (value.ok !== true
1032
+ || !ownKeysEqual(value, ['event', 'exit_code', 'ok', 'stderr', 'stdout'])
1033
+ || !Number.isInteger(value.exit_code)
1034
+ || typeof value.stdout !== 'string'
1035
+ || typeof value.stderr !== 'string') {
1036
+ throw new Error('Agent exec WebSocket returned an invalid result frame');
1037
+ }
1038
+ return execResultFromDict(value);
1039
+ }
1040
+ function agentStateFromDict(data) {
1041
+ const launchConfig = isPlainRecord(data.launch_config) ? structuredClone(data.launch_config) : null;
1042
+ if (launchConfig) {
1043
+ delete launchConfig.secrets;
1044
+ }
1045
+ return {
1046
+ id: data.id ?? '',
1047
+ userId: data.user_id ?? '',
1048
+ state: data.state ?? 'unknown',
1049
+ name: data.name ?? null,
1050
+ handle: data.handle ?? null,
1051
+ displayName: data.display_name ?? data.name ?? null,
1052
+ avatarUrl: data.avatar_url ?? null,
1053
+ avatarAudioUrl: data.avatar_audio_url ?? null,
1054
+ displayIdentity: data.display_identity ? structuredClone(data.display_identity) : null,
1055
+ runtime: data.runtime ?? null,
1056
+ managed: data.managed ?? null,
1057
+ isLaunchable: data.is_launchable ?? data.managed !== false,
1058
+ gatewayId: data.gateway_id ?? null,
1059
+ relayKey: data.relay_key ?? null,
1060
+ cpu: data.cpu ?? 0,
1061
+ memory: data.memory ?? 0,
1062
+ requestedSize: data.requested_size == null
1063
+ ? null
1064
+ : parseAgentSlotSize(data.requested_size, 'Agent requested_size'),
1065
+ hostname: data.hostname ?? null,
1066
+ tags: Array.isArray(data.tags) ? data.tags : [],
1067
+ jwtToken: data.jwt_token ?? null,
1068
+ jwtExpiresAt: parseDate(data.jwt_expires_at),
1069
+ startedAt: parseDate(data.started_at),
1070
+ stoppedAt: parseDate(data.stopped_at),
1071
+ archivedAt: parseDate(data.archived_at),
1072
+ // Independently nullable from archivedAt: SPEC has a new Agent with
1073
+ // neither, an ARCHIVED Agent with both, and a restored Agent with a
1074
+ // prefix but no archivedAt. Dropping it made that tri-state unreadable.
1075
+ archivePrefix: typeof data.archive_prefix === 'string' ? data.archive_prefix : null,
1076
+ deletedAt: parseDate(data.deleted_at),
1077
+ disconnectedAt: parseDate(data.disconnected_at),
1078
+ agentSlotId: typeof data.agent_slot_id === 'string' ? data.agent_slot_id : null,
1079
+ clusterId: data.cluster_id ?? null,
1080
+ runner: isPlainRecord(data.runner)
1081
+ ? {
1082
+ tags: Array.isArray(data.runner.tags) ? data.runner.tags.map(String) : [],
1083
+ runnerId: typeof data.runner.runner_id === 'string' ? data.runner.runner_id : null,
1084
+ }
1085
+ : null,
1086
+ launchEpoch: data.launch_epoch ?? 0,
1087
+ createdAt: parseDate(data.created_at),
1088
+ updatedAt: parseDate(data.updated_at),
1089
+ launchConfig,
1090
+ meta: agentMetaFromDict(data.meta),
1091
+ routes: data.routes ?? (isPlainRecord(launchConfig?.routes)
1092
+ ? launchConfig.routes
1093
+ : {}),
1094
+ command: data.command ?? (Array.isArray(launchConfig?.command) ? launchConfig.command : []),
1095
+ entrypoint: data.entrypoint ?? (Array.isArray(launchConfig?.entrypoint) ? launchConfig.entrypoint : []),
1096
+ dryRun: Boolean(data.dry_run),
1097
+ };
1098
+ }
1099
+ function normalizeExecutor(executor) {
1100
+ // undefined/null leaves the stored executor alone; the Backend treats
1101
+ // pre-existing runner rows without one as docker.
1102
+ if (executor === undefined || executor === null)
1103
+ return undefined;
1104
+ if (executor !== 'process' && executor !== 'docker') {
1105
+ throw new Error("executor must be 'process' or 'docker'");
1106
+ }
1107
+ return executor;
1108
+ }
1109
+ function normalizeDockerOptions(docker) {
1110
+ // undefined leaves stored runner docker options alone; null (or an empty
1111
+ // volumes list) clears them, because the Backend treats provided-but-empty
1112
+ // docker as absent on a replacement write.
1113
+ if (docker === undefined)
1114
+ return undefined;
1115
+ if (docker === null)
1116
+ return null;
1117
+ if (!isPlainRecord(docker))
1118
+ throw new Error('docker accepts only a volumes list');
1119
+ const extra = Object.keys(docker).filter((key) => key !== 'volumes');
1120
+ if (extra.length > 0)
1121
+ throw new Error(`Unsupported docker settings: ${extra.sort().join(', ')}`);
1122
+ const volumes = docker.volumes ?? [];
1123
+ if (!Array.isArray(volumes) || volumes.some((volume) => typeof volume !== 'string')) {
1124
+ throw new Error('docker volumes must be a list of strings');
1125
+ }
1126
+ if (volumes.length > MAX_DOCKER_VOLUMES) {
1127
+ throw new Error(`docker volumes accept at most ${MAX_DOCKER_VOLUMES} entries`);
1128
+ }
1129
+ for (const volume of volumes) {
1130
+ const segments = volume.split(':');
1131
+ if (segments.length < 2 || segments.length > 3) {
1132
+ throw new Error(`docker volume must be source:target[:ro]: ${volume}`);
1133
+ }
1134
+ const [source, target] = segments;
1135
+ if (!source || !target || volume.includes('\0')) {
1136
+ throw new Error(`docker volume paths must be non-empty and NUL-free: ${volume}`);
1137
+ }
1138
+ if (!source.startsWith('/') || !target.startsWith('/')) {
1139
+ throw new Error(`docker volume source and target must be absolute: ${volume}`);
1140
+ }
1141
+ if (segments.length === 3 && segments[2] !== 'ro') {
1142
+ throw new Error(`docker volume mode must be ro or omitted: ${volume}`);
1143
+ }
1144
+ }
1145
+ if (volumes.length === 0)
1146
+ return null;
1147
+ return { volumes: [...volumes] };
1148
+ }
1149
+ export function buildAgentConfig(config = {}, options = {}) {
1150
+ const preparedConfig = structuredClone(config);
1151
+ const nestedLaunchKeys = Object.keys(preparedConfig).filter((key) => LAUNCH_CONFIG_KEYS.has(key));
1152
+ if (nestedLaunchKeys.length) {
1153
+ throw new Error(`Launch settings must be top-level fields, not nested under config: ${nestedLaunchKeys.join(', ')}`);
1154
+ }
1155
+ const env = { ...(options.env ?? {}) };
1156
+ const secrets = { ...(options.secrets ?? {}) };
1157
+ const collidingKeys = Object.keys(env).filter((key) => Object.prototype.hasOwnProperty.call(secrets, key));
1158
+ if (collidingKeys.length > 0) {
1159
+ throw new Error(`Launch keys cannot appear in both env and secrets: ${collidingKeys.join(', ')}`);
1160
+ }
1161
+ const normalizeSyncOwner = (value, field) => {
1162
+ if (value === undefined || value === null)
1163
+ return undefined;
1164
+ if (!Number.isSafeInteger(value) || value < 0 || value > 4_294_967_294) {
1165
+ throw new Error(`${field} must be an integer between 0 and 4294967294`);
1166
+ }
1167
+ return value;
1168
+ };
1169
+ const syncUid = normalizeSyncOwner(options.syncUid, 'syncUid');
1170
+ const syncGid = normalizeSyncOwner(options.syncGid, 'syncGid');
1171
+ let registryAuth = {};
1172
+ if (options.registryAuth !== undefined && options.registryAuth !== null) {
1173
+ const keys = Object.keys(options.registryAuth).sort();
1174
+ if (keys.length !== 2 || keys[0] !== 'password' || keys[1] !== 'username') {
1175
+ throw new Error('registryAuth requires exactly username and password');
1176
+ }
1177
+ const username = options.registryAuth.username.trim();
1178
+ if (!username)
1179
+ throw new Error('registryAuth username must be non-empty');
1180
+ if (!options.registryAuth.password)
1181
+ throw new Error('registryAuth password must be non-empty');
1182
+ registryAuth = { username, password: options.registryAuth.password };
1183
+ }
1184
+ const prepared = {
1185
+ image: options.image ?? null,
1186
+ env,
1187
+ secrets,
1188
+ routes: routesConfigBody(options.routes),
1189
+ command: [...(options.command ?? [])],
1190
+ entrypoint: [...(options.entrypoint ?? [])],
1191
+ restart: options.restart ?? false,
1192
+ sync_root: options.syncRoot ?? null,
1193
+ sync_uid: syncUid ?? null,
1194
+ sync_gid: syncGid ?? null,
1195
+ registry_url: options.registryUrl ?? null,
1196
+ registry_auth: registryAuth,
1197
+ runtime_scopes: [...(options.runtimeScopes ?? DEFAULT_AGENT_RUNTIME_SCOPES)],
1198
+ };
1199
+ if (Object.keys(preparedConfig).length > 0)
1200
+ prepared.config = preparedConfig;
1201
+ if (options.cors !== undefined)
1202
+ prepared.cors = options.cors === null ? null : structuredClone(options.cors);
1203
+ if (options.syncInclude !== undefined) {
1204
+ if (options.syncInclude !== null && options.syncInclude.length === 0) {
1205
+ throw new Error('syncInclude must contain at least one path; omit it to sync all');
1206
+ }
1207
+ prepared.sync_include = options.syncInclude === null ? null : [...options.syncInclude];
1208
+ }
1209
+ if (options.syncInclude === undefined && options.syncExclude !== undefined) {
1210
+ if (options.syncExclude !== null && (options.syncExclude.includes('*') || options.syncExclude.includes('**'))) {
1211
+ throw new Error('syncExclude cannot exclude the entire sync root; omit it to sync all');
1212
+ }
1213
+ prepared.sync_exclude = options.syncExclude === null ? null : [...options.syncExclude];
1214
+ }
1215
+ const docker = normalizeDockerOptions(options.docker);
1216
+ const executor = normalizeExecutor(options.executor);
1217
+ if (executor === 'process' && docker != null) {
1218
+ throw new Error('docker launch options require the docker executor');
1219
+ }
1220
+ if (docker !== undefined)
1221
+ prepared.docker = docker;
1222
+ if (executor !== undefined)
1223
+ prepared.executor = executor;
1224
+ if (options.cwd !== undefined)
1225
+ prepared.cwd = options.cwd;
1226
+ return { config: prepared };
1227
+ }
1228
+ function buildAgentCreateConfig(config, options) {
1229
+ const complete = buildAgentConfig(config, options).config;
1230
+ const prepared = {};
1231
+ if (complete.config && Object.keys(complete.config).length > 0)
1232
+ prepared.config = complete.config;
1233
+ if (Object.keys(complete.env).length > 0)
1234
+ prepared.env = complete.env;
1235
+ if (Object.keys(complete.secrets).length > 0)
1236
+ prepared.secrets = complete.secrets;
1237
+ if (options.routes !== undefined && options.routes !== null)
1238
+ prepared.routes = complete.routes;
1239
+ if (options.command !== undefined && options.command !== null)
1240
+ prepared.command = complete.command;
1241
+ if (options.entrypoint !== undefined && options.entrypoint !== null)
1242
+ prepared.entrypoint = complete.entrypoint;
1243
+ if (options.image !== undefined && options.image !== null)
1244
+ prepared.image = complete.image;
1245
+ if (options.syncRoot !== undefined && options.syncRoot !== null)
1246
+ prepared.sync_root = complete.sync_root;
1247
+ if (Object.prototype.hasOwnProperty.call(complete, 'sync_include')) {
1248
+ prepared.sync_include = complete.sync_include;
1249
+ }
1250
+ else if (Object.prototype.hasOwnProperty.call(complete, 'sync_exclude')) {
1251
+ prepared.sync_exclude = complete.sync_exclude;
1252
+ }
1253
+ if (options.syncUid !== undefined && options.syncUid !== null)
1254
+ prepared.sync_uid = complete.sync_uid;
1255
+ if (options.syncGid !== undefined && options.syncGid !== null)
1256
+ prepared.sync_gid = complete.sync_gid;
1257
+ if (options.registryUrl !== undefined && options.registryUrl !== null)
1258
+ prepared.registry_url = complete.registry_url;
1259
+ if (options.registryAuth !== undefined && options.registryAuth !== null)
1260
+ prepared.registry_auth = complete.registry_auth;
1261
+ if (Object.prototype.hasOwnProperty.call(complete, 'cors') && complete.cors != null) {
1262
+ prepared.cors = complete.cors;
1263
+ }
1264
+ prepared.restart = complete.restart;
1265
+ if (options.runtimeScopes !== undefined && options.runtimeScopes !== null)
1266
+ prepared.runtime_scopes = complete.runtime_scopes;
1267
+ if (complete.docker)
1268
+ prepared.docker = complete.docker;
1269
+ if (complete.executor !== undefined)
1270
+ prepared.executor = complete.executor;
1271
+ if (complete.cwd !== undefined)
1272
+ prepared.cwd = complete.cwd;
1273
+ return prepared;
1274
+ }
1275
+ export async function startSlackOAuth(options) {
1276
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1277
+ if (!relayBaseUrl)
1278
+ throw new Error('Slack relay base URL is required');
1279
+ if (!options.token)
1280
+ throw new Error('Slack OAuth requires an app token');
1281
+ const response = await fetch(`${relayBaseUrl}/slack/oauth/start`, {
1282
+ method: 'GET',
1283
+ headers: {
1284
+ 'Authorization': `Bearer ${options.token}`,
1285
+ },
1286
+ });
1287
+ if (!response.ok) {
1288
+ let detail = response.statusText || 'Slack OAuth start failed';
1289
+ try {
1290
+ const payload = await response.json();
1291
+ if (typeof payload.detail === 'string' && payload.detail)
1292
+ detail = payload.detail;
1293
+ }
1294
+ catch {
1295
+ // Keep the HTTP status text when the error body is not JSON.
1296
+ }
1297
+ throw new APIError(response.status, detail);
1298
+ }
1299
+ const payload = await response.json();
1300
+ const authorizeUrl = typeof payload.authorize_url === 'string' ? payload.authorize_url : '';
1301
+ if (!authorizeUrl)
1302
+ throw new Error('Slack OAuth start did not return an authorization URL');
1303
+ return {
1304
+ authorizeUrl,
1305
+ expiresAt: typeof payload.expires_at === 'string' ? payload.expires_at : null,
1306
+ };
1307
+ }
1308
+ export async function getSlackInstallStatus(options) {
1309
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1310
+ if (!relayBaseUrl)
1311
+ throw new Error('Slack relay base URL is required');
1312
+ if (!options.token)
1313
+ throw new Error('Slack install status requires an app token');
1314
+ const response = await fetch(`${relayBaseUrl}/slack/install`, {
1315
+ method: 'GET',
1316
+ headers: { 'Authorization': `Bearer ${options.token}` },
1317
+ });
1318
+ if (!response.ok) {
1319
+ let detail = response.statusText || 'Slack install status failed';
1320
+ try {
1321
+ const payload = await response.json();
1322
+ if (typeof payload.detail === 'string' && payload.detail)
1323
+ detail = payload.detail;
1324
+ }
1325
+ catch {
1326
+ // Keep the HTTP status text when the error body is not JSON.
1327
+ }
1328
+ throw new APIError(response.status, detail);
1329
+ }
1330
+ const payload = await response.json();
1331
+ return {
1332
+ connected: payload.connected === true,
1333
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
1334
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
1335
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
1336
+ installerUserId: typeof payload.installer_user_id === 'string' ? payload.installer_user_id : null,
1337
+ updatedAt: typeof payload.updated_at === 'string' ? payload.updated_at : null,
1338
+ };
1339
+ }
1340
+ function slackRelayAuthorizedUrl(path, options) {
1341
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1342
+ if (!relayBaseUrl)
1343
+ throw new Error('Slack relay base URL is required');
1344
+ if (!options.token)
1345
+ throw new Error('Slack directory lookup requires an app token');
1346
+ const url = new URL(path, `${relayBaseUrl}/`);
1347
+ if (options.cursor)
1348
+ url.searchParams.set('cursor', options.cursor);
1349
+ if (typeof options.limit === 'number' && Number.isFinite(options.limit)) {
1350
+ url.searchParams.set('limit', String(Math.trunc(options.limit)));
1351
+ }
1352
+ return url;
1353
+ }
1354
+ export async function listSlackDirectoryConversations(options) {
1355
+ const url = slackRelayAuthorizedUrl('/slack/directory/conversations', options);
1356
+ if (options.types)
1357
+ url.searchParams.set('types', options.types);
1358
+ const response = await fetch(url.toString(), {
1359
+ method: 'GET',
1360
+ headers: { 'Authorization': `Bearer ${options.token}` },
1361
+ });
1362
+ if (!response.ok) {
1363
+ let detail = response.statusText || 'Slack conversation lookup failed';
1364
+ try {
1365
+ const payload = await response.json();
1366
+ if (typeof payload.detail === 'string' && payload.detail)
1367
+ detail = payload.detail;
1368
+ }
1369
+ catch {
1370
+ // Keep the HTTP status text when the error body is not JSON.
1371
+ }
1372
+ throw new APIError(response.status, detail);
1373
+ }
1374
+ const payload = await response.json();
1375
+ const conversations = Array.isArray(payload.conversations) ? payload.conversations : [];
1376
+ return {
1377
+ conversations: conversations
1378
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
1379
+ .map((item) => ({
1380
+ id: item.id,
1381
+ name: typeof item.name === 'string' ? item.name : null,
1382
+ isChannel: typeof item.is_channel === 'boolean' ? item.is_channel : null,
1383
+ isGroup: typeof item.is_group === 'boolean' ? item.is_group : null,
1384
+ isIm: typeof item.is_im === 'boolean' ? item.is_im : null,
1385
+ isMpim: typeof item.is_mpim === 'boolean' ? item.is_mpim : null,
1386
+ isMember: typeof item.is_member === 'boolean' ? item.is_member : null,
1387
+ isPrivate: typeof item.is_private === 'boolean' ? item.is_private : null,
1388
+ })),
1389
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
1390
+ };
1391
+ }
1392
+ export async function listSlackDirectoryUsers(options) {
1393
+ const url = slackRelayAuthorizedUrl('/slack/directory/users', options);
1394
+ const response = await fetch(url.toString(), {
1395
+ method: 'GET',
1396
+ headers: { 'Authorization': `Bearer ${options.token}` },
1397
+ });
1398
+ if (!response.ok) {
1399
+ let detail = response.statusText || 'Slack user lookup failed';
1400
+ try {
1401
+ const payload = await response.json();
1402
+ if (typeof payload.detail === 'string' && payload.detail)
1403
+ detail = payload.detail;
1404
+ }
1405
+ catch {
1406
+ // Keep the HTTP status text when the error body is not JSON.
1407
+ }
1408
+ throw new APIError(response.status, detail);
1409
+ }
1410
+ const payload = await response.json();
1411
+ const users = Array.isArray(payload.users) ? payload.users : [];
1412
+ return {
1413
+ users: users
1414
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
1415
+ .map((item) => ({
1416
+ id: item.id,
1417
+ name: typeof item.name === 'string' ? item.name : null,
1418
+ realName: typeof item.real_name === 'string' ? item.real_name : null,
1419
+ teamId: typeof item.team_id === 'string' ? item.team_id : null,
1420
+ isBot: typeof item.is_bot === 'boolean' ? item.is_bot : null,
1421
+ deleted: typeof item.deleted === 'boolean' ? item.deleted : null,
1422
+ })),
1423
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
1424
+ };
1425
+ }
1426
+ export async function attachSlackRelayAgent(options) {
1427
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1428
+ const agentId = options.agentId.trim();
1429
+ if (!relayBaseUrl)
1430
+ throw new Error('Slack relay base URL is required');
1431
+ if (!options.token)
1432
+ throw new Error('Slack relay attach requires an app token');
1433
+ if (!agentId)
1434
+ throw new Error('Slack relay attach requires an agent id');
1435
+ const response = await fetch(`${relayBaseUrl}/slack/agents/${encodeURIComponent(agentId)}/relay`, {
1436
+ method: 'POST',
1437
+ headers: { 'Authorization': `Bearer ${options.token}` },
1438
+ });
1439
+ if (!response.ok) {
1440
+ let detail = response.statusText || 'Slack relay attach failed';
1441
+ try {
1442
+ const payload = await response.json();
1443
+ if (typeof payload.detail === 'string' && payload.detail)
1444
+ detail = payload.detail;
1445
+ }
1446
+ catch {
1447
+ // Keep the HTTP status text when the error body is not JSON.
1448
+ }
1449
+ throw new APIError(response.status, detail);
1450
+ }
1451
+ const payload = await response.json();
1452
+ const agentIdValue = typeof payload.agent_id === 'string' ? payload.agent_id : '';
1453
+ const gatewayIdValue = typeof payload.gateway_id === 'string' ? payload.gateway_id : '';
1454
+ if (!agentIdValue || !gatewayIdValue)
1455
+ throw new Error('Slack relay attach response is missing agent identity');
1456
+ return {
1457
+ connected: payload.connected === true,
1458
+ agentId: agentIdValue,
1459
+ gatewayId: gatewayIdValue,
1460
+ restartRequired: payload.restart_required !== false,
1461
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
1462
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
1463
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
1464
+ };
1465
+ }
1466
+ export function buildOpenClawDesktopRoute(options = {}) {
1467
+ return {
1468
+ desktop: {
1469
+ port: options.desktopPort ?? 3000,
1470
+ auth: options.desktopAuth ?? true,
1471
+ prefix: options.desktopPrefix ?? 'desktop',
1472
+ },
1473
+ };
1474
+ }
1475
+ function envBool(value) {
1476
+ return value ? '1' : '0';
1477
+ }
1478
+ function envNonNegativeInteger(name, value) {
1479
+ const integer = Number(value);
1480
+ if (!Number.isInteger(integer) || integer < 0) {
1481
+ throw new Error(`${name} must be a non-negative integer`);
1482
+ }
1483
+ return String(integer);
1484
+ }
1485
+ export function buildOpenClawMemoryIndexEnv(memoryIndex = null) {
1486
+ if (!memoryIndex)
1487
+ return {};
1488
+ const env = { ...OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS };
1489
+ if (memoryIndex.enabled !== undefined && memoryIndex.enabled !== null) {
1490
+ env.OPENCLAW_MEMORY_SEARCH_ENABLED = envBool(memoryIndex.enabled);
1491
+ }
1492
+ if (memoryIndex.onSessionStart !== undefined && memoryIndex.onSessionStart !== null) {
1493
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START = envBool(memoryIndex.onSessionStart);
1494
+ }
1495
+ if (memoryIndex.onSearch !== undefined && memoryIndex.onSearch !== null) {
1496
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH = envBool(memoryIndex.onSearch);
1497
+ }
1498
+ if (memoryIndex.watch !== undefined && memoryIndex.watch !== null) {
1499
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_WATCH = envBool(memoryIndex.watch);
1500
+ }
1501
+ if (memoryIndex.watchDebounceMs !== undefined && memoryIndex.watchDebounceMs !== null) {
1502
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS = envNonNegativeInteger('watchDebounceMs', memoryIndex.watchDebounceMs);
1503
+ }
1504
+ if (memoryIndex.intervalMinutes !== undefined && memoryIndex.intervalMinutes !== null) {
1505
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES = envNonNegativeInteger('intervalMinutes', memoryIndex.intervalMinutes);
1506
+ }
1507
+ return env;
1508
+ }
1509
+ export function buildOpenClawCronEnv(enabled = null) {
1510
+ return {
1511
+ ...OPENCLAW_CRON_ENV_DEFAULTS,
1512
+ ...(enabled !== null ? { OPENCLAW_CRON_ENABLED: envBool(enabled) } : {}),
1513
+ };
1514
+ }
1515
+ export function buildHermesCronEnv(enabled = null) {
1516
+ return {
1517
+ ...HERMES_CRON_ENV_DEFAULTS,
1518
+ ...(enabled !== null ? { HERMES_CRON_ENABLED: envBool(enabled) } : {}),
1519
+ };
1520
+ }
1521
+ export function buildWorkspacesSyncEnv(workspacesSync = null) {
1522
+ if (workspacesSync === false)
1523
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1524
+ const options = typeof workspacesSync === 'object' && workspacesSync !== null ? workspacesSync : {};
1525
+ if (options.enabled === false)
1526
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1527
+ const env = { ...WORKSPACES_SYNC_ENV_DEFAULTS };
1528
+ if (options.enabled !== undefined && options.enabled !== null) {
1529
+ env.HYPER_WORKSPACES_BOOT_SYNC = envBool(options.enabled);
1530
+ }
1531
+ if (options.readyOnly !== undefined && options.readyOnly !== null) {
1532
+ env.HYPER_WORKSPACES_SYNC_READY_ONLY = envBool(options.readyOnly);
1533
+ }
1534
+ if (options.workspace) {
1535
+ env.HYPER_WORKSPACES_SYNC_WORKSPACE = options.workspace;
1536
+ }
1537
+ return env;
1538
+ }
1539
+ async function getFsPromises() {
1540
+ return import('node:fs/promises');
1541
+ }
1542
+ function bindAgent(agent, deployments) {
1543
+ agent._deployments = deployments;
1544
+ return agent;
1545
+ }
1546
+ export class Agent {
1547
+ id;
1548
+ userId;
1549
+ state;
1550
+ name;
1551
+ handle;
1552
+ displayName;
1553
+ avatarUrl;
1554
+ avatarAudioUrl;
1555
+ displayIdentity;
1556
+ runtime;
1557
+ managed;
1558
+ isLaunchable;
1559
+ gatewayId;
1560
+ relayKey;
1561
+ cpu;
1562
+ memory;
1563
+ requestedSize;
1564
+ hostname;
1565
+ tags;
1566
+ jwtToken;
1567
+ jwtExpiresAt;
1568
+ startedAt;
1569
+ stoppedAt;
1570
+ archivedAt;
1571
+ archivePrefix;
1572
+ deletedAt;
1573
+ disconnectedAt;
1574
+ agentSlotId;
1575
+ clusterId;
1576
+ runner;
1577
+ launchEpoch;
1578
+ createdAt;
1579
+ updatedAt;
1580
+ launchConfig;
1581
+ meta;
1582
+ routes;
1583
+ command;
1584
+ entrypoint;
1585
+ dryRun;
1586
+ _deployments = null;
1587
+ constructor(fields) {
1588
+ this.id = fields.id;
1589
+ this.userId = fields.userId;
1590
+ this.state = fields.state;
1591
+ this.name = fields.name ?? null;
1592
+ this.handle = fields.handle ?? null;
1593
+ this.displayName = fields.displayName ?? this.name;
1594
+ this.avatarUrl = fields.avatarUrl ?? null;
1595
+ this.avatarAudioUrl = fields.avatarAudioUrl ?? null;
1596
+ this.displayIdentity = fields.displayIdentity ? structuredClone(fields.displayIdentity) : null;
1597
+ this.runtime = fields.runtime ?? null;
1598
+ this.managed = fields.managed ?? null;
1599
+ this.isLaunchable = fields.isLaunchable ?? true;
1600
+ this.gatewayId = fields.gatewayId ?? null;
1601
+ this.relayKey = fields.relayKey ? structuredClone(fields.relayKey) : null;
1602
+ this.cpu = fields.cpu;
1603
+ this.memory = fields.memory;
1604
+ this.requestedSize = fields.requestedSize ?? null;
1605
+ this.hostname = fields.hostname ?? null;
1606
+ this.tags = [...(fields.tags ?? [])];
1607
+ this.jwtToken = fields.jwtToken ?? null;
1608
+ this.jwtExpiresAt = fields.jwtExpiresAt ?? null;
1609
+ this.startedAt = fields.startedAt ?? null;
1610
+ this.stoppedAt = fields.stoppedAt ?? null;
1611
+ this.archivedAt = fields.archivedAt ?? null;
1612
+ this.archivePrefix = fields.archivePrefix ?? null;
1613
+ this.deletedAt = fields.deletedAt ?? null;
1614
+ this.disconnectedAt = fields.disconnectedAt ?? null;
1615
+ this.agentSlotId = fields.agentSlotId ?? null;
1616
+ this.clusterId = fields.clusterId ?? null;
1617
+ this.runner = fields.runner ?? null;
1618
+ this.launchEpoch = fields.launchEpoch ?? 0;
1619
+ this.createdAt = fields.createdAt ?? null;
1620
+ this.updatedAt = fields.updatedAt ?? null;
1621
+ this.launchConfig = fields.launchConfig ?? null;
1622
+ this.meta = fields.meta ? structuredClone(fields.meta) : null;
1623
+ this.routes = { ...fields.routes };
1624
+ this.command = [...fields.command];
1625
+ this.entrypoint = [...fields.entrypoint];
1626
+ this.dryRun = fields.dryRun;
1627
+ }
1628
+ static fromDict(data) {
1629
+ return new Agent(agentStateFromDict(data));
1630
+ }
1631
+ get publicUrl() {
1632
+ return this.hostname ? `https://${this.hostname}` : null;
1633
+ }
1634
+ routePrefix(routeName, defaultPrefix = null) {
1635
+ const route = this.routes[routeName] ?? {};
1636
+ const prefix = route.prefix;
1637
+ if (typeof prefix === 'undefined' || prefix === null) {
1638
+ return defaultPrefix;
1639
+ }
1640
+ return String(prefix);
1641
+ }
1642
+ routeUrl(routeName, defaultPrefix = null) {
1643
+ if (!this.hostname)
1644
+ return null;
1645
+ const prefix = this.routePrefix(routeName, defaultPrefix);
1646
+ if (prefix === null)
1647
+ return null;
1648
+ return prefix === '' ? `https://${this.hostname}` : `https://${prefix}-${this.hostname}`;
1649
+ }
1650
+ get desktopUrl() {
1651
+ return this.routeUrl('desktop', 'desktop');
1652
+ }
1653
+ get vncUrl() {
1654
+ return this.desktopUrl;
1655
+ }
1656
+ browserDesktopUrl(token, options = {}) {
1657
+ if (!this.desktopUrl)
1658
+ return null;
1659
+ return buildBrowserDesktopUrl(this.desktopUrl, token, options);
1660
+ }
1661
+ get shellUrl() {
1662
+ return this.routeUrl('shell');
1663
+ }
1664
+ get isRunning() {
1665
+ return this.state.toLowerCase() === 'running';
1666
+ }
1667
+ get isTransitioning() {
1668
+ return isAgentTransitionalState(this.state);
1669
+ }
1670
+ /** True when the agent is cold-restorable from its verified archive. */
1671
+ get isArchived() {
1672
+ return this.state.toUpperCase() === 'ARCHIVED';
1673
+ }
1674
+ /** True only for an explicitly included deletion tombstone. */
1675
+ get isDeleted() {
1676
+ return this.state.toUpperCase() === 'DELETED';
1677
+ }
1678
+ get hasDesktop() {
1679
+ return agentConfigHasDesktop({
1680
+ launchConfig: this.launchConfig,
1681
+ routes: this.routes,
1682
+ });
1683
+ }
1684
+ requireDeployments() {
1685
+ if (!this._deployments) {
1686
+ throw new Error('Agent is not bound to a Deployments client');
1687
+ }
1688
+ return this._deployments;
1689
+ }
1690
+ /** Runtime auth flows for this agent's pod (coding runtimes only). */
1691
+ get auth() {
1692
+ return new RuntimeAuthClient(this);
1693
+ }
1694
+ /**
1695
+ * Connect to this agent's ACP surface through the backend session proxy.
1696
+ *
1697
+ * Default (`options.transport` unset): dials `/ws/acp` — the client-facing
1698
+ * ACP session authority (sessions/README §14). The proxy owns the backend
1699
+ * session record `{session_id → legs}`, fans runtime frames out to every
1700
+ * attached session client, and answers `session/new` with the backend
1701
+ * session id (backend-keyed, not the pod-side ACP id). Create-or-attach
1702
+ * semantics key on `options.sessionId`:
1703
+ *
1704
+ * - omitted: the socket starts session-less; `newSession()` runs the
1705
+ * proxy's `session/new`, minting the backend session.
1706
+ * - provided: the dial itself is session-less; AFTER the `initialize`
1707
+ * handshake the client resumes the session over the connected socket
1708
+ * (`session/resume` on v1), without replay. History uses the separate
1709
+ * platform sessions API. A refused resume rejects acpConnect with the
1710
+ * original error and closes its client; it never creates a replacement
1711
+ * session.
1712
+ *
1713
+ * `transport: 'direct'` dials the agent-keyed `/ws` bridge instead
1714
+ * (`?agent_id&token`), the pre-proxy path. Infra/debug only: `/ws` is
1715
+ * being hardened to runtime + backend-service identities, and combining
1716
+ * it with `sessionId` throws (the bridge has no session binding).
1717
+ *
1718
+ * The ACP `initialize` handshake offers protocol version 1 only
1719
+ * (see `client.negotiatedProtocolVersion`). The proxy authority is
1720
+ * v1-only and the client closes on any answered version other than 1.
1721
+ * The `cwd` default comes from the runtime's validated launch-directory
1722
+ * advertisement, never from the pod sync root or the caller's machine.
1723
+ *
1724
+ * Auth: the dial carries the client API key on the `token` query param;
1725
+ * `options.token` overrides that credential.
1726
+ */
1727
+ async acpConnect(options = {}) {
1728
+ const deployments = this.requireDeployments();
1729
+ const transport = options.transport ?? 'proxy';
1730
+ if (transport === 'direct' && options.sessionId) {
1731
+ throw new Error("acpConnect: sessionId is a proxy-transport option; the direct /ws bridge " +
1732
+ "has no session binding (drop sessionId or use the default 'proxy' transport)");
1733
+ }
1734
+ const url = new URL(transport === 'direct'
1735
+ ? defaultHyperAcpWsUrl(deployments.agentApiBase)
1736
+ : defaultAcpProxyWsUrl(deployments.agentApiBase));
1737
+ url.searchParams.set('agent_id', this.id);
1738
+ url.searchParams.set('token', options.token ?? deployments.agentApiKey);
1739
+ const client = await CodingAgentAcpClient.connect({ url: url.toString(), token: '' }, { ...options,
1740
+ resolveDefaultCwd: options.resolveDefaultCwd ?? (async () => (await deployments.runtimePaths(this.id, { signal: options.signal })).cwd),
1741
+ });
1742
+ if (options.sessionId) {
1743
+ try {
1744
+ await client.resumeSession(options.sessionId);
1745
+ }
1746
+ catch (error) {
1747
+ client.close();
1748
+ throw error;
1749
+ }
1750
+ }
1751
+ return client;
1752
+ }
1753
+ acpPoolValue = null;
1754
+ /**
1755
+ * Lazily created connection pool for this agent's ACP surface. Two
1756
+ * clients dialed to the same agent share one pod-side stdio session, so all
1757
+ * ACP consumers (chat panes, session sweeps, turn drivers) must ride ONE
1758
+ * pooled connection via leases instead of dialing their own — the bridge
1759
+ * rejects a duplicate runtime attach. The pool closes the connection when
1760
+ * the last lease releases, so a lease taken by a long-lived consumer (a
1761
+ * turn driver) outlives callers that unmount (a chat pane) and vice versa.
1762
+ *
1763
+ * Under the default proxy transport the pooled connection dials session-less:
1764
+ * prompting a session id the proxy has never seen (a stored id whose
1765
+ * runtime was evicted backend-side) fails with an unknown-session error.
1766
+ * Prompt against ids created through this proxy, or attach first with a
1767
+ * dedicated `acpConnect({ sessionId })`.
1768
+ */
1769
+ get acpPool() {
1770
+ if (this.acpPoolValue === null) {
1771
+ this.acpPoolValue = new CodingAgentAcpPool({ connect: (_key, options) => this.acpConnect(options) });
1772
+ }
1773
+ return this.acpPoolValue;
1774
+ }
1775
+ /**
1776
+ * Acquire a lease on this agent's pooled ACP connection and return a ready
1777
+ * per-session {@link AcpTurnDriver} bound to it. `options.sessionId` is the
1778
+ * pinned ACP session id (the id the app persists, e.g. under
1779
+ * `localStorage["acp-session:<agentId>"]`); drivers are cheap — one per
1780
+ * session, all sharing the same lease-held connection. The driver holds
1781
+ * its lease until
1782
+ * `driver.close()`; the pooled connection stays up for other leaseholders.
1783
+ */
1784
+ async acpTurnDriver(options) {
1785
+ const lease = await this.acpPool.acquire(this.id);
1786
+ try {
1787
+ return new AcpTurnDriver(lease, options);
1788
+ }
1789
+ catch (error) {
1790
+ lease.release();
1791
+ throw error;
1792
+ }
1793
+ }
1794
+ routeRequiresAuth(routeName, defaultValue = true) {
1795
+ const route = this.routes[routeName];
1796
+ if (!route || typeof route.auth === 'undefined') {
1797
+ return defaultValue;
1798
+ }
1799
+ return Boolean(route.auth);
1800
+ }
1801
+ async refreshToken() {
1802
+ const data = await this.requireDeployments().refreshToken(this.id);
1803
+ this.jwtToken = data.token ?? null;
1804
+ this.jwtExpiresAt = parseDate(data.expires_at);
1805
+ return data;
1806
+ }
1807
+ async waitRunning(timeoutMs = 300_000, pollIntervalMs = 5_000) {
1808
+ return this.requireDeployments().waitRunning(this.id, timeoutMs, pollIntervalMs, this.launchEpoch > 0 ? this.launchEpoch : undefined);
1809
+ }
1810
+ async update(options) {
1811
+ return this.requireDeployments().update(this.id, options);
1812
+ }
1813
+ async resetRuntimeDefaults(options) {
1814
+ return this.requireDeployments().resetRuntimeDefaults(this.id, options);
1815
+ }
1816
+ async resize(options) {
1817
+ return this.requireDeployments().resize(this.id, options);
1818
+ }
1819
+ /** Accept background archival and return its transitional Agent projection. */
1820
+ async archive(options) {
1821
+ return this.requireDeployments().archive(this.id, options);
1822
+ }
1823
+ async env() {
1824
+ const response = await this.requireDeployments().env(this.id);
1825
+ if (response.launch_epoch < this.launchEpoch) {
1826
+ throw new Error('agent env belongs to an older launch epoch');
1827
+ }
1828
+ return response.env;
1829
+ }
1830
+ async setEnv(key, value) {
1831
+ return this.requireDeployments().setEnv(this.id, key, value);
1832
+ }
1833
+ async deleteEnv(key) {
1834
+ return this.requireDeployments().deleteEnv(this.id, key);
1835
+ }
1836
+ async secretNames() {
1837
+ const response = await this.requireDeployments().secretNames(this.id);
1838
+ if (response.launch_epoch < this.launchEpoch) {
1839
+ throw new Error('agent Secret names belong to an older launch epoch');
1840
+ }
1841
+ return response.names;
1842
+ }
1843
+ async secret(key) {
1844
+ const response = await this.requireDeployments().secret(this.id, key);
1845
+ if (response.launch_epoch < this.launchEpoch) {
1846
+ throw new Error('agent Secret belongs to an older launch epoch');
1847
+ }
1848
+ return response.value;
1849
+ }
1850
+ async setSecret(key, value) {
1851
+ return this.requireDeployments().setSecret(this.id, key, value);
1852
+ }
1853
+ async deleteSecret(key) {
1854
+ return this.requireDeployments().deleteSecret(this.id, key);
1855
+ }
1856
+ async exec(command, options = {}) {
1857
+ return this.requireDeployments().exec(this, command, options);
1858
+ }
1859
+ /** Reef-backed files scoped to this agent's configured sync root. */
1860
+ get files() {
1861
+ return new AgentFiles(this, this.requireDeployments());
1862
+ }
1863
+ async filesList(path = '') {
1864
+ return this.files.list(path);
1865
+ }
1866
+ async fileReadBytes(path, options) {
1867
+ return this.files.readBytes(path, options);
1868
+ }
1869
+ async fileReadBytesWithMetadata(path, options) {
1870
+ return this.files.readBytesWithMetadata(path, options);
1871
+ }
1872
+ async fileRead(path, options) {
1873
+ return this.files.read(path, options);
1874
+ }
1875
+ async fileWriteBytes(path, content) {
1876
+ return this.files.writeBytes(path, content);
1877
+ }
1878
+ async fileWrite(path, content) {
1879
+ return this.files.write(path, content);
1880
+ }
1881
+ async fileDelete(path, options = {}) {
1882
+ return this.files.delete(path, options);
1883
+ }
1884
+ async cpTo(localPath, remotePath) {
1885
+ return this.requireDeployments().cpTo(this, localPath, remotePath);
1886
+ }
1887
+ async cpFrom(remotePath, localPath) {
1888
+ return this.requireDeployments().cpFrom(this, remotePath, localPath);
1889
+ }
1890
+ async shellConnect(shell, options) {
1891
+ return this.requireDeployments().shellConnect(this.id, shell, options);
1892
+ }
1893
+ }
1894
+ // Runtime auth surfaces are launch-shape data per runtime label, not class
1895
+ // behavior. openclaw/hermes-agent pods have no in-pod login flow and carry
1896
+ // no table entry.
1897
+ const RUNTIME_AUTH_CONFIG = {
1898
+ 'buzz-agent': {
1899
+ agentCommand: ['buzz-agent'],
1900
+ statusCommand: null,
1901
+ logoutCommand: null,
1902
+ nativeMethods: [],
1903
+ },
1904
+ opencode: {
1905
+ agentCommand: ['opencode', 'acp'],
1906
+ statusCommand: ['opencode', 'auth', 'list'],
1907
+ logoutCommand: ['opencode', 'auth', 'logout'],
1908
+ nativeMethods: [],
1909
+ },
1910
+ codex: {
1911
+ agentCommand: ['codex-acp'],
1912
+ statusCommand: ['codex', 'login', 'status'],
1913
+ logoutCommand: ['codex', 'logout'],
1914
+ nativeMethods: [{
1915
+ id: 'device',
1916
+ name: 'Device authentication',
1917
+ description: 'Authenticate Codex with a device code.',
1918
+ kind: 'native',
1919
+ command: ['codex', 'login', '--device-auth'],
1920
+ metadata: {},
1921
+ }],
1922
+ },
1923
+ 'claude-code': {
1924
+ agentCommand: ['claude-agent-acp'],
1925
+ statusCommand: ['claude', 'auth', 'status', '--json'],
1926
+ logoutCommand: ['claude', 'auth', 'logout'],
1927
+ nativeMethods: [
1928
+ { id: 'claude-ai', name: 'Claude.ai', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--claudeai'], metadata: {} },
1929
+ { id: 'console', name: 'Anthropic Console', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--console'], metadata: {} },
1930
+ { id: 'sso', name: 'Enterprise SSO', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--sso'], metadata: {} },
1931
+ ],
1932
+ },
1933
+ goose: {
1934
+ agentCommand: ['goose', 'acp'],
1935
+ statusCommand: null,
1936
+ logoutCommand: null,
1937
+ nativeMethods: [],
1938
+ },
1939
+ 'kimi-code': {
1940
+ agentCommand: ['kimi', 'acp'],
1941
+ statusCommand: null,
1942
+ logoutCommand: null,
1943
+ nativeMethods: [],
1944
+ },
1945
+ pi: {
1946
+ agentCommand: ['pi-acp'],
1947
+ statusCommand: null,
1948
+ logoutCommand: null,
1949
+ nativeMethods: [],
1950
+ },
1951
+ };
1952
+ function shellQuote(value) {
1953
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1954
+ }
1955
+ function commandString(command) {
1956
+ return command.map(shellQuote).join(' ');
1957
+ }
1958
+ // Terminal control-sequence matching intentionally includes ESC and BEL.
1959
+ /* eslint-disable no-control-regex */
1960
+ const TERMINAL_ESCAPE_PATTERN = new RegExp('\\x1B(?:\\[[0-?]*[ -/]*[@-~]|\\][^\\x07]*(?:\\x07|\\x1B\\\\))', 'g');
1961
+ /* eslint-enable no-control-regex */
1962
+ function stripTerminalCodes(value) {
1963
+ return value.replace(TERMINAL_ESCAPE_PATTERN, '').replace(/\r/g, '');
1964
+ }
1965
+ async function websocketMessageText(data) {
1966
+ if (typeof data === 'string')
1967
+ return data;
1968
+ if (data instanceof ArrayBuffer)
1969
+ return decodeUtf8(new Uint8Array(data));
1970
+ if (ArrayBuffer.isView(data))
1971
+ return decodeUtf8(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
1972
+ if (typeof Blob !== 'undefined' && data instanceof Blob)
1973
+ return data.text();
1974
+ return String(data ?? '');
1975
+ }
1976
+ export class RuntimeLoginSession {
1977
+ authClient;
1978
+ socket;
1979
+ requiresDeviceChallenge;
1980
+ output = '';
1981
+ verificationUrl = null;
1982
+ userCode = null;
1983
+ interactiveRequired = false;
1984
+ rawOutput = '';
1985
+ exitCode = null;
1986
+ marker;
1987
+ completion;
1988
+ ready;
1989
+ complete;
1990
+ markReady;
1991
+ constructor(authClient, socket, command, requiresDeviceChallenge) {
1992
+ this.authClient = authClient;
1993
+ this.socket = socket;
1994
+ this.requiresDeviceChallenge = requiresDeviceChallenge;
1995
+ this.marker = `__HYPERCLI_AUTH_EXIT_${randomHexToken(12)}__=`;
1996
+ this.completion = new Promise((resolve) => { this.complete = resolve; });
1997
+ this.ready = new Promise((resolve) => { this.markReady = resolve; });
1998
+ socket.onmessage = (event) => {
1999
+ void websocketMessageText(event.data).then((chunk) => this.consume(chunk));
2000
+ };
2001
+ socket.onclose = () => {
2002
+ this.markReady();
2003
+ this.complete();
2004
+ };
2005
+ socket.send(`${commandString(command)}; _hypercli_auth_rc=$?; printf '\\n${this.marker}%s\\n' "$_hypercli_auth_rc"\n`);
2006
+ }
2007
+ static async start(authClient, command, challengeTimeoutMs = 45_000, requiresDeviceChallenge = false) {
2008
+ const socket = await authClient.agent.shellConnect();
2009
+ const session = new RuntimeLoginSession(authClient, socket, command, requiresDeviceChallenge);
2010
+ let timer;
2011
+ try {
2012
+ await Promise.race([
2013
+ session.ready,
2014
+ new Promise((_, reject) => {
2015
+ timer = setTimeout(() => reject(new Error('Timed out waiting for runtime login instructions')), challengeTimeoutMs);
2016
+ }),
2017
+ ]);
2018
+ return session;
2019
+ }
2020
+ catch (error) {
2021
+ session.cancel();
2022
+ throw error;
2023
+ }
2024
+ finally {
2025
+ if (timer)
2026
+ clearTimeout(timer);
2027
+ }
2028
+ }
2029
+ consume(chunk) {
2030
+ this.rawOutput += chunk;
2031
+ this.output = stripTerminalCodes(this.rawOutput);
2032
+ const markerIndex = this.output.lastIndexOf(this.marker);
2033
+ if (markerIndex >= 0) {
2034
+ const match = this.output.slice(markerIndex + this.marker.length).match(/^(-?\d+)/);
2035
+ if (match) {
2036
+ this.exitCode = Number(match[1]);
2037
+ this.markReady();
2038
+ this.complete();
2039
+ }
2040
+ }
2041
+ this.verificationUrl ??= this.output.match(/https?:\/\/[^\s"'<>]+(?=[\s"'<>])/)?.[0] ?? null;
2042
+ 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;
2043
+ this.interactiveRequired ||= /\b(select|choose)\b.*\b(provider|login method)\b/i.test(this.output);
2044
+ const challengeReady = this.requiresDeviceChallenge
2045
+ ? Boolean(this.verificationUrl && this.userCode)
2046
+ : Boolean(this.verificationUrl || this.userCode);
2047
+ if (challengeReady || this.interactiveRequired)
2048
+ this.markReady();
2049
+ }
2050
+ send(text) {
2051
+ this.socket.send(text.endsWith('\n') ? text : `${text}\n`);
2052
+ }
2053
+ async wait(timeoutMs = 600_000) {
2054
+ let timer;
2055
+ try {
2056
+ await Promise.race([
2057
+ this.completion,
2058
+ new Promise((_, reject) => {
2059
+ timer = setTimeout(() => reject(new Error('Runtime authentication timed out')), timeoutMs);
2060
+ }),
2061
+ ]);
2062
+ }
2063
+ catch (error) {
2064
+ this.cancel();
2065
+ throw error;
2066
+ }
2067
+ finally {
2068
+ if (timer)
2069
+ clearTimeout(timer);
2070
+ }
2071
+ if (this.exitCode !== null && this.exitCode !== 0) {
2072
+ throw new Error(`Runtime authentication failed (${this.exitCode}): ${this.output.trim()}`);
2073
+ }
2074
+ return this.authClient.status();
2075
+ }
2076
+ cancel() {
2077
+ // The SDK supports Node runtimes where the socket is supplied by `ws` and
2078
+ // no global WebSocket constructor exists. OPEN is the protocol state 1.
2079
+ if (this.socket.readyState === 1)
2080
+ this.socket.send('\x03');
2081
+ this.socket.close();
2082
+ }
2083
+ }
2084
+ export class RuntimeAuthClient {
2085
+ agent;
2086
+ config;
2087
+ constructor(agent) {
2088
+ this.agent = agent;
2089
+ const config = RUNTIME_AUTH_CONFIG[agent.runtime];
2090
+ if (!config) {
2091
+ throw new Error(`Runtime authentication is not available for runtime '${agent.runtime ?? 'generic'}'`);
2092
+ }
2093
+ this.config = config;
2094
+ }
2095
+ async methods() {
2096
+ const discovered = [];
2097
+ if (this.agent.runtime === 'opencode' && discovered.length === 0) {
2098
+ discovered.push({
2099
+ id: 'provider',
2100
+ name: 'Provider login',
2101
+ description: '',
2102
+ kind: 'native',
2103
+ command: ['opencode', 'auth', 'login'],
2104
+ metadata: {},
2105
+ });
2106
+ }
2107
+ const seen = new Set(discovered.map((method) => method.id));
2108
+ for (const method of this.config.nativeMethods) {
2109
+ if (!seen.has(method.id))
2110
+ discovered.push({ ...method, command: [...method.command], metadata: { ...method.metadata } });
2111
+ }
2112
+ return discovered;
2113
+ }
2114
+ async status() {
2115
+ if (!this.config.statusCommand) {
2116
+ throw new Error(`Runtime authentication status is unsupported for '${this.agent.runtime}'; use the runtime's native authentication interface`);
2117
+ }
2118
+ const result = await this.agent.exec([...this.config.statusCommand]);
2119
+ const output = stripTerminalCodes([result.stdout, result.stderr].filter(Boolean).join('\n')).trim();
2120
+ const detail = { exitCode: result.exitCode, output };
2121
+ if (this.agent.runtime === 'claude-code') {
2122
+ try {
2123
+ const payload = JSON.parse(stripTerminalCodes(result.stdout));
2124
+ Object.assign(detail, payload);
2125
+ const loginMethod = payload.loginMethod ?? payload.authMethod;
2126
+ return {
2127
+ authenticated: payload.loggedIn === true || payload.authenticated === true ||
2128
+ (typeof loginMethod === 'string' && loginMethod.toLowerCase() !== 'none'),
2129
+ provider: typeof (payload.subscriptionType ?? payload.provider ?? payload.apiProvider) === 'string'
2130
+ ? String(payload.subscriptionType ?? payload.provider ?? payload.apiProvider) : null,
2131
+ account: typeof payload.email === 'string' ? payload.email : null,
2132
+ method: typeof loginMethod === 'string' ? loginMethod : null,
2133
+ detail,
2134
+ };
2135
+ }
2136
+ catch {
2137
+ // Fall through to the generic status parser.
2138
+ }
2139
+ }
2140
+ const negative = /\b(not logged|not authenticated|unauthenticated|no credentials|0 credentials)\b/i.test(output);
2141
+ return { authenticated: result.exitCode === 0 && !negative, detail };
2142
+ }
2143
+ async login(options = {}) {
2144
+ const methods = await this.methods();
2145
+ const method = options.method
2146
+ ? methods.find((candidate) => candidate.id === options.method)
2147
+ : methods.find((candidate) => candidate.command.length > 0) ?? methods[0];
2148
+ if (!method) {
2149
+ throw new Error(options.method
2150
+ ? `Unknown authentication method: ${options.method}`
2151
+ : 'No authentication methods are available');
2152
+ }
2153
+ let command = [...method.command];
2154
+ if (!command.length) {
2155
+ throw new Error(`Authentication method '${method.id}' has no native login command`);
2156
+ }
2157
+ if (this.agent.runtime === 'opencode') {
2158
+ if (options.provider)
2159
+ command.push('--provider', options.provider);
2160
+ if (options.providerMethod)
2161
+ command.push('--method', options.providerMethod);
2162
+ }
2163
+ if (this.agent.runtime === 'claude-code' && options.email)
2164
+ command.push('--email', options.email);
2165
+ const requiresDeviceChallenge = method.kind === 'device'
2166
+ || method.id === 'device'
2167
+ || command.some((part) => part.toLowerCase().includes('device-auth'));
2168
+ return RuntimeLoginSession.start(this, command, options.challengeTimeoutMs, requiresDeviceChallenge);
2169
+ }
2170
+ async logout(provider) {
2171
+ if (this.config.logoutCommand === null) {
2172
+ const reason = this.agent.runtime === 'goose'
2173
+ ? 'uses its injected deployment credential'
2174
+ : 'does not expose a noninteractive logout command';
2175
+ throw new Error(`${this.agent.runtime} ${reason} and cannot log out`);
2176
+ }
2177
+ const command = [...this.config.logoutCommand];
2178
+ if (this.agent.runtime === 'opencode' && provider)
2179
+ command.push(provider);
2180
+ const result = await this.agent.exec(command);
2181
+ if (result.exitCode !== 0) {
2182
+ throw new Error(`Runtime logout failed (${result.exitCode}): ${stripTerminalCodes(result.stderr || result.stdout).trim()}`);
2183
+ }
2184
+ return this.status();
2185
+ }
2186
+ }
2187
+ const OPENCLAW_ACP_ROW = {
2188
+ image: DEFAULT_OPENCLAW_IMAGE,
2189
+ syncRoot: DEFAULT_OPENCLAW_SYNC_ROOT,
2190
+ syncUid: 1000,
2191
+ syncGid: 1000,
2192
+ // Mirrors the openclaw image ENV (`openclaw` resolves via PATH on the
2193
+ // runner host; the image pins it in ENV for the container instead).
2194
+ agentCommand: ['openclaw', 'acp'],
2195
+ env: DEFAULT_OPENCLAW_MODEL_ENV,
2196
+ cronEnv: buildOpenClawCronEnv,
2197
+ syncExclude: DEFAULT_OPENCLAW_SYNC_EXCLUDE,
2198
+ workspacesSyncEnv: true,
2199
+ stripConfigBag: true,
2200
+ openclawControlUi: true,
2201
+ openclawMemoryIndex: true,
2202
+ // openclaw.json reseeds on boot; the openclaw.sqlite trio clears
2203
+ // device_pairing_paired/device_auth_tokens/device_pairing_pending rows so a
2204
+ // stale scope-capped pairing (operator.pairing-only → FORBIDDEN on
2205
+ // operator.read/write) can never survive a runtime reset — the in-pod ACP
2206
+ // client re-pairs on loopback with silent auto-approve (operator.admin).
2207
+ // The sqlite holds only disposable metadata (audit/diagnostic events,
2208
+ // boot-reseeded cron, config tables); chat history lives in the agent db.
2209
+ resetFiles: [
2210
+ '.openclaw/openclaw.json',
2211
+ '.openclaw/state/openclaw.sqlite',
2212
+ '.openclaw/state/openclaw.sqlite-wal',
2213
+ '.openclaw/state/openclaw.sqlite-shm',
2214
+ ],
2215
+ // Legacy inert per-device pairing records (devices/*.json*); unknown names,
2216
+ // so the whole directory goes.
2217
+ resetDirs: ['.openclaw/devices'],
2218
+ };
2219
+ const ACP_RUNTIME_TABLE = {
2220
+ openclaw_acp: OPENCLAW_ACP_ROW,
2221
+ hermes_acp: {
2222
+ image: DEFAULT_HERMES_AGENT_IMAGE,
2223
+ syncRoot: DEFAULT_HERMES_AGENT_SYNC_ROOT,
2224
+ syncUid: DEFAULT_HERMES_AGENT_SYNC_UID,
2225
+ syncGid: DEFAULT_HERMES_AGENT_SYNC_GID,
2226
+ // The hermes image ENV pins the venv absolute path; on the runner host
2227
+ // the hermes-acp binary resolves via PATH instead.
2228
+ agentCommand: ['hermes-acp'],
2229
+ env: DEFAULT_HERMES_MODEL_ENV,
2230
+ cronEnv: buildHermesCronEnv,
2231
+ syncExclude: DEFAULT_HERMES_AGENT_SYNC_EXCLUDE,
2232
+ resetFiles: ['.hermes/config.yaml', '.hermes/mem0.json'],
2233
+ },
2234
+ 'buzz-agent': {
2235
+ image: DEFAULT_BUZZ_AGENT_IMAGE,
2236
+ syncRoot: DEFAULT_CODING_AGENT_SYNC_ROOT,
2237
+ syncUid: 1000,
2238
+ syncGid: 1000,
2239
+ // Coding harnesses share their agent command with the runtime-auth table.
2240
+ agentCommand: RUNTIME_AUTH_CONFIG['buzz-agent'].agentCommand,
2241
+ workspacesSyncEnv: true,
2242
+ codingHarness: true,
2243
+ },
2244
+ opencode: {
2245
+ image: DEFAULT_OPENCODE_IMAGE,
2246
+ syncRoot: DEFAULT_CODING_AGENT_SYNC_ROOT,
2247
+ syncUid: 1000,
2248
+ syncGid: 1000,
2249
+ agentCommand: RUNTIME_AUTH_CONFIG.opencode.agentCommand,
2250
+ workspacesSyncEnv: true,
2251
+ codingHarness: true,
2252
+ },
2253
+ codex: {
2254
+ image: DEFAULT_CODEX_IMAGE,
2255
+ syncRoot: DEFAULT_CODING_AGENT_SYNC_ROOT,
2256
+ syncUid: 1000,
2257
+ syncGid: 1000,
2258
+ agentCommand: RUNTIME_AUTH_CONFIG.codex.agentCommand,
2259
+ workspacesSyncEnv: true,
2260
+ codingHarness: true,
2261
+ },
2262
+ 'claude-code': {
2263
+ image: DEFAULT_CLAUDE_CODE_IMAGE,
2264
+ syncRoot: DEFAULT_CODING_AGENT_SYNC_ROOT,
2265
+ syncUid: 1000,
2266
+ syncGid: 1000,
2267
+ agentCommand: RUNTIME_AUTH_CONFIG['claude-code'].agentCommand,
2268
+ workspacesSyncEnv: true,
2269
+ codingHarness: true,
2270
+ },
2271
+ goose: {
2272
+ image: DEFAULT_GOOSE_IMAGE,
2273
+ syncRoot: DEFAULT_CODING_AGENT_SYNC_ROOT,
2274
+ syncUid: 1000,
2275
+ syncGid: 1000,
2276
+ agentCommand: RUNTIME_AUTH_CONFIG.goose.agentCommand,
2277
+ workspacesSyncEnv: true,
2278
+ codingHarness: true,
2279
+ },
2280
+ 'kimi-code': {
2281
+ image: DEFAULT_KIMI_CODE_IMAGE,
2282
+ syncRoot: DEFAULT_CODING_AGENT_SYNC_ROOT,
2283
+ syncUid: 1000,
2284
+ syncGid: 1000,
2285
+ agentCommand: RUNTIME_AUTH_CONFIG['kimi-code'].agentCommand,
2286
+ workspacesSyncEnv: true,
2287
+ codingHarness: true,
2288
+ },
2289
+ pi: {
2290
+ image: DEFAULT_PI_IMAGE,
2291
+ syncRoot: DEFAULT_CODING_AGENT_SYNC_ROOT,
2292
+ syncUid: 1000,
2293
+ syncGid: 1000,
2294
+ agentCommand: RUNTIME_AUTH_CONFIG.pi.agentCommand,
2295
+ workspacesSyncEnv: true,
2296
+ codingHarness: true,
2297
+ },
2298
+ };
2299
+ export class Deployments {
2300
+ apiKey;
2301
+ apiBase;
2302
+ agentsWsUrl;
2303
+ agentHttp;
2304
+ constructor(http, agentApiKey, agentApiBase, agentsWsUrl, requestTimeout) {
2305
+ this.apiKey = agentApiKey || http.apiKey;
2306
+ this.apiBase = resolveAgentsApiBase(agentApiBase || getAgentsApiBaseUrl(), true);
2307
+ this.agentsWsUrl = agentsWsUrl || defaultAgentsWsUrl(this.apiBase);
2308
+ const agentTimeout = requestTimeout ?? (http instanceof HTTPClient ? http.timeout : undefined);
2309
+ this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey, agentTimeout) : http;
2310
+ }
2311
+ get agentApiKey() {
2312
+ return this.apiKey;
2313
+ }
2314
+ get agentApiBase() {
2315
+ return this.apiBase;
2316
+ }
2317
+ /** Authoritative runtime-host launch directory, resolved by the platform. */
2318
+ runtimePaths(agentId, requestOptions = {}) {
2319
+ return this.agentHttp.get(`/deployments/${encodeURIComponent(agentId)}/runtime-paths`, undefined, requestOptions);
2320
+ }
2321
+ hydrateAgent(data) {
2322
+ // One flat Agent for every runtime; the peer determines ACP availability.
2323
+ return bindAgent(Agent.fromDict(data), this);
2324
+ }
2325
+ async getById(agentId, requestOptions = {}) {
2326
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}`;
2327
+ const data = Object.keys(requestOptions).length === 0
2328
+ ? await this.agentHttp.get(path)
2329
+ : await this.agentHttp.get(path, undefined, requestOptions);
2330
+ return this.hydrateAgent(data);
2331
+ }
2332
+ async resolveAgent(agentIdOrName, requestOptions = {}) {
2333
+ const raw = String(agentIdOrName || '').trim();
2334
+ if (!raw) {
2335
+ throw new Error('agentIdOrName is required');
2336
+ }
2337
+ if (isUuidRef(raw)) {
2338
+ return this.getById(raw, requestOptions);
2339
+ }
2340
+ const matches = [];
2341
+ for (const agent of await this.list(requestOptions)) {
2342
+ const values = [agent.id, agent.name, agent.handle, agent.hostname];
2343
+ if (values.some((value) => String(value || '') === raw)) {
2344
+ matches.push(agent);
2345
+ continue;
2346
+ }
2347
+ if (values.some((value) => String(value || '').startsWith(raw))) {
2348
+ matches.push(agent);
2349
+ }
2350
+ }
2351
+ if (matches.length === 0) {
2352
+ throw new Error(`Agent not found: ${raw}`);
2353
+ }
2354
+ if (matches.length > 1) {
2355
+ throw new Error(`Agent reference is ambiguous: ${raw} (${matches.slice(0, 5).map((agent) => agent.id).join(', ')})`);
2356
+ }
2357
+ return this.getById(matches[0].id, requestOptions);
2358
+ }
2359
+ async resolveAgentId(agentIdOrName, requestOptions = {}) {
2360
+ const raw = String(agentIdOrName || '').trim();
2361
+ if (!raw) {
2362
+ throw new Error('agentIdOrName is required');
2363
+ }
2364
+ if (isSelfAgentRef(raw)) {
2365
+ // An Agent introspects itself; it does not start, stop, or edit its own
2366
+ // routes. Status is the only self operation, and it is served directly
2367
+ // by GET /deployments/self -- nothing resolves a self reference to an id
2368
+ // any more.
2369
+ throw new Error('self is only supported for status');
2370
+ }
2371
+ if (isDirectAgentIdRef(raw)) {
2372
+ return raw;
2373
+ }
2374
+ return (await this.resolveAgent(raw, requestOptions)).id;
2375
+ }
2376
+ async routesTarget(agentIdOrName) {
2377
+ const raw = String(agentIdOrName || '').trim();
2378
+ if (isSelfAgentRef(raw)) {
2379
+ return 'self';
2380
+ }
2381
+ return this.resolveAgentId(raw);
2382
+ }
2383
+ async agentIdFor(target) {
2384
+ return typeof target === 'string' ? this.resolveAgentId(target) : target.id;
2385
+ }
2386
+ async fetchRaw(path, init = {}) {
2387
+ const headers = new Headers(init.headers ?? {});
2388
+ headers.set('Authorization', `Bearer ${this.apiKey}`);
2389
+ const contentType = headers.get('Content-Type');
2390
+ const body = init.body && contentType?.includes('application/json') && typeof init.body !== 'string'
2391
+ ? JSON.stringify(init.body)
2392
+ : init.body;
2393
+ const response = await fetch(`${this.apiBase}${path}`, {
2394
+ ...init,
2395
+ headers,
2396
+ body,
2397
+ });
2398
+ if (!response.ok) {
2399
+ let detail = response.statusText;
2400
+ try {
2401
+ const payload = await response.clone().json();
2402
+ detail = typeof payload.detail === 'string' ? payload.detail : response.statusText;
2403
+ }
2404
+ catch {
2405
+ const text = await response.text();
2406
+ detail = text || response.statusText;
2407
+ }
2408
+ throw new APIError(response.status, detail);
2409
+ }
2410
+ return response;
2411
+ }
2412
+ async fileAccess(agentId, signal) {
2413
+ const payload = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/token`, undefined, { redirect: 'error', ...(signal ? { signal } : {}) });
2414
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
2415
+ throw new Error('Backend returned an invalid Agent file token response');
2416
+ }
2417
+ const fields = 'transport' in payload ? ['transport', 'executor', 'max_bytes', 'chunk_bytes'] : ['url', 'token', 'expires_at'];
2418
+ if (Object.keys(payload).length !== fields.length || fields.some((key) => !(key in payload))) {
2419
+ throw new Error('Backend returned an invalid Agent file token response');
2420
+ }
2421
+ if ('transport' in payload) {
2422
+ // Executor is informational and null until observed; the file operation determines availability.
2423
+ if (payload.transport !== 'runner' || (payload.executor !== null && payload.executor !== 'process' && payload.executor !== 'docker') || !Number.isSafeInteger(payload.max_bytes) || payload.max_bytes <= 0) {
2424
+ throw new Error('Backend returned an invalid runner file transport');
2425
+ }
2426
+ const chunkBytes = payload.chunk_bytes;
2427
+ if (!Number.isSafeInteger(chunkBytes) || chunkBytes <= 0 || chunkBytes > payload.max_bytes) {
2428
+ throw new Error('Backend returned an invalid runner chunk size');
2429
+ }
2430
+ return { transport: 'runner', maxBytes: Math.min(payload.max_bytes, RUNNER_FILE_MAX_BYTES), chunkBytes };
2431
+ }
2432
+ const token = typeof payload?.token === 'string' ? payload.token.trim() : '';
2433
+ const expiresAt = typeof payload?.expires_at === 'string' ? payload.expires_at.trim() : '';
2434
+ let url;
2435
+ try {
2436
+ url = new URL(typeof payload?.url === 'string' ? payload.url : '');
2437
+ }
2438
+ catch {
2439
+ throw new Error('Backend returned an invalid Agent file token response');
2440
+ }
2441
+ if (url.protocol !== 'https:' ||
2442
+ !url.hostname ||
2443
+ url.username ||
2444
+ url.password ||
2445
+ url.search ||
2446
+ url.hash ||
2447
+ url.pathname !== '/_reef' ||
2448
+ !token ||
2449
+ !expiresAt) {
2450
+ throw new Error('Backend returned an invalid Agent file token response');
2451
+ }
2452
+ return { url: url.toString().replace(/\/+$/, ''), token };
2453
+ }
2454
+ async fetchReef(access, path, init = {}) {
2455
+ const headers = new Headers(init.headers ?? {});
2456
+ headers.set('Authorization', `Bearer ${access.token}`);
2457
+ const response = await fetch(`${access.url}${path}`, {
2458
+ ...init,
2459
+ headers,
2460
+ redirect: 'error',
2461
+ });
2462
+ if (!response.ok) {
2463
+ let detail = response.statusText;
2464
+ try {
2465
+ const payload = await response.clone().json();
2466
+ detail = typeof payload.detail === 'string' ? payload.detail : response.statusText;
2467
+ }
2468
+ catch {
2469
+ const text = await response.text();
2470
+ detail = text || response.statusText;
2471
+ }
2472
+ throw new APIError(response.status, detail);
2473
+ }
2474
+ return response;
2475
+ }
2476
+ async create(options = {}) {
2477
+ const config = buildAgentCreateConfig(options.config ?? {}, options);
2478
+ const body = { ...config };
2479
+ if (options.dryRun)
2480
+ body.dry_run = true;
2481
+ if (options.name)
2482
+ body.name = options.name;
2483
+ if (options.handle !== undefined)
2484
+ body.handle = options.handle;
2485
+ if (options.size)
2486
+ body.size = options.size;
2487
+ if (options.meta?.ui)
2488
+ body.meta = { ui: structuredClone(options.meta.ui) };
2489
+ if (options.meta?.integrations)
2490
+ body.meta = { ...body.meta, integrations: structuredClone(options.meta.integrations) };
2491
+ if (options.tags?.length)
2492
+ body.tags = [...options.tags];
2493
+ if (options.runner) {
2494
+ body.runner = {
2495
+ ...(options.runner.tags ? { tags: [...options.runner.tags] } : {}),
2496
+ ...(options.runner.runnerId ? { runner_id: options.runner.runnerId } : {}),
2497
+ };
2498
+ }
2499
+ if (options.runtime)
2500
+ body.runtime = options.runtime;
2501
+ const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body, { retries: 1 });
2502
+ return this.hydrateAgent(data);
2503
+ }
2504
+ /**
2505
+ * Create a managed agent for a runtime in one call.
2506
+ *
2507
+ * `runtime` selects the launch defaults (image, sync root and
2508
+ * include/exclude presets, uid/gid, env presets, routes, boot command) from
2509
+ * the ACP_RUNTIME_TABLE data table; `options` folds the old per-runtime
2510
+ * facade option types — each runtime reads the knobs it understands and
2511
+ * ignores the rest. `create()` stays the raw generic entry; `createAgent`
2512
+ * is the typed one. Every non-generic runtime boots behind hyper-acp;
2513
+ * `buzz` switches to the Buzz launch (coding runtimes only).
2514
+ */
2515
+ async createAgent(runtime, options = {}) {
2516
+ if (runtime === 'generic')
2517
+ return this.create(options);
2518
+ return this.createAcpAgentDeployment(runtime, options);
2519
+ }
2520
+ /**
2521
+ * Create an ACP-fronted coding agent. All coding runtimes share one launch
2522
+ * contract; `runtime` selects the default image and sync includes.
2523
+ * Runtime configuration roots are selected on the runtime host.
2524
+ *
2525
+ * @deprecated Use {@link Deployments.createAgent} with the runtime label;
2526
+ * the folded entry takes the same options bag.
2527
+ */
2528
+ async createCodingAgent(runtime, options = {}) {
2529
+ return this.createAgent(runtime, options);
2530
+ }
2531
+ /**
2532
+ * The one canonical ACP launch path: every non-generic runtime launches
2533
+ * through here, with per-family differences read from ACP_RUNTIME_TABLE.
2534
+ */
2535
+ async createAcpAgentDeployment(runtime, options) {
2536
+ const family = ACP_RUNTIME_TABLE[runtime];
2537
+ const processExecutor = options.executor === 'process';
2538
+ if (processExecutor && options.image !== undefined && options.image !== null) {
2539
+ // Runner-process launch: the runner host executes `command` directly,
2540
+ // so there is no image to pull. Mirrors the Backend's 400
2541
+ // "image is only supported for the docker executor".
2542
+ throw new Error('image requires the docker executor');
2543
+ }
2544
+ if (options.buzzEnabled && options.buzz) {
2545
+ throw new Error('buzzEnabled cannot be combined with buzz');
2546
+ }
2547
+ if ((options.buzzEnabled || options.buzz) && options.command !== undefined && options.command !== null) {
2548
+ throw new Error('Buzz launch cannot be combined with an explicit command');
2549
+ }
2550
+ if ((options.buzzEnabled || options.buzz) && !family.codingHarness) {
2551
+ throw new Error(`Buzz launch is supported on coding-agent runtimes only, not '${runtime}'`);
2552
+ }
2553
+ const buzzLaunch = options.buzzEnabled || options.buzz !== undefined && options.buzz !== null;
2554
+ if (buzzLaunch && options.size !== undefined && options.size !== 'large' && options.size !== 'medium') {
2555
+ throw new Error("Buzz coding agents require size='large' or 'medium'");
2556
+ }
2557
+ rejectOpenClawSecretOnlyEnv({ ...(options.env ?? {}) });
2558
+ const effectiveEnv = {
2559
+ ...(family.workspacesSyncEnv ? buildWorkspacesSyncEnv(options.workspacesSync ?? null) : null),
2560
+ ...(family.cronEnv ? family.cronEnv(options.cronEnabled ?? null) : null),
2561
+ ...(family.openclawMemoryIndex ? buildOpenClawMemoryIndexEnv(options.memoryIndex) : null),
2562
+ ...(family.env ?? null),
2563
+ ...(options.env ?? {}),
2564
+ };
2565
+ if (family.openclawControlUi) {
2566
+ // OpenClaw treats this env as a full replace for
2567
+ // gateway.controlUi.allowedOrigins, and every HyperCLI surface (desktop,
2568
+ // console) drives the control UI from dynamic origins. The only value
2569
+ // that lands reliably is the wildcard — always write it.
2570
+ effectiveEnv.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN = '*';
2571
+ Object.assign(effectiveEnv, buildOpenClawTrustedProxiesEnv(options.trustedProxies));
2572
+ }
2573
+ if (options.permissionMode !== undefined) {
2574
+ throw new Error('permissionMode launch presets are retired; select an advertised native session mode');
2575
+ }
2576
+ const effectiveSecrets = { ...(options.secrets ?? {}) };
2577
+ if (family.codingHarness) {
2578
+ for (const key of ['BUZZ_PRIVATE_KEY', 'NOSTR_PRIVATE_KEY']) {
2579
+ const value = effectiveEnv[key];
2580
+ if (value === undefined)
2581
+ continue;
2582
+ delete effectiveEnv[key];
2583
+ if (effectiveSecrets[key] !== undefined && effectiveSecrets[key] !== value) {
2584
+ throw new Error(`${key} conflicts between env and secrets`);
2585
+ }
2586
+ effectiveSecrets[key] = value;
2587
+ }
2588
+ }
2589
+ // The process executor has no image: the runner host runs `command`,
2590
+ // resolving the bare hyper-acp binary via the host's PATH. Hosted/docker
2591
+ // launches keep the container path.
2592
+ const resolvedImage = processExecutor ? undefined : options.image ?? family.image;
2593
+ const hyperAcpBinary = processExecutor ? 'hyper-acp' : '/usr/local/bin/hyper-acp';
2594
+ const buzzConfig = buzzLaunch ? buildBuzzLaunchConfig(runtime, options.buzz ?? null, this.apiBase, { env: effectiveEnv, secrets: effectiveSecrets,
2595
+ executor: options.executor }) : null;
2596
+ let syncInclude;
2597
+ let syncExclude;
2598
+ if (options.syncInclude !== undefined && options.syncInclude !== null) {
2599
+ syncInclude = options.syncInclude;
2600
+ syncExclude = undefined;
2601
+ }
2602
+ else if (options.syncExclude !== undefined) {
2603
+ syncInclude = undefined;
2604
+ syncExclude = options.syncExclude ?? undefined;
2605
+ }
2606
+ else if (options.syncInclude === null) {
2607
+ syncInclude = undefined;
2608
+ syncExclude = undefined;
2609
+ }
2610
+ else if (family.codingHarness) {
2611
+ // Read live: tests and embedders may retarget the include presets.
2612
+ const defaultInclude = DEFAULT_CODING_AGENT_SYNC_INCLUDES[runtime];
2613
+ syncInclude = defaultInclude ?? undefined;
2614
+ syncExclude = defaultInclude === null ? [] : undefined;
2615
+ }
2616
+ else if (family.syncExclude) {
2617
+ syncExclude = [...family.syncExclude];
2618
+ }
2619
+ const defaultRoutes = options.openClawRoutes?.includeDesktop
2620
+ ? buildOpenClawDesktopRoute(options.openClawRoutes) : {};
2621
+ const effectiveOptions = {
2622
+ ...options,
2623
+ meta: buzzLaunch ? {
2624
+ ...options.meta,
2625
+ integrations: { ...options.meta?.integrations, buzz: { enabled: true } },
2626
+ } : options.meta,
2627
+ runtime,
2628
+ size: buzzLaunch ? (options.size ?? 'large') : options.size,
2629
+ env: buzzConfig?.env ?? effectiveEnv,
2630
+ secrets: buzzConfig?.secrets ?? effectiveSecrets,
2631
+ routes: buzzConfig?.routes ?? options.routes ?? defaultRoutes,
2632
+ image: resolvedImage,
2633
+ command: options.buzzEnabled || options.buzz
2634
+ ? buzzConfig.command
2635
+ // Runner-verbatim-spawn contract: the runner spawns
2636
+ // launch_config.command exactly as stored, so the SDK constructs the
2637
+ // full hyper-acp argv per runtime at create time — hyper-acp plus
2638
+ // `--agent-command <runtime agent command>` and one repeated
2639
+ // `--agent-arg` per extra agent argv element, mirroring the image's
2640
+ // HYPER_ACP_AGENT_COMMAND/HYPER_ACP_AGENT_ARGS env split. There is no
2641
+ // HYPER_ACP_AGENT_COMMAND env injection anywhere and no backend
2642
+ // command map or runner fallback. ACP_RUNTIME_TABLE.agentCommand
2643
+ // covers every runtime the process executor admits, so a bare
2644
+ // hyper-acp (which would spawn a guaranteed-dead child-less bridge)
2645
+ // can never ship. A caller-provided `command` override still wins.
2646
+ // Container launches are unchanged: the image ENV/CMD already
2647
+ // carries the agent command (openclaw/hermes images need no explicit
2648
+ // command), so only coding-harness containers keep the bare
2649
+ // container-path hyper-acp binary.
2650
+ : options.command ?? (processExecutor
2651
+ ? [
2652
+ hyperAcpBinary,
2653
+ '--agent-command', family.agentCommand[0],
2654
+ ...family.agentCommand.slice(1).flatMap((arg) => ['--agent-arg', arg]),
2655
+ ]
2656
+ : family.codingHarness ? [hyperAcpBinary] : undefined),
2657
+ syncRoot: options.syncRoot ?? family.syncRoot,
2658
+ syncInclude,
2659
+ syncExclude,
2660
+ syncUid: options.syncUid ?? family.syncUid,
2661
+ syncGid: options.syncGid ?? family.syncGid,
2662
+ // Native process exit leaves hosted Buzz stopped; generic launch options
2663
+ // cannot opt it back into automatic restart.
2664
+ restart: buzzLaunch ? false : options.restart,
2665
+ runtimeScopes: options.runtimeScopes ?? DEFAULT_AGENT_RUNTIME_SCOPES,
2666
+ };
2667
+ if (family.stripConfigBag) {
2668
+ delete effectiveOptions.config;
2669
+ }
2670
+ const agent = await this.create(effectiveOptions);
2671
+ if (agent.runtime !== runtime) {
2672
+ throw new Error(`Deployment response did not identify runtime '${runtime}'`);
2673
+ }
2674
+ return agent;
2675
+ }
2676
+ async budget() {
2677
+ return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/budget`);
2678
+ }
2679
+ async oneShotAgentWebSocket(agentId, purpose, request, timeoutMs = 45_000) {
2680
+ const rawToken = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/${purpose}/token`);
2681
+ const token = validateAgentWsToken(rawToken, agentId, purpose);
2682
+ const parsed = new URL(token.ws_url);
2683
+ parsed.searchParams.set('token', token.token);
2684
+ const WebSocketImpl = preferredWebSocket();
2685
+ let ws;
2686
+ try {
2687
+ ws = new WebSocketImpl(parsed.toString());
2688
+ }
2689
+ catch (error) {
2690
+ throw new Error(`Agent ${purpose} WebSocket connection failed`, { cause: error });
2691
+ }
2692
+ return await new Promise((resolve, reject) => {
2693
+ let opened = false;
2694
+ let settled = false;
2695
+ let socketError;
2696
+ let result;
2697
+ let resultCount = 0;
2698
+ const timer = setTimeout(() => {
2699
+ finish(new Error(`Agent ${purpose} WebSocket timed out`));
2700
+ ws.close(1000, 'Client timeout');
2701
+ }, timeoutMs);
2702
+ const finish = (error) => {
2703
+ if (settled)
2704
+ return;
2705
+ settled = true;
2706
+ clearTimeout(timer);
2707
+ if (error)
2708
+ reject(error);
2709
+ else
2710
+ resolve(result);
2711
+ };
2712
+ ws.onopen = () => {
2713
+ opened = true;
2714
+ if (request !== undefined)
2715
+ ws.send(JSON.stringify(request));
2716
+ };
2717
+ ws.onmessage = (event) => {
2718
+ const rejectFrame = (message, reason) => {
2719
+ const error = new Error(message);
2720
+ try {
2721
+ ws.close(1008, reason);
2722
+ }
2723
+ finally {
2724
+ finish(error);
2725
+ }
2726
+ };
2727
+ if (typeof event.data !== 'string') {
2728
+ rejectFrame(`Agent ${purpose} WebSocket returned a non-text result frame`, 'Invalid result frame');
2729
+ return;
2730
+ }
2731
+ resultCount += 1;
2732
+ if (resultCount !== 1) {
2733
+ rejectFrame(`Agent ${purpose} WebSocket returned more than one result frame`, 'Too many result frames');
2734
+ return;
2735
+ }
2736
+ try {
2737
+ result = JSON.parse(event.data);
2738
+ }
2739
+ catch {
2740
+ rejectFrame(`Agent ${purpose} WebSocket returned invalid JSON`, 'Invalid result frame');
2741
+ }
2742
+ };
2743
+ ws.onerror = (event) => {
2744
+ const socketEvent = event;
2745
+ const error = typeof socketEvent.error === 'object'
2746
+ && socketEvent.error instanceof Error
2747
+ ? socketEvent.error
2748
+ : undefined;
2749
+ socketError = new Error(`Agent ${purpose} WebSocket connection failed`, { cause: error });
2750
+ };
2751
+ ws.onclose = (event) => {
2752
+ if (socketError) {
2753
+ finish(socketError);
2754
+ return;
2755
+ }
2756
+ if (event.code !== 1000) {
2757
+ const reason = event.reason ? `: ${event.reason}` : '';
2758
+ finish(new Error(`Agent ${purpose} WebSocket closed with code ${event.code}${reason}`));
2759
+ return;
2760
+ }
2761
+ if (!opened || resultCount !== 1) {
2762
+ finish(new Error(`Agent ${purpose} WebSocket closed without one result frame`));
2763
+ return;
2764
+ }
2765
+ finish();
2766
+ };
2767
+ });
2768
+ }
2769
+ async metrics(agentIdOrName) {
2770
+ const agentId = await this.resolveAgentId(agentIdOrName);
2771
+ return validateAgentMetricsResult(await this.oneShotAgentWebSocket(agentId, 'metrics'));
2772
+ }
2773
+ async list(options = {}) {
2774
+ return (await this.listWithCapacity(options)).items;
2775
+ }
2776
+ async listWithCapacity(options = {}) {
2777
+ const params = {};
2778
+ if (options.state)
2779
+ params.state = options.state;
2780
+ if (options.handle)
2781
+ params.handle = options.handle;
2782
+ if (options.name)
2783
+ params.name = options.name;
2784
+ if (options.query)
2785
+ params.q = options.query;
2786
+ if (options.includeDeleted !== undefined && options.includeDeleted !== null) {
2787
+ params.include_deleted = options.includeDeleted ? 'true' : 'false';
2788
+ }
2789
+ const requestOptions = {
2790
+ retries: options.retries,
2791
+ backoff: options.backoff,
2792
+ timeout: options.timeout,
2793
+ signal: options.signal,
2794
+ retryStatuses: options.retryStatuses,
2795
+ };
2796
+ const cleanRequestOptions = {};
2797
+ for (const key of Object.keys(requestOptions)) {
2798
+ const value = requestOptions[key];
2799
+ if (value !== undefined) {
2800
+ cleanRequestOptions[key] = value;
2801
+ }
2802
+ }
2803
+ const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX, Object.keys(params).length ? params : undefined, Object.keys(cleanRequestOptions).length ? cleanRequestOptions : undefined);
2804
+ const payload = Array.isArray(data) ? { items: data } : data;
2805
+ const items = (payload.items ?? []).map((item) => this.hydrateAgent(item));
2806
+ const runningFallback = items.filter((agent) => !isAgentRuntimeInactiveState(agent.state)).length;
2807
+ return {
2808
+ items,
2809
+ totalAgents: Number(payload.total_agents ?? items.length),
2810
+ maxAgentsPerAccount: Number(payload.max_agents_per_account ?? 0),
2811
+ runningAgents: Number(payload.running_agents ?? runningFallback),
2812
+ slots: Object.fromEntries(Object.entries(payload.slots ?? {}).map(([size, raw]) => {
2813
+ const inventory = raw;
2814
+ return [size, {
2815
+ granted: Number(inventory.granted ?? 0),
2816
+ used: Number(inventory.used ?? inventory.occupied ?? 0),
2817
+ available: Number(inventory.available ?? 0),
2818
+ }];
2819
+ })),
2820
+ agentSlots: (payload.agent_slots ?? []).map(agentSlotFromDict),
2821
+ pooledTpd: Number(payload.pooled_tpd ?? 0),
2822
+ };
2823
+ }
2824
+ async get(agentIdOrName, requestOptions = {}) {
2825
+ const raw = String(agentIdOrName || '').trim();
2826
+ if (!raw)
2827
+ throw new Error('agentIdOrName is required');
2828
+ if (isSelfAgentRef(raw)) {
2829
+ return this.getById('self', requestOptions);
2830
+ }
2831
+ if (!isDirectAgentIdRef(raw)) {
2832
+ return this.resolveAgent(raw, requestOptions);
2833
+ }
2834
+ try {
2835
+ return await this.getById(raw, requestOptions);
2836
+ }
2837
+ catch (error) {
2838
+ if (!(error instanceof APIError) || ![404, 422].includes(error.statusCode) || isUuidRef(raw)) {
2839
+ throw error;
2840
+ }
2841
+ return this.resolveAgent(raw, requestOptions);
2842
+ }
2843
+ }
2844
+ async attachSlackRelayAgent(agentIdOrName, options) {
2845
+ const agentId = await this.resolveAgentId(agentIdOrName);
2846
+ return attachSlackRelayAgent({
2847
+ relayBaseUrl: options.relayBaseUrl,
2848
+ token: options.token || this.apiKey,
2849
+ agentId,
2850
+ });
2851
+ }
2852
+ async subscribe(handler, options = {}) {
2853
+ const stableConnectionMs = 10_000;
2854
+ let retryDelay = 250;
2855
+ const waitBeforeReconnect = async () => {
2856
+ let abortRetry = () => { };
2857
+ const aborted = new Promise((resolve) => {
2858
+ if (options.signal?.aborted)
2859
+ resolve();
2860
+ else {
2861
+ abortRetry = resolve;
2862
+ options.signal?.addEventListener('abort', abortRetry, { once: true });
2863
+ }
2864
+ });
2865
+ try {
2866
+ await Promise.race([sleep(retryDelay), aborted]);
2867
+ }
2868
+ finally {
2869
+ options.signal?.removeEventListener('abort', abortRetry);
2870
+ }
2871
+ retryDelay = Math.min(retryDelay * 2, 5_000);
2872
+ };
2873
+ while (!options.signal?.aborted) {
2874
+ try {
2875
+ const token = validateDeploymentEventToken(await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/events/token`, undefined, { signal: options.signal }));
2876
+ const eventUrl = new URL(token.ws_url);
2877
+ eventUrl.searchParams.set('token', token.token);
2878
+ const WebSocketImpl = preferredWebSocket();
2879
+ const ws = new WebSocketImpl(eventUrl.toString());
2880
+ let readyAt = null;
2881
+ let closedAt = null;
2882
+ await new Promise((resolve, reject) => {
2883
+ let opened = false;
2884
+ let ready = false;
2885
+ let processing = Promise.resolve();
2886
+ const readyTimer = setTimeout(() => {
2887
+ ws.close(4002, 'Deployment event ready timed out');
2888
+ reject(new Error('Deployment event ready timed out'));
2889
+ }, 10_000);
2890
+ const abort = () => ws.close(1000, 'Subscription cancelled');
2891
+ options.signal?.addEventListener('abort', abort, { once: true });
2892
+ ws.addEventListener('open', () => { opened = true; });
2893
+ ws.addEventListener('message', (message) => {
2894
+ processing = processing.then(async () => {
2895
+ const frame = JSON.parse(await websocketMessageText(message.data));
2896
+ if (!ready) {
2897
+ if (frame.type !== 'ready') {
2898
+ throw new Error('Deployment event socket did not send ready');
2899
+ }
2900
+ ready = true;
2901
+ readyAt = Date.now();
2902
+ clearTimeout(readyTimer);
2903
+ await options.onReady?.();
2904
+ return;
2905
+ }
2906
+ if (((frame.type === 'deployment.transition' || frame.type === 'deployment.import_status')
2907
+ && typeof frame.agent_id === 'string'
2908
+ && frame.agent_id.length > 0) || (frame.type === 'runner.presence'
2909
+ && typeof frame.runner_id === 'string'
2910
+ && frame.runner_id.length > 0) || ((frame.type === 'addReaction' || frame.type === 'removeReaction')
2911
+ && typeof frame.agent_id === 'string' && frame.agent_id.length > 0
2912
+ && typeof frame.session_id === 'string' && frame.session_id.length > 0
2913
+ && Number.isSafeInteger(frame.seq) && frame.seq > 0
2914
+ && typeof frame.message_id === 'string' && frame.message_id.length > 0)) {
2915
+ if (frame.type === 'addReaction' || frame.type === 'removeReaction') {
2916
+ frame.meta = sessionMessageMetaFromWire(frame.meta ?? null, false);
2917
+ }
2918
+ await handler(frame);
2919
+ }
2920
+ }).catch((error) => {
2921
+ ws.close(4002, 'Invalid deployment event');
2922
+ reject(error);
2923
+ });
2924
+ });
2925
+ ws.addEventListener('error', () => reject(new Error('Deployment event WebSocket failed')));
2926
+ ws.addEventListener('close', () => {
2927
+ closedAt = Date.now();
2928
+ clearTimeout(readyTimer);
2929
+ options.signal?.removeEventListener('abort', abort);
2930
+ processing.then(resolve, reject);
2931
+ });
2932
+ if (options.signal?.aborted)
2933
+ abort();
2934
+ void opened;
2935
+ });
2936
+ if (!options.signal?.aborted) {
2937
+ // A `ready` frame followed by an immediate close is not a healthy
2938
+ // stream. Reset only after a useful stable interval so reconnects
2939
+ // and their authoritative REST resyncs retain exponential backoff.
2940
+ if (readyAt !== null
2941
+ && closedAt !== null
2942
+ && closedAt - readyAt >= stableConnectionMs) {
2943
+ retryDelay = 250;
2944
+ }
2945
+ await waitBeforeReconnect();
2946
+ }
2947
+ }
2948
+ catch (error) {
2949
+ if (options.signal?.aborted)
2950
+ break;
2951
+ if (error instanceof TypeError && error.message === 'Invalid session reaction metadata')
2952
+ throw error;
2953
+ if (error instanceof APIError && [401, 403].includes(error.statusCode))
2954
+ throw error;
2955
+ if (error instanceof Error && error.message === 'Backend returned an invalid deployment event token response') {
2956
+ throw error;
2957
+ }
2958
+ await waitBeforeReconnect();
2959
+ void error;
2960
+ }
2961
+ }
2962
+ }
2963
+ async waitForState(agentIdOrName, states, timeoutMs = 300_000, failureStates = [], minimumLaunchEpoch, pollIntervalMs = 5_000) {
2964
+ if (!states.length)
2965
+ throw new Error('states must not be empty');
2966
+ if (minimumLaunchEpoch !== undefined && minimumLaunchEpoch < 0) {
2967
+ throw new Error('minimumLaunchEpoch must be non-negative');
2968
+ }
2969
+ const agentId = await this.resolveAgentId(agentIdOrName);
2970
+ const deadline = Date.now() + timeoutMs;
2971
+ let lastState = '';
2972
+ let wakePending = true;
2973
+ let wake = null;
2974
+ const controller = new AbortController();
2975
+ const desired = new Set(states.map((state) => state.toLowerCase()));
2976
+ const failures = new Set(failureStates.map((state) => state.toLowerCase()));
2977
+ const effectivePollIntervalMs = Math.max(1, pollIntervalMs);
2978
+ const stateLabel = states.join(', ');
2979
+ let pendingFailureState = null;
2980
+ const refresh = async (confirmFailure = false) => {
2981
+ const agent = await this.get(agentId);
2982
+ lastState = String(agent.state || '');
2983
+ if (minimumLaunchEpoch !== undefined
2984
+ && agent.launchEpoch < minimumLaunchEpoch)
2985
+ return null;
2986
+ const normalizedState = lastState.toLowerCase();
2987
+ if (desired.has(normalizedState))
2988
+ return agent;
2989
+ if (failures.has(normalizedState)) {
2990
+ // Accepting a lifecycle request publishes the new launch epoch a beat
2991
+ // before the state leaves the previous terminal value, so a single
2992
+ // terminal read under the accepted epoch is not proof of a terminal
2993
+ // launch. Require it to survive one more observation.
2994
+ if (!confirmFailure && pendingFailureState !== normalizedState) {
2995
+ pendingFailureState = normalizedState;
2996
+ return null;
2997
+ }
2998
+ throw new Error(`Agent entered ${lastState} while waiting for ${stateLabel}`);
2999
+ }
3000
+ pendingFailureState = null;
3001
+ return null;
3002
+ };
3003
+ const subscription = this.subscribe((event) => {
3004
+ if (event.type === 'deployment.transition' && event.agent_id === agentId) {
3005
+ wakePending = true;
3006
+ wake?.();
3007
+ }
3008
+ }, {
3009
+ signal: controller.signal,
3010
+ onReady: () => {
3011
+ wakePending = true;
3012
+ wake?.();
3013
+ },
3014
+ }).catch(() => {
3015
+ // Lifecycle events reduce latency, but REST polling remains authoritative.
3016
+ wakePending = true;
3017
+ wake?.();
3018
+ });
3019
+ try {
3020
+ while (Date.now() < deadline) {
3021
+ if (wakePending) {
3022
+ wakePending = false;
3023
+ const agent = await refresh();
3024
+ if (agent)
3025
+ return agent;
3026
+ continue;
3027
+ }
3028
+ const remaining = deadline - Date.now();
3029
+ if (remaining <= 0)
3030
+ break;
3031
+ await Promise.race([
3032
+ new Promise((resolve) => { wake = resolve; }),
3033
+ sleep(Math.min(remaining, effectivePollIntervalMs)).then(() => {
3034
+ wakePending = true;
3035
+ }),
3036
+ ]);
3037
+ wake = null;
3038
+ }
3039
+ const finalAgent = await refresh(true);
3040
+ if (finalAgent)
3041
+ return finalAgent;
3042
+ }
3043
+ finally {
3044
+ controller.abort();
3045
+ await subscription;
3046
+ }
3047
+ throw new Error(`Timed out waiting for agent ${agentId} to reach ${stateLabel} (last=${lastState || 'unknown'})`);
3048
+ }
3049
+ async waitRunning(agentIdOrName, timeoutMs = 300_000, pollIntervalMs = 5_000, minimumLaunchEpoch) {
3050
+ return this.waitForState(agentIdOrName, ['RUNNING'], timeoutMs, ['STOPPED', 'ARCHIVED', 'DELETED', 'FAILED', 'NO_NAMESPACE'], minimumLaunchEpoch, pollIntervalMs);
3051
+ }
3052
+ /**
3053
+ * Read back every launch secret value the projection refuses to return.
3054
+ *
3055
+ * Agent projections list secret *names* and expose values only through the
3056
+ * per-secret retrieval endpoint, so a complete `secrets` mapping has to be
3057
+ * reassembled one key at a time. Every response is checked against
3058
+ * `launchEpoch` so a rebuild never silently mixes values from an older
3059
+ * launch generation into a new one.
3060
+ */
3061
+ async recoverRedactedSecrets(agentId, launchEpoch) {
3062
+ const namesData = await this.secretNames(agentId);
3063
+ if (Number(namesData.launch_epoch ?? 0) < launchEpoch) {
3064
+ throw new Error('agent secret names belong to an older launch epoch');
3065
+ }
3066
+ const secrets = {};
3067
+ for (const name of namesData.names ?? []) {
3068
+ const secretData = await this.secret(agentId, String(name));
3069
+ if (Number(secretData.launch_epoch ?? 0) < launchEpoch) {
3070
+ throw new Error('agent secret belongs to an older launch epoch');
3071
+ }
3072
+ secrets[String(name)] = String(secretData.value ?? '');
3073
+ }
3074
+ return secrets;
3075
+ }
3076
+ /**
3077
+ * Restore the two launch_config keys an Agent projection redacts.
3078
+ *
3079
+ * WHY THIS EXISTS — do not delete it as redundant validation sugar. The
3080
+ * Backend's owner-facing Agent projection deliberately strips `secrets` and
3081
+ * `registry_auth` before returning an Agent to a user-scoped caller
3082
+ * (`hydrate_managed_agent` pops both), and this SDK's own hydrator drops
3083
+ * `secrets` again. A caller that writes launch_config before start still
3084
+ * needs a complete replacement object. Without this step the obvious round
3085
+ * trip can never succeed, because the read side is structurally incapable of
3086
+ * returning what the write side requires:
3087
+ *
3088
+ * ```ts
3089
+ * const agent = await client.deployments.get(agentId);
3090
+ * await client.deployments.update(agentId, { launchConfig: agent.launchConfig });
3091
+ * // Error: launchConfig is incomplete; missing: secrets, registry_auth
3092
+ * ```
3093
+ *
3094
+ * The fix is to complete the object honestly, never to weaken the
3095
+ * completeness contract — launch_config writes are replacements, not merges.
3096
+ *
3097
+ * Only keys that are genuinely ABSENT are rebuilt. A caller-supplied
3098
+ * `secrets` or `registry_auth` is honoured verbatim, including an explicit
3099
+ * empty object, so "redacted by the projection" and "deliberately empty"
3100
+ * remain distinguishable.
3101
+ *
3102
+ * `secrets` is recoverable because values can be read back one name at a
3103
+ * time. `registry_auth` is NOT: it is caller-held, write-only, and never
3104
+ * stored server-side. It therefore falls back to an explicitly supplied
3105
+ * `registryAuth`, then to `{}` only when the configuration pulls from no
3106
+ * `registry_url`; when a registry is configured an empty credential would
3107
+ * silently break the image pull, so the caller is told to supply it instead.
3108
+ */
3109
+ async rehydrateRedactedLaunchConfig(resolvedAgentId, launchConfig, registryAuth) {
3110
+ if (!isPlainRecord(launchConfig)) {
3111
+ throw new Error('launchConfig must be a complete object');
3112
+ }
3113
+ const absent = REQUIRED_START_LAUNCH_CONFIG_KEYS.filter((key) => !Object.prototype.hasOwnProperty.call(launchConfig, key));
3114
+ // Nothing missing, or missing more than the projection ever redacts: in
3115
+ // both cases hand the object straight to the completeness gatekeeper. Only
3116
+ // a config whose *sole* gaps are the two redacted keys is a projection
3117
+ // round trip worth spending API calls to repair.
3118
+ if (absent.length === 0 || absent.some((key) => key !== 'secrets' && key !== 'registry_auth')) {
3119
+ return launchConfig;
3120
+ }
3121
+ const prepared = structuredClone(launchConfig);
3122
+ if (absent.includes('secrets')) {
3123
+ const agent = await this.getById(resolvedAgentId);
3124
+ prepared.secrets = await this.recoverRedactedSecrets(resolvedAgentId, agent.launchEpoch);
3125
+ }
3126
+ if (absent.includes('registry_auth')) {
3127
+ const registryUrl = String(prepared.registry_url ?? '').trim();
3128
+ if (registryAuth) {
3129
+ prepared.registry_auth = structuredClone(registryAuth);
3130
+ }
3131
+ else if (registryUrl) {
3132
+ throw new Error(`Agent ${resolvedAgentId} pulls from registry_url ${JSON.stringify(registryUrl)} but `
3133
+ + 'launchConfig carries no registry_auth; registry_auth is caller-held and write-only, '
3134
+ + 'so the owner-facing projection can never return it and the SDK will not substitute '
3135
+ + 'an empty credential that would break the private-registry pull — pass registryAuth '
3136
+ + 'with the launch_config update');
3137
+ }
3138
+ else {
3139
+ prepared.registry_auth = {};
3140
+ }
3141
+ }
3142
+ return prepared;
3143
+ }
3144
+ /**
3145
+ * Rebuild a complete replacement launch_config from
3146
+ * nothing but the Agent's stored projection.
3147
+ *
3148
+ * This SDK starts stored config without reading it. This method is retained
3149
+ * for callers that want to explicitly update stored launch_config;
3150
+ * it is also the only place that canonicalizes the two legacy projection
3151
+ * shapes the inline repair never sees, because the repair fills absent keys
3152
+ * and touches nothing else:
3153
+ * nullable `restart`, and a projection carrying both or neither sync policy.
3154
+ *
3155
+ * Reads the stored Agent projection, rehydrates redacted secrets through the
3156
+ * per-secret retrieval endpoint, requires caller-held registry_auth whenever
3157
+ * the stored config references a registry_url, normalizes those legacy
3158
+ * shapes, and self-checks through the completeness gatekeeper.
3159
+ */
3160
+ async storedLaunchConfig(agentIdOrName, options = {}) {
3161
+ const agent = await this.get(agentIdOrName);
3162
+ if (!isPlainRecord(agent.launchConfig)) {
3163
+ throw new Error(`Agent ${agent.id} has no stored launch_config projection`);
3164
+ }
3165
+ const launchConfig = structuredClone(agent.launchConfig);
3166
+ // Legacy projections may still carry the old nullable restart
3167
+ // representation; replacement writes receive one explicit boolean.
3168
+ if ('restart' in launchConfig && launchConfig.restart === null) {
3169
+ launchConfig.restart = false;
3170
+ }
3171
+ launchConfig.secrets = await this.recoverRedactedSecrets(agent.id, agent.launchEpoch);
3172
+ const registryUrl = String(launchConfig.registry_url ?? '').trim();
3173
+ if (registryUrl && !options.registryAuth) {
3174
+ throw new Error(`Agent ${agent.id} pulls from registry_url ${JSON.stringify(registryUrl)}; `
3175
+ + 'registry_auth is caller-held and never stored server-side, so it must '
3176
+ + 'be supplied to rebuild a complete launch_config replacement');
3177
+ }
3178
+ launchConfig.registry_auth = options.registryAuth ? structuredClone(options.registryAuth) : {};
3179
+ // Replacement writes require exactly one sync policy. Includes win when a legacy
3180
+ // projection carries both; carrying neither canonicalizes to the
3181
+ // explicit sync-everything exclusion list.
3182
+ if (Object.prototype.hasOwnProperty.call(launchConfig, 'sync_include')) {
3183
+ delete launchConfig.sync_exclude;
3184
+ }
3185
+ else if (!Object.prototype.hasOwnProperty.call(launchConfig, 'sync_exclude')) {
3186
+ launchConfig.sync_exclude = [];
3187
+ }
3188
+ return cloneCompleteLaunchConfig(await this.rehydrateRedactedLaunchConfig(agent.id, launchConfig, options.registryAuth));
3189
+ }
3190
+ async start(agentIdOrName, options) {
3191
+ const agentId = await this.resolveAgentId(agentIdOrName);
3192
+ const suppliedOptions = options;
3193
+ const mutationOptions = suppliedOptions
3194
+ ? ['launchConfig', 'trustedProxies', 'registryAuth'].filter((key) => Object.prototype.hasOwnProperty.call(suppliedOptions, key))
3195
+ : [];
3196
+ if (mutationOptions.length > 0) {
3197
+ throw new Error(`start no longer accepts launch mutation options: ${mutationOptions.join(', ')}; `
3198
+ + 'update launchConfig before starting');
3199
+ }
3200
+ const body = {};
3201
+ if (options?.dryRun)
3202
+ body.dry_run = true;
3203
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, Object.keys(body).length ? body : undefined, { retries: 1 });
3204
+ return this.hydrateAgent(data);
3205
+ }
3206
+ async update(agentIdOrName, options = {}) {
3207
+ // Only fields the backend UpdateAgentRequest accepts (it is extra="forbid"):
3208
+ // name, handle, size, launch_config, runtime, reset_image, ui, integrations.
3209
+ // refresh_from_lagoon/error are rejected.
3210
+ const body = {};
3211
+ if (options.name !== undefined)
3212
+ body.name = options.name;
3213
+ if (options.handle !== undefined)
3214
+ body.handle = options.handle;
3215
+ if (options.size !== undefined)
3216
+ body.size = options.size;
3217
+ if (options.launchConfig !== undefined)
3218
+ body.launch_config = options.launchConfig;
3219
+ if (options.runtime !== undefined)
3220
+ body.runtime = options.runtime;
3221
+ if (options.resetImage !== undefined)
3222
+ body.reset_image = options.resetImage;
3223
+ if (options.ui !== undefined)
3224
+ body.ui = options.ui;
3225
+ if (options.integrations !== undefined)
3226
+ body.integrations = options.integrations;
3227
+ const agentId = await this.resolveAgentId(agentIdOrName);
3228
+ const data = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}`, body);
3229
+ return this.hydrateAgent(data);
3230
+ }
3231
+ /**
3232
+ * Reset a deployment back to this runtime's platform defaults.
3233
+ *
3234
+ * The empty launch_config is intentional: Backend treats `{}` as "clear every
3235
+ * mutable launch setting" while preserving protected launch identity and
3236
+ * retained-storage fields. Runtime-specific files are deleted best-effort so
3237
+ * the image can reseed its own config on the next boot.
3238
+ */
3239
+ async resetRuntimeDefaults(agentIdOrName, options) {
3240
+ const agentId = await this.resolveAgentId(agentIdOrName);
3241
+ const resetData = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}`, {
3242
+ runtime: options.runtime,
3243
+ reset_image: true,
3244
+ launch_config: {},
3245
+ });
3246
+ const droppedLaunchKeys = droppedLaunchConfigKeys(resetData);
3247
+ const family = options.runtime === 'generic' ? undefined : ACP_RUNTIME_TABLE[options.runtime];
3248
+ if (family?.openclawControlUi) {
3249
+ await this.setEnv(agentId, 'OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN', '*');
3250
+ }
3251
+ for (const file of family?.resetFiles ?? []) {
3252
+ await this.fileDelete(agentId, file).catch(() => undefined);
3253
+ }
3254
+ for (const dir of family?.resetDirs ?? []) {
3255
+ // Runner transport forbids recursive deletes; on reef it lands, elsewhere best-effort.
3256
+ await this.fileDelete(agentId, dir, { recursive: true }).catch(() => undefined);
3257
+ }
3258
+ return { agent: await this.get(agentId), droppedLaunchKeys };
3259
+ }
3260
+ async uploadProfileImage(agentId, content, contentType) {
3261
+ const resolvedAgentId = await this.resolveAgentId(agentId);
3262
+ const resolvedContentType = contentType || (content instanceof Blob ? content.type : '') || 'image/png';
3263
+ return this.agentHttp.postRaw(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`, content, resolvedContentType);
3264
+ }
3265
+ async deleteProfileImage(agentId) {
3266
+ const resolvedAgentId = await this.resolveAgentId(agentId);
3267
+ return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`);
3268
+ }
3269
+ async resize(agentId, options) {
3270
+ return this.update(agentId, options);
3271
+ }
3272
+ /**
3273
+ * Request an agent stop.
3274
+ *
3275
+ * The returned agent remains `STOPPING` while runtime cleanup is in
3276
+ * progress. Fetch it again until it becomes `STOPPED` before treating its
3277
+ * deployment slot as released.
3278
+ *
3279
+ * With `dryRun: true` the Backend only validates the request and returns
3280
+ * the current Agent unchanged -- nothing mutates and no stop is started.
3281
+ */
3282
+ async stop(agentIdOrName, options) {
3283
+ const agentId = await this.resolveAgentId(agentIdOrName);
3284
+ const body = {};
3285
+ if (options?.dryRun)
3286
+ body.dry_run = true;
3287
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/stop`, Object.keys(body).length ? body : undefined, { retries: 1 });
3288
+ return this.hydrateAgent(data);
3289
+ }
3290
+ /**
3291
+ * Archive durable storage without launching the agent.
3292
+ *
3293
+ * With `dryRun: true` the Backend only validates the request and returns
3294
+ * the current Agent unchanged -- nothing mutates and no archive is started.
3295
+ */
3296
+ async archive(agentIdOrName, options) {
3297
+ const agentId = await this.resolveAgentId(agentIdOrName);
3298
+ const body = {};
3299
+ if (options?.dryRun)
3300
+ body.dry_run = true;
3301
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/archive`, Object.keys(body).length ? body : undefined, { retries: 1 });
3302
+ return this.hydrateAgent(data);
3303
+ }
3304
+ /**
3305
+ * Restore durable storage. The accepted snapshot is `RESTORING`; completion is `STOPPED`.
3306
+ *
3307
+ * With `dryRun: true` the Backend only validates the request and returns
3308
+ * the current Agent unchanged -- nothing mutates and no restore is started.
3309
+ */
3310
+ async restore(agentIdOrName, options) {
3311
+ const agentId = await this.resolveAgentId(agentIdOrName);
3312
+ const body = {};
3313
+ if (options?.dryRun)
3314
+ body.dry_run = true;
3315
+ const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/restore`, Object.keys(body).length ? body : undefined, { retries: 1 });
3316
+ return this.hydrateAgent(data);
3317
+ }
3318
+ /**
3319
+ * Resolve who the presented credential is, per the Backend
3320
+ * (`GET /deployments/auth/me`).
3321
+ *
3322
+ * Answers the three questions a credential should be able to ask about
3323
+ * itself: which Agent it is (`agentId`, set only for an Agent runtime key),
3324
+ * which account owns it (`userId`, `teamId`, `planId`), and what it may do
3325
+ * (`tags`, `capabilities`). It returns only what the credential already
3326
+ * carries, so it is unscoped and safe for any caller.
3327
+ *
3328
+ * Distinct from the product-wide `client.user.authMe()`: this is the agent
3329
+ * product's own introspection and is the only one that reports `agentId`.
3330
+ */
3331
+ async accessIdentity(requestOptions = {}) {
3332
+ const path = `${DEPLOYMENTS_API_PREFIX}/auth/me`;
3333
+ const data = Object.keys(requestOptions).length === 0
3334
+ ? await this.agentHttp.get(path)
3335
+ : await this.agentHttp.get(path, undefined, requestOptions);
3336
+ return agentAccessIdentityFromData(data);
3337
+ }
3338
+ async getRoutes(agentIdOrName, options = {}) {
3339
+ const agentId = await this.routesTarget(agentIdOrName);
3340
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/routes`;
3341
+ const data = Object.keys(options).length > 0
3342
+ ? await this.agentHttp.get(path, undefined, options)
3343
+ : await this.agentHttp.get(path);
3344
+ return agentRoutesStateFromData(data);
3345
+ }
3346
+ async setRoutes(agentIdOrName, routes, options = {}) {
3347
+ const agentId = await this.routesTarget(agentIdOrName);
3348
+ const body = {
3349
+ routes: Object.fromEntries(Object.entries(routes).map(([name, route]) => [name, routeConfigBody(route)])),
3350
+ };
3351
+ if (Object.prototype.hasOwnProperty.call(options, 'cors') && options.cors !== undefined) {
3352
+ body.cors = options.cors === null ? null : structuredClone(options.cors);
3353
+ }
3354
+ const data = await this.agentHttp.put(`${DEPLOYMENTS_API_PREFIX}/${agentId}/routes`, body);
3355
+ return agentRoutesStateFromData(data);
3356
+ }
3357
+ async setRoute(agentIdOrName, name, route) {
3358
+ const agentId = await this.routesTarget(agentIdOrName);
3359
+ const body = routeConfigBody(route);
3360
+ const data = await this.agentHttp.put(`${DEPLOYMENTS_API_PREFIX}/${agentId}/routes/${encodeURIComponent(name)}`, body);
3361
+ return agentRoutesStateFromData(data);
3362
+ }
3363
+ async removeRoute(agentIdOrName, name) {
3364
+ const agentId = await this.routesTarget(agentIdOrName);
3365
+ const data = await this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}/routes/${encodeURIComponent(name)}`);
3366
+ return agentRoutesStateFromData(data);
3367
+ }
3368
+ /**
3369
+ * Delete a deployment.
3370
+ *
3371
+ * With `dryRun: true` the Backend only validates the request and returns
3372
+ * the current Agent record unchanged -- nothing mutates and the deployment
3373
+ * is not deleted.
3374
+ */
3375
+ async delete(agentIdOrName, options) {
3376
+ // HTTP 200 accepts the durable soft delete. Cluster-local cleanup continues
3377
+ // in the background and is not proven complete by this response.
3378
+ const agentId = await this.resolveAgentId(agentIdOrName);
3379
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}`;
3380
+ const result = options?.dryRun
3381
+ ? await this.agentHttp.delete(path, { dry_run: true })
3382
+ : await this.agentHttp.delete(path);
3383
+ return result;
3384
+ }
3385
+ async refreshToken(agentIdOrName) {
3386
+ const agentId = await this.resolveAgentId(agentIdOrName);
3387
+ return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/token`);
3388
+ }
3389
+ /**
3390
+ * Signed URL for the agent's VNC desktop. Requires a running agent with the
3391
+ * `desktop` route enabled; throws otherwise.
3392
+ */
3393
+ async desktopUrl(agentIdOrName, options = {}) {
3394
+ const agentId = await this.resolveAgentId(agentIdOrName);
3395
+ const [token, agent] = await Promise.all([
3396
+ this.refreshToken(agentId),
3397
+ this.getById(agentId),
3398
+ ]);
3399
+ const jwt = (token.token ?? '').trim();
3400
+ if (!jwt)
3401
+ throw new Error('Desktop token is missing');
3402
+ if (!agent.isRunning)
3403
+ throw new Error('Start the agent to open its desktop');
3404
+ const base = agent.desktopUrl;
3405
+ if (!base)
3406
+ throw new Error('Desktop route is not enabled for this agent');
3407
+ const url = buildBrowserDesktopUrl(base, jwt, {
3408
+ redirect: 'vnc_lite.html',
3409
+ resize: 'scale',
3410
+ ...options,
3411
+ });
3412
+ return { url, expiresAt: parseDate(token.expires_at), viewport: token.desktop_viewport ?? null };
3413
+ }
3414
+ async createScopedKey(agentIdOrName, name) {
3415
+ const payload = {};
3416
+ if (name)
3417
+ payload.name = name;
3418
+ const agentId = await this.resolveAgentId(agentIdOrName);
3419
+ return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/keys`, Object.keys(payload).length ? payload : undefined);
3420
+ }
3421
+ async webSearch(query, options = {}) {
3422
+ const params = {
3423
+ q: query,
3424
+ count: options.count ?? 5,
3425
+ };
3426
+ if (options.country)
3427
+ params.country = options.country;
3428
+ if (options.searchLang)
3429
+ params.search_lang = options.searchLang;
3430
+ if (options.uiLang)
3431
+ params.ui_lang = options.uiLang;
3432
+ if (options.freshness)
3433
+ params.freshness = options.freshness;
3434
+ const headers = { 'X-Subscription-Token': this.apiKey };
3435
+ if (this.agentHttp instanceof HTTPClient) {
3436
+ return this.agentHttp.getWithHeaders('/brave/res/v1/web/search', params, headers);
3437
+ }
3438
+ 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 });
3439
+ return (await response.json());
3440
+ }
3441
+ async logsToken(agentIdOrName) {
3442
+ const agentId = await this.resolveAgentId(agentIdOrName);
3443
+ return validateAgentLogsToken(await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/logs/token`));
3444
+ }
3445
+ async env(agentIdOrName, requestOptions = {}) {
3446
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3447
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/env`;
3448
+ return Object.keys(requestOptions).length === 0
3449
+ ? this.agentHttp.get(path)
3450
+ : this.agentHttp.get(path, undefined, requestOptions);
3451
+ }
3452
+ async setEnv(agentIdOrName, key, value) {
3453
+ if (!key)
3454
+ throw new Error('env key is required');
3455
+ const agentId = await this.resolveAgentId(agentIdOrName);
3456
+ return this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}/env/${encodeURIComponent(key)}`, { value });
3457
+ }
3458
+ async deleteEnv(agentIdOrName, key) {
3459
+ if (!key)
3460
+ throw new Error('env key is required');
3461
+ const agentId = await this.resolveAgentId(agentIdOrName);
3462
+ return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}/env/${encodeURIComponent(key)}`);
3463
+ }
3464
+ async secretNames(agentIdOrName, requestOptions = {}) {
3465
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3466
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/secrets`;
3467
+ return Object.keys(requestOptions).length === 0
3468
+ ? this.agentHttp.get(path)
3469
+ : this.agentHttp.get(path, undefined, requestOptions);
3470
+ }
3471
+ async secret(agentIdOrName, key, requestOptions = {}) {
3472
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3473
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/secrets/${encodeURIComponent(key)}`;
3474
+ return Object.keys(requestOptions).length === 0
3475
+ ? this.agentHttp.get(path)
3476
+ : this.agentHttp.get(path, undefined, requestOptions);
3477
+ }
3478
+ async setSecret(agentIdOrName, key, value) {
3479
+ if (!key)
3480
+ throw new Error('secret key is required');
3481
+ const agentId = await this.resolveAgentId(agentIdOrName);
3482
+ const result = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}/secrets/${encodeURIComponent(key)}`, { value });
3483
+ return result;
3484
+ }
3485
+ async deleteSecret(agentIdOrName, key) {
3486
+ if (!key)
3487
+ throw new Error('secret key is required');
3488
+ const agentId = await this.resolveAgentId(agentIdOrName);
3489
+ const result = await this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}/secrets/${encodeURIComponent(key)}`);
3490
+ return result;
3491
+ }
3492
+ async exec(target, command, options = {}) {
3493
+ if (!Array.isArray(command)
3494
+ || command.length < 1
3495
+ || command.some((argument) => typeof argument !== 'string')
3496
+ || command[0].length === 0
3497
+ || command.some((argument) => argument.includes('\0'))
3498
+ || command.reduce((size, argument) => size + encodeUtf8(argument).byteLength, 0) > 65_536) {
3499
+ throw new Error('command must be a nonempty argv list of strings with a nonempty executable, at most 65536 UTF-8 bytes, and no NUL');
3500
+ }
3501
+ command = [...command];
3502
+ const timeout = options.timeout ?? 30;
3503
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 300) {
3504
+ throw new Error('timeout must be an integer from 1 through 300');
3505
+ }
3506
+ let stdin;
3507
+ if (options.stdin !== undefined) {
3508
+ const bytes = typeof options.stdin === 'string'
3509
+ ? encodeUtf8(options.stdin)
3510
+ : options.stdin;
3511
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength > AGENT_EXEC_STDIN_MAX_BYTES) {
3512
+ throw new Error(`stdin must be a string or Uint8Array of at most ${AGENT_EXEC_STDIN_MAX_BYTES} bytes`);
3513
+ }
3514
+ stdin = encodeBase64(bytes);
3515
+ }
3516
+ const agentId = await this.agentIdFor(target);
3517
+ const payload = {
3518
+ command,
3519
+ timeout,
3520
+ dry_run: options.dryRun ?? false,
3521
+ ...(stdin !== undefined ? { stdin } : {}),
3522
+ };
3523
+ return validateAgentExecResult(await this.oneShotAgentWebSocket(agentId, 'exec', payload, (timeout + 10) * 1_000));
3524
+ }
3525
+ /**
3526
+ * Wait until the Agent file API is serving. Native assignments use a bounded
3527
+ * file read; hosted assignments use the Reef readiness checks below.
3528
+ *
3529
+ * Probing the Agent hostname alone cannot answer this. The Agent domain is a
3530
+ * wildcard, so a host with no route still resolves and the edge answers a
3531
+ * plain-text `404 page not found` — byte for byte what a route that has not
3532
+ * converged yet returns. A caller polling the hostname therefore cannot tell
3533
+ * "not ready" from "never will be", and will retry until its deadline against
3534
+ * a host that was never going to work.
3535
+ *
3536
+ * So ask the API for the authoritative Agent state first: a deleted or failed
3537
+ * Agent rejects immediately with that state rather than timing out. Then
3538
+ * require consecutive successful reads, because one success only proves the
3539
+ * route answered once — the next request can still 404 while the edge settles.
3540
+ */
3541
+ async waitForFileApiReady(target, options = {}) {
3542
+ const timeoutMs = options.timeoutMs ?? 90_000;
3543
+ const consecutive = options.consecutive ?? 2;
3544
+ const pollMs = options.pollMs ?? 1_000;
3545
+ const agentId = await this.agentIdFor(target);
3546
+ const deadline = Date.now() + timeoutMs;
3547
+ let streak = 0;
3548
+ let lastError = null;
3549
+ let lastState = '';
3550
+ for (;;) {
3551
+ const agent = await this.get(agentId);
3552
+ lastState = String(agent.state ?? '').toUpperCase();
3553
+ if (lastState === 'DELETED' || lastState === 'FAILED' || lastState === 'NO_NAMESPACE') {
3554
+ throw new Error(`Agent ${agentId} is ${lastState}; its Reef file API will not serve. Waiting longer cannot help.`);
3555
+ }
3556
+ try {
3557
+ await this.filesList(agentId, '');
3558
+ streak += 1;
3559
+ if (streak >= consecutive)
3560
+ return;
3561
+ }
3562
+ catch (error) {
3563
+ if (error instanceof APIError && error.statusCode === 501)
3564
+ throw error;
3565
+ lastError = error;
3566
+ streak = 0;
3567
+ }
3568
+ if (Date.now() >= deadline) {
3569
+ throw new Error(`Agent ${agentId} Reef file API did not serve ${consecutive} consecutive reads within ` +
3570
+ `${Math.round(timeoutMs / 1000)}s (agent state=${lastState || 'unknown'}, last error=${String(lastError)})`);
3571
+ }
3572
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
3573
+ }
3574
+ }
3575
+ async filesList(target, path = '') {
3576
+ const resolvedPath = resolveSyncRootFilePath(path);
3577
+ const agentId = await this.agentIdFor(target);
3578
+ const access = await this.fileAccess(agentId);
3579
+ let payload;
3580
+ if ('transport' in access) {
3581
+ payload = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/list`, { path: resolvedPath ? nativeFilePath(resolvedPath) : '' }, { redirect: 'error' });
3582
+ }
3583
+ else {
3584
+ const suffix = resolvedPath ? `/${encodeFilePath(resolvedPath)}` : '';
3585
+ const response = await this.fetchReef(access, `/directories${suffix}`);
3586
+ payload = (await response.json());
3587
+ }
3588
+ if (!isDirectoryListingPayload(payload)) {
3589
+ throw new Error('Reef returned an invalid directory listing');
3590
+ }
3591
+ return [...(payload.directories ?? []), ...(payload.files ?? [])];
3592
+ }
3593
+ async fileReadBytesWithMetadata(target, path, options) {
3594
+ // Preserve absolute runtime addresses for the existing runner read command.
3595
+ // Relative callers retain workspace semantics; hosted reads remain sync-root relative.
3596
+ const absolute = path.startsWith('/') || /^[a-z]:[\\/]/i.test(path) || path.startsWith('\\\\');
3597
+ const resolvedPath = absolute ? path : resolveSyncRootFilePath(path);
3598
+ if (!resolvedPath)
3599
+ throw new Error('agent file path is required');
3600
+ const agentId = await this.agentIdFor(target);
3601
+ const access = await this.fileAccess(agentId, options?.signal);
3602
+ if ('transport' in access) {
3603
+ const maxBytes = Math.min(options?.maxBytes ?? access.maxBytes, access.maxBytes);
3604
+ if (!Number.isInteger(maxBytes) || maxBytes < 0)
3605
+ throw new Error('maxBytes must be a nonnegative integer');
3606
+ const query = new URLSearchParams({ path: absolute ? resolvedPath : nativeFilePath(resolvedPath), max_bytes: String(maxBytes) });
3607
+ return this.agentHttp.getStream(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/stream?${query}`, async (response) => {
3608
+ const content = await readResponseBytes(response, path, maxBytes);
3609
+ const length = response.headers.get('content-length');
3610
+ if (length === null || Number(length) !== content.byteLength)
3611
+ throw new Error('Incomplete runner file stream');
3612
+ return { content };
3613
+ }, { signal: options?.signal });
3614
+ }
3615
+ if (absolute)
3616
+ resolveSyncRootFilePath(path); // Hosted transport has no runtime-absolute address space.
3617
+ const response = await this.fetchReef(access, `/files/${encodeFilePath(resolvedPath)}`, {
3618
+ signal: options?.signal,
3619
+ });
3620
+ const maxBytes = options?.maxBytes === undefined
3621
+ ? AGENT_FILE_MAX_BYTES
3622
+ : Math.min(options.maxBytes, AGENT_FILE_MAX_BYTES);
3623
+ const bytes = await readResponseBytes(response, path, maxBytes);
3624
+ const contentType = response.headers.get('content-type') || '';
3625
+ return { content: bytes, mimeType: contentType || undefined };
3626
+ }
3627
+ async fileReadBytes(target, path, options) {
3628
+ return (await this.fileReadBytesWithMetadata(target, path, options)).content;
3629
+ }
3630
+ async fileRead(target, path, options) {
3631
+ // ignoreBOM means treat the BOM as content, matching Python/Rust UTF-8 readers.
3632
+ return new TextDecoder('utf-8', { ignoreBOM: true }).decode(await this.fileReadBytes(target, path, options));
3633
+ }
3634
+ /**
3635
+ * Write bytes to a relative path through Reef or the native runner transport.
3636
+ *
3637
+ * Per-file writes are limited to 100 MiB (`AGENT_FILE_WRITE_MAX_BYTES`,
3638
+ * the Cloudflare edge request-body cap on the agent hostname). Larger data
3639
+ * should be split across files or synced via the agent's own tooling.
3640
+ * Native runner files are limited to 100 MiB under the retained assignment root.
3641
+ */
3642
+ async fileWriteBytes(target, path, content, options = {}) {
3643
+ path = normalizeWritableBackendFilePath(path);
3644
+ if (!path)
3645
+ throw new Error('agent file path is required');
3646
+ const encodedPath = encodeFilePath(path);
3647
+ const bytes = toUint8Array(content);
3648
+ if (bytes.byteLength > AGENT_FILE_WRITE_MAX_BYTES) {
3649
+ throw new Error(`Agent file writes are limited to ${AGENT_FILE_WRITE_MAX_BYTES / 1024 / 1024} MiB `
3650
+ + '(Cloudflare request-body cap on the agent hostname); '
3651
+ + 'split larger data or sync it via the agent\'s own tooling');
3652
+ }
3653
+ const agentId = await this.agentIdFor(target);
3654
+ const access = await this.fileAccess(agentId, options.signal);
3655
+ if ('transport' in access) {
3656
+ if (bytes.byteLength > access.maxBytes)
3657
+ throw new Error(`Runner files are limited to ${access.maxBytes} bytes by this backend`);
3658
+ const runnerPath = nativeFilePath(path);
3659
+ const transferId = crypto.randomUUID();
3660
+ let offset = 0;
3661
+ const command = async (action, body = new Uint8Array(0), signal = options.signal) => {
3662
+ const query = new URLSearchParams({ transfer_id: transferId, action, total: String(bytes.byteLength),
3663
+ offset: String(offset), path: action === 'open_write' ? runnerPath : '' });
3664
+ const receipt = await this.agentHttp.postRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/transfer?${query}`, body, 'application/octet-stream', { signal, retries: 1, redirect: 'error', ...(action === 'abort' ? { timeout: 1000 } : {}) });
3665
+ const expected = offset + body.byteLength;
3666
+ if (receipt.ok !== true || receipt.transfer_id !== transferId || receipt.offset !== expected || receipt.total !== bytes.byteLength) {
3667
+ throw new Error('Invalid runner file receipt');
3668
+ }
3669
+ offset = expected;
3670
+ return receipt;
3671
+ };
3672
+ try {
3673
+ await command('open_write');
3674
+ while (offset < bytes.byteLength) {
3675
+ await command('write', bytes.subarray(offset, Math.min(offset + access.chunkBytes, bytes.byteLength)));
3676
+ }
3677
+ await command('commit');
3678
+ return { ok: true };
3679
+ }
3680
+ catch (error) {
3681
+ // Abort is cleanup, never replay of an uncertain write or commit.
3682
+ await command('abort', new Uint8Array(0), new AbortController().signal).catch(() => undefined);
3683
+ throw error;
3684
+ }
3685
+ }
3686
+ const response = await this.fetchReef(access, `/files/${encodedPath}`, {
3687
+ method: 'PUT',
3688
+ headers: { 'Content-Type': 'application/octet-stream' },
3689
+ body: bytes,
3690
+ });
3691
+ return (await response.json());
3692
+ }
3693
+ /**
3694
+ * Write a UTF-8 text file to an agent.
3695
+ *
3696
+ * Subject to the 100 MiB per-file write limit; see `fileWriteBytes`.
3697
+ */
3698
+ async fileWrite(target, path, content) {
3699
+ return this.fileWriteBytes(target, path, content);
3700
+ }
3701
+ async fileDelete(target, path, options = {}) {
3702
+ path = normalizeWritableBackendFilePath(path);
3703
+ if (!path)
3704
+ throw new Error('agent file path is required');
3705
+ const encodedPath = encodeFilePath(path);
3706
+ const params = new URLSearchParams();
3707
+ if (options.recursive)
3708
+ params.set('recursive', 'true');
3709
+ const suffix = params.toString() ? `?${params.toString()}` : '';
3710
+ const agentId = await this.agentIdFor(target);
3711
+ const access = await this.fileAccess(agentId);
3712
+ if ('transport' in access) {
3713
+ if (options.recursive)
3714
+ throw new Error('Runner file deletion is never recursive');
3715
+ // Backend POST performs exactly one attempt; an uncertain delete is never replayed.
3716
+ const receipt = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/delete`, { path: nativeFilePath(path) }, { redirect: 'error' });
3717
+ if (receipt?.status !== 'deleted' || receipt?.path !== path)
3718
+ throw new Error('Invalid runner file receipt');
3719
+ return receipt;
3720
+ }
3721
+ const response = await this.fetchReef(access, `/files/${encodedPath}${suffix}`, { method: 'DELETE' });
3722
+ return (await response.json());
3723
+ }
3724
+ async cpTo(target, localPath, remotePath) {
3725
+ const fs = await getFsPromises();
3726
+ const content = await fs.readFile(localPath);
3727
+ return this.fileWriteBytes(target, remotePath, new Uint8Array(content));
3728
+ }
3729
+ async cpFrom(target, remotePath, localPath) {
3730
+ const fs = await getFsPromises();
3731
+ const content = await this.fileReadBytes(target, remotePath);
3732
+ const destination = new URL(`file://${localPath}`).pathname;
3733
+ const parts = destination.split('/');
3734
+ parts.pop();
3735
+ const parent = parts.join('/') || '/';
3736
+ await fs.mkdir(parent, { recursive: true });
3737
+ await fs.writeFile(destination, content);
3738
+ return destination;
3739
+ }
3740
+ /** One bounded display/capture read. All-unavailable capture errors retain their metadata. */
3741
+ async getLogs(agentIdOrName, options = {}) {
3742
+ const agentId = await this.resolveAgentId(agentIdOrName);
3743
+ try {
3744
+ return await this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/logs`, agentLogQuery(options));
3745
+ }
3746
+ catch (error) {
3747
+ if (error instanceof APIError) {
3748
+ let snapshot;
3749
+ try {
3750
+ snapshot = JSON.parse(error.detail);
3751
+ }
3752
+ catch { /* Ordinary HTTP error. */ }
3753
+ if (isCapturedLogsSnapshot(snapshot))
3754
+ throw new AgentCapturedLogsError(error, snapshot);
3755
+ }
3756
+ throw error;
3757
+ }
3758
+ }
3759
+ async logsConnect(agentIdOrName, options = {}) {
3760
+ return this.connectLogs(agentIdOrName, options);
3761
+ }
3762
+ async connectLogs(agentIdOrName, options, onConnected) {
3763
+ options.signal?.throwIfAborted();
3764
+ const agentId = await this.resolveAgentId(agentIdOrName);
3765
+ const tokenData = await this.logsToken(agentId);
3766
+ options.signal?.throwIfAborted();
3767
+ const container = options.container ?? 'reef';
3768
+ const tailLines = options.tailLines ?? 100;
3769
+ const wsUrl = `${this.agentsWsUrl}/logs/${agentId}` +
3770
+ `?token=${encodeURIComponent(tokenData.token)}` +
3771
+ `&container=${encodeURIComponent(container)}` +
3772
+ `&tail_lines=${encodeURIComponent(String(tailLines))}`;
3773
+ const url = new URL(wsUrl);
3774
+ for (const [name, value] of Object.entries(agentLogQuery(options)))
3775
+ url.searchParams.set(name, String(value));
3776
+ const Socket = preferredWebSocket();
3777
+ const ws = new Socket(url.toString());
3778
+ return await new Promise((resolve, reject) => {
3779
+ let settled = false;
3780
+ const cleanup = () => {
3781
+ clearTimeout(timer);
3782
+ options.signal?.removeEventListener('abort', onAbort);
3783
+ ws.onopen = null;
3784
+ ws.onerror = null;
3785
+ ws.onclose = null;
3786
+ };
3787
+ const fail = (error) => {
3788
+ if (settled)
3789
+ return;
3790
+ settled = true;
3791
+ cleanup();
3792
+ // Node ws emits an error when close aborts an unfinished handshake.
3793
+ // Keep that owned teardown error handled until the socket closes.
3794
+ ws.onerror = () => { };
3795
+ ws.onclose = () => { ws.onerror = null; ws.onclose = null; };
3796
+ try {
3797
+ ws.close();
3798
+ }
3799
+ catch { /* Already closed. */ }
3800
+ reject(error);
3801
+ };
3802
+ const onAbort = () => fail(new Error('Agent logs connection aborted'));
3803
+ const timer = setTimeout(() => fail(new Error('Agent logs connection timed out')), 30_000);
3804
+ options.signal?.addEventListener('abort', onAbort, { once: true });
3805
+ ws.onopen = () => {
3806
+ if (settled)
3807
+ return;
3808
+ cleanup();
3809
+ try {
3810
+ // Install subscription handlers in the open callback, before the first
3811
+ // backend snapshot can arrive (not after an await in subscribeLogs).
3812
+ onConnected?.(ws);
3813
+ settled = true;
3814
+ resolve(ws);
3815
+ }
3816
+ catch (error) {
3817
+ fail(error instanceof Error ? error : new Error('Agent logs setup failed'));
3818
+ }
3819
+ };
3820
+ ws.onerror = () => fail(new Error('WebSocket connection failed'));
3821
+ ws.onclose = (event) => fail(new Error(`Agent logs connection closed (${event.code ?? 1006})`));
3822
+ if (options.signal?.aborted)
3823
+ onAbort();
3824
+ });
3825
+ }
3826
+ /**
3827
+ * Cluster logs replay history then stream lines. Runner logs instead deliver
3828
+ * bounded replacement snapshots to onCapturedLogs, without history_end.
3829
+ *
3830
+ * One mechanism rather than a REST read plus a separate subscribe. The two-step
3831
+ * form leaves a seam between the two calls that no server change can close,
3832
+ * because the fetch and the subscribe share no lock; the backend takes the
3833
+ * history snapshot and registers the subscriber under a single lock, so a line
3834
+ * arriving mid-replay is delivered exactly once.
3835
+ *
3836
+ * Deliberately does not reconnect. A reconnect replays history again, which
3837
+ * would duplicate lines into a consumer that has already rendered them;
3838
+ * reconnect policy belongs to the caller, which knows whether it is resuming
3839
+ * or restarting the view.
3840
+ */
3841
+ async subscribeLogs(agentIdOrName, handler, options = {}) {
3842
+ const follow = options.follow ?? true;
3843
+ return await new Promise((resolve, reject) => {
3844
+ void this.connectLogs(agentIdOrName, options, (ws) => {
3845
+ let settled = false;
3846
+ const finish = (error) => {
3847
+ if (settled)
3848
+ return;
3849
+ settled = true;
3850
+ options.signal?.removeEventListener('abort', onAbort);
3851
+ ws.onmessage = null;
3852
+ ws.onclose = null;
3853
+ ws.onerror = null;
3854
+ try {
3855
+ ws.close();
3856
+ }
3857
+ catch { /* Already closed by the peer. */ }
3858
+ if (error)
3859
+ reject(error);
3860
+ else
3861
+ resolve();
3862
+ };
3863
+ function onAbort() { finish(); }
3864
+ if (options.signal?.aborted) {
3865
+ finish();
3866
+ return;
3867
+ }
3868
+ options.signal?.addEventListener('abort', onAbort, { once: true });
3869
+ let queue = Promise.resolve();
3870
+ const enqueue = (callback) => {
3871
+ queue = queue.then(() => { if (!settled)
3872
+ return callback(); }).catch(error => {
3873
+ finish(error instanceof Error ? error : new Error('Agent logs callback failed'));
3874
+ });
3875
+ };
3876
+ enqueue(() => options.onReady?.());
3877
+ ws.onmessage = (event) => {
3878
+ const raw = typeof event.data === 'string' ? event.data : Buffer.isBuffer(event.data) ? event.data.toString() : '';
3879
+ if (!raw)
3880
+ return;
3881
+ const frame = parseAgentLogFrame(raw);
3882
+ enqueue(async () => {
3883
+ switch (frame.kind) {
3884
+ case 'log':
3885
+ await handler(frame.line);
3886
+ return;
3887
+ case 'capturedLogs':
3888
+ if (!options.onCapturedLogs)
3889
+ throw new Error('Runner log snapshots require an onCapturedLogs replacement handler');
3890
+ await options.onCapturedLogs(frame.snapshot);
3891
+ if (!follow)
3892
+ finish();
3893
+ return;
3894
+ case 'historyEnd':
3895
+ await options.onHistoryEnd?.();
3896
+ if (!follow)
3897
+ finish();
3898
+ return;
3899
+ case 'error':
3900
+ finish(new Error(frame.detail));
3901
+ return;
3902
+ default:
3903
+ return;
3904
+ }
3905
+ });
3906
+ };
3907
+ ws.onclose = (event) => {
3908
+ enqueue(async () => {
3909
+ await options.onClose?.({ code: event?.code ?? 1006, reason: event?.reason ?? '' });
3910
+ finish();
3911
+ });
3912
+ };
3913
+ ws.onerror = () => finish(new Error('WebSocket connection failed'));
3914
+ }).catch(reject);
3915
+ });
3916
+ }
3917
+ /**
3918
+ * Buzz-backed agents publish observer telemetry to a Nostr relay (kind
3919
+ * 24200, NIP-44-v2-encrypted to the owner). This subscribes the relay
3920
+ * directly and decrypts locally; see {@link subscribeBuzzActivity}.
3921
+ */
3922
+ async subscribeBuzzActivity(agentIdOrName, handlers) {
3923
+ return subscribeBuzzActivity(this, agentIdOrName, handlers);
3924
+ }
3925
+ /**
3926
+ * Legacy Buzz activity through an agent `hyper-acp` WS route. Hosted Buzz
3927
+ * launches no longer provision this route; the raw ACP path is outbound
3928
+ * `HYPER_ACP_WS_URL`. See {@link subscribeBuzzActivityRoute}.
3929
+ */
3930
+ async subscribeBuzzActivityRoute(agentIdOrName, handlers) {
3931
+ return subscribeBuzzActivityRoute(this, agentIdOrName, handlers);
3932
+ }
3933
+ async shellToken(agentIdOrName, shell, requestOptions = {}) {
3934
+ const selectedShell = shell ?? '/bin/bash';
3935
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3936
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/shell/token`;
3937
+ const rawToken = Object.keys(requestOptions).length === 0
3938
+ ? await this.agentHttp.post(path, { shell: selectedShell })
3939
+ : await this.agentHttp.post(path, { shell: selectedShell }, requestOptions);
3940
+ return validateAgentWsToken(rawToken, agentId, 'shell', selectedShell);
3941
+ }
3942
+ async shellConnect(agentIdOrName, shell, options = {}) {
3943
+ if (options.signal?.aborted)
3944
+ throw shellAbortError();
3945
+ const tokenTimeoutMs = options.tokenTimeoutMs ?? 15_000;
3946
+ const openTimeoutMs = options.openTimeoutMs ?? 10_000;
3947
+ const tokenDeadline = Date.now() + tokenTimeoutMs;
3948
+ const remainingTokenTime = () => Math.max(0, tokenDeadline - Date.now());
3949
+ const runBeforeTokenDeadline = (operation) => {
3950
+ const remaining = remainingTokenTime();
3951
+ if (remaining <= 0)
3952
+ return Promise.reject(new Error('Shell token request timed out'));
3953
+ return runShellOperation(operation, options.signal, remaining, 'Shell token request timed out');
3954
+ };
3955
+ const agentId = await runBeforeTokenDeadline((signal) => this.resolveAgentId(agentIdOrName, { signal }));
3956
+ const connectWithShell = async (requestedShell) => {
3957
+ const remaining = remainingTokenTime();
3958
+ if (remaining <= 0)
3959
+ throw new Error('Shell token request timed out');
3960
+ const requestOptions = {
3961
+ retries: 3,
3962
+ timeout: Math.max(500, Math.floor((tokenTimeoutMs - 3_000) / 3)),
3963
+ retryStatuses: [429, 502, 503, 504],
3964
+ };
3965
+ const tokenData = await runBeforeTokenDeadline((signal) => {
3966
+ requestOptions.signal = signal;
3967
+ return this.shellToken(agentId, requestedShell, requestOptions);
3968
+ });
3969
+ const parsed = new URL(tokenData.ws_url);
3970
+ parsed.searchParams.set('token', tokenData.token);
3971
+ parsed.searchParams.set('shell', tokenData.shell);
3972
+ const WebSocketImpl = preferredWebSocket();
3973
+ const ws = new WebSocketImpl(parsed.toString());
3974
+ ws.binaryType = 'arraybuffer';
3975
+ return await new Promise((resolve, reject) => {
3976
+ let settled = false;
3977
+ const abortConnection = () => finish(shellAbortError());
3978
+ const openTimer = setTimeout(() => finish(new Error('Shell connection timed out')), openTimeoutMs);
3979
+ const cleanup = () => {
3980
+ clearTimeout(openTimer);
3981
+ options.signal?.removeEventListener('abort', abortConnection);
3982
+ };
3983
+ const finish = (error) => {
3984
+ if (settled)
3985
+ return;
3986
+ settled = true;
3987
+ cleanup();
3988
+ if (error) {
3989
+ try {
3990
+ ws.close();
3991
+ }
3992
+ catch {
3993
+ // The browser may have already finalized a failed socket.
3994
+ }
3995
+ reject(error);
3996
+ }
3997
+ else {
3998
+ resolve(ws);
3999
+ }
4000
+ };
4001
+ if (options.signal?.aborted) {
4002
+ finish(shellAbortError());
4003
+ return;
4004
+ }
4005
+ options.signal?.addEventListener('abort', abortConnection, { once: true });
4006
+ ws.onopen = () => {
4007
+ finish();
4008
+ };
4009
+ ws.onerror = () => undefined;
4010
+ ws.onclose = (event) => {
4011
+ const reason = event.reason ? `: ${event.reason}` : '';
4012
+ const error = new Error(`WebSocket closed before opening${reason}`);
4013
+ error.closeCode = event.code;
4014
+ error.closeReason = event.reason;
4015
+ finish(error);
4016
+ };
4017
+ });
4018
+ };
4019
+ return connectWithShell(shell ?? '/bin/bash');
4020
+ }
4021
+ }
4022
+ //# sourceMappingURL=agents.js.map