u-foo 3.0.21 → 3.0.22
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 +13 -7
- package/README.zh-CN.md +11 -7
- package/package.json +1 -1
- package/src/agents/launch/launcher.js +52 -29
- package/src/app/chat/commandExecutor.js +15 -0
- package/src/app/chat/daemonConnection.js +7 -2
- package/src/app/chat/daemonCoordinator.js +1 -0
- package/src/app/chat/daemonTransport.js +21 -5
- package/src/app/chat/projectCloseController.js +6 -1
- package/src/app/chat/transport.js +84 -8
- package/src/app/cli/run.js +44 -7
- package/src/config.js +48 -0
- package/src/runtime/contracts/eventContract.js +1 -0
- package/src/runtime/daemon/agentProcessManager.js +34 -14
- package/src/runtime/daemon/controlPlaneService.js +14 -5
- package/src/runtime/daemon/endpoint.js +79 -0
- package/src/runtime/daemon/globalDaemon.js +394 -0
- package/src/runtime/daemon/groupOrchestrator.js +6 -0
- package/src/runtime/daemon/index.js +272 -109
- package/src/runtime/daemon/mcpHttpServer.js +5 -0
- package/src/runtime/daemon/ops.js +48 -17
- package/src/runtime/daemon/processLifecycle.js +127 -0
- package/src/runtime/daemon/projectContext.js +46 -0
- package/src/runtime/daemon/projectRuntime.js +221 -0
- package/src/runtime/daemon/projectRuntimeGateway.js +72 -0
- package/src/runtime/daemon/projectRuntimeManager.js +268 -0
- package/src/runtime/daemon/run.js +66 -12
- package/src/runtime/projects/registry.js +26 -4
- package/src/runtime/projects/runtimes.js +1 -1
- package/src/ui/rustChatHost.js +46 -8
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
defaultAgentModelForProvider,
|
|
8
|
+
loadConfig,
|
|
9
|
+
normalizeDaemonTopology,
|
|
10
|
+
} = require("../../config");
|
|
11
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
12
|
+
const {
|
|
13
|
+
loadAgentsData,
|
|
14
|
+
saveAgentsData,
|
|
15
|
+
} = require("../../coordination/state/agentsStore");
|
|
16
|
+
const {
|
|
17
|
+
canonicalProjectRoot,
|
|
18
|
+
markProjectStopped,
|
|
19
|
+
resolveGlobalControllerProjectRoot,
|
|
20
|
+
} = require("../projects");
|
|
21
|
+
const { startDaemon } = require("./index");
|
|
22
|
+
const { createProjectRuntime } = require("./projectRuntime");
|
|
23
|
+
const { ProjectRuntimeManager } = require("./projectRuntimeManager");
|
|
24
|
+
const {
|
|
25
|
+
CONTROL_PLANE_OPERATIONS,
|
|
26
|
+
MCP_EXPOSED_SHARED_TOOLS,
|
|
27
|
+
executeProjectRuntimeOperation,
|
|
28
|
+
} = require("./projectRuntimeGateway");
|
|
29
|
+
|
|
30
|
+
class GlobalDaemon {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.controllerRoot = canonicalProjectRoot(
|
|
33
|
+
options.controllerRoot || resolveGlobalControllerProjectRoot()
|
|
34
|
+
);
|
|
35
|
+
this.topology = normalizeDaemonTopology(options.topology || "global");
|
|
36
|
+
this.startProjectRuntime = options.startProjectRuntime || startDaemon;
|
|
37
|
+
this.loadProjectConfig = options.loadProjectConfig || loadConfig;
|
|
38
|
+
this.authorizeProjectRoot = typeof options.authorizeProjectRoot === "function"
|
|
39
|
+
? options.authorizeProjectRoot
|
|
40
|
+
: (projectRoot) => fs.existsSync(getUfooPaths(projectRoot).ufooDir);
|
|
41
|
+
this.controller = null;
|
|
42
|
+
this.runtimeManager = options.runtimeManager || new ProjectRuntimeManager({
|
|
43
|
+
authorizeProjectRoot: this.authorizeProjectRoot,
|
|
44
|
+
idleGraceMs: options.idleGraceMs,
|
|
45
|
+
sweepIntervalMs: options.sweepIntervalMs,
|
|
46
|
+
maxActiveRuntimes: options.maxActiveRuntimes,
|
|
47
|
+
maxConcurrentRequests: options.maxConcurrentRequests,
|
|
48
|
+
runtimeFactory: (context) => this.createHostedRuntime(context),
|
|
49
|
+
});
|
|
50
|
+
this.activeGatewayRequests = new Map();
|
|
51
|
+
this.projectRuntimeGateway = {
|
|
52
|
+
call: (projectRoot, operation, args, context) =>
|
|
53
|
+
this.callProjectOperation(projectRoot, operation, args, context),
|
|
54
|
+
cancel: (requestId) => {
|
|
55
|
+
const id = String(requestId || "");
|
|
56
|
+
const projectRoot = this.activeGatewayRequests.get(id);
|
|
57
|
+
return projectRoot ? this.runtimeManager.cancel(projectRoot, id) : false;
|
|
58
|
+
},
|
|
59
|
+
status: () => this.runtimeManager.status(),
|
|
60
|
+
// GlobalDaemon owns the shared manager; MCP listener restart/cleanup
|
|
61
|
+
// must not independently dispose project runtimes.
|
|
62
|
+
dispose: () => {},
|
|
63
|
+
};
|
|
64
|
+
this.disposed = false;
|
|
65
|
+
this.startedAt = "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createHostedRuntime(context) {
|
|
69
|
+
let hostHandle = null;
|
|
70
|
+
const cleanupHost = (reason) => {
|
|
71
|
+
const current = hostHandle;
|
|
72
|
+
hostHandle = null;
|
|
73
|
+
runtime.hostHandle = null;
|
|
74
|
+
if (current && typeof current.cleanup === "function") current.cleanup(reason);
|
|
75
|
+
};
|
|
76
|
+
const runtime = createProjectRuntime(context, {
|
|
77
|
+
onActivate: () => {
|
|
78
|
+
hostHandle = this.startProjectRuntime({
|
|
79
|
+
projectRoot: context.projectRoot,
|
|
80
|
+
provider: context.provider,
|
|
81
|
+
model: context.model,
|
|
82
|
+
resumeMode: "none",
|
|
83
|
+
daemonTopology: this.topology,
|
|
84
|
+
runtimeGeneration: context.runtimeGeneration,
|
|
85
|
+
globalRuntimeRouter: this,
|
|
86
|
+
manageProcessState: false,
|
|
87
|
+
listenProjectSocket: this.topology !== "global",
|
|
88
|
+
registrySocketPath: getUfooPaths(this.controllerRoot).ufooSock,
|
|
89
|
+
});
|
|
90
|
+
runtime.hostHandle = hostHandle;
|
|
91
|
+
},
|
|
92
|
+
canSuspend: () => this.canSuspendHostedRuntime(hostHandle),
|
|
93
|
+
onSuspend: () => cleanupHost("global-runtime-idle"),
|
|
94
|
+
onDispose: () => cleanupHost("global-runtime-dispose"),
|
|
95
|
+
});
|
|
96
|
+
runtime.hostHandle = null;
|
|
97
|
+
runtime.registerOperation("ipc_request", (_args, callContext) => {
|
|
98
|
+
if (!hostHandle || typeof hostHandle.handleRequest !== "function") {
|
|
99
|
+
const err = new Error(`project runtime is unavailable: ${context.projectRoot}`);
|
|
100
|
+
err.code = "PROJECT_RUNTIME_UNAVAILABLE";
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
return hostHandle.handleRequest(
|
|
104
|
+
callContext.requestContext.request,
|
|
105
|
+
callContext.requestContext.socket
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
for (const operation of [
|
|
109
|
+
...CONTROL_PLANE_OPERATIONS,
|
|
110
|
+
...MCP_EXPOSED_SHARED_TOOLS.filter((name) => name !== "read_project_registry"),
|
|
111
|
+
]) {
|
|
112
|
+
runtime.registerOperation(operation, (args, callContext) =>
|
|
113
|
+
executeProjectRuntimeOperation(
|
|
114
|
+
context.projectRoot,
|
|
115
|
+
operation,
|
|
116
|
+
args,
|
|
117
|
+
{
|
|
118
|
+
...callContext.requestContext,
|
|
119
|
+
signal: callContext.signal,
|
|
120
|
+
}
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
return runtime;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
canSuspendHostedRuntime(hostHandle) {
|
|
127
|
+
if (!hostHandle) return true;
|
|
128
|
+
const ipcServer = hostHandle.runtime && hostHandle.runtime.resource("ipcServer");
|
|
129
|
+
if (ipcServer && typeof ipcServer.hasClients === "function" && ipcServer.hasClients()) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const cronController = hostHandle.runtime && hostHandle.runtime.resource("cronController");
|
|
133
|
+
if (
|
|
134
|
+
cronController
|
|
135
|
+
&& typeof cronController.listTasks === "function"
|
|
136
|
+
&& cronController.listTasks().length > 0
|
|
137
|
+
) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const status = typeof hostHandle.status === "function" ? hostHandle.status() : null;
|
|
141
|
+
return !status || !Array.isArray(status.active) || status.active.length === 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
resolveProjectRoot(projectRoot) {
|
|
145
|
+
const canonicalRoot = canonicalProjectRoot(projectRoot);
|
|
146
|
+
if (canonicalRoot === this.controllerRoot) return canonicalRoot;
|
|
147
|
+
if (this.authorizeProjectRoot(canonicalRoot) !== true) {
|
|
148
|
+
const err = new Error(`project runtime access denied: ${canonicalRoot}`);
|
|
149
|
+
err.code = "PROJECT_RUNTIME_ACCESS_DENIED";
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
return canonicalRoot;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async activateProject(projectRoot) {
|
|
156
|
+
if (this.disposed) {
|
|
157
|
+
const err = new Error("global daemon is disposed");
|
|
158
|
+
err.code = "GLOBAL_DAEMON_DISPOSED";
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
const canonicalRoot = this.resolveProjectRoot(projectRoot);
|
|
162
|
+
if (canonicalRoot === this.controllerRoot) return this.controller;
|
|
163
|
+
const config = this.loadProjectConfig(canonicalRoot);
|
|
164
|
+
const provider = config.agentProvider || "codex-cli";
|
|
165
|
+
const model = config.agentModel || defaultAgentModelForProvider(provider);
|
|
166
|
+
const runtime = await this.runtimeManager.activate(canonicalRoot, {
|
|
167
|
+
config: {
|
|
168
|
+
...config,
|
|
169
|
+
daemonTopology: this.topology,
|
|
170
|
+
},
|
|
171
|
+
provider,
|
|
172
|
+
model,
|
|
173
|
+
daemonTopology: this.topology,
|
|
174
|
+
});
|
|
175
|
+
return runtime.hostHandle;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async handleRequest(projectRoot, request, socket) {
|
|
179
|
+
const canonicalRoot = this.resolveProjectRoot(projectRoot);
|
|
180
|
+
const config = this.loadProjectConfig(canonicalRoot);
|
|
181
|
+
const provider = config.agentProvider || "codex-cli";
|
|
182
|
+
const model = config.agentModel || defaultAgentModelForProvider(provider);
|
|
183
|
+
return this.runtimeManager.call(canonicalRoot, "ipc_request", {}, {
|
|
184
|
+
request,
|
|
185
|
+
socket,
|
|
186
|
+
config: {
|
|
187
|
+
...config,
|
|
188
|
+
daemonTopology: this.topology,
|
|
189
|
+
},
|
|
190
|
+
provider,
|
|
191
|
+
model,
|
|
192
|
+
daemonTopology: this.topology,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async callProjectOperation(projectRoot, operation, args = {}, context = {}) {
|
|
197
|
+
const canonicalRoot = this.resolveProjectRoot(projectRoot);
|
|
198
|
+
const config = this.loadProjectConfig(canonicalRoot);
|
|
199
|
+
const provider = config.agentProvider || "codex-cli";
|
|
200
|
+
const model = config.agentModel || defaultAgentModelForProvider(provider);
|
|
201
|
+
const requestId = String(context.requestId || context.toolCallId || randomUUID());
|
|
202
|
+
this.activeGatewayRequests.set(requestId, canonicalRoot);
|
|
203
|
+
try {
|
|
204
|
+
return await this.runtimeManager.call(canonicalRoot, operation, args, {
|
|
205
|
+
...context,
|
|
206
|
+
requestId,
|
|
207
|
+
config: {
|
|
208
|
+
...config,
|
|
209
|
+
daemonTopology: this.topology,
|
|
210
|
+
},
|
|
211
|
+
provider,
|
|
212
|
+
model,
|
|
213
|
+
daemonTopology: this.topology,
|
|
214
|
+
});
|
|
215
|
+
} finally {
|
|
216
|
+
this.activeGatewayRequests.delete(requestId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async request(projectRoot, request, options = {}) {
|
|
221
|
+
const timeoutMs = Number(options.timeoutMs) || 12000;
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
let settled = false;
|
|
224
|
+
const finish = (value) => {
|
|
225
|
+
if (settled) return;
|
|
226
|
+
settled = true;
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
resolve(value);
|
|
229
|
+
};
|
|
230
|
+
const socket = {
|
|
231
|
+
destroyed: false,
|
|
232
|
+
write: (data) => {
|
|
233
|
+
for (const line of String(data || "").split(/\r?\n/)) {
|
|
234
|
+
if (!line.trim()) continue;
|
|
235
|
+
let payload;
|
|
236
|
+
try {
|
|
237
|
+
payload = JSON.parse(line);
|
|
238
|
+
} catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (payload.type === "response") {
|
|
242
|
+
finish({
|
|
243
|
+
ok: true,
|
|
244
|
+
payload: payload.data || {},
|
|
245
|
+
opsResults: payload.opsResults || [],
|
|
246
|
+
});
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
if (payload.type === "error") {
|
|
250
|
+
finish({
|
|
251
|
+
ok: false,
|
|
252
|
+
error: payload.error || "project runtime error",
|
|
253
|
+
});
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
const timer = setTimeout(() => {
|
|
261
|
+
socket.destroyed = true;
|
|
262
|
+
finish({ ok: false, error: "Project runtime request timeout" });
|
|
263
|
+
}, timeoutMs);
|
|
264
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
265
|
+
this.handleRequest(projectRoot, request, socket).catch((err) => {
|
|
266
|
+
finish({
|
|
267
|
+
ok: false,
|
|
268
|
+
error: err && err.message ? err.message : String(err || "project runtime error"),
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
closeProject(projectRoot, options = {}) {
|
|
275
|
+
const canonicalRoot = this.resolveProjectRoot(projectRoot);
|
|
276
|
+
if (canonicalRoot === this.controllerRoot) {
|
|
277
|
+
const err = new Error("global controller runtime cannot be closed as a project");
|
|
278
|
+
err.code = "GLOBAL_CONTROLLER_CLOSE_DENIED";
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
const entry = this.runtimeManager.entryForRoot(canonicalRoot);
|
|
282
|
+
const terminated = [];
|
|
283
|
+
if (options.terminateAgents === true) {
|
|
284
|
+
const processManager = entry
|
|
285
|
+
&& entry.runtime.hostHandle
|
|
286
|
+
&& entry.runtime.hostHandle.runtime
|
|
287
|
+
&& entry.runtime.hostHandle.runtime.resource("processManager");
|
|
288
|
+
if (processManager) processManager.cleanup({ terminate: true });
|
|
289
|
+
|
|
290
|
+
const paths = getUfooPaths(canonicalRoot);
|
|
291
|
+
const data = loadAgentsData(paths.agentsFile);
|
|
292
|
+
for (const [subscriber, meta] of Object.entries(data.agents || {})) {
|
|
293
|
+
const pid = Number.parseInt(meta && meta.pid, 10);
|
|
294
|
+
const isController =
|
|
295
|
+
subscriber === "ufoo-agent"
|
|
296
|
+
|| String((meta && meta.agent_type) || "") === "ufoo-agent";
|
|
297
|
+
if (
|
|
298
|
+
!isController
|
|
299
|
+
&& Number.isFinite(pid)
|
|
300
|
+
&& pid > 0
|
|
301
|
+
&& pid !== process.pid
|
|
302
|
+
) {
|
|
303
|
+
try {
|
|
304
|
+
process.kill(pid, "SIGTERM");
|
|
305
|
+
terminated.push(subscriber);
|
|
306
|
+
} catch {
|
|
307
|
+
// Already-exited workloads are still marked inactive below.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (meta && meta.status === "active") {
|
|
311
|
+
meta.status = "inactive";
|
|
312
|
+
meta.last_seen = new Date().toISOString();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
saveAgentsData(paths.agentsFile, data);
|
|
316
|
+
}
|
|
317
|
+
const removed = this.runtimeManager.remove(canonicalRoot);
|
|
318
|
+
markProjectStopped(canonicalRoot);
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
project_root: canonicalRoot,
|
|
322
|
+
runtime_removed: removed,
|
|
323
|
+
terminated_agents: terminated,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
start(options = {}) {
|
|
328
|
+
if (this.controller) return this;
|
|
329
|
+
if (this.disposed) {
|
|
330
|
+
const err = new Error("disposed global daemon cannot be started");
|
|
331
|
+
err.code = "GLOBAL_DAEMON_DISPOSED";
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
const config = this.loadProjectConfig(this.controllerRoot);
|
|
335
|
+
const provider = options.provider || config.agentProvider || "codex-cli";
|
|
336
|
+
const model =
|
|
337
|
+
options.model
|
|
338
|
+
|| config.agentModel
|
|
339
|
+
|| defaultAgentModelForProvider(provider);
|
|
340
|
+
this.controller = this.startProjectRuntime({
|
|
341
|
+
projectRoot: this.controllerRoot,
|
|
342
|
+
provider,
|
|
343
|
+
model,
|
|
344
|
+
resumeMode: options.resumeMode || "none",
|
|
345
|
+
daemonTopology: this.topology,
|
|
346
|
+
globalRuntimeRouter: this,
|
|
347
|
+
beforeCleanup: () => this.disposeProjectRuntimes("global-controller-cleanup"),
|
|
348
|
+
});
|
|
349
|
+
this.startedAt = new Date().toISOString();
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
disposeProjectRuntimes(reason = "global-daemon-stop") {
|
|
354
|
+
void reason;
|
|
355
|
+
this.activeGatewayRequests.clear();
|
|
356
|
+
this.runtimeManager.dispose();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
stop(reason = "global-daemon-stop") {
|
|
360
|
+
if (this.disposed) return;
|
|
361
|
+
this.disposed = true;
|
|
362
|
+
this.disposeProjectRuntimes(reason);
|
|
363
|
+
const controller = this.controller;
|
|
364
|
+
this.controller = null;
|
|
365
|
+
if (controller && typeof controller.cleanup === "function") {
|
|
366
|
+
controller.cleanup(reason);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
status() {
|
|
371
|
+
const manager = this.runtimeManager.status();
|
|
372
|
+
return {
|
|
373
|
+
topology: this.topology,
|
|
374
|
+
pid: process.pid,
|
|
375
|
+
controller_root: this.controllerRoot,
|
|
376
|
+
started_at: this.startedAt || null,
|
|
377
|
+
runtime_count: manager.runtime_count,
|
|
378
|
+
active_runtime_count: manager.active_runtime_count,
|
|
379
|
+
active_request_count: manager.active_request_count,
|
|
380
|
+
activating_runtime_count: this.runtimeManager.activationPromises.size,
|
|
381
|
+
runtimes: manager.runtimes,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function startGlobalDaemon(options = {}) {
|
|
387
|
+
const daemon = new GlobalDaemon(options);
|
|
388
|
+
return daemon.start(options);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
GlobalDaemon,
|
|
393
|
+
startGlobalDaemon,
|
|
394
|
+
};
|
|
@@ -246,12 +246,18 @@ function buildLaunchHostContext(params = {}) {
|
|
|
246
246
|
const hostName = asTrimmedString(params.host_name || params.hostName);
|
|
247
247
|
const hostSessionId = asTrimmedString(params.host_session_id || params.hostSessionId);
|
|
248
248
|
const terminalApp = asTrimmedString(params.terminal_app || params.terminalApp);
|
|
249
|
+
const tmuxTarget = asTrimmedString(params.tmux_target || params.tmuxTarget);
|
|
250
|
+
const tmuxPane = asTrimmedString(params.tmux_pane || params.tmuxPane);
|
|
251
|
+
const tmuxSession = asTrimmedString(params.tmux_session || params.tmuxSession);
|
|
249
252
|
const context = {};
|
|
250
253
|
if (hostInjectSock) context.host_inject_sock = hostInjectSock;
|
|
251
254
|
if (hostDaemonSock) context.host_daemon_sock = hostDaemonSock;
|
|
252
255
|
if (hostName) context.host_name = hostName;
|
|
253
256
|
if (hostSessionId) context.host_session_id = hostSessionId;
|
|
254
257
|
if (terminalApp) context.terminal_app = terminalApp;
|
|
258
|
+
if (tmuxTarget) context.tmux_target = tmuxTarget;
|
|
259
|
+
if (tmuxPane) context.tmux_pane = tmuxPane;
|
|
260
|
+
if (tmuxSession) context.tmux_session = tmuxSession;
|
|
255
261
|
if (params.host_capabilities && typeof params.host_capabilities === "object") {
|
|
256
262
|
context.host_capabilities = { ...params.host_capabilities };
|
|
257
263
|
} else if (params.hostCapabilities && typeof params.hostCapabilities === "object") {
|