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