u-foo 3.0.17 → 3.0.19
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/OPTIONAL_SKILLS/ufoo-bus-poll/SKILL.md +24 -11
- package/README.md +53 -29
- package/README.zh-CN.md +45 -31
- package/SKILLS/ufoo/SKILL.md +22 -10
- package/SKILLS/ufoo-bus/SKILL.md +29 -15
- package/bin/ufoo.js +19 -5
- package/package.json +2 -1
- package/src/agents/prompts/groupBootstrap.js +1 -1
- package/src/agents/prompts/native/ufoo.js +1 -1
- package/src/app/chat/commandExecutor.js +11 -5
- package/src/app/cli/run.js +2 -3
- package/src/code/UCODE_PROMPT.md +1 -1
- package/src/config.js +10 -0
- package/src/coordination/bus/subscriber.js +32 -0
- package/src/coordination/state/paths.js +4 -0
- package/src/runtime/contracts/eventContract.js +5 -0
- package/src/runtime/daemon/controlPlaneService.js +130 -12
- package/src/runtime/daemon/index.js +65 -0
- package/src/runtime/daemon/mcpConfigure.js +175 -0
- package/src/runtime/daemon/mcpControl.js +123 -0
- package/src/runtime/daemon/mcpHttpServer.js +412 -0
- package/src/runtime/daemon/mcpServer.js +94 -103
- package/src/runtime/daemon/mcpStdioProxy.js +247 -0
- package/src/runtime/daemon/projectRuntimeControlPlane.js +115 -0
- package/src/runtime/daemon/projectRuntimeGateway.js +358 -0
|
@@ -357,10 +357,12 @@ function createCommandExecutor(options = {}) {
|
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
function logMcpHelp() {
|
|
360
|
-
logMessage("system", "{cyan-fg}MCP
|
|
361
|
-
logMessage("system", " • Configure
|
|
360
|
+
logMessage("system", "{cyan-fg}MCP server:{/cyan-fg} singleton listener in the global controller daemon");
|
|
361
|
+
logMessage("system", " • Configure Codex direct HTTP: ufoo mcp configure codex");
|
|
362
|
+
logMessage("system", " • Compatibility proxy command: ufoo mcp");
|
|
362
363
|
logMessage("system", " • Disable daemon auto-start: ufoo mcp --no-auto-start");
|
|
363
|
-
logMessage("system", " •
|
|
364
|
+
logMessage("system", " • Listener control: ufoo mcp status | ufoo mcp restart");
|
|
365
|
+
logMessage("system", " • Topology: one global tool router; project calls use registered project runtimes");
|
|
364
366
|
logMessage("system", " • Chat diagnostics: /mcp status, /mcp tools, /mcp help");
|
|
365
367
|
}
|
|
366
368
|
|
|
@@ -417,8 +419,12 @@ function createCommandExecutor(options = {}) {
|
|
|
417
419
|
throw new Error("empty MCP status response");
|
|
418
420
|
}
|
|
419
421
|
const projects = Array.isArray(status.projects) ? status.projects : [];
|
|
420
|
-
logMessage("system", "{cyan-fg}MCP
|
|
421
|
-
logMessage("system",
|
|
422
|
+
logMessage("system", "{cyan-fg}MCP server:{/cyan-fg} global Streamable HTTP listener");
|
|
423
|
+
logMessage("system", ` • listener: ${status.http?.running ? "{green-fg}running{/green-fg}" : "{yellow-fg}not running{/yellow-fg}"}`);
|
|
424
|
+
if (status.http?.endpoint) {
|
|
425
|
+
logMessage("system", ` • endpoint: ${escapeBlessed(status.http.endpoint)}`);
|
|
426
|
+
}
|
|
427
|
+
logMessage("system", " • stdio fallback: {cyan-fg}ufoo mcp{/cyan-fg}");
|
|
422
428
|
logMessage("system", ` • global controller: ${escapeBlessed(status.global_controller_root || "")}`);
|
|
423
429
|
logMessage("system", ` • daemon: ${status.global_controller_running ? "{green-fg}running{/green-fg}" : "{yellow-fg}not running{/yellow-fg}"}`);
|
|
424
430
|
logMessage("system", " • client auto-start: enabled by default");
|
package/src/app/cli/run.js
CHANGED
|
@@ -587,7 +587,7 @@ async function runCli(argv) {
|
|
|
587
587
|
});
|
|
588
588
|
program
|
|
589
589
|
.command("mcp")
|
|
590
|
-
.description("Run the
|
|
590
|
+
.description("Run the global MCP stdio proxy; use bin entry for listener status/configuration")
|
|
591
591
|
.option("--no-auto-start", "Do not auto-start the home-scoped global controller daemon")
|
|
592
592
|
.action((opts) => {
|
|
593
593
|
const repoRoot = getPackageRoot();
|
|
@@ -1809,8 +1809,7 @@ async function runCli(argv) {
|
|
|
1809
1809
|
return;
|
|
1810
1810
|
}
|
|
1811
1811
|
if (cmd === "mcp") {
|
|
1812
|
-
const mcpArgs = ["mcp"];
|
|
1813
|
-
if (rest.includes("--no-auto-start")) mcpArgs.push("--no-auto-start");
|
|
1812
|
+
const mcpArgs = ["mcp", ...rest];
|
|
1814
1813
|
run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), ...mcpArgs]);
|
|
1815
1814
|
return;
|
|
1816
1815
|
}
|
package/src/code/UCODE_PROMPT.md
CHANGED
|
@@ -17,7 +17,7 @@ ufoo integration requirements:
|
|
|
17
17
|
- Use shared memory for durable project facts. Read existing memory before writing new memory; do not use it for transient task state.
|
|
18
18
|
- Support launch/close/resume/inject flows managed by ufoo daemon.
|
|
19
19
|
- Prefer canonical ufoo commands (`ufoo ctx`, `ufoo bus`, `ufoo memory`, `ufoo report`) for coordination and status sync.
|
|
20
|
-
- A nonempty `UFOO_SUBSCRIBER_ID` means the ufoo wrapper/daemon already registered this Agent and can inject directly into its monitored session. Reuse that identity; never call MCP `register_agent`, run bare `ufoo bus join`, or start resident `ufoo bus poll`.
|
|
20
|
+
- A nonempty `UFOO_SUBSCRIBER_ID` in this Agent's inherited launch environment means the ufoo wrapper/daemon already registered this Agent and can inject directly into its monitored session. Reuse that identity; never call MCP `register_agent`, run bare `ufoo bus join`, or start resident `ufoo bus poll`.
|
|
21
21
|
- After sending a bus message, do not poll `ufoo bus check`, invoke `/ubus`, sleep, or wait for a reply. Continue the current task; any follow-up message will be automatically injected into your prompt/session.
|
|
22
22
|
|
|
23
23
|
Execution protocol:
|
package/src/config.js
CHANGED
|
@@ -36,6 +36,7 @@ const DEFAULT_CONFIG = {
|
|
|
36
36
|
launchMode: "auto",
|
|
37
37
|
agentProvider: "codex-cli",
|
|
38
38
|
controllerMode: "main",
|
|
39
|
+
mcpPort: 47631,
|
|
39
40
|
codexInternalThreadMode: "api",
|
|
40
41
|
codexAuthPath: "",
|
|
41
42
|
codexOauthRefreshWindowSec: 300,
|
|
@@ -125,6 +126,12 @@ function normalizeCodexInternalThreadMode(value) {
|
|
|
125
126
|
return "legacy";
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
function normalizeMcpPort(value) {
|
|
130
|
+
const parsed = Number.parseInt(value, 10);
|
|
131
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) return DEFAULT_CONFIG.mcpPort;
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
function normalizeCodexAuthPath(value) {
|
|
129
136
|
return typeof value === "string" ? value.trim() : "";
|
|
130
137
|
}
|
|
@@ -184,6 +191,7 @@ function loadConfig(projectRoot) {
|
|
|
184
191
|
controllerMode: Object.prototype.hasOwnProperty.call(raw, "controllerMode")
|
|
185
192
|
? normalizeControllerMode(raw.controllerMode)
|
|
186
193
|
: DEFAULT_CONFIG.controllerMode,
|
|
194
|
+
mcpPort: normalizeMcpPort(raw.mcpPort),
|
|
187
195
|
codexInternalThreadMode: Object.prototype.hasOwnProperty.call(raw, "codexInternalThreadMode")
|
|
188
196
|
? normalizeCodexInternalThreadMode(raw.codexInternalThreadMode)
|
|
189
197
|
: DEFAULT_CONFIG.codexInternalThreadMode,
|
|
@@ -240,6 +248,7 @@ function saveConfig(projectRoot, config) {
|
|
|
240
248
|
merged.routerProvider = typeof merged.routerProvider === "string" ? merged.routerProvider.trim() : "";
|
|
241
249
|
merged.routerModel = typeof merged.routerModel === "string" ? merged.routerModel.trim() : "";
|
|
242
250
|
merged.controllerMode = normalizeControllerMode(merged.controllerMode);
|
|
251
|
+
merged.mcpPort = normalizeMcpPort(merged.mcpPort);
|
|
243
252
|
merged.codexInternalThreadMode = normalizeCodexInternalThreadMode(merged.codexInternalThreadMode);
|
|
244
253
|
merged.codexAuthPath = normalizeCodexAuthPath(merged.codexAuthPath);
|
|
245
254
|
merged.codexOauthRefreshWindowSec = normalizeCodexOauthRefreshWindowSec(merged.codexOauthRefreshWindowSec);
|
|
@@ -291,6 +300,7 @@ module.exports = {
|
|
|
291
300
|
defaultRouterProviderForAgentProvider,
|
|
292
301
|
defaultRouterModelForProvider,
|
|
293
302
|
normalizeControllerMode,
|
|
303
|
+
normalizeMcpPort,
|
|
294
304
|
normalizeCodexInternalThreadMode,
|
|
295
305
|
normalizeCodexAuthPath,
|
|
296
306
|
normalizeCodexOauthRefreshWindowSec,
|
|
@@ -100,6 +100,15 @@ function hasProviderSession(meta) {
|
|
|
100
100
|
return typeof meta?.provider_session_id === "string" && meta.provider_session_id.trim() !== "";
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function isManagedMcpRegistration(meta) {
|
|
104
|
+
return Boolean(meta && meta.mcp_bridge === true && meta.mcp_agent_handle_hash);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isRecentMcpHeartbeat(meta, nowMs = Date.now()) {
|
|
108
|
+
const lastSeenMs = Date.parse(String(meta?.last_seen || ""));
|
|
109
|
+
return Number.isFinite(lastSeenMs) && nowMs - lastSeenMs <= 30 * 1000;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
/**
|
|
104
113
|
* 订阅者管理
|
|
105
114
|
*/
|
|
@@ -458,6 +467,29 @@ class SubscriberManager {
|
|
|
458
467
|
if (!this.busData.agents) return;
|
|
459
468
|
|
|
460
469
|
for (const [id, meta] of Object.entries(this.busData.agents)) {
|
|
470
|
+
if (isManagedMcpRegistration(meta)) {
|
|
471
|
+
const expiresAtMs = Date.parse(String(meta.mcp_lease_expires_at || ""));
|
|
472
|
+
const expired = !Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now();
|
|
473
|
+
if (!expired || isRecentMcpHeartbeat(meta)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (meta.status === "active") {
|
|
477
|
+
this.logRegistry("cleanup_inactive_mark", {
|
|
478
|
+
source: "bus.subscriber.cleanupInactive",
|
|
479
|
+
subscriber: id,
|
|
480
|
+
reason: "mcp_agent_lease_expired",
|
|
481
|
+
status: meta.status || "",
|
|
482
|
+
launch_mode: meta.launch_mode || "",
|
|
483
|
+
last_seen: meta.last_seen || "",
|
|
484
|
+
lease_expires_at: meta.mcp_lease_expires_at || "",
|
|
485
|
+
});
|
|
486
|
+
meta.status = "inactive";
|
|
487
|
+
meta.activity_state = "";
|
|
488
|
+
meta.mcp_revoked_at = getTimestamp();
|
|
489
|
+
this.cleanupSubscriberArtifacts(id);
|
|
490
|
+
}
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
461
493
|
if (isInternalLaunchMode(meta)) {
|
|
462
494
|
const recoverable = hasProviderSession(meta);
|
|
463
495
|
if (meta.status === "inactive") {
|
|
@@ -24,6 +24,8 @@ function getUfooPaths(projectRoot) {
|
|
|
24
24
|
const ufooDaemonPid = path.join(runDir, "ufoo-daemon.pid");
|
|
25
25
|
const ufooDaemonLog = path.join(runDir, "ufoo-daemon.log");
|
|
26
26
|
const ufooSock = path.join(runDir, "ufoo.sock");
|
|
27
|
+
const mcpToken = path.join(runDir, "mcp-token");
|
|
28
|
+
const mcpEndpoint = path.join(runDir, "mcp-endpoint.json");
|
|
27
29
|
|
|
28
30
|
return {
|
|
29
31
|
ufooDir,
|
|
@@ -46,6 +48,8 @@ function getUfooPaths(projectRoot) {
|
|
|
46
48
|
ufooDaemonPid,
|
|
47
49
|
ufooDaemonLog,
|
|
48
50
|
ufooSock,
|
|
51
|
+
mcpToken,
|
|
52
|
+
mcpEndpoint,
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -20,6 +20,10 @@ const IPC_REQUEST_TYPES = {
|
|
|
20
20
|
AGENT_REPORT: "agent_report",
|
|
21
21
|
ASSIGN_ROLE: "assign_role",
|
|
22
22
|
REFRESH_STATUS: "refresh_status",
|
|
23
|
+
CONTROL_PLANE_CALL: "control_plane_call",
|
|
24
|
+
CONTROL_PLANE_CANCEL: "control_plane_cancel",
|
|
25
|
+
MCP_STATUS: "mcp_status",
|
|
26
|
+
MCP_RESTART: "mcp_restart",
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
const IPC_RESPONSE_TYPES = {
|
|
@@ -29,6 +33,7 @@ const IPC_RESPONSE_TYPES = {
|
|
|
29
33
|
ERROR: "error",
|
|
30
34
|
BUS_SEND_OK: "bus_send_ok",
|
|
31
35
|
REGISTER_OK: "register_ok",
|
|
36
|
+
CONTROL_PLANE_RESULT: "control_plane_result",
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
const BUS_STATUS_PHASES = {
|
|
@@ -22,6 +22,8 @@ const WAIT_FOR_MESSAGE_DEFAULT_TIMEOUT_SECONDS = 600;
|
|
|
22
22
|
const WAIT_FOR_MESSAGE_MAX_TIMEOUT_SECONDS = 600;
|
|
23
23
|
const WAIT_FOR_MESSAGE_POLL_INTERVAL_MS = 1000;
|
|
24
24
|
const WAIT_FOR_MESSAGE_HEARTBEAT_INTERVAL_MS = 15000;
|
|
25
|
+
const MCP_AGENT_LEASE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
26
|
+
const MCP_AGENT_RECENT_HEARTBEAT_MS = 30 * 1000;
|
|
25
27
|
|
|
26
28
|
function nowIso() {
|
|
27
29
|
return new Date().toISOString();
|
|
@@ -70,6 +72,61 @@ function createCryptoSessionId() {
|
|
|
70
72
|
return crypto.randomBytes(4).toString("hex");
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
function createAgentHandle() {
|
|
76
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hashAgentHandle(handle = "") {
|
|
80
|
+
return crypto.createHash("sha256").update(String(handle || ""), "utf8").digest("hex");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function leaseExpiryIso(nowMs = Date.now()) {
|
|
84
|
+
return new Date(nowMs + MCP_AGENT_LEASE_TTL_MS).toISOString();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extendMcpAgentLease(meta, nowMs = Date.now()) {
|
|
88
|
+
meta.mcp_lease_expires_at = leaseExpiryIso(nowMs);
|
|
89
|
+
delete meta.mcp_revoked_at;
|
|
90
|
+
return meta.mcp_lease_expires_at;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertAgentHandle(bus, subscriber, args = {}, options = {}) {
|
|
94
|
+
const meta = assertSubscriberExists(bus, subscriber);
|
|
95
|
+
if (meta.mcp_bridge !== true || !meta.mcp_agent_handle_hash) {
|
|
96
|
+
const err = new Error(`subscriber is not an MCP-registered Agent: ${subscriber}`);
|
|
97
|
+
err.code = "agent_handle_not_available";
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
const handle = String(args.agent_handle || args.agentHandle || "").trim();
|
|
101
|
+
if (!handle) {
|
|
102
|
+
const err = new Error("agent_handle is required");
|
|
103
|
+
err.code = "agent_handle_required";
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
const actual = Buffer.from(hashAgentHandle(handle), "hex");
|
|
107
|
+
const expected = Buffer.from(String(meta.mcp_agent_handle_hash || ""), "hex");
|
|
108
|
+
if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
|
|
109
|
+
const err = new Error("agent_handle does not own this subscriber");
|
|
110
|
+
err.code = "invalid_agent_handle";
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
if (options.allowInactive !== true && meta.status !== "active") {
|
|
114
|
+
const err = new Error(`Agent registration is inactive: ${subscriber}`);
|
|
115
|
+
err.code = "agent_lease_inactive";
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
const expiresAtMs = Date.parse(String(meta.mcp_lease_expires_at || ""));
|
|
119
|
+
if (
|
|
120
|
+
options.allowExpired !== true
|
|
121
|
+
&& (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now())
|
|
122
|
+
) {
|
|
123
|
+
const err = new Error(`Agent lease expired: ${subscriber}`);
|
|
124
|
+
err.code = "agent_lease_expired";
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
return meta;
|
|
128
|
+
}
|
|
129
|
+
|
|
73
130
|
function notifyDaemonRefresh(projectRoot) {
|
|
74
131
|
if (!isRunning(projectRoot)) return;
|
|
75
132
|
const sock = socketPath(projectRoot);
|
|
@@ -99,6 +156,10 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
|
|
|
99
156
|
const hostCapabilities = args.hostCapabilities && typeof args.hostCapabilities === "object"
|
|
100
157
|
? args.hostCapabilities
|
|
101
158
|
: capabilities;
|
|
159
|
+
const clientInstanceId = String(
|
|
160
|
+
args.client_instance_id || args.clientInstanceId || ""
|
|
161
|
+
).trim();
|
|
162
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
102
163
|
|
|
103
164
|
// Session ID: explicit > reuse > generate
|
|
104
165
|
let sessionId;
|
|
@@ -113,7 +174,22 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
|
|
|
113
174
|
const reuseProviderSessionId = typeof reuseSession?.providerSessionId === "string"
|
|
114
175
|
? reuseSession.providerSessionId.trim() : "";
|
|
115
176
|
|
|
116
|
-
|
|
177
|
+
const recoveredEntry = !validateParentPid && clientInstanceId
|
|
178
|
+
? Object.entries(bus.busData.agents || {}).find(([, meta]) => (
|
|
179
|
+
meta
|
|
180
|
+
&& meta.mcp_bridge === true
|
|
181
|
+
&& meta.agent_type === agentType
|
|
182
|
+
&& meta.mcp_client_instance_id === clientInstanceId
|
|
183
|
+
))
|
|
184
|
+
: null;
|
|
185
|
+
const recoveredSubscriber = recoveredEntry ? recoveredEntry[0] : "";
|
|
186
|
+
const recoveredSessionId = recoveredSubscriber.startsWith(`${agentType}:`)
|
|
187
|
+
? recoveredSubscriber.slice(agentType.length + 1)
|
|
188
|
+
: "";
|
|
189
|
+
|
|
190
|
+
if (recoveredSessionId) {
|
|
191
|
+
sessionId = recoveredSessionId;
|
|
192
|
+
} else if (explicitSessionId) {
|
|
117
193
|
sessionId = explicitSessionId;
|
|
118
194
|
} else if (reuseSessionId && reuseSubscriberId === `${agentType}:${reuseSessionId}`) {
|
|
119
195
|
sessionId = reuseSessionId;
|
|
@@ -165,7 +241,18 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
|
|
|
165
241
|
if (reuseSessionId) joinOptions.reuseSessionId = reuseSessionId;
|
|
166
242
|
if (reuseProviderSessionId) joinOptions.reuseProviderSessionId = reuseProviderSessionId;
|
|
167
243
|
|
|
168
|
-
const
|
|
244
|
+
const candidateSubscriber = `${agentType}:${sessionId}`;
|
|
245
|
+
const existingCandidate = bus.subscriberManager.getSubscriber(candidateSubscriber);
|
|
246
|
+
if (
|
|
247
|
+
!validateParentPid
|
|
248
|
+
&& existingCandidate
|
|
249
|
+
&& existingCandidate.mcp_agent_handle_hash
|
|
250
|
+
&& (!clientInstanceId || existingCandidate.mcp_client_instance_id !== clientInstanceId)
|
|
251
|
+
) {
|
|
252
|
+
const err = new Error(`subscriber is already registered: ${candidateSubscriber}`);
|
|
253
|
+
err.code = "subscriber_already_registered";
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
169
256
|
const result = await bus.subscriberManager.join(sessionId, agentType, finalNickname, joinOptions);
|
|
170
257
|
const subscriber = result.subscriber;
|
|
171
258
|
if (finalNickname) {
|
|
@@ -175,6 +262,14 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
|
|
|
175
262
|
meta.activity_state = String(args.activity_state || "ready");
|
|
176
263
|
meta.activity_since = nowIso();
|
|
177
264
|
meta.mcp_bridge = !validateParentPid;
|
|
265
|
+
let agentHandle = "";
|
|
266
|
+
if (!validateParentPid) {
|
|
267
|
+
agentHandle = createAgentHandle();
|
|
268
|
+
meta.mcp_agent_handle_hash = hashAgentHandle(agentHandle);
|
|
269
|
+
meta.mcp_client_instance_id = clientInstanceId;
|
|
270
|
+
meta.mcp_registered_at = meta.mcp_registered_at || nowIso();
|
|
271
|
+
extendMcpAgentLease(meta);
|
|
272
|
+
}
|
|
178
273
|
if (hostCapabilities) meta.mcp_capabilities = hostCapabilities;
|
|
179
274
|
bus.saveBusData();
|
|
180
275
|
notifyDaemonRefresh(projectRoot);
|
|
@@ -188,6 +283,12 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
|
|
|
188
283
|
nickname: meta.nickname || result.nickname || finalNickname || "",
|
|
189
284
|
scoped_nickname: meta.scoped_nickname || result.scopedNickname || scopedNickname || "",
|
|
190
285
|
launch_mode: launchMode,
|
|
286
|
+
...(agentHandle ? {
|
|
287
|
+
agent_handle: agentHandle,
|
|
288
|
+
lease_expires_at: meta.mcp_lease_expires_at,
|
|
289
|
+
client_instance_id: clientInstanceId,
|
|
290
|
+
recovered: Boolean(recoveredSubscriber),
|
|
291
|
+
} : {}),
|
|
191
292
|
reuseProviderSessionId,
|
|
192
293
|
skipSessionResolve: !!args.skipSessionResolve,
|
|
193
294
|
};
|
|
@@ -203,9 +304,10 @@ async function registerAgent(projectRoot, args = {}) {
|
|
|
203
304
|
async function heartbeatAgent(projectRoot, args = {}) {
|
|
204
305
|
const subscriber = resolveSubscriberArg(args);
|
|
205
306
|
const bus = ensureBusLoaded(projectRoot);
|
|
206
|
-
const meta =
|
|
307
|
+
const meta = assertAgentHandle(bus, subscriber, args);
|
|
207
308
|
bus.subscriberManager.updateLastSeen(subscriber);
|
|
208
309
|
meta.status = "active";
|
|
310
|
+
const leaseExpiresAt = extendMcpAgentLease(meta);
|
|
209
311
|
bus.saveBusData();
|
|
210
312
|
notifyDaemonRefresh(projectRoot);
|
|
211
313
|
return {
|
|
@@ -213,6 +315,7 @@ async function heartbeatAgent(projectRoot, args = {}) {
|
|
|
213
315
|
project_root: projectRoot,
|
|
214
316
|
subscriber,
|
|
215
317
|
last_seen: meta.last_seen,
|
|
318
|
+
lease_expires_at: leaseExpiresAt,
|
|
216
319
|
};
|
|
217
320
|
}
|
|
218
321
|
|
|
@@ -225,7 +328,7 @@ async function publishActivityState(projectRoot, args = {}) {
|
|
|
225
328
|
throw err;
|
|
226
329
|
}
|
|
227
330
|
const bus = ensureBusLoaded(projectRoot);
|
|
228
|
-
const meta =
|
|
331
|
+
const meta = assertAgentHandle(bus, subscriber, args);
|
|
229
332
|
bus.subscriberManager.updateLastSeen(subscriber);
|
|
230
333
|
meta.status = "active";
|
|
231
334
|
meta.activity_state = activityState;
|
|
@@ -246,7 +349,7 @@ async function publishActivityState(projectRoot, args = {}) {
|
|
|
246
349
|
async function updateAgentMetadata(projectRoot, args = {}) {
|
|
247
350
|
const subscriber = resolveSubscriberArg(args);
|
|
248
351
|
const bus = ensureBusLoaded(projectRoot);
|
|
249
|
-
const meta =
|
|
352
|
+
const meta = assertAgentHandle(bus, subscriber, args);
|
|
250
353
|
const nickname = String(args.nickname || "").trim();
|
|
251
354
|
if (nickname) {
|
|
252
355
|
await bus.subscriberManager.rename(subscriber, nickname);
|
|
@@ -278,7 +381,7 @@ async function pollInbox(projectRoot, args = {}) {
|
|
|
278
381
|
? Math.floor(Number(args.limit))
|
|
279
382
|
: 50;
|
|
280
383
|
const bus = ensureBusLoaded(projectRoot);
|
|
281
|
-
|
|
384
|
+
assertAgentHandle(bus, subscriber, args);
|
|
282
385
|
bus.subscriberManager.updateLastSeen(subscriber);
|
|
283
386
|
bus.saveBusData();
|
|
284
387
|
const pending = await bus.messageManager.check(subscriber);
|
|
@@ -365,14 +468,15 @@ function eventSeq(event = {}) {
|
|
|
365
468
|
return Number.isInteger(seq) && seq > 0 ? seq : 0;
|
|
366
469
|
}
|
|
367
470
|
|
|
368
|
-
function touchWaitingSubscriber(bus, subscriber) {
|
|
471
|
+
function touchWaitingSubscriber(bus, subscriber, args = {}) {
|
|
369
472
|
// Long waits span concurrent metadata updates from other agents. Reload
|
|
370
473
|
// before writing heartbeat state so a stale in-memory registry cannot
|
|
371
474
|
// overwrite those updates.
|
|
372
475
|
bus.loadBusData();
|
|
373
|
-
const meta =
|
|
476
|
+
const meta = assertAgentHandle(bus, subscriber, args);
|
|
374
477
|
meta.status = "active";
|
|
375
478
|
bus.subscriberManager.updateLastSeen(subscriber);
|
|
479
|
+
extendMcpAgentLease(meta);
|
|
376
480
|
bus.saveBusData();
|
|
377
481
|
}
|
|
378
482
|
|
|
@@ -393,7 +497,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
|
|
|
393
497
|
const timeoutMs = timeoutSeconds * 1000;
|
|
394
498
|
|
|
395
499
|
const bus = ensureBusLoaded(projectRoot);
|
|
396
|
-
touchWaitingSubscriber(bus, subscriber);
|
|
500
|
+
touchWaitingSubscriber(bus, subscriber, args);
|
|
397
501
|
const lease = acquirePollLease(path.join(
|
|
398
502
|
bus.busDir,
|
|
399
503
|
"pids",
|
|
@@ -418,7 +522,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
|
|
|
418
522
|
if (unseen.length > 0) {
|
|
419
523
|
const messages = unseen.slice(0, limit);
|
|
420
524
|
const lastSeq = Math.max(afterSeq, ...messages.map(eventSeq));
|
|
421
|
-
touchWaitingSubscriber(bus, subscriber);
|
|
525
|
+
touchWaitingSubscriber(bus, subscriber, args);
|
|
422
526
|
return {
|
|
423
527
|
ok: true,
|
|
424
528
|
project_root: projectRoot,
|
|
@@ -436,7 +540,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
|
|
|
436
540
|
|
|
437
541
|
const current = now();
|
|
438
542
|
if (current >= deadline) {
|
|
439
|
-
touchWaitingSubscriber(bus, subscriber);
|
|
543
|
+
touchWaitingSubscriber(bus, subscriber, args);
|
|
440
544
|
return {
|
|
441
545
|
ok: true,
|
|
442
546
|
project_root: projectRoot,
|
|
@@ -453,7 +557,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
|
|
|
453
557
|
}
|
|
454
558
|
|
|
455
559
|
if (current >= nextHeartbeatAt) {
|
|
456
|
-
touchWaitingSubscriber(bus, subscriber);
|
|
560
|
+
touchWaitingSubscriber(bus, subscriber, args);
|
|
457
561
|
nextHeartbeatAt = current + heartbeatIntervalMs;
|
|
458
562
|
}
|
|
459
563
|
|
|
@@ -469,6 +573,8 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
|
|
|
469
573
|
|
|
470
574
|
async function reportAgentStatus(projectRoot, args = {}) {
|
|
471
575
|
const subscriber = resolveSubscriberArg(args);
|
|
576
|
+
const bus = ensureBusLoaded(projectRoot);
|
|
577
|
+
assertAgentHandle(bus, subscriber, args);
|
|
472
578
|
const report = normalizeReportInput({
|
|
473
579
|
...args,
|
|
474
580
|
agent_id: subscriber,
|
|
@@ -488,6 +594,12 @@ async function reportAgentStatus(projectRoot, args = {}) {
|
|
|
488
594
|
async function unregisterAgent(projectRoot, args = {}) {
|
|
489
595
|
const subscriber = resolveSubscriberArg(args);
|
|
490
596
|
const bus = ensureBusLoaded(projectRoot);
|
|
597
|
+
const meta = assertAgentHandle(bus, subscriber, args, {
|
|
598
|
+
allowExpired: true,
|
|
599
|
+
allowInactive: true,
|
|
600
|
+
});
|
|
601
|
+
meta.mcp_revoked_at = nowIso();
|
|
602
|
+
meta.mcp_lease_expires_at = meta.mcp_revoked_at;
|
|
491
603
|
const ok = await bus.subscriberManager.leave(subscriber);
|
|
492
604
|
bus.saveBusData();
|
|
493
605
|
notifyDaemonRefresh(projectRoot);
|
|
@@ -504,6 +616,10 @@ module.exports = {
|
|
|
504
616
|
assertSubscriberExists,
|
|
505
617
|
resolveSubscriberArg,
|
|
506
618
|
createSessionId,
|
|
619
|
+
createAgentHandle,
|
|
620
|
+
hashAgentHandle,
|
|
621
|
+
assertAgentHandle,
|
|
622
|
+
extendMcpAgentLease,
|
|
507
623
|
notifyDaemonRefresh,
|
|
508
624
|
registerAgentFull,
|
|
509
625
|
registerAgent,
|
|
@@ -517,4 +633,6 @@ module.exports = {
|
|
|
517
633
|
normalizeWaitForMessageArgs,
|
|
518
634
|
WAIT_FOR_MESSAGE_DEFAULT_TIMEOUT_SECONDS,
|
|
519
635
|
WAIT_FOR_MESSAGE_MAX_TIMEOUT_SECONDS,
|
|
636
|
+
MCP_AGENT_LEASE_TTL_MS,
|
|
637
|
+
MCP_AGENT_RECENT_HEARTBEAT_MS,
|
|
520
638
|
};
|
|
@@ -45,6 +45,10 @@ const {
|
|
|
45
45
|
checkAndCleanupNickname,
|
|
46
46
|
} = require("./nicknameScope");
|
|
47
47
|
const { resolveNodeExecutable } = require("../process/nodeExecutable");
|
|
48
|
+
const {
|
|
49
|
+
createProjectRuntimeControlPlane,
|
|
50
|
+
} = require("./projectRuntimeControlPlane");
|
|
51
|
+
const { loadConfig, normalizeMcpPort } = require("../../config");
|
|
48
52
|
|
|
49
53
|
let providerSessions = null;
|
|
50
54
|
let sessionResolveHandles = new Map();
|
|
@@ -1351,9 +1355,63 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1351
1355
|
},
|
|
1352
1356
|
});
|
|
1353
1357
|
deliveryScheduler.start();
|
|
1358
|
+
const runtimeControlPlane = createProjectRuntimeControlPlane({ projectRoot });
|
|
1359
|
+
let mcpHttpServer = null;
|
|
1360
|
+
if (isGlobalControllerProjectRoot(projectRoot) && process.env.UFOO_MCP_HTTP_DISABLED !== "1") {
|
|
1361
|
+
const { createGlobalMcpHttpServer } = require("./mcpHttpServer");
|
|
1362
|
+
const config = loadConfig(projectRoot);
|
|
1363
|
+
const configuredPort = process.env.UFOO_MCP_PORT || config.mcpPort;
|
|
1364
|
+
mcpHttpServer = createGlobalMcpHttpServer({
|
|
1365
|
+
projectRoot,
|
|
1366
|
+
port: normalizeMcpPort(configuredPort),
|
|
1367
|
+
log,
|
|
1368
|
+
});
|
|
1369
|
+
mcpHttpServer.start().catch((err) => {
|
|
1370
|
+
logSync(`MCP HTTP startup failed: ${formatFatalReason(err)}`);
|
|
1371
|
+
setImmediate(() => {
|
|
1372
|
+
throw err;
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1354
1376
|
|
|
1355
1377
|
handleIpcRequest = async (req, socket) => {
|
|
1356
1378
|
if (!req || typeof req !== "object") return;
|
|
1379
|
+
if (await runtimeControlPlane.handleRequest(req, socket)) return;
|
|
1380
|
+
if (req.type === IPC_REQUEST_TYPES.MCP_STATUS || req.type === IPC_REQUEST_TYPES.MCP_RESTART) {
|
|
1381
|
+
if (!isGlobalControllerProjectRoot(projectRoot)) {
|
|
1382
|
+
socket.write(`${JSON.stringify({
|
|
1383
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1384
|
+
error: "MCP control is owned by the global controller daemon",
|
|
1385
|
+
})}\n`);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
try {
|
|
1389
|
+
if (req.type === IPC_REQUEST_TYPES.MCP_RESTART) {
|
|
1390
|
+
if (!mcpHttpServer) {
|
|
1391
|
+
throw new Error("MCP HTTP listener is disabled");
|
|
1392
|
+
}
|
|
1393
|
+
await mcpHttpServer.stop();
|
|
1394
|
+
await mcpHttpServer.start();
|
|
1395
|
+
}
|
|
1396
|
+
socket.write(`${JSON.stringify({
|
|
1397
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1398
|
+
data: {
|
|
1399
|
+
ok: true,
|
|
1400
|
+
operation: req.type === IPC_REQUEST_TYPES.MCP_RESTART ? "restart" : "status",
|
|
1401
|
+
mcp: mcpHttpServer
|
|
1402
|
+
? mcpHttpServer.getStatus()
|
|
1403
|
+
: { running: false, disabled: true },
|
|
1404
|
+
},
|
|
1405
|
+
})}\n`);
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
socket.write(`${JSON.stringify({
|
|
1408
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1409
|
+
error: err.message || String(err),
|
|
1410
|
+
code: err.code || "mcp_control_error",
|
|
1411
|
+
})}\n`);
|
|
1412
|
+
}
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1357
1415
|
if (req.type === IPC_REQUEST_TYPES.STATUS) {
|
|
1358
1416
|
cleanupInactiveSubscribers();
|
|
1359
1417
|
const status = buildRuntimeStatus();
|
|
@@ -2595,6 +2653,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2595
2653
|
}
|
|
2596
2654
|
daemonGroupOrchestrator = null;
|
|
2597
2655
|
|
|
2656
|
+
runtimeControlPlane.stop();
|
|
2657
|
+
if (mcpHttpServer) {
|
|
2658
|
+
void mcpHttpServer.stop().catch((err) => {
|
|
2659
|
+
writeLog(`MCP HTTP shutdown failed: ${formatFatalReason(err)}`);
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2598
2663
|
// 清理所有子进程
|
|
2599
2664
|
processManager.cleanup();
|
|
2600
2665
|
|