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.
@@ -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") {