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