@hypercli.com/sdk 2026.6.26 → 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 +78 -0
- 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 +534 -43
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +1890 -110
- 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 +650 -15
- package/dist/openclaw/gateway.d.ts.map +1 -1
- package/dist/openclaw/gateway.js +1708 -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,13 +6,96 @@ 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
|
-
export const DEFAULT_OPENCLAW_PRO_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-
|
|
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
|
+
]);
|
|
16
99
|
export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
|
|
17
100
|
OPENCLAW_MEMORY_SEARCH_ENABLED: '1',
|
|
18
101
|
OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START: '0',
|
|
@@ -21,11 +104,306 @@ export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
|
|
|
21
104
|
OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS: '30000',
|
|
22
105
|
OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES: '0',
|
|
23
106
|
};
|
|
24
|
-
const
|
|
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
|
+
]);
|
|
25
127
|
const DEFAULT_OPENCLAW_SYNC_ROOT = '/home/node';
|
|
26
|
-
export const AGENT_FILE_MAX_BYTES =
|
|
128
|
+
export const AGENT_FILE_MAX_BYTES = 250 * 1024 * 1024;
|
|
27
129
|
export const AGENT_FILE_TRANSFER_CHUNK_BYTES = 64 * 1024;
|
|
28
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
|
+
}
|
|
29
407
|
function parseDate(value) {
|
|
30
408
|
if (typeof value !== 'string' || !value)
|
|
31
409
|
return null;
|
|
@@ -52,6 +430,8 @@ function sleep(ms) {
|
|
|
52
430
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
431
|
}
|
|
54
432
|
function isOpenClawHydrationData(data) {
|
|
433
|
+
if (data.runtime === 'openclaw' || data.runtime === 'openclaw-pro')
|
|
434
|
+
return true;
|
|
55
435
|
const routes = data.routes;
|
|
56
436
|
if (routes && typeof routes === 'object' && !Array.isArray(routes) && routes.openclaw) {
|
|
57
437
|
return true;
|
|
@@ -59,15 +439,128 @@ function isOpenClawHydrationData(data) {
|
|
|
59
439
|
const launchRoutes = data.launch_config?.routes;
|
|
60
440
|
return !!(launchRoutes && typeof launchRoutes === 'object' && !Array.isArray(launchRoutes) && launchRoutes.openclaw);
|
|
61
441
|
}
|
|
442
|
+
function isCodingAgentRuntime(runtime) {
|
|
443
|
+
return typeof runtime === 'string' && CODING_AGENT_RUNTIMES.has(runtime);
|
|
444
|
+
}
|
|
62
445
|
function isTruthyEnv(value) {
|
|
63
446
|
return ['1', 'true', 'yes', 'on', 'enabled'].includes(String(value ?? '').trim().toLowerCase());
|
|
64
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
|
+
}
|
|
65
559
|
function isOpenClawProHydrationData(data) {
|
|
66
560
|
const launchConfig = data.launch_config;
|
|
67
561
|
if (!launchConfig || typeof launchConfig !== 'object' || Array.isArray(launchConfig))
|
|
68
562
|
return false;
|
|
69
|
-
|
|
70
|
-
if (env && typeof env === 'object' && !Array.isArray(env) && isTruthyEnv(env.OPENCLAW_DESKTOP_ENABLED)) {
|
|
563
|
+
if (launchConfigHasDesktop(launchConfig)) {
|
|
71
564
|
return true;
|
|
72
565
|
}
|
|
73
566
|
const image = String(launchConfig.image ?? '');
|
|
@@ -164,12 +657,78 @@ function encodeFilePath(path) {
|
|
|
164
657
|
.map((part) => encodeURIComponent(part))
|
|
165
658
|
.join('/');
|
|
166
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
|
+
}
|
|
167
672
|
function decodeUtf8(content) {
|
|
168
673
|
return new TextDecoder().decode(content);
|
|
169
674
|
}
|
|
170
675
|
function encodeUtf8(content) {
|
|
171
676
|
return new TextEncoder().encode(content);
|
|
172
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
|
+
}
|
|
173
732
|
function toUint8Array(content) {
|
|
174
733
|
if (typeof content === 'string')
|
|
175
734
|
return encodeUtf8(content);
|
|
@@ -192,6 +751,16 @@ function agentStateFromDict(data) {
|
|
|
192
751
|
podName: data.pod_name ?? '',
|
|
193
752
|
state: data.state ?? 'unknown',
|
|
194
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,
|
|
195
764
|
cpu: data.cpu ?? 0,
|
|
196
765
|
memory: data.memory ?? 0,
|
|
197
766
|
hostname: data.hostname ?? null,
|
|
@@ -233,15 +802,18 @@ export function buildAgentConfig(config = {}, options = {}) {
|
|
|
233
802
|
preparedConfig.agents = agentsConfig;
|
|
234
803
|
}
|
|
235
804
|
const env = { ...(options.env ?? {}) };
|
|
236
|
-
let gatewayToken =
|
|
237
|
-
if (
|
|
238
|
-
gatewayToken =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
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
|
+
}
|
|
245
817
|
}
|
|
246
818
|
}
|
|
247
819
|
const prepared = {};
|
|
@@ -271,6 +843,8 @@ export function buildAgentConfig(config = {}, options = {}) {
|
|
|
271
843
|
prepared.registry_url = options.registryUrl;
|
|
272
844
|
if (options.registryAuth !== undefined && options.registryAuth !== null)
|
|
273
845
|
prepared.registry_auth = options.registryAuth;
|
|
846
|
+
if (options.restart !== undefined && options.restart !== null)
|
|
847
|
+
prepared.restart = options.restart;
|
|
274
848
|
return { config: prepared, gatewayToken };
|
|
275
849
|
}
|
|
276
850
|
function defaultOpenClawImage(image) {
|
|
@@ -283,6 +857,203 @@ function defaultOpenClawProImage(image) {
|
|
|
283
857
|
return image;
|
|
284
858
|
return DEFAULT_OPENCLAW_PRO_IMAGE;
|
|
285
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
|
+
}
|
|
286
1057
|
export function buildOpenClawRoutes(options = {}) {
|
|
287
1058
|
const routes = {};
|
|
288
1059
|
if (options.includeGateway ?? true) {
|
|
@@ -292,7 +1063,7 @@ export function buildOpenClawRoutes(options = {}) {
|
|
|
292
1063
|
prefix: options.gatewayPrefix ?? '',
|
|
293
1064
|
};
|
|
294
1065
|
}
|
|
295
|
-
if (options.includeDesktop ??
|
|
1066
|
+
if (options.includeDesktop ?? false) {
|
|
296
1067
|
routes.desktop = {
|
|
297
1068
|
port: options.desktopPort ?? 3000,
|
|
298
1069
|
auth: options.desktopAuth ?? true,
|
|
@@ -335,6 +1106,27 @@ export function buildOpenClawMemoryIndexEnv(memoryIndex = null) {
|
|
|
335
1106
|
}
|
|
336
1107
|
return env;
|
|
337
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
|
+
}
|
|
338
1130
|
async function getFsPromises() {
|
|
339
1131
|
return import('node:fs/promises');
|
|
340
1132
|
}
|
|
@@ -349,6 +1141,16 @@ export class Agent {
|
|
|
349
1141
|
podName;
|
|
350
1142
|
state;
|
|
351
1143
|
name;
|
|
1144
|
+
handle;
|
|
1145
|
+
displayName;
|
|
1146
|
+
avatarUrl;
|
|
1147
|
+
displayIdentity;
|
|
1148
|
+
runtime;
|
|
1149
|
+
managed;
|
|
1150
|
+
isLaunchable;
|
|
1151
|
+
gatewayId;
|
|
1152
|
+
runtimeKeyAlias;
|
|
1153
|
+
relayKey;
|
|
352
1154
|
cpu;
|
|
353
1155
|
memory;
|
|
354
1156
|
hostname;
|
|
@@ -375,6 +1177,16 @@ export class Agent {
|
|
|
375
1177
|
this.podName = fields.podName;
|
|
376
1178
|
this.state = fields.state;
|
|
377
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;
|
|
378
1190
|
this.cpu = fields.cpu;
|
|
379
1191
|
this.memory = fields.memory;
|
|
380
1192
|
this.hostname = fields.hostname ?? null;
|
|
@@ -422,6 +1234,11 @@ export class Agent {
|
|
|
422
1234
|
get vncUrl() {
|
|
423
1235
|
return this.desktopUrl;
|
|
424
1236
|
}
|
|
1237
|
+
browserDesktopUrl(token, options = {}) {
|
|
1238
|
+
if (!this.desktopUrl)
|
|
1239
|
+
return null;
|
|
1240
|
+
return buildBrowserDesktopUrl(this.desktopUrl, token, options);
|
|
1241
|
+
}
|
|
425
1242
|
get shellUrl() {
|
|
426
1243
|
return this.routeUrl('shell');
|
|
427
1244
|
}
|
|
@@ -431,6 +1248,13 @@ export class Agent {
|
|
|
431
1248
|
get isRunning() {
|
|
432
1249
|
return this.state.toLowerCase() === 'running';
|
|
433
1250
|
}
|
|
1251
|
+
get hasDesktop() {
|
|
1252
|
+
return agentConfigHasDesktop({
|
|
1253
|
+
launchConfig: this.launchConfig,
|
|
1254
|
+
routes: this.routes,
|
|
1255
|
+
ports: this.ports,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
434
1258
|
requireDeployments() {
|
|
435
1259
|
if (!this._deployments) {
|
|
436
1260
|
throw new Error('Agent is not bound to a Deployments client');
|
|
@@ -469,23 +1293,45 @@ export class Agent {
|
|
|
469
1293
|
async health() {
|
|
470
1294
|
return this.requireDeployments().health(this);
|
|
471
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
|
+
}
|
|
472
1315
|
async filesList(path = '', source = 'auto') {
|
|
473
|
-
return this.
|
|
1316
|
+
return this.files.list(path, source);
|
|
474
1317
|
}
|
|
475
|
-
async fileReadBytes(path, source = 'auto') {
|
|
476
|
-
return this.
|
|
1318
|
+
async fileReadBytes(path, source = 'auto', options) {
|
|
1319
|
+
return this.files.readBytes(path, source, options);
|
|
477
1320
|
}
|
|
478
|
-
async
|
|
479
|
-
return
|
|
1321
|
+
async fileReadBytesWithMetadata(path, source = 'auto', options) {
|
|
1322
|
+
return this.files.readBytesWithMetadata(path, source, options);
|
|
1323
|
+
}
|
|
1324
|
+
async fileRead(path, source = 'auto', options) {
|
|
1325
|
+
return this.files.read(path, source, options);
|
|
480
1326
|
}
|
|
481
1327
|
async fileWriteBytes(path, content, destination = 'auto') {
|
|
482
|
-
return this.
|
|
1328
|
+
return this.files.writeBytes(path, content, destination);
|
|
483
1329
|
}
|
|
484
1330
|
async fileWrite(path, content, destination = 'auto') {
|
|
485
|
-
return this.
|
|
1331
|
+
return this.files.write(path, content, destination);
|
|
486
1332
|
}
|
|
487
1333
|
async fileDelete(path, options = {}) {
|
|
488
|
-
return this.
|
|
1334
|
+
return this.files.delete(path, options);
|
|
489
1335
|
}
|
|
490
1336
|
async cpTo(localPath, remotePath) {
|
|
491
1337
|
return this.requireDeployments().cpTo(this, localPath, remotePath);
|
|
@@ -493,8 +1339,346 @@ export class Agent {
|
|
|
493
1339
|
async cpFrom(remotePath, localPath) {
|
|
494
1340
|
return this.requireDeployments().cpFrom(this, remotePath, localPath);
|
|
495
1341
|
}
|
|
496
|
-
async shellConnect(shell) {
|
|
497
|
-
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));
|
|
498
1682
|
}
|
|
499
1683
|
}
|
|
500
1684
|
export class OpenClawAgent extends Agent {
|
|
@@ -508,7 +1692,7 @@ export class OpenClawAgent extends Agent {
|
|
|
508
1692
|
static fromDict(data) {
|
|
509
1693
|
return new OpenClawAgent({
|
|
510
1694
|
...agentStateFromDict(data),
|
|
511
|
-
gatewayUrl:
|
|
1695
|
+
gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
|
|
512
1696
|
gatewayToken: data.gateway_token ?? null,
|
|
513
1697
|
});
|
|
514
1698
|
}
|
|
@@ -537,6 +1721,15 @@ export class OpenClawAgent extends Agent {
|
|
|
537
1721
|
* token from `OPENCLAW_GATEWAY_TOKEN` in the agent env route.
|
|
538
1722
|
*/
|
|
539
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();
|
|
540
1733
|
if (this.gatewayToken && this.gatewayUrl) {
|
|
541
1734
|
return {
|
|
542
1735
|
agent_id: this.id,
|
|
@@ -549,29 +1742,98 @@ export class OpenClawAgent extends Agent {
|
|
|
549
1742
|
const deployments = this.requireDeployments();
|
|
550
1743
|
const deadline = Date.now() + timeoutMs;
|
|
551
1744
|
let lastError = null;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
}));
|
|
563
1809
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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);
|
|
573
1832
|
}
|
|
574
|
-
|
|
1833
|
+
}
|
|
1834
|
+
finally {
|
|
1835
|
+
clearTimeout(timeoutId);
|
|
1836
|
+
options.signal?.removeEventListener('abort', forwardAbort);
|
|
575
1837
|
}
|
|
576
1838
|
}
|
|
577
1839
|
async resolveGatewayToken() {
|
|
@@ -603,6 +1865,7 @@ export class OpenClawAgent extends Agent {
|
|
|
603
1865
|
onHello: options.onHello,
|
|
604
1866
|
onClose: options.onClose,
|
|
605
1867
|
onGap: options.onGap,
|
|
1868
|
+
onProtocolError: options.onProtocolError,
|
|
606
1869
|
onPairing: options.onPairing,
|
|
607
1870
|
});
|
|
608
1871
|
}
|
|
@@ -614,6 +1877,47 @@ export class OpenClawAgent extends Agent {
|
|
|
614
1877
|
await client.connect();
|
|
615
1878
|
return client;
|
|
616
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
|
+
}
|
|
617
1921
|
async gatewayStatus(options = {}) {
|
|
618
1922
|
const client = await this.connect(options);
|
|
619
1923
|
try {
|
|
@@ -665,6 +1969,64 @@ export class OpenClawAgent extends Agent {
|
|
|
665
1969
|
client.close();
|
|
666
1970
|
}
|
|
667
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
|
+
}
|
|
668
2030
|
async configApply(config, options = {}) {
|
|
669
2031
|
const client = await this.connect(options);
|
|
670
2032
|
try {
|
|
@@ -692,6 +2054,15 @@ export class OpenClawAgent extends Agent {
|
|
|
692
2054
|
client.close();
|
|
693
2055
|
}
|
|
694
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
|
+
}
|
|
695
2066
|
async *chatSend(message, sessionKey, options = {}) {
|
|
696
2067
|
const client = await this.connect(options);
|
|
697
2068
|
try {
|
|
@@ -706,7 +2077,25 @@ export class OpenClawAgent extends Agent {
|
|
|
706
2077
|
async channelsStatus(options = {}) {
|
|
707
2078
|
const client = await this.connect(options);
|
|
708
2079
|
try {
|
|
709
|
-
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);
|
|
710
2099
|
}
|
|
711
2100
|
finally {
|
|
712
2101
|
client.close();
|
|
@@ -741,6 +2130,7 @@ export class OpenClawAgent extends Agent {
|
|
|
741
2130
|
return await client.webLoginWait({
|
|
742
2131
|
timeoutMs: options.timeoutMs,
|
|
743
2132
|
accountId: options.accountId,
|
|
2133
|
+
currentQrDataUrl: options.currentQrDataUrl,
|
|
744
2134
|
});
|
|
745
2135
|
}
|
|
746
2136
|
finally {
|
|
@@ -841,7 +2231,7 @@ export class OpenClawAgent extends Agent {
|
|
|
841
2231
|
async chatSendMessage(message, options = {}) {
|
|
842
2232
|
const client = await this.connect(options);
|
|
843
2233
|
try {
|
|
844
|
-
return await client.sendChat(message, options.sessionKey
|
|
2234
|
+
return await client.sendChat(message, options.sessionKey, options.agentId, options.attachments);
|
|
845
2235
|
}
|
|
846
2236
|
finally {
|
|
847
2237
|
client.close();
|
|
@@ -984,12 +2374,39 @@ export class OpenClawAgent extends Agent {
|
|
|
984
2374
|
client.close();
|
|
985
2375
|
}
|
|
986
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
|
+
}
|
|
987
2404
|
}
|
|
988
2405
|
export class OpenClawProAgent extends OpenClawAgent {
|
|
989
2406
|
static fromDict(data) {
|
|
990
2407
|
return new OpenClawProAgent({
|
|
991
2408
|
...agentStateFromDict(data),
|
|
992
|
-
gatewayUrl:
|
|
2409
|
+
gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
|
|
993
2410
|
gatewayToken: data.gateway_token ?? null,
|
|
994
2411
|
});
|
|
995
2412
|
}
|
|
@@ -1000,12 +2417,13 @@ export class Deployments {
|
|
|
1000
2417
|
apiBase;
|
|
1001
2418
|
agentsWsUrl;
|
|
1002
2419
|
agentHttp;
|
|
1003
|
-
constructor(http, agentApiKey, agentApiBase, agentsWsUrl) {
|
|
2420
|
+
constructor(http, agentApiKey, agentApiBase, agentsWsUrl, requestTimeout) {
|
|
1004
2421
|
this.http = http;
|
|
1005
2422
|
this.apiKey = agentApiKey || http.apiKey;
|
|
1006
2423
|
this.apiBase = resolveAgentsApiBase(agentApiBase || getAgentsApiBaseUrl());
|
|
1007
2424
|
this.agentsWsUrl = normalizeAgentsWsUrl(agentsWsUrl || getConfigValue('AGENTS_WS_URL') || defaultAgentsWsUrl(this.apiBase));
|
|
1008
|
-
|
|
2425
|
+
const agentTimeout = requestTimeout ?? (http instanceof HTTPClient ? http.timeout : undefined);
|
|
2426
|
+
this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey, agentTimeout) : http;
|
|
1009
2427
|
}
|
|
1010
2428
|
get agentApiKey() {
|
|
1011
2429
|
return this.apiKey;
|
|
@@ -1015,7 +2433,22 @@ export class Deployments {
|
|
|
1015
2433
|
}
|
|
1016
2434
|
hydrateAgent(data) {
|
|
1017
2435
|
let agent;
|
|
1018
|
-
if (
|
|
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)) {
|
|
1019
2452
|
agent = OpenClawProAgent.fromDict(data);
|
|
1020
2453
|
}
|
|
1021
2454
|
else if (isOpenClawHydrationData(data)) {
|
|
@@ -1026,8 +2459,52 @@ export class Deployments {
|
|
|
1026
2459
|
}
|
|
1027
2460
|
return bindAgent(agent, this);
|
|
1028
2461
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
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;
|
|
1031
2508
|
}
|
|
1032
2509
|
async fetchRaw(path, init = {}) {
|
|
1033
2510
|
const headers = new Headers(init.headers ?? {});
|
|
@@ -1056,18 +2533,25 @@ export class Deployments {
|
|
|
1056
2533
|
return response;
|
|
1057
2534
|
}
|
|
1058
2535
|
async create(options = {}) {
|
|
1059
|
-
const { config, gatewayToken } = buildAgentConfig(options.config ?? {},
|
|
2536
|
+
const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, {
|
|
2537
|
+
...options,
|
|
2538
|
+
injectGatewayToken: options.injectGatewayToken ?? !isCodingAgentRuntime(options.runtime),
|
|
2539
|
+
});
|
|
1060
2540
|
const body = { ...config, start: options.start ?? true };
|
|
1061
2541
|
if (options.dryRun)
|
|
1062
2542
|
body.dry_run = true;
|
|
1063
2543
|
if (options.name)
|
|
1064
2544
|
body.name = options.name;
|
|
2545
|
+
if (options.handle !== undefined)
|
|
2546
|
+
body.handle = options.handle;
|
|
1065
2547
|
if (options.size)
|
|
1066
2548
|
body.size = options.size;
|
|
1067
2549
|
if (options.meta?.ui)
|
|
1068
2550
|
body.meta = { ui: structuredClone(options.meta.ui) };
|
|
1069
2551
|
if (options.tags?.length)
|
|
1070
2552
|
body.tags = [...options.tags];
|
|
2553
|
+
if (options.runtime)
|
|
2554
|
+
body.runtime = options.runtime;
|
|
1071
2555
|
const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body);
|
|
1072
2556
|
const agent = this.hydrateAgent(data);
|
|
1073
2557
|
if (agent instanceof OpenClawAgent) {
|
|
@@ -1079,8 +2563,13 @@ export class Deployments {
|
|
|
1079
2563
|
return agent;
|
|
1080
2564
|
}
|
|
1081
2565
|
async createOpenClaw(options = {}) {
|
|
1082
|
-
const effectiveOptions = { ...options };
|
|
1083
|
-
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
|
+
};
|
|
1084
2573
|
if (options.routes === undefined) {
|
|
1085
2574
|
effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
|
|
1086
2575
|
}
|
|
@@ -1094,47 +2583,226 @@ export class Deployments {
|
|
|
1094
2583
|
async createOpenClawPro(options = {}) {
|
|
1095
2584
|
return this.createOpenClaw({
|
|
1096
2585
|
...options,
|
|
2586
|
+
runtime: 'openclaw-pro',
|
|
1097
2587
|
env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
|
|
1098
2588
|
image: defaultOpenClawProImage(options.image),
|
|
1099
2589
|
openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
|
|
1100
2590
|
});
|
|
1101
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
|
+
}
|
|
1102
2654
|
async budget() {
|
|
1103
2655
|
return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/budget`);
|
|
1104
2656
|
}
|
|
1105
|
-
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);
|
|
1106
2665
|
return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/metrics`);
|
|
1107
2666
|
}
|
|
1108
|
-
async list() {
|
|
1109
|
-
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);
|
|
1110
2695
|
const items = Array.isArray(data) ? data : data.items ?? [];
|
|
1111
2696
|
return items.map((item) => this.hydrateAgent(item));
|
|
1112
2697
|
}
|
|
1113
|
-
async get(
|
|
1114
|
-
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);
|
|
1115
2728
|
return this.hydrateAgent(data);
|
|
1116
2729
|
}
|
|
1117
|
-
async
|
|
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}`);
|
|
2749
|
+
return this.hydrateAgent(data);
|
|
2750
|
+
}
|
|
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);
|
|
1118
2772
|
const deadline = Date.now() + timeoutMs;
|
|
1119
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
|
+
};
|
|
1120
2786
|
while (Date.now() < deadline) {
|
|
1121
2787
|
const agent = await this.get(agentId);
|
|
2788
|
+
lastAgent = agent;
|
|
1122
2789
|
lastState = String(agent.state || '');
|
|
1123
2790
|
if (lastState.toLowerCase() === 'running') {
|
|
1124
2791
|
return agent;
|
|
1125
2792
|
}
|
|
1126
|
-
if (
|
|
1127
|
-
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)}`);
|
|
1128
2795
|
}
|
|
1129
2796
|
await sleep(pollIntervalMs);
|
|
1130
2797
|
}
|
|
1131
|
-
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)})`);
|
|
1132
2799
|
}
|
|
1133
|
-
async start(
|
|
2800
|
+
async start(agentIdOrName, options = {}) {
|
|
1134
2801
|
const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, options);
|
|
1135
2802
|
const body = { ...config };
|
|
1136
2803
|
if (options.dryRun)
|
|
1137
2804
|
body.dry_run = true;
|
|
2805
|
+
const agentId = await this.resolveAgentId(agentIdOrName);
|
|
1138
2806
|
const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, body);
|
|
1139
2807
|
const agent = this.hydrateAgent(data);
|
|
1140
2808
|
if (agent instanceof OpenClawAgent) {
|
|
@@ -1145,9 +2813,14 @@ export class Deployments {
|
|
|
1145
2813
|
agent.entrypoint = [...(config.entrypoint ?? [])];
|
|
1146
2814
|
return agent;
|
|
1147
2815
|
}
|
|
1148
|
-
async startOpenClaw(
|
|
2816
|
+
async startOpenClaw(agentIdOrName, options = {}) {
|
|
1149
2817
|
const effectiveOptions = { ...options };
|
|
1150
|
-
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
|
+
};
|
|
1151
2824
|
if (options.routes === undefined) {
|
|
1152
2825
|
effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
|
|
1153
2826
|
}
|
|
@@ -1156,46 +2829,71 @@ export class Deployments {
|
|
|
1156
2829
|
effectiveOptions.syncRoot = DEFAULT_OPENCLAW_SYNC_ROOT;
|
|
1157
2830
|
if (effectiveOptions.syncEnabled === undefined)
|
|
1158
2831
|
effectiveOptions.syncEnabled = true;
|
|
1159
|
-
return this.start(
|
|
2832
|
+
return this.start(agentIdOrName, effectiveOptions);
|
|
1160
2833
|
}
|
|
1161
|
-
async startOpenClawPro(
|
|
1162
|
-
return this.startOpenClaw(
|
|
2834
|
+
async startOpenClawPro(agentIdOrName, options = {}) {
|
|
2835
|
+
return this.startOpenClaw(agentIdOrName, {
|
|
1163
2836
|
...options,
|
|
1164
2837
|
env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
|
|
1165
2838
|
image: defaultOpenClawProImage(options.image),
|
|
1166
2839
|
openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
|
|
1167
2840
|
});
|
|
1168
2841
|
}
|
|
1169
|
-
async update(
|
|
2842
|
+
async update(agentIdOrName, options = {}) {
|
|
1170
2843
|
const body = {};
|
|
1171
2844
|
if (options.name !== undefined)
|
|
1172
2845
|
body.name = options.name;
|
|
2846
|
+
if (options.handle !== undefined)
|
|
2847
|
+
body.handle = options.handle;
|
|
1173
2848
|
if (options.size !== undefined)
|
|
1174
2849
|
body.size = options.size;
|
|
2850
|
+
if (options.launchConfig !== undefined)
|
|
2851
|
+
body.launch_config = options.launchConfig;
|
|
1175
2852
|
if (options.refreshFromLagoon !== undefined)
|
|
1176
2853
|
body.refresh_from_lagoon = options.refreshFromLagoon;
|
|
1177
2854
|
if (options.lastError !== undefined)
|
|
1178
2855
|
body.last_error = options.lastError;
|
|
2856
|
+
const agentId = await this.resolveAgentId(agentIdOrName);
|
|
1179
2857
|
const data = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}`, body);
|
|
1180
2858
|
return this.hydrateAgent(data);
|
|
1181
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
|
+
}
|
|
1182
2869
|
async resize(agentId, options) {
|
|
1183
2870
|
return this.update(agentId, options);
|
|
1184
2871
|
}
|
|
1185
|
-
|
|
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);
|
|
1186
2881
|
const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/stop`);
|
|
1187
2882
|
return this.hydrateAgent(data);
|
|
1188
2883
|
}
|
|
1189
|
-
async delete(
|
|
2884
|
+
async delete(agentIdOrName) {
|
|
2885
|
+
const agentId = await this.resolveAgentId(agentIdOrName);
|
|
1190
2886
|
return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
|
|
1191
2887
|
}
|
|
1192
|
-
async refreshToken(
|
|
2888
|
+
async refreshToken(agentIdOrName) {
|
|
2889
|
+
const agentId = await this.resolveAgentId(agentIdOrName);
|
|
1193
2890
|
return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/token`);
|
|
1194
2891
|
}
|
|
1195
|
-
async createScopedKey(
|
|
2892
|
+
async createScopedKey(agentIdOrName, name) {
|
|
1196
2893
|
const payload = {};
|
|
1197
2894
|
if (name)
|
|
1198
2895
|
payload.name = name;
|
|
2896
|
+
const agentId = await this.resolveAgentId(agentIdOrName);
|
|
1199
2897
|
return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/keys`, Object.keys(payload).length ? payload : undefined);
|
|
1200
2898
|
}
|
|
1201
2899
|
async webSearch(query, options = {}) {
|
|
@@ -1218,14 +2916,19 @@ export class Deployments {
|
|
|
1218
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 });
|
|
1219
2917
|
return (await response.json());
|
|
1220
2918
|
}
|
|
1221
|
-
async logsToken(
|
|
2919
|
+
async logsToken(agentIdOrName) {
|
|
2920
|
+
const agentId = await this.resolveAgentId(agentIdOrName);
|
|
1222
2921
|
return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/logs/token`);
|
|
1223
2922
|
}
|
|
1224
|
-
async env(
|
|
1225
|
-
|
|
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);
|
|
1226
2929
|
}
|
|
1227
2930
|
async exec(target, command, options = {}) {
|
|
1228
|
-
const agentId = this.agentIdFor(target);
|
|
2931
|
+
const agentId = await this.agentIdFor(target);
|
|
1229
2932
|
const payload = {
|
|
1230
2933
|
command,
|
|
1231
2934
|
timeout: options.timeout ?? 30,
|
|
@@ -1251,20 +2954,20 @@ export class Deployments {
|
|
|
1251
2954
|
return (await response.json());
|
|
1252
2955
|
}
|
|
1253
2956
|
async filesList(target, path = '', source = 'auto') {
|
|
1254
|
-
const
|
|
1255
|
-
const
|
|
1256
|
-
const
|
|
1257
|
-
const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files${suffix}?${params.toString()}`);
|
|
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()}`);
|
|
1258
2960
|
const payload = (await response.json());
|
|
1259
2961
|
return [...(payload.directories ?? []), ...(payload.files ?? [])];
|
|
1260
2962
|
}
|
|
1261
|
-
async
|
|
1262
|
-
const
|
|
1263
|
-
const
|
|
1264
|
-
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()}`, {
|
|
1265
2967
|
redirect: 'follow',
|
|
2968
|
+
signal: options?.signal,
|
|
1266
2969
|
});
|
|
1267
|
-
const bytes =
|
|
2970
|
+
const bytes = await readResponseBytes(response, path, options?.maxBytes);
|
|
1268
2971
|
const contentType = response.headers.get('content-type') || '';
|
|
1269
2972
|
if (contentType.includes('application/json')) {
|
|
1270
2973
|
try {
|
|
@@ -1279,19 +2982,24 @@ export class Deployments {
|
|
|
1279
2982
|
}
|
|
1280
2983
|
}
|
|
1281
2984
|
}
|
|
1282
|
-
return bytes;
|
|
2985
|
+
return { content: bytes, mimeType: contentType || undefined };
|
|
2986
|
+
}
|
|
2987
|
+
async fileReadBytes(target, path, source = 'auto', options) {
|
|
2988
|
+
return (await this.fileReadBytesWithMetadata(target, path, source, options)).content;
|
|
1283
2989
|
}
|
|
1284
|
-
async fileRead(target, path, source = 'auto') {
|
|
1285
|
-
return decodeUtf8(await this.fileReadBytes(target, path, source));
|
|
2990
|
+
async fileRead(target, path, source = 'auto', options) {
|
|
2991
|
+
return decodeUtf8(await this.fileReadBytes(target, path, source, options));
|
|
1286
2992
|
}
|
|
1287
2993
|
async fileWriteBytes(target, path, content, destination = 'auto') {
|
|
2994
|
+
path = normalizeWritableBackendFilePath(path);
|
|
1288
2995
|
const encodedPath = encodeFilePath(path);
|
|
1289
2996
|
const params = new URLSearchParams({ destination });
|
|
1290
2997
|
const bytes = toUint8Array(content);
|
|
1291
2998
|
if (bytes.byteLength > AGENT_FILE_MAX_BYTES) {
|
|
1292
2999
|
throw new Error(`Agent file writes are limited to ${AGENT_FILE_MAX_BYTES / 1024 / 1024} MiB`);
|
|
1293
3000
|
}
|
|
1294
|
-
const
|
|
3001
|
+
const agentId = await this.agentIdFor(target);
|
|
3002
|
+
const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}?${params.toString()}`, {
|
|
1295
3003
|
method: 'POST',
|
|
1296
3004
|
headers: { 'Content-Type': 'application/octet-stream' },
|
|
1297
3005
|
body: bytes,
|
|
@@ -1302,12 +3010,16 @@ export class Deployments {
|
|
|
1302
3010
|
return this.fileWriteBytes(target, path, content, destination);
|
|
1303
3011
|
}
|
|
1304
3012
|
async fileDelete(target, path, options = {}) {
|
|
3013
|
+
path = normalizeWritableBackendFilePath(path);
|
|
1305
3014
|
const encodedPath = encodeFilePath(path);
|
|
1306
3015
|
const params = new URLSearchParams();
|
|
1307
3016
|
if (options.recursive)
|
|
1308
3017
|
params.set('recursive', 'true');
|
|
3018
|
+
if (options.source && options.source !== 'auto')
|
|
3019
|
+
params.set('source', options.source);
|
|
1309
3020
|
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
1310
|
-
const
|
|
3021
|
+
const agentId = await this.agentIdFor(target);
|
|
3022
|
+
const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}${suffix}`, { method: 'DELETE' });
|
|
1311
3023
|
return (await response.json());
|
|
1312
3024
|
}
|
|
1313
3025
|
async cpTo(target, localPath, remotePath) {
|
|
@@ -1326,7 +3038,8 @@ export class Deployments {
|
|
|
1326
3038
|
await fs.writeFile(destination, content);
|
|
1327
3039
|
return destination;
|
|
1328
3040
|
}
|
|
1329
|
-
async logsConnect(
|
|
3041
|
+
async logsConnect(agentIdOrName, options = {}) {
|
|
3042
|
+
const agentId = await this.resolveAgentId(agentIdOrName);
|
|
1330
3043
|
const tokenData = await this.logsToken(agentId);
|
|
1331
3044
|
const container = options.container ?? 'reef';
|
|
1332
3045
|
const tailLines = options.tailLines ?? 100;
|
|
@@ -1348,31 +3061,91 @@ export class Deployments {
|
|
|
1348
3061
|
};
|
|
1349
3062
|
});
|
|
1350
3063
|
}
|
|
1351
|
-
async shellToken(
|
|
3064
|
+
async shellToken(agentIdOrName, shell, dryRun = false, requestOptions = {}) {
|
|
1352
3065
|
const selectedShell = shell ?? '/bin/bash';
|
|
1353
3066
|
const payload = { shell: selectedShell };
|
|
1354
3067
|
if (dryRun)
|
|
1355
3068
|
payload.dry_run = true;
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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 }));
|
|
1359
3089
|
const connectWithShell = async (requestedShell) => {
|
|
1360
|
-
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
|
+
});
|
|
1361
3102
|
const baseUrl = `${this.agentsWsUrl}/shell/${agentId}`;
|
|
1362
3103
|
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
1363
3104
|
const wsUrl = `${baseUrl}${separator}jwt=${encodeURIComponent(tokenData.jwt)}` +
|
|
1364
3105
|
`&shell=${encodeURIComponent(tokenData.shell || requestedShell)}`;
|
|
1365
3106
|
const ws = new WebSocket(wsUrl);
|
|
3107
|
+
ws.binaryType = 'arraybuffer';
|
|
1366
3108
|
return await new Promise((resolve, reject) => {
|
|
1367
3109
|
let settled = false;
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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);
|
|
1371
3115
|
};
|
|
1372
|
-
|
|
1373
|
-
if (
|
|
1374
|
-
|
|
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);
|
|
1375
3129
|
}
|
|
3130
|
+
else {
|
|
3131
|
+
resolve(ws);
|
|
3132
|
+
}
|
|
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);
|
|
1376
3149
|
};
|
|
1377
3150
|
});
|
|
1378
3151
|
};
|
|
@@ -1382,7 +3155,14 @@ export class Deployments {
|
|
|
1382
3155
|
try {
|
|
1383
3156
|
return await connectWithShell('/bin/bash');
|
|
1384
3157
|
}
|
|
1385
|
-
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;
|
|
1386
3166
|
return connectWithShell('/bin/sh');
|
|
1387
3167
|
}
|
|
1388
3168
|
}
|