@alfe.ai/gateway 0.9.3 → 0.9.5
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/dist/health.js +1527 -1474
- package/dist/src/index.d.ts +3 -4
- package/dist/upgrade.js +1 -1
- package/package.json +6 -6
package/dist/health.js
CHANGED
|
@@ -2,20 +2,20 @@ import { n as logger$1 } from "./logger.js";
|
|
|
2
2
|
import { a as captureFatal, c as captureRuntimeCrash, d as initAgentSentry, f as setAgentContext, l as captureRuntimeErrorOutput, o as captureIntegrationFailure, s as captureMcpFailure, u as flushSentry } from "./sentry.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import { execFile, execSync, spawn } from "node:child_process";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
5
|
import { dirname, join } from "node:path";
|
|
8
6
|
import { homedir } from "node:os";
|
|
9
7
|
import pino from "pino";
|
|
10
8
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
9
|
import { getEndpointFromToken, readConfig } from "@alfe.ai/config";
|
|
12
10
|
import crypto from "crypto";
|
|
11
|
+
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
13
12
|
import { parse } from "smol-toml";
|
|
14
13
|
import WebSocket from "ws";
|
|
15
|
-
import {
|
|
14
|
+
import { execFile, execSync, spawn } from "node:child_process";
|
|
16
15
|
import { ClaudeCodeApplier, ClaudeCodeMcpSync, HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, NoopOpenClawCliLock, OpenClawApplier, SerialOpenClawCliLock } from "@alfe.ai/integrations";
|
|
17
|
-
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
18
16
|
import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
import { createConnection, createServer } from "node:net";
|
|
19
19
|
import stream, { Readable } from "stream";
|
|
20
20
|
import util, { format } from "util";
|
|
21
21
|
import http from "http";
|
|
@@ -4327,6 +4327,7 @@ enumValues({
|
|
|
4327
4327
|
//#endregion
|
|
4328
4328
|
//#region ../../packages-internal/types/dist/models.js
|
|
4329
4329
|
const AnthropicModel = {
|
|
4330
|
+
Opus5: "claude-opus-5",
|
|
4330
4331
|
Opus48: "claude-opus-4-8",
|
|
4331
4332
|
Opus47: "claude-opus-4-7",
|
|
4332
4333
|
Opus46: "claude-opus-4-6",
|
|
@@ -4414,8 +4415,7 @@ const MoonshotModel = {
|
|
|
4414
4415
|
K3: "kimi-k3",
|
|
4415
4416
|
K26: "kimi-k2.6",
|
|
4416
4417
|
K27Code: "kimi-k2.7-code",
|
|
4417
|
-
K27CodeHighSpeed: "kimi-k2.7-code-highspeed"
|
|
4418
|
-
K25: "kimi-k2.5"
|
|
4418
|
+
K27CodeHighSpeed: "kimi-k2.7-code-highspeed"
|
|
4419
4419
|
};
|
|
4420
4420
|
const MOONSHOT_MODELS = enumValues(MoonshotModel);
|
|
4421
4421
|
const QwenModel = {
|
|
@@ -4449,8 +4449,8 @@ _enum(ZHIPU_MODELS);
|
|
|
4449
4449
|
_enum(MOONSHOT_MODELS);
|
|
4450
4450
|
_enum(QWEN_MODELS);
|
|
4451
4451
|
string().min(1);
|
|
4452
|
-
AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed,
|
|
4453
|
-
AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed,
|
|
4452
|
+
AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
|
|
4453
|
+
AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
|
|
4454
4454
|
enumValues({
|
|
4455
4455
|
PendingChallenge: "pending_challenge",
|
|
4456
4456
|
Creating: "creating",
|
|
@@ -4528,6 +4528,7 @@ const NotificationType = {
|
|
|
4528
4528
|
SubscriptionCancelled: "subscription.cancelled",
|
|
4529
4529
|
StartupGrantEnded: "startup.grant_ended",
|
|
4530
4530
|
GrantEnded: "subscription.grant_ended",
|
|
4531
|
+
EnterpriseGrantEnded: "subscription.enterprise_grant_ended",
|
|
4531
4532
|
SubscriptionPastDue: "subscription.past_due",
|
|
4532
4533
|
BalanceThresholdWarning: "balance.threshold.warning",
|
|
4533
4534
|
PlatformTierPriceIncrease: "platform.tier_price_increase",
|
|
@@ -4552,7 +4553,7 @@ const RecipientStrategy = {
|
|
|
4552
4553
|
TenantAdmins: "tenant_admins",
|
|
4553
4554
|
SpecificUser: "specific_user"
|
|
4554
4555
|
};
|
|
4555
|
-
NotificationType.PaymentSucceeded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PaymentFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.TopUpCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AutoRechargeCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AutoRechargeFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.SubscriptionCreated, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.SubscriptionCancelled, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.StartupGrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.GrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PlatformTierPriceIncrease, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.BrowserTakeoverRequested, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
|
|
4556
|
+
NotificationType.PaymentSucceeded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PaymentFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.TopUpCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AutoRechargeCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AutoRechargeFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.SubscriptionCreated, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.SubscriptionCancelled, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.StartupGrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.GrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.EnterpriseGrantEnded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PlatformTierPriceIncrease, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.BrowserTakeoverRequested, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
|
|
4556
4557
|
//#endregion
|
|
4557
4558
|
//#region ../../packages-internal/api-client/dist/services/auth.js
|
|
4558
4559
|
var AuthService = class {
|
|
@@ -5203,63 +5204,54 @@ async function loadDaemonConfig() {
|
|
|
5203
5204
|
* Returns null if the agent has no template assigned or the fetch fails.
|
|
5204
5205
|
*/
|
|
5205
5206
|
async function fetchAgentConfig(apiKey, apiEndpoint) {
|
|
5207
|
+
const client = new AgentApiClient({
|
|
5208
|
+
apiKey,
|
|
5209
|
+
apiUrl: apiEndpoint
|
|
5210
|
+
});
|
|
5211
|
+
logger$1.debug({ apiEndpoint }, "Fetching agent workspace config...");
|
|
5212
|
+
let workspace;
|
|
5206
5213
|
try {
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
logger$1.debug({
|
|
5231
|
-
templateKey,
|
|
5232
|
-
version: pinnedVersion
|
|
5233
|
-
}, "Fetching template files...");
|
|
5234
|
-
const filesResponse = await fetch(filesUrl.toString(), {
|
|
5235
|
-
method: "GET",
|
|
5236
|
-
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
5237
|
-
});
|
|
5238
|
-
if (!filesResponse.ok) {
|
|
5239
|
-
logger$1.debug({ status: filesResponse.status }, "Template files fetch failed");
|
|
5240
|
-
return {
|
|
5241
|
-
templateKey,
|
|
5242
|
-
defaultModel,
|
|
5243
|
-
files: {}
|
|
5244
|
-
};
|
|
5245
|
-
}
|
|
5246
|
-
const files = (await filesResponse.json()).data?.files ?? {};
|
|
5247
|
-
const fileCount = Object.keys(files).length;
|
|
5248
|
-
logger$1.debug({
|
|
5249
|
-
templateKey,
|
|
5250
|
-
version: pinnedVersion,
|
|
5251
|
-
fileCount
|
|
5252
|
-
}, "Template files fetched");
|
|
5214
|
+
workspace = await client.getWorkspace();
|
|
5215
|
+
} catch (err) {
|
|
5216
|
+
logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Workspace config fetch failed");
|
|
5217
|
+
return null;
|
|
5218
|
+
}
|
|
5219
|
+
const { templateKey, installedFrom } = workspace;
|
|
5220
|
+
const defaultModel = workspace.defaultModel ?? void 0;
|
|
5221
|
+
if (!templateKey) {
|
|
5222
|
+
logger$1.debug("No templateKey in workspace response");
|
|
5223
|
+
return defaultModel ? {
|
|
5224
|
+
defaultModel,
|
|
5225
|
+
files: {}
|
|
5226
|
+
} : null;
|
|
5227
|
+
}
|
|
5228
|
+
const pinnedVersion = installedFrom?.templateKey === templateKey ? installedFrom.version : void 0;
|
|
5229
|
+
logger$1.debug({
|
|
5230
|
+
templateKey,
|
|
5231
|
+
version: pinnedVersion
|
|
5232
|
+
}, "Fetching template files...");
|
|
5233
|
+
let files;
|
|
5234
|
+
try {
|
|
5235
|
+
files = (await client.getTemplateFiles(templateKey, { version: pinnedVersion })).files;
|
|
5236
|
+
} catch (err) {
|
|
5237
|
+
logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Template files fetch failed");
|
|
5253
5238
|
return {
|
|
5254
5239
|
templateKey,
|
|
5255
5240
|
defaultModel,
|
|
5256
|
-
files
|
|
5257
|
-
personaFiles: Object.keys(files)
|
|
5241
|
+
files: {}
|
|
5258
5242
|
};
|
|
5259
|
-
} catch (err) {
|
|
5260
|
-
logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "fetchAgentConfig failed");
|
|
5261
|
-
return null;
|
|
5262
5243
|
}
|
|
5244
|
+
logger$1.debug({
|
|
5245
|
+
templateKey,
|
|
5246
|
+
version: pinnedVersion,
|
|
5247
|
+
fileCount: Object.keys(files).length
|
|
5248
|
+
}, "Template files fetched");
|
|
5249
|
+
return {
|
|
5250
|
+
templateKey,
|
|
5251
|
+
defaultModel,
|
|
5252
|
+
files,
|
|
5253
|
+
personaFiles: Object.keys(files)
|
|
5254
|
+
};
|
|
5263
5255
|
}
|
|
5264
5256
|
//#endregion
|
|
5265
5257
|
//#region src/protocol.ts
|
|
@@ -6359,190 +6351,759 @@ var ConfigReconciler = class {
|
|
|
6359
6351
|
}
|
|
6360
6352
|
};
|
|
6361
6353
|
//#endregion
|
|
6362
|
-
//#region src/
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
*
|
|
6371
|
-
* Each connected plugin registers with its name/version and receives
|
|
6372
|
-
* commands from the daemon (forwarded from cloud).
|
|
6373
|
-
*/
|
|
6374
|
-
var IPCServer = class {
|
|
6375
|
-
server = null;
|
|
6376
|
-
connections = /* @__PURE__ */ new Map();
|
|
6377
|
-
socketPath;
|
|
6378
|
-
requestHandler = null;
|
|
6379
|
-
constructor(socketPath) {
|
|
6380
|
-
this.socketPath = socketPath;
|
|
6381
|
-
}
|
|
6382
|
-
/**
|
|
6383
|
-
* Set the handler for incoming IPC requests from plugins.
|
|
6384
|
-
*/
|
|
6385
|
-
setRequestHandler(handler) {
|
|
6386
|
-
this.requestHandler = handler;
|
|
6354
|
+
//#region src/command-queue.ts
|
|
6355
|
+
const DEFAULT_TTL_MS$1 = 300 * 1e3;
|
|
6356
|
+
var CommandQueue = class {
|
|
6357
|
+
queues = /* @__PURE__ */ new Map();
|
|
6358
|
+
ttlMs;
|
|
6359
|
+
gcTimer = null;
|
|
6360
|
+
constructor(ttlMs = DEFAULT_TTL_MS$1) {
|
|
6361
|
+
this.ttlMs = ttlMs;
|
|
6387
6362
|
}
|
|
6388
6363
|
/**
|
|
6389
|
-
* Start
|
|
6364
|
+
* Start periodic garbage collection of expired commands.
|
|
6390
6365
|
*/
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
} catch {}
|
|
6395
|
-
return new Promise((resolve, reject) => {
|
|
6396
|
-
this.server = createServer((socket) => {
|
|
6397
|
-
this.handleConnection(socket);
|
|
6398
|
-
});
|
|
6399
|
-
this.server.on("error", (err) => {
|
|
6400
|
-
logger$1.error({ err: err.message }, "IPC server error");
|
|
6401
|
-
reject(err);
|
|
6402
|
-
});
|
|
6403
|
-
this.server.listen(this.socketPath, () => {
|
|
6404
|
-
try {
|
|
6405
|
-
chmodSync(this.socketPath, 384);
|
|
6406
|
-
} catch (err) {
|
|
6407
|
-
logger$1.warn({ err }, "Failed to chmod socket");
|
|
6408
|
-
}
|
|
6409
|
-
logger$1.info({ path: this.socketPath }, "IPC server listening");
|
|
6410
|
-
resolve();
|
|
6411
|
-
});
|
|
6412
|
-
});
|
|
6366
|
+
startGC(intervalMs = 3e4) {
|
|
6367
|
+
this.stopGC();
|
|
6368
|
+
this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
|
|
6413
6369
|
}
|
|
6414
6370
|
/**
|
|
6415
|
-
* Stop
|
|
6371
|
+
* Stop periodic garbage collection.
|
|
6416
6372
|
*/
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
this.
|
|
6420
|
-
|
|
6421
|
-
}
|
|
6422
|
-
return new Promise((resolve) => {
|
|
6423
|
-
if (!this.server) {
|
|
6424
|
-
resolve();
|
|
6425
|
-
return;
|
|
6426
|
-
}
|
|
6427
|
-
this.server.close(() => {
|
|
6428
|
-
try {
|
|
6429
|
-
unlinkSync(this.socketPath);
|
|
6430
|
-
} catch {}
|
|
6431
|
-
logger$1.info("IPC server stopped");
|
|
6432
|
-
resolve();
|
|
6433
|
-
});
|
|
6434
|
-
for (const conn of this.connections.values()) conn.socket.destroy();
|
|
6435
|
-
this.connections.clear();
|
|
6436
|
-
});
|
|
6373
|
+
stopGC() {
|
|
6374
|
+
if (this.gcTimer) {
|
|
6375
|
+
clearInterval(this.gcTimer);
|
|
6376
|
+
this.gcTimer = null;
|
|
6377
|
+
}
|
|
6437
6378
|
}
|
|
6438
6379
|
/**
|
|
6439
|
-
*
|
|
6380
|
+
* Enqueue a command for a specific service.
|
|
6440
6381
|
*/
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
if (!
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
const id = pluginConnectionId();
|
|
6452
|
-
const request = {
|
|
6453
|
-
type: "req",
|
|
6454
|
-
id,
|
|
6455
|
-
method,
|
|
6456
|
-
params
|
|
6457
|
-
};
|
|
6458
|
-
return new Promise((resolve, reject) => {
|
|
6459
|
-
const timer = setTimeout(() => {
|
|
6460
|
-
conn.pending.delete(id);
|
|
6461
|
-
resolve({
|
|
6462
|
-
id,
|
|
6463
|
-
ok: false,
|
|
6464
|
-
error: {
|
|
6465
|
-
code: "TIMEOUT",
|
|
6466
|
-
message: `Request ${method} timed out after ${String(timeoutMs)}ms`
|
|
6467
|
-
}
|
|
6468
|
-
});
|
|
6469
|
-
}, timeoutMs);
|
|
6470
|
-
conn.pending.set(id, {
|
|
6471
|
-
resolve,
|
|
6472
|
-
reject,
|
|
6473
|
-
timer
|
|
6474
|
-
});
|
|
6475
|
-
try {
|
|
6476
|
-
conn.socket.write(JSON.stringify(request) + "\n");
|
|
6477
|
-
} catch (err) {
|
|
6478
|
-
clearTimeout(timer);
|
|
6479
|
-
conn.pending.delete(id);
|
|
6480
|
-
resolve({
|
|
6481
|
-
id,
|
|
6482
|
-
ok: false,
|
|
6483
|
-
error: {
|
|
6484
|
-
code: "SEND_FAILED",
|
|
6485
|
-
message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
|
|
6486
|
-
}
|
|
6487
|
-
});
|
|
6488
|
-
}
|
|
6382
|
+
enqueue(serviceId, request, commandId) {
|
|
6383
|
+
let queue = this.queues.get(serviceId);
|
|
6384
|
+
if (!queue) {
|
|
6385
|
+
queue = [];
|
|
6386
|
+
this.queues.set(serviceId, queue);
|
|
6387
|
+
}
|
|
6388
|
+
queue.push({
|
|
6389
|
+
request,
|
|
6390
|
+
queuedAt: Date.now(),
|
|
6391
|
+
commandId
|
|
6489
6392
|
});
|
|
6490
6393
|
}
|
|
6491
6394
|
/**
|
|
6492
|
-
*
|
|
6493
|
-
* Returns
|
|
6395
|
+
* Drain all pending (non-expired) commands for a service.
|
|
6396
|
+
* Returns them in order and removes them from the queue.
|
|
6494
6397
|
*/
|
|
6495
|
-
|
|
6496
|
-
const
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
return results;
|
|
6398
|
+
drain(serviceId) {
|
|
6399
|
+
const queue = this.queues.get(serviceId);
|
|
6400
|
+
if (!queue || queue.length === 0) return [];
|
|
6401
|
+
const now = Date.now();
|
|
6402
|
+
const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
|
|
6403
|
+
this.queues.delete(serviceId);
|
|
6404
|
+
return valid;
|
|
6503
6405
|
}
|
|
6504
6406
|
/**
|
|
6505
|
-
*
|
|
6407
|
+
* Get the number of pending commands for a service.
|
|
6506
6408
|
*/
|
|
6507
|
-
|
|
6508
|
-
const
|
|
6509
|
-
if (!
|
|
6510
|
-
|
|
6409
|
+
pendingCount(serviceId) {
|
|
6410
|
+
const queue = this.queues.get(serviceId);
|
|
6411
|
+
if (!queue) return 0;
|
|
6412
|
+
const now = Date.now();
|
|
6413
|
+
return queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs).length;
|
|
6511
6414
|
}
|
|
6512
6415
|
/**
|
|
6513
|
-
*
|
|
6416
|
+
* Get total pending commands across all services.
|
|
6514
6417
|
*/
|
|
6515
|
-
|
|
6516
|
-
|
|
6418
|
+
totalPending() {
|
|
6419
|
+
let total = 0;
|
|
6420
|
+
for (const [serviceId] of this.queues) total += this.pendingCount(serviceId);
|
|
6421
|
+
return total;
|
|
6517
6422
|
}
|
|
6518
6423
|
/**
|
|
6519
|
-
*
|
|
6424
|
+
* Remove expired commands from all queues.
|
|
6520
6425
|
*/
|
|
6521
|
-
|
|
6522
|
-
const
|
|
6523
|
-
|
|
6524
|
-
|
|
6426
|
+
purgeExpired() {
|
|
6427
|
+
const now = Date.now();
|
|
6428
|
+
let purged = 0;
|
|
6429
|
+
for (const [serviceId, queue] of this.queues) {
|
|
6430
|
+
const before = queue.length;
|
|
6431
|
+
const remaining = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
|
|
6432
|
+
if (remaining.length === 0) this.queues.delete(serviceId);
|
|
6433
|
+
else this.queues.set(serviceId, remaining);
|
|
6434
|
+
purged += before - remaining.length;
|
|
6435
|
+
}
|
|
6436
|
+
return purged;
|
|
6525
6437
|
}
|
|
6526
6438
|
/**
|
|
6527
|
-
*
|
|
6439
|
+
* Clear all queues.
|
|
6528
6440
|
*/
|
|
6529
|
-
|
|
6530
|
-
|
|
6441
|
+
clear() {
|
|
6442
|
+
this.queues.clear();
|
|
6531
6443
|
}
|
|
6532
6444
|
/**
|
|
6533
|
-
* Get
|
|
6445
|
+
* Get all service IDs that have pending commands.
|
|
6534
6446
|
*/
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
for (const conn of this.connections.values()) if (conn.info) count++;
|
|
6538
|
-
return count;
|
|
6447
|
+
serviceIds() {
|
|
6448
|
+
return Array.from(this.queues.keys());
|
|
6539
6449
|
}
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6450
|
+
};
|
|
6451
|
+
//#endregion
|
|
6452
|
+
//#region src/process-manager.ts
|
|
6453
|
+
/**
|
|
6454
|
+
* Process management — launchd/systemd service installation.
|
|
6455
|
+
*
|
|
6456
|
+
* Generates and installs user-space service units for auto-start on boot.
|
|
6457
|
+
* No root required — uses user agents (Mac) or user units (Linux).
|
|
6458
|
+
*/
|
|
6459
|
+
const LAUNCHD_LABEL = "ai.alfe.gateway";
|
|
6460
|
+
const SYSTEMD_SERVICE = "alfe-gateway";
|
|
6461
|
+
/**
|
|
6462
|
+
* On-disk path for the boot-time self-heal guard script (Linux only).
|
|
6463
|
+
* Written next to the systemd unit at setup time and invoked via
|
|
6464
|
+
* `ExecStartPre=` — see `writeGuardScript` and `generateSystemdUnit`.
|
|
6465
|
+
*/
|
|
6466
|
+
function getGuardScriptPath() {
|
|
6467
|
+
return isRootUser() ? "/usr/local/lib/alfe/alfe-cli-guard.sh" : join(homedir(), ".alfe", "alfe-cli-guard.sh");
|
|
6468
|
+
}
|
|
6469
|
+
function isRootUser() {
|
|
6470
|
+
return process.getuid?.() === 0;
|
|
6471
|
+
}
|
|
6472
|
+
function getSystemdSystemServicePath() {
|
|
6473
|
+
return `/etc/systemd/system/${SYSTEMD_SERVICE}.service`;
|
|
6474
|
+
}
|
|
6475
|
+
function getLaunchdPlistPath() {
|
|
6476
|
+
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
6477
|
+
}
|
|
6478
|
+
function getSystemdServicePath() {
|
|
6479
|
+
return join(homedir(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
|
|
6480
|
+
}
|
|
6481
|
+
/**
|
|
6482
|
+
* Resolve the alfe CLI path. Globally installed via npm, always in PATH.
|
|
6483
|
+
*/
|
|
6484
|
+
function getAlfeBinPath() {
|
|
6485
|
+
try {
|
|
6486
|
+
return execSync("which alfe", { encoding: "utf-8" }).trim();
|
|
6487
|
+
} catch {
|
|
6488
|
+
return "alfe";
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
/**
|
|
6492
|
+
* Build the PATH for the launchd plist, ensuring the directory containing the
|
|
6493
|
+
* resolved alfe binary is included (e.g. /opt/homebrew/bin on Apple Silicon).
|
|
6494
|
+
*/
|
|
6495
|
+
function buildLaunchdPath(alfeBin) {
|
|
6496
|
+
const basePaths = [
|
|
6497
|
+
"/usr/local/bin",
|
|
6498
|
+
"/usr/bin",
|
|
6499
|
+
"/bin",
|
|
6500
|
+
join(homedir(), ".local", "bin")
|
|
6501
|
+
];
|
|
6502
|
+
const binDir = dirname(alfeBin);
|
|
6503
|
+
if (binDir && !basePaths.includes(binDir)) basePaths.unshift(binDir);
|
|
6504
|
+
return basePaths.join(":");
|
|
6505
|
+
}
|
|
6506
|
+
/**
|
|
6507
|
+
* Generate a launchd plist for macOS.
|
|
6508
|
+
*/
|
|
6509
|
+
function generateLaunchdPlist() {
|
|
6510
|
+
const alfeBin = getAlfeBinPath();
|
|
6511
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
6512
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
6513
|
+
<plist version="1.0">
|
|
6514
|
+
<dict>
|
|
6515
|
+
<key>Label</key>
|
|
6516
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
6517
|
+
<key>ProgramArguments</key>
|
|
6518
|
+
<array>
|
|
6519
|
+
<string>${alfeBin}</string>
|
|
6520
|
+
<string>gateway</string>
|
|
6521
|
+
<string>daemon</string>
|
|
6522
|
+
</array>
|
|
6523
|
+
<key>RunAtLoad</key>
|
|
6524
|
+
<true/>
|
|
6525
|
+
<key>KeepAlive</key>
|
|
6526
|
+
<true/>
|
|
6527
|
+
<key>ThrottleInterval</key>
|
|
6528
|
+
<integer>10</integer>
|
|
6529
|
+
<key>StandardOutPath</key>
|
|
6530
|
+
<string>${join(homedir(), ".alfe", "logs", "gateway.log")}</string>
|
|
6531
|
+
<key>StandardErrorPath</key>
|
|
6532
|
+
<string>${join(homedir(), ".alfe", "logs", "gateway.err.log")}</string>
|
|
6533
|
+
<key>EnvironmentVariables</key>
|
|
6534
|
+
<dict>
|
|
6535
|
+
<key>NODE_ENV</key>
|
|
6536
|
+
<string>production</string>
|
|
6537
|
+
<key>PATH</key>
|
|
6538
|
+
<string>${buildLaunchdPath(alfeBin)}</string>
|
|
6539
|
+
</dict>
|
|
6540
|
+
</dict>
|
|
6541
|
+
</plist>`;
|
|
6542
|
+
}
|
|
6543
|
+
/**
|
|
6544
|
+
* Boot-time self-heal guard (Linux only).
|
|
6545
|
+
*
|
|
6546
|
+
* Runs as an `ExecStartPre=` BEFORE the daemon launches, OUTSIDE the
|
|
6547
|
+
* (possibly-broken) `alfe` binary. It exists for the case defense #1
|
|
6548
|
+
* (verify-before-exit in `upgrade.ts`) structurally cannot cover: the daemon
|
|
6549
|
+
* is KILLED during `npm install -g @alfe.ai/cli` (reboot / OOM), leaving the
|
|
6550
|
+
* package with deps but no `dist/` and a dangling `/usr/bin/alfe` symlink.
|
|
6551
|
+
* systemd then execs a dangling binary forever → 203/EXEC crash-loop, a
|
|
6552
|
+
* permanent brick with no automatic recovery (real prod incident,
|
|
6553
|
+
* ~13,570 restarts over 2 days).
|
|
6554
|
+
*
|
|
6555
|
+
* The guard:
|
|
6556
|
+
* - checks the CLI is intact: the symlink target (`dist/index.js`) exists AND
|
|
6557
|
+
* `alfe --version` exits 0;
|
|
6558
|
+
* - if broken, reinstalls `@alfe.ai/cli@$ALFE_CLI_VERSION` (the exact version
|
|
6559
|
+
* baked into the unit's `Environment=`; falls back to `@latest` when
|
|
6560
|
+
* unset), so the daemon then execs a working binary;
|
|
6561
|
+
* - is idempotent + fast (no-op when healthy) and NEVER wedges boot: any of
|
|
6562
|
+
* its own failures log + `exit 0` so a human can still SSH in.
|
|
6563
|
+
*
|
|
6564
|
+
* POSIX sh (dash-safe): the box may not have bash. Keep the two intactness
|
|
6565
|
+
* checks in lockstep with `verifyCliInstall` in `verify-cli-install.ts`.
|
|
6566
|
+
*/
|
|
6567
|
+
/** @internal exported for unit tests; not re-exported from the barrel. */
|
|
6568
|
+
function generateGuardScript() {
|
|
6569
|
+
const version = process.env.ALFE_CLI_VERSION;
|
|
6570
|
+
return `#!/bin/sh
|
|
6571
|
+
# Alfe CLI boot-time self-heal guard. Auto-generated by 'alfe setup' — do not edit.
|
|
6572
|
+
# Repairs an interrupted 'npm install -g @alfe.ai/cli' before the daemon starts,
|
|
6573
|
+
# so a mid-install reboot can't brick the agent with a 203/EXEC crash-loop.
|
|
6574
|
+
# Must never wedge boot: every failure path logs and exits 0.
|
|
6575
|
+
set -u
|
|
6576
|
+
|
|
6577
|
+
TARGET='${version && version.length > 0 ? `@alfe.ai/cli@${version}` : "@alfe.ai/cli@latest"}'
|
|
6578
|
+
log() { echo "[alfe-cli-guard] $*" >&2; }
|
|
6579
|
+
|
|
6580
|
+
# Resolve the alfe bin and its real target (dist/index.js). readlink -f follows
|
|
6581
|
+
# the whole symlink chain; fall back to the bin path itself if unavailable.
|
|
6582
|
+
BIN="$(command -v alfe 2>/dev/null || true)"
|
|
6583
|
+
if [ -n "$BIN" ]; then
|
|
6584
|
+
ENTRY="$(readlink -f "$BIN" 2>/dev/null || echo "$BIN")"
|
|
6585
|
+
else
|
|
6586
|
+
ENTRY=""
|
|
6587
|
+
fi
|
|
6588
|
+
|
|
6589
|
+
healthy=1
|
|
6590
|
+
if [ -z "$ENTRY" ] || [ ! -f "$ENTRY" ]; then
|
|
6591
|
+
healthy=0
|
|
6592
|
+
log "CLI entry missing (bin=\${BIN:-<none>} entry=\${ENTRY:-<none>})"
|
|
6593
|
+
elif ! alfe --version >/dev/null 2>&1; then
|
|
6594
|
+
healthy=0
|
|
6595
|
+
log "alfe --version failed"
|
|
6596
|
+
fi
|
|
6597
|
+
|
|
6598
|
+
if [ "$healthy" -eq 1 ]; then
|
|
6599
|
+
exit 0
|
|
6600
|
+
fi
|
|
6601
|
+
|
|
6602
|
+
log "CLI install looks broken — reinstalling $TARGET"
|
|
6603
|
+
# Blow away a partial tree first so npm re-materialises dist/ cleanly. Best-effort.
|
|
6604
|
+
GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
6605
|
+
if [ -n "$GLOBAL_ROOT" ] && [ -d "$GLOBAL_ROOT/@alfe.ai/cli" ]; then
|
|
6606
|
+
rm -rf "$GLOBAL_ROOT/@alfe.ai/cli" 2>/dev/null || true
|
|
6607
|
+
fi
|
|
6608
|
+
|
|
6609
|
+
# Bound the reinstall so a slow/unreachable registry at boot can't stall the
|
|
6610
|
+
# ExecStartPre up to systemd's DefaultTimeoutStartSec (~90s). Two layers:
|
|
6611
|
+
# - npm --fetch-timeout/--fetch-retries cap per-request/hung-socket time;
|
|
6612
|
+
# - a wall-clock 'timeout' wrapper caps the whole install when present
|
|
6613
|
+
# ('timeout' isn't guaranteed, so fall back to a bare install).
|
|
6614
|
+
# stdout->stderr ('>&2') so npm's chatter lands in the journal, not on fd1.
|
|
6615
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
6616
|
+
timeout 120 npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
|
|
6617
|
+
rc=$?
|
|
6618
|
+
else
|
|
6619
|
+
npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
|
|
6620
|
+
rc=$?
|
|
6621
|
+
fi
|
|
6622
|
+
if [ "$rc" -eq 0 ]; then
|
|
6623
|
+
log "reinstall succeeded"
|
|
6624
|
+
else
|
|
6625
|
+
log "reinstall FAILED (rc=$rc) — daemon start may still fail; a human can SSH in to repair"
|
|
6626
|
+
fi
|
|
6627
|
+
|
|
6628
|
+
# Always exit 0: even a failed repair must not block the unit from attempting
|
|
6629
|
+
# ExecStart (which then fails visibly) rather than wedging silently in pre-start.
|
|
6630
|
+
exit 0
|
|
6631
|
+
`;
|
|
6632
|
+
}
|
|
6633
|
+
/**
|
|
6634
|
+
* Generate a systemd unit for Linux.
|
|
6635
|
+
* Root users get a system-level unit; non-root get a user-level unit.
|
|
6636
|
+
*/
|
|
6637
|
+
/** @internal exported for unit tests; not re-exported from the barrel. */
|
|
6638
|
+
function generateSystemdUnit() {
|
|
6639
|
+
const alfeBin = getAlfeBinPath();
|
|
6640
|
+
const root = isRootUser();
|
|
6641
|
+
const envLines = [
|
|
6642
|
+
"ALFE_MANAGED",
|
|
6643
|
+
"ALFE_API_KEY",
|
|
6644
|
+
"LOG_LEVEL",
|
|
6645
|
+
"ALFE_CLI_VERSION"
|
|
6646
|
+
].filter((key) => process.env[key]).map((key) => `Environment=${key}=${process.env[key] ?? ""}`).join("\n");
|
|
6647
|
+
return `[Unit]
|
|
6648
|
+
Description=Alfe Gateway Daemon
|
|
6649
|
+
After=network-online.target
|
|
6650
|
+
Wants=network-online.target
|
|
6651
|
+
|
|
6652
|
+
[Service]
|
|
6653
|
+
Type=simple
|
|
6654
|
+
ExecStartPre=-/bin/sh ${getGuardScriptPath()}
|
|
6655
|
+
ExecStart=${alfeBin} gateway daemon
|
|
6656
|
+
Restart=always
|
|
6657
|
+
RestartSec=10
|
|
6658
|
+
# SIGTERM only the daemon on stop/restart; it closes its MCP/runtime children
|
|
6659
|
+
# itself. The default (control-group) SIGTERMs the children simultaneously, so
|
|
6660
|
+
# they die before the daemon's orderly dispose reaches them and every planned
|
|
6661
|
+
# restart reports MCP "server-crash" noise to Sentry. Stragglers still get
|
|
6662
|
+
# SIGKILL when the stop timeout expires.
|
|
6663
|
+
KillMode=mixed
|
|
6664
|
+
Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
|
|
6665
|
+
${envLines}
|
|
6666
|
+
|
|
6667
|
+
[Install]
|
|
6668
|
+
WantedBy=${root ? "multi-user.target" : "default.target"}`;
|
|
6669
|
+
}
|
|
6670
|
+
/**
|
|
6671
|
+
* Write the boot-time self-heal guard script to disk (Linux only) and make it
|
|
6672
|
+
* executable. Called from `installSystemd` before the unit is written.
|
|
6673
|
+
*/
|
|
6674
|
+
async function writeGuardScript() {
|
|
6675
|
+
const guardPath = getGuardScriptPath();
|
|
6676
|
+
await mkdir(dirname(guardPath), { recursive: true });
|
|
6677
|
+
await writeFile(guardPath, generateGuardScript(), {
|
|
6678
|
+
encoding: "utf-8",
|
|
6679
|
+
mode: 493
|
|
6680
|
+
});
|
|
6681
|
+
logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
|
|
6682
|
+
}
|
|
6683
|
+
/**
|
|
6684
|
+
* Install the service unit for the current platform.
|
|
6685
|
+
*/
|
|
6686
|
+
async function installService() {
|
|
6687
|
+
const platform = process.platform;
|
|
6688
|
+
if (platform === "darwin") return installLaunchd();
|
|
6689
|
+
if (platform === "linux") return installSystemd();
|
|
6690
|
+
throw new Error(`Unsupported platform: ${platform}. Only macOS and Linux are supported.`);
|
|
6691
|
+
}
|
|
6692
|
+
/**
|
|
6693
|
+
* Uninstall the service unit for the current platform.
|
|
6694
|
+
*/
|
|
6695
|
+
async function uninstallService() {
|
|
6696
|
+
const platform = process.platform;
|
|
6697
|
+
if (platform === "darwin") return uninstallLaunchd();
|
|
6698
|
+
if (platform === "linux") return uninstallSystemd();
|
|
6699
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
6700
|
+
}
|
|
6701
|
+
function getLaunchdUid() {
|
|
6702
|
+
return execSync("id -u", { encoding: "utf-8" }).trim();
|
|
6703
|
+
}
|
|
6704
|
+
async function installLaunchd() {
|
|
6705
|
+
const plistPath = getLaunchdPlistPath();
|
|
6706
|
+
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
6707
|
+
const logsDir = join(homedir(), ".alfe", "logs");
|
|
6708
|
+
await mkdir(dir, { recursive: true });
|
|
6709
|
+
await mkdir(logsDir, { recursive: true });
|
|
6710
|
+
await writeFile(plistPath, generateLaunchdPlist(), "utf-8");
|
|
6711
|
+
logger$1.info({ path: plistPath }, "Wrote launchd plist");
|
|
6712
|
+
const uid = getLaunchdUid();
|
|
6713
|
+
try {
|
|
6714
|
+
execSync(`launchctl bootout gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
|
|
6715
|
+
} catch {}
|
|
6716
|
+
try {
|
|
6717
|
+
execSync(`launchctl bootstrap gui/${uid} ${plistPath}`, { stdio: "pipe" });
|
|
6718
|
+
} catch (err) {
|
|
6719
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6720
|
+
logger$1.error({
|
|
6721
|
+
err: msg,
|
|
6722
|
+
plistPath
|
|
6723
|
+
}, "launchctl bootstrap failed");
|
|
6724
|
+
return `Installed plist at ${plistPath}, but failed to bootstrap service: ${msg}`;
|
|
6725
|
+
}
|
|
6726
|
+
return `Installed: ${plistPath}\nService will start on boot and restart on crash.`;
|
|
6727
|
+
}
|
|
6728
|
+
async function uninstallLaunchd() {
|
|
6729
|
+
const plistPath = getLaunchdPlistPath();
|
|
6730
|
+
const uid = getLaunchdUid();
|
|
6731
|
+
try {
|
|
6732
|
+
execSync(`launchctl bootout gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
|
|
6733
|
+
} catch {}
|
|
6734
|
+
try {
|
|
6735
|
+
await unlink(plistPath);
|
|
6736
|
+
} catch {}
|
|
6737
|
+
return `Uninstalled: ${plistPath}`;
|
|
6738
|
+
}
|
|
6739
|
+
async function installSystemd() {
|
|
6740
|
+
const root = isRootUser();
|
|
6741
|
+
const unitPath = root ? getSystemdSystemServicePath() : getSystemdServicePath();
|
|
6742
|
+
const dir = root ? "/etc/systemd/system" : join(homedir(), ".config", "systemd", "user");
|
|
6743
|
+
const ctl = root ? "systemctl" : "systemctl --user";
|
|
6744
|
+
if (!root) await mkdir(dir, { recursive: true });
|
|
6745
|
+
await writeGuardScript();
|
|
6746
|
+
await writeFile(unitPath, generateSystemdUnit(), "utf-8");
|
|
6747
|
+
logger$1.info({
|
|
6748
|
+
path: unitPath,
|
|
6749
|
+
root
|
|
6750
|
+
}, "Wrote systemd unit");
|
|
6751
|
+
try {
|
|
6752
|
+
execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
|
|
6753
|
+
execSync(`${ctl} enable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
6754
|
+
} catch {}
|
|
6755
|
+
return `Installed: ${unitPath}\nService enabled${root ? " (system-level)" : " for user session"}.`;
|
|
6756
|
+
}
|
|
6757
|
+
async function uninstallSystemd() {
|
|
6758
|
+
const root = isRootUser();
|
|
6759
|
+
const unitPath = root ? getSystemdSystemServicePath() : getSystemdServicePath();
|
|
6760
|
+
const ctl = root ? "systemctl" : "systemctl --user";
|
|
6761
|
+
try {
|
|
6762
|
+
execSync(`${ctl} disable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
6763
|
+
execSync(`${ctl} stop ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
6764
|
+
} catch {}
|
|
6765
|
+
try {
|
|
6766
|
+
await unlink(unitPath);
|
|
6767
|
+
} catch {}
|
|
6768
|
+
try {
|
|
6769
|
+
await unlink(getGuardScriptPath());
|
|
6770
|
+
} catch {}
|
|
6771
|
+
try {
|
|
6772
|
+
execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
|
|
6773
|
+
} catch {}
|
|
6774
|
+
return `Uninstalled: ${unitPath}`;
|
|
6775
|
+
}
|
|
6776
|
+
/**
|
|
6777
|
+
* Bootstrap (load) the launchd service from its plist if it isn't already
|
|
6778
|
+
* loaded. Already-loaded is an error we ignore. Needed so `start`/`restart`
|
|
6779
|
+
* work after `stopService()` boots the service OUT.
|
|
6780
|
+
*/
|
|
6781
|
+
function ensureLaunchdLoaded(uid) {
|
|
6782
|
+
try {
|
|
6783
|
+
execSync(`launchctl bootstrap gui/${uid} ${getLaunchdPlistPath()}`, { stdio: "pipe" });
|
|
6784
|
+
} catch {}
|
|
6785
|
+
}
|
|
6786
|
+
/**
|
|
6787
|
+
* Start the installed service via systemctl/launchctl. Bootstraps the launchd
|
|
6788
|
+
* service first so this also works after `stopService()` booted it out.
|
|
6789
|
+
*/
|
|
6790
|
+
function startService() {
|
|
6791
|
+
const platform = process.platform;
|
|
6792
|
+
if (platform === "darwin") {
|
|
6793
|
+
const uid = getLaunchdUid();
|
|
6794
|
+
ensureLaunchdLoaded(uid);
|
|
6795
|
+
execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
|
|
6796
|
+
logger$1.info("Started launchd service");
|
|
6797
|
+
return;
|
|
6798
|
+
}
|
|
6799
|
+
if (platform === "linux") {
|
|
6800
|
+
execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} start ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
6801
|
+
logger$1.info("Started systemd service");
|
|
6802
|
+
return;
|
|
6803
|
+
}
|
|
6804
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
6805
|
+
}
|
|
6806
|
+
/**
|
|
6807
|
+
* Restart the installed service via systemctl/launchctl. This is the
|
|
6808
|
+
* service-manager-native restart (`kickstart -k` / `systemctl restart`) — the
|
|
6809
|
+
* CLI uses it instead of killing the process inline, which would fight launchd
|
|
6810
|
+
* `KeepAlive` / systemd `Restart=always`.
|
|
6811
|
+
*/
|
|
6812
|
+
function restartService() {
|
|
6813
|
+
const platform = process.platform;
|
|
6814
|
+
if (platform === "darwin") {
|
|
6815
|
+
const uid = getLaunchdUid();
|
|
6816
|
+
ensureLaunchdLoaded(uid);
|
|
6817
|
+
execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
|
|
6818
|
+
logger$1.info("Restarted launchd service");
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
if (platform === "linux") {
|
|
6822
|
+
execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} restart ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
6823
|
+
logger$1.info("Restarted systemd service");
|
|
6824
|
+
return;
|
|
6825
|
+
}
|
|
6826
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
6827
|
+
}
|
|
6828
|
+
/**
|
|
6829
|
+
* Stop the installed service via systemctl/launchctl. On macOS this boots the
|
|
6830
|
+
* service OUT (unloads it) so launchd's `KeepAlive` does NOT respawn it;
|
|
6831
|
+
* `startService()` bootstraps it again. On Linux the unit stays enabled (starts
|
|
6832
|
+
* on next boot); `systemctl stop` just halts the current run.
|
|
6833
|
+
*/
|
|
6834
|
+
function stopService() {
|
|
6835
|
+
const platform = process.platform;
|
|
6836
|
+
if (platform === "darwin") {
|
|
6837
|
+
execSync(`launchctl bootout gui/${getLaunchdUid()}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
|
|
6838
|
+
logger$1.info("Stopped launchd service");
|
|
6839
|
+
return;
|
|
6840
|
+
}
|
|
6841
|
+
if (platform === "linux") {
|
|
6842
|
+
execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} stop ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
6843
|
+
logger$1.info("Stopped systemd service");
|
|
6844
|
+
return;
|
|
6845
|
+
}
|
|
6846
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
6847
|
+
}
|
|
6848
|
+
/**
|
|
6849
|
+
* True when the gateway is installed as a system service (launchd plist /
|
|
6850
|
+
* systemd unit present). Lets the CLI restart/stop/start THROUGH the service
|
|
6851
|
+
* manager instead of driving the daemon inline via the PID file.
|
|
6852
|
+
*/
|
|
6853
|
+
function isServiceInstalled() {
|
|
6854
|
+
const platform = process.platform;
|
|
6855
|
+
if (platform === "darwin") return existsSync(getLaunchdPlistPath());
|
|
6856
|
+
if (platform === "linux") return existsSync(isRootUser() ? getSystemdSystemServicePath() : getSystemdServicePath());
|
|
6857
|
+
return false;
|
|
6858
|
+
}
|
|
6859
|
+
/**
|
|
6860
|
+
* Write the current process PID to the PID file.
|
|
6861
|
+
*/
|
|
6862
|
+
async function writePidFile() {
|
|
6863
|
+
await writeFile(PID_PATH, String(process.pid), "utf-8");
|
|
6864
|
+
}
|
|
6865
|
+
/**
|
|
6866
|
+
* Remove the PID file.
|
|
6867
|
+
*/
|
|
6868
|
+
async function removePidFile() {
|
|
6869
|
+
try {
|
|
6870
|
+
await unlink(PID_PATH);
|
|
6871
|
+
} catch {}
|
|
6872
|
+
}
|
|
6873
|
+
/**
|
|
6874
|
+
* Check if a daemon is already running.
|
|
6875
|
+
* Returns the PID if alive, null if not running.
|
|
6876
|
+
*/
|
|
6877
|
+
async function checkExistingDaemon() {
|
|
6878
|
+
if (!existsSync(PID_PATH)) return null;
|
|
6879
|
+
try {
|
|
6880
|
+
const pidStr = await readFile(PID_PATH, "utf-8");
|
|
6881
|
+
const pid = parseInt(pidStr.trim(), 10);
|
|
6882
|
+
if (isNaN(pid)) {
|
|
6883
|
+
await removePidFile();
|
|
6884
|
+
return null;
|
|
6885
|
+
}
|
|
6886
|
+
try {
|
|
6887
|
+
process.kill(pid, 0);
|
|
6888
|
+
return pid;
|
|
6889
|
+
} catch {
|
|
6890
|
+
await removePidFile();
|
|
6891
|
+
return null;
|
|
6892
|
+
}
|
|
6893
|
+
} catch {
|
|
6894
|
+
return null;
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
/**
|
|
6898
|
+
* Send SIGTERM to an existing daemon process.
|
|
6899
|
+
*/
|
|
6900
|
+
async function stopExistingDaemon() {
|
|
6901
|
+
const pid = await checkExistingDaemon();
|
|
6902
|
+
if (!pid) return false;
|
|
6903
|
+
try {
|
|
6904
|
+
process.kill(pid, "SIGTERM");
|
|
6905
|
+
for (let i = 0; i < 50; i++) {
|
|
6906
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
6907
|
+
try {
|
|
6908
|
+
process.kill(pid, 0);
|
|
6909
|
+
} catch {
|
|
6910
|
+
await removePidFile();
|
|
6911
|
+
return true;
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
process.kill(pid, "SIGKILL");
|
|
6915
|
+
await removePidFile();
|
|
6916
|
+
return true;
|
|
6917
|
+
} catch {
|
|
6918
|
+
await removePidFile();
|
|
6919
|
+
return false;
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
//#endregion
|
|
6923
|
+
//#region src/ipc-server.ts
|
|
6924
|
+
/**
|
|
6925
|
+
* IPC Server — Unix socket server for local plugin connections.
|
|
6926
|
+
*
|
|
6927
|
+
* Plugins connect to ~/.alfe/gateway.sock and speak the IPC protocol:
|
|
6928
|
+
* Request: { type: 'req', id, method, params }
|
|
6929
|
+
* Response: { id, ok, payload?, error? }
|
|
6930
|
+
* Event: { type: 'event', event, payload }
|
|
6931
|
+
*
|
|
6932
|
+
* Each connected plugin registers with its name/version and receives
|
|
6933
|
+
* commands from the daemon (forwarded from cloud).
|
|
6934
|
+
*/
|
|
6935
|
+
var IPCServer = class {
|
|
6936
|
+
server = null;
|
|
6937
|
+
connections = /* @__PURE__ */ new Map();
|
|
6938
|
+
socketPath;
|
|
6939
|
+
requestHandler = null;
|
|
6940
|
+
constructor(socketPath) {
|
|
6941
|
+
this.socketPath = socketPath;
|
|
6942
|
+
}
|
|
6943
|
+
/**
|
|
6944
|
+
* Set the handler for incoming IPC requests from plugins.
|
|
6945
|
+
*/
|
|
6946
|
+
setRequestHandler(handler) {
|
|
6947
|
+
this.requestHandler = handler;
|
|
6948
|
+
}
|
|
6949
|
+
/**
|
|
6950
|
+
* Start listening on the Unix socket.
|
|
6951
|
+
*/
|
|
6952
|
+
async start() {
|
|
6953
|
+
try {
|
|
6954
|
+
unlinkSync(this.socketPath);
|
|
6955
|
+
} catch {}
|
|
6956
|
+
return new Promise((resolve, reject) => {
|
|
6957
|
+
this.server = createServer((socket) => {
|
|
6958
|
+
this.handleConnection(socket);
|
|
6959
|
+
});
|
|
6960
|
+
this.server.on("error", (err) => {
|
|
6961
|
+
logger$1.error({ err: err.message }, "IPC server error");
|
|
6962
|
+
reject(err);
|
|
6963
|
+
});
|
|
6964
|
+
this.server.listen(this.socketPath, () => {
|
|
6965
|
+
try {
|
|
6966
|
+
chmodSync(this.socketPath, 384);
|
|
6967
|
+
} catch (err) {
|
|
6968
|
+
logger$1.warn({ err }, "Failed to chmod socket");
|
|
6969
|
+
}
|
|
6970
|
+
logger$1.info({ path: this.socketPath }, "IPC server listening");
|
|
6971
|
+
resolve();
|
|
6972
|
+
});
|
|
6973
|
+
});
|
|
6974
|
+
}
|
|
6975
|
+
/**
|
|
6976
|
+
* Stop the IPC server and disconnect all plugins.
|
|
6977
|
+
*/
|
|
6978
|
+
async stop() {
|
|
6979
|
+
for (const conn of this.connections.values()) try {
|
|
6980
|
+
this.sendEvent(conn, "daemon.shutdown", { reason: "daemon stopping" });
|
|
6981
|
+
conn.socket.end();
|
|
6982
|
+
} catch {}
|
|
6983
|
+
return new Promise((resolve) => {
|
|
6984
|
+
if (!this.server) {
|
|
6985
|
+
resolve();
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
this.server.close(() => {
|
|
6989
|
+
try {
|
|
6990
|
+
unlinkSync(this.socketPath);
|
|
6991
|
+
} catch {}
|
|
6992
|
+
logger$1.info("IPC server stopped");
|
|
6993
|
+
resolve();
|
|
6994
|
+
});
|
|
6995
|
+
for (const conn of this.connections.values()) conn.socket.destroy();
|
|
6996
|
+
this.connections.clear();
|
|
6997
|
+
});
|
|
6998
|
+
}
|
|
6999
|
+
/**
|
|
7000
|
+
* Send an IPC request to a specific plugin and wait for response.
|
|
7001
|
+
*/
|
|
7002
|
+
async sendRequest(pluginId, method, params, timeoutMs = 3e4) {
|
|
7003
|
+
const conn = this.connections.get(pluginId);
|
|
7004
|
+
if (!conn?.info) return {
|
|
7005
|
+
id: "",
|
|
7006
|
+
ok: false,
|
|
7007
|
+
error: {
|
|
7008
|
+
code: "PLUGIN_NOT_CONNECTED",
|
|
7009
|
+
message: `Plugin ${pluginId} not connected`
|
|
7010
|
+
}
|
|
7011
|
+
};
|
|
7012
|
+
const id = pluginConnectionId();
|
|
7013
|
+
const request = {
|
|
7014
|
+
type: "req",
|
|
7015
|
+
id,
|
|
7016
|
+
method,
|
|
7017
|
+
params
|
|
7018
|
+
};
|
|
7019
|
+
return new Promise((resolve, reject) => {
|
|
7020
|
+
const timer = setTimeout(() => {
|
|
7021
|
+
conn.pending.delete(id);
|
|
7022
|
+
resolve({
|
|
7023
|
+
id,
|
|
7024
|
+
ok: false,
|
|
7025
|
+
error: {
|
|
7026
|
+
code: "TIMEOUT",
|
|
7027
|
+
message: `Request ${method} timed out after ${String(timeoutMs)}ms`
|
|
7028
|
+
}
|
|
7029
|
+
});
|
|
7030
|
+
}, timeoutMs);
|
|
7031
|
+
conn.pending.set(id, {
|
|
7032
|
+
resolve,
|
|
7033
|
+
reject,
|
|
7034
|
+
timer
|
|
7035
|
+
});
|
|
7036
|
+
try {
|
|
7037
|
+
conn.socket.write(JSON.stringify(request) + "\n");
|
|
7038
|
+
} catch (err) {
|
|
7039
|
+
clearTimeout(timer);
|
|
7040
|
+
conn.pending.delete(id);
|
|
7041
|
+
resolve({
|
|
7042
|
+
id,
|
|
7043
|
+
ok: false,
|
|
7044
|
+
error: {
|
|
7045
|
+
code: "SEND_FAILED",
|
|
7046
|
+
message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
|
|
7047
|
+
}
|
|
7048
|
+
});
|
|
7049
|
+
}
|
|
7050
|
+
});
|
|
7051
|
+
}
|
|
7052
|
+
/**
|
|
7053
|
+
* Send an IPC request to ALL registered plugins.
|
|
7054
|
+
* Returns responses keyed by plugin ID.
|
|
7055
|
+
*/
|
|
7056
|
+
async broadcastRequest(method, params, timeoutMs = 3e4) {
|
|
7057
|
+
const results = /* @__PURE__ */ new Map();
|
|
7058
|
+
const registered = this.getRegisteredPlugins();
|
|
7059
|
+
await Promise.all(registered.map(async ([pluginId]) => {
|
|
7060
|
+
const response = await this.sendRequest(pluginId, method, params, timeoutMs);
|
|
7061
|
+
results.set(pluginId, response);
|
|
7062
|
+
}));
|
|
7063
|
+
return results;
|
|
7064
|
+
}
|
|
7065
|
+
/**
|
|
7066
|
+
* Send an event to a specific plugin (fire-and-forget).
|
|
7067
|
+
*/
|
|
7068
|
+
sendEventToPlugin(pluginId, event, payload) {
|
|
7069
|
+
const conn = this.connections.get(pluginId);
|
|
7070
|
+
if (!conn) return false;
|
|
7071
|
+
return this.sendEvent(conn, event, payload);
|
|
7072
|
+
}
|
|
7073
|
+
/**
|
|
7074
|
+
* Broadcast an event to all connected plugins.
|
|
7075
|
+
*/
|
|
7076
|
+
broadcastEvent(event, payload) {
|
|
7077
|
+
for (const conn of this.connections.values()) if (conn.info) this.sendEvent(conn, event, payload);
|
|
7078
|
+
}
|
|
7079
|
+
/**
|
|
7080
|
+
* Get all registered plugins and their info.
|
|
7081
|
+
*/
|
|
7082
|
+
getRegisteredPlugins() {
|
|
7083
|
+
const plugins = [];
|
|
7084
|
+
for (const [id, conn] of this.connections) if (conn.info) plugins.push([id, conn.info]);
|
|
7085
|
+
return plugins;
|
|
7086
|
+
}
|
|
7087
|
+
/**
|
|
7088
|
+
* Get number of connected plugins (including unregistered).
|
|
7089
|
+
*/
|
|
7090
|
+
get connectionCount() {
|
|
7091
|
+
return this.connections.size;
|
|
7092
|
+
}
|
|
7093
|
+
/**
|
|
7094
|
+
* Get number of registered plugins.
|
|
7095
|
+
*/
|
|
7096
|
+
get registeredCount() {
|
|
7097
|
+
let count = 0;
|
|
7098
|
+
for (const conn of this.connections.values()) if (conn.info) count++;
|
|
7099
|
+
return count;
|
|
7100
|
+
}
|
|
7101
|
+
handleConnection(socket) {
|
|
7102
|
+
const connId = pluginConnectionId();
|
|
7103
|
+
const conn = {
|
|
7104
|
+
id: connId,
|
|
7105
|
+
socket,
|
|
7106
|
+
info: null,
|
|
6546
7107
|
buffer: "",
|
|
6547
7108
|
pending: /* @__PURE__ */ new Map()
|
|
6548
7109
|
};
|
|
@@ -6687,590 +7248,576 @@ var IPCServer = class {
|
|
|
6687
7248
|
sendResponse(conn, response) {
|
|
6688
7249
|
try {
|
|
6689
7250
|
conn.socket.write(JSON.stringify(response) + "\n");
|
|
6690
|
-
} catch (err) {
|
|
6691
|
-
logger$1.error({
|
|
6692
|
-
connId: conn.id,
|
|
6693
|
-
err
|
|
6694
|
-
}, "IPC: failed to send response");
|
|
6695
|
-
}
|
|
6696
|
-
}
|
|
6697
|
-
sendEvent(conn, event, payload) {
|
|
6698
|
-
try {
|
|
6699
|
-
const msg = createIPCEvent(event, payload);
|
|
6700
|
-
conn.socket.write(JSON.stringify(msg) + "\n");
|
|
6701
|
-
return true;
|
|
6702
|
-
} catch {
|
|
6703
|
-
return false;
|
|
6704
|
-
}
|
|
6705
|
-
}
|
|
6706
|
-
};
|
|
6707
|
-
//#endregion
|
|
6708
|
-
//#region src/command-queue.ts
|
|
6709
|
-
const DEFAULT_TTL_MS$1 = 300 * 1e3;
|
|
6710
|
-
var CommandQueue = class {
|
|
6711
|
-
queues = /* @__PURE__ */ new Map();
|
|
6712
|
-
ttlMs;
|
|
6713
|
-
gcTimer = null;
|
|
6714
|
-
constructor(ttlMs = DEFAULT_TTL_MS$1) {
|
|
6715
|
-
this.ttlMs = ttlMs;
|
|
6716
|
-
}
|
|
6717
|
-
/**
|
|
6718
|
-
* Start periodic garbage collection of expired commands.
|
|
6719
|
-
*/
|
|
6720
|
-
startGC(intervalMs = 3e4) {
|
|
6721
|
-
this.stopGC();
|
|
6722
|
-
this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
|
|
6723
|
-
}
|
|
6724
|
-
/**
|
|
6725
|
-
* Stop periodic garbage collection.
|
|
6726
|
-
*/
|
|
6727
|
-
stopGC() {
|
|
6728
|
-
if (this.gcTimer) {
|
|
6729
|
-
clearInterval(this.gcTimer);
|
|
6730
|
-
this.gcTimer = null;
|
|
6731
|
-
}
|
|
6732
|
-
}
|
|
6733
|
-
/**
|
|
6734
|
-
* Enqueue a command for a specific service.
|
|
6735
|
-
*/
|
|
6736
|
-
enqueue(serviceId, request, commandId) {
|
|
6737
|
-
let queue = this.queues.get(serviceId);
|
|
6738
|
-
if (!queue) {
|
|
6739
|
-
queue = [];
|
|
6740
|
-
this.queues.set(serviceId, queue);
|
|
6741
|
-
}
|
|
6742
|
-
queue.push({
|
|
6743
|
-
request,
|
|
6744
|
-
queuedAt: Date.now(),
|
|
6745
|
-
commandId
|
|
6746
|
-
});
|
|
6747
|
-
}
|
|
6748
|
-
/**
|
|
6749
|
-
* Drain all pending (non-expired) commands for a service.
|
|
6750
|
-
* Returns them in order and removes them from the queue.
|
|
6751
|
-
*/
|
|
6752
|
-
drain(serviceId) {
|
|
6753
|
-
const queue = this.queues.get(serviceId);
|
|
6754
|
-
if (!queue || queue.length === 0) return [];
|
|
6755
|
-
const now = Date.now();
|
|
6756
|
-
const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
|
|
6757
|
-
this.queues.delete(serviceId);
|
|
6758
|
-
return valid;
|
|
6759
|
-
}
|
|
6760
|
-
/**
|
|
6761
|
-
* Get the number of pending commands for a service.
|
|
6762
|
-
*/
|
|
6763
|
-
pendingCount(serviceId) {
|
|
6764
|
-
const queue = this.queues.get(serviceId);
|
|
6765
|
-
if (!queue) return 0;
|
|
6766
|
-
const now = Date.now();
|
|
6767
|
-
return queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs).length;
|
|
6768
|
-
}
|
|
6769
|
-
/**
|
|
6770
|
-
* Get total pending commands across all services.
|
|
6771
|
-
*/
|
|
6772
|
-
totalPending() {
|
|
6773
|
-
let total = 0;
|
|
6774
|
-
for (const [serviceId] of this.queues) total += this.pendingCount(serviceId);
|
|
6775
|
-
return total;
|
|
6776
|
-
}
|
|
6777
|
-
/**
|
|
6778
|
-
* Remove expired commands from all queues.
|
|
6779
|
-
*/
|
|
6780
|
-
purgeExpired() {
|
|
6781
|
-
const now = Date.now();
|
|
6782
|
-
let purged = 0;
|
|
6783
|
-
for (const [serviceId, queue] of this.queues) {
|
|
6784
|
-
const before = queue.length;
|
|
6785
|
-
const remaining = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
|
|
6786
|
-
if (remaining.length === 0) this.queues.delete(serviceId);
|
|
6787
|
-
else this.queues.set(serviceId, remaining);
|
|
6788
|
-
purged += before - remaining.length;
|
|
6789
|
-
}
|
|
6790
|
-
return purged;
|
|
6791
|
-
}
|
|
6792
|
-
/**
|
|
6793
|
-
* Clear all queues.
|
|
6794
|
-
*/
|
|
6795
|
-
clear() {
|
|
6796
|
-
this.queues.clear();
|
|
7251
|
+
} catch (err) {
|
|
7252
|
+
logger$1.error({
|
|
7253
|
+
connId: conn.id,
|
|
7254
|
+
err
|
|
7255
|
+
}, "IPC: failed to send response");
|
|
7256
|
+
}
|
|
6797
7257
|
}
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
7258
|
+
sendEvent(conn, event, payload) {
|
|
7259
|
+
try {
|
|
7260
|
+
const msg = createIPCEvent(event, payload);
|
|
7261
|
+
conn.socket.write(JSON.stringify(msg) + "\n");
|
|
7262
|
+
return true;
|
|
7263
|
+
} catch {
|
|
7264
|
+
return false;
|
|
7265
|
+
}
|
|
6803
7266
|
}
|
|
6804
7267
|
};
|
|
6805
7268
|
//#endregion
|
|
6806
|
-
//#region src/
|
|
7269
|
+
//#region src/daemon-bootstrap.ts
|
|
6807
7270
|
/**
|
|
6808
|
-
*
|
|
7271
|
+
* Daemon bootstrap helpers — startup ordering, config/version resolution,
|
|
7272
|
+
* AI-proxy + IPC server wiring.
|
|
6809
7273
|
*
|
|
6810
|
-
*
|
|
6811
|
-
*
|
|
6812
|
-
|
|
6813
|
-
const LAUNCHD_LABEL = "ai.alfe.gateway";
|
|
6814
|
-
const SYSTEMD_SERVICE = "alfe-gateway";
|
|
6815
|
-
/**
|
|
6816
|
-
* On-disk path for the boot-time self-heal guard script (Linux only).
|
|
6817
|
-
* Written next to the systemd unit at setup time and invoked via
|
|
6818
|
-
* `ExecStartPre=` — see `writeGuardScript` and `generateSystemdUnit`.
|
|
6819
|
-
*/
|
|
6820
|
-
function getGuardScriptPath() {
|
|
6821
|
-
return isRootUser() ? "/usr/local/lib/alfe/alfe-cli-guard.sh" : join(homedir(), ".alfe", "alfe-cli-guard.sh");
|
|
6822
|
-
}
|
|
6823
|
-
function isRootUser() {
|
|
6824
|
-
return process.getuid?.() === 0;
|
|
6825
|
-
}
|
|
6826
|
-
function getSystemdSystemServicePath() {
|
|
6827
|
-
return `/etc/systemd/system/${SYSTEMD_SERVICE}.service`;
|
|
6828
|
-
}
|
|
6829
|
-
function getLaunchdPlistPath() {
|
|
6830
|
-
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
6831
|
-
}
|
|
6832
|
-
function getSystemdServicePath() {
|
|
6833
|
-
return join(homedir(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
|
|
6834
|
-
}
|
|
6835
|
-
/**
|
|
6836
|
-
* Resolve the alfe CLI path. Globally installed via npm, always in PATH.
|
|
6837
|
-
*/
|
|
6838
|
-
function getAlfeBinPath() {
|
|
6839
|
-
try {
|
|
6840
|
-
return execSync("which alfe", { encoding: "utf-8" }).trim();
|
|
6841
|
-
} catch {
|
|
6842
|
-
return "alfe";
|
|
6843
|
-
}
|
|
6844
|
-
}
|
|
6845
|
-
/**
|
|
6846
|
-
* Build the PATH for the launchd plist, ensuring the directory containing the
|
|
6847
|
-
* resolved alfe binary is included (e.g. /opt/homebrew/bin on Apple Silicon).
|
|
6848
|
-
*/
|
|
6849
|
-
function buildLaunchdPath(alfeBin) {
|
|
6850
|
-
const basePaths = [
|
|
6851
|
-
"/usr/local/bin",
|
|
6852
|
-
"/usr/bin",
|
|
6853
|
-
"/bin",
|
|
6854
|
-
join(homedir(), ".local", "bin")
|
|
6855
|
-
];
|
|
6856
|
-
const binDir = dirname(alfeBin);
|
|
6857
|
-
if (binDir && !basePaths.includes(binDir)) basePaths.unshift(binDir);
|
|
6858
|
-
return basePaths.join(":");
|
|
6859
|
-
}
|
|
6860
|
-
/**
|
|
6861
|
-
* Generate a launchd plist for macOS.
|
|
7274
|
+
* Extracted from daemon.ts (which remains the orchestrator). These are the
|
|
7275
|
+
* standalone pieces of `startDaemon()`: version detection, the one-shot
|
|
7276
|
+
* persona-file migration, the AI proxy listener, and the IPC server start.
|
|
6862
7277
|
*/
|
|
6863
|
-
|
|
6864
|
-
const alfeBin = getAlfeBinPath();
|
|
6865
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
6866
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
6867
|
-
<plist version="1.0">
|
|
6868
|
-
<dict>
|
|
6869
|
-
<key>Label</key>
|
|
6870
|
-
<string>${LAUNCHD_LABEL}</string>
|
|
6871
|
-
<key>ProgramArguments</key>
|
|
6872
|
-
<array>
|
|
6873
|
-
<string>${alfeBin}</string>
|
|
6874
|
-
<string>gateway</string>
|
|
6875
|
-
<string>daemon</string>
|
|
6876
|
-
</array>
|
|
6877
|
-
<key>RunAtLoad</key>
|
|
6878
|
-
<true/>
|
|
6879
|
-
<key>KeepAlive</key>
|
|
6880
|
-
<true/>
|
|
6881
|
-
<key>ThrottleInterval</key>
|
|
6882
|
-
<integer>10</integer>
|
|
6883
|
-
<key>StandardOutPath</key>
|
|
6884
|
-
<string>${join(homedir(), ".alfe", "logs", "gateway.log")}</string>
|
|
6885
|
-
<key>StandardErrorPath</key>
|
|
6886
|
-
<string>${join(homedir(), ".alfe", "logs", "gateway.err.log")}</string>
|
|
6887
|
-
<key>EnvironmentVariables</key>
|
|
6888
|
-
<dict>
|
|
6889
|
-
<key>NODE_ENV</key>
|
|
6890
|
-
<string>production</string>
|
|
6891
|
-
<key>PATH</key>
|
|
6892
|
-
<string>${buildLaunchdPath(alfeBin)}</string>
|
|
6893
|
-
</dict>
|
|
6894
|
-
</dict>
|
|
6895
|
-
</plist>`;
|
|
6896
|
-
}
|
|
7278
|
+
const execFileAsync$2 = promisify(execFile);
|
|
6897
7279
|
/**
|
|
6898
|
-
*
|
|
6899
|
-
*
|
|
6900
|
-
* Runs as an `ExecStartPre=` BEFORE the daemon launches, OUTSIDE the
|
|
6901
|
-
* (possibly-broken) `alfe` binary. It exists for the case defense #1
|
|
6902
|
-
* (verify-before-exit in `upgrade.ts`) structurally cannot cover: the daemon
|
|
6903
|
-
* is KILLED during `npm install -g @alfe.ai/cli` (reboot / OOM), leaving the
|
|
6904
|
-
* package with deps but no `dist/` and a dangling `/usr/bin/alfe` symlink.
|
|
6905
|
-
* systemd then execs a dangling binary forever → 203/EXEC crash-loop, a
|
|
6906
|
-
* permanent brick with no automatic recovery (real prod incident,
|
|
6907
|
-
* ~13,570 restarts over 2 days).
|
|
6908
|
-
*
|
|
6909
|
-
* The guard:
|
|
6910
|
-
* - checks the CLI is intact: the symlink target (`dist/index.js`) exists AND
|
|
6911
|
-
* `alfe --version` exits 0;
|
|
6912
|
-
* - if broken, reinstalls `@alfe.ai/cli@$ALFE_CLI_VERSION` (the exact version
|
|
6913
|
-
* baked into the unit's `Environment=`; falls back to `@latest` when
|
|
6914
|
-
* unset), so the daemon then execs a working binary;
|
|
6915
|
-
* - is idempotent + fast (no-op when healthy) and NEVER wedges boot: any of
|
|
6916
|
-
* its own failures log + `exit 0` so a human can still SSH in.
|
|
7280
|
+
* Resolve the installed @alfe.ai/cli version.
|
|
6917
7281
|
*
|
|
6918
|
-
*
|
|
6919
|
-
*
|
|
7282
|
+
* Strategy (in order):
|
|
7283
|
+
* 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
|
|
7284
|
+
* 2. Walk up from this file to find @alfe.ai/cli/package.json
|
|
7285
|
+
* (works when systemd runs the gateway binary directly, since
|
|
7286
|
+
* the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
|
|
6920
7287
|
*/
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
ENTRY=""
|
|
6941
|
-
fi
|
|
6942
|
-
|
|
6943
|
-
healthy=1
|
|
6944
|
-
if [ -z "$ENTRY" ] || [ ! -f "$ENTRY" ]; then
|
|
6945
|
-
healthy=0
|
|
6946
|
-
log "CLI entry missing (bin=\${BIN:-<none>} entry=\${ENTRY:-<none>})"
|
|
6947
|
-
elif ! alfe --version >/dev/null 2>&1; then
|
|
6948
|
-
healthy=0
|
|
6949
|
-
log "alfe --version failed"
|
|
6950
|
-
fi
|
|
6951
|
-
|
|
6952
|
-
if [ "$healthy" -eq 1 ]; then
|
|
6953
|
-
exit 0
|
|
6954
|
-
fi
|
|
6955
|
-
|
|
6956
|
-
log "CLI install looks broken — reinstalling $TARGET"
|
|
6957
|
-
# Blow away a partial tree first so npm re-materialises dist/ cleanly. Best-effort.
|
|
6958
|
-
GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
6959
|
-
if [ -n "$GLOBAL_ROOT" ] && [ -d "$GLOBAL_ROOT/@alfe.ai/cli" ]; then
|
|
6960
|
-
rm -rf "$GLOBAL_ROOT/@alfe.ai/cli" 2>/dev/null || true
|
|
6961
|
-
fi
|
|
6962
|
-
|
|
6963
|
-
# Bound the reinstall so a slow/unreachable registry at boot can't stall the
|
|
6964
|
-
# ExecStartPre up to systemd's DefaultTimeoutStartSec (~90s). Two layers:
|
|
6965
|
-
# - npm --fetch-timeout/--fetch-retries cap per-request/hung-socket time;
|
|
6966
|
-
# - a wall-clock 'timeout' wrapper caps the whole install when present
|
|
6967
|
-
# ('timeout' isn't guaranteed, so fall back to a bare install).
|
|
6968
|
-
# stdout->stderr ('>&2') so npm's chatter lands in the journal, not on fd1.
|
|
6969
|
-
if command -v timeout >/dev/null 2>&1; then
|
|
6970
|
-
timeout 120 npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
|
|
6971
|
-
rc=$?
|
|
6972
|
-
else
|
|
6973
|
-
npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
|
|
6974
|
-
rc=$?
|
|
6975
|
-
fi
|
|
6976
|
-
if [ "$rc" -eq 0 ]; then
|
|
6977
|
-
log "reinstall succeeded"
|
|
6978
|
-
else
|
|
6979
|
-
log "reinstall FAILED (rc=$rc) — daemon start may still fail; a human can SSH in to repair"
|
|
6980
|
-
fi
|
|
6981
|
-
|
|
6982
|
-
# Always exit 0: even a failed repair must not block the unit from attempting
|
|
6983
|
-
# ExecStart (which then fails visibly) rather than wedging silently in pre-start.
|
|
6984
|
-
exit 0
|
|
6985
|
-
`;
|
|
7288
|
+
async function getCliVersion() {
|
|
7289
|
+
if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
|
|
7290
|
+
try {
|
|
7291
|
+
const { fileURLToPath } = await import("node:url");
|
|
7292
|
+
const { dirname } = await import("node:path");
|
|
7293
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7294
|
+
for (let i = 0; i < 10; i++) {
|
|
7295
|
+
const candidate = join(dir, "package.json");
|
|
7296
|
+
try {
|
|
7297
|
+
const raw = await readFile(candidate, "utf-8");
|
|
7298
|
+
const pkg = JSON.parse(raw);
|
|
7299
|
+
if (pkg.name === "@alfe.ai/cli") return pkg.version;
|
|
7300
|
+
} catch {}
|
|
7301
|
+
const parent = dirname(dir);
|
|
7302
|
+
if (parent === dir) break;
|
|
7303
|
+
dir = parent;
|
|
7304
|
+
}
|
|
7305
|
+
} catch {}
|
|
7306
|
+
logger$1.debug("Could not resolve @alfe.ai/cli version");
|
|
6986
7307
|
}
|
|
6987
7308
|
/**
|
|
6988
|
-
*
|
|
6989
|
-
*
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
"
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
"
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
Type=simple
|
|
7008
|
-
ExecStartPre=-/bin/sh ${getGuardScriptPath()}
|
|
7009
|
-
ExecStart=${alfeBin} gateway daemon
|
|
7010
|
-
Restart=always
|
|
7011
|
-
RestartSec=10
|
|
7012
|
-
# SIGTERM only the daemon on stop/restart; it closes its MCP/runtime children
|
|
7013
|
-
# itself. The default (control-group) SIGTERMs the children simultaneously, so
|
|
7014
|
-
# they die before the daemon's orderly dispose reaches them and every planned
|
|
7015
|
-
# restart reports MCP "server-crash" noise to Sentry. Stragglers still get
|
|
7016
|
-
# SIGKILL when the stop timeout expires.
|
|
7017
|
-
KillMode=mixed
|
|
7018
|
-
Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
|
|
7019
|
-
${envLines}
|
|
7020
|
-
|
|
7021
|
-
[Install]
|
|
7022
|
-
WantedBy=${root ? "multi-user.target" : "default.target"}`;
|
|
7023
|
-
}
|
|
7309
|
+
* Per-runtime "print installed version" commands. Local to the gateway — we do
|
|
7310
|
+
* NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
|
|
7311
|
+
* `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
|
|
7312
|
+
* connection-status report degrades gracefully instead of crashing.
|
|
7313
|
+
*/
|
|
7314
|
+
const RUNTIME_VERSION_COMMANDS = {
|
|
7315
|
+
openclaw: {
|
|
7316
|
+
command: "openclaw",
|
|
7317
|
+
args: ["--version"]
|
|
7318
|
+
},
|
|
7319
|
+
hermes: {
|
|
7320
|
+
command: "hermes",
|
|
7321
|
+
args: ["version"]
|
|
7322
|
+
},
|
|
7323
|
+
"claude-code": {
|
|
7324
|
+
command: "alfe-claude-host",
|
|
7325
|
+
args: ["--version"]
|
|
7326
|
+
}
|
|
7327
|
+
};
|
|
7024
7328
|
/**
|
|
7025
|
-
*
|
|
7026
|
-
*
|
|
7329
|
+
* Pure resolver (exported for tests) — the per-runtime version command, or
|
|
7330
|
+
* `undefined` for an unknown runtime.
|
|
7027
7331
|
*/
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
await mkdir(dirname(guardPath), { recursive: true });
|
|
7031
|
-
await writeFile(guardPath, generateGuardScript(), {
|
|
7032
|
-
encoding: "utf-8",
|
|
7033
|
-
mode: 493
|
|
7034
|
-
});
|
|
7035
|
-
logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
|
|
7332
|
+
function resolveRuntimeVersionCommand(runtime) {
|
|
7333
|
+
return RUNTIME_VERSION_COMMANDS[runtime];
|
|
7036
7334
|
}
|
|
7037
7335
|
/**
|
|
7038
|
-
*
|
|
7336
|
+
* Resolve the installed runtime version by running the per-runtime version
|
|
7337
|
+
* command and returning the trimmed stdout. An unknown runtime (no command in
|
|
7338
|
+
* the map) returns `undefined` without spawning — it must never throw.
|
|
7039
7339
|
*/
|
|
7040
|
-
async function
|
|
7041
|
-
const
|
|
7042
|
-
if (
|
|
7043
|
-
|
|
7044
|
-
|
|
7340
|
+
async function getRuntimeVersion(runtime) {
|
|
7341
|
+
const cmd = resolveRuntimeVersionCommand(runtime);
|
|
7342
|
+
if (!cmd) {
|
|
7343
|
+
logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
try {
|
|
7347
|
+
const { stdout } = await execFileAsync$2(cmd.command, cmd.args);
|
|
7348
|
+
return stdout.trim() || void 0;
|
|
7349
|
+
} catch {
|
|
7350
|
+
logger$1.debug({ runtime }, "Could not resolve runtime version");
|
|
7351
|
+
return;
|
|
7352
|
+
}
|
|
7045
7353
|
}
|
|
7046
7354
|
/**
|
|
7047
|
-
*
|
|
7355
|
+
* Flush pino's async transport and exit.
|
|
7356
|
+
* process.exit() can drop buffered log lines — this ensures they're written first.
|
|
7048
7357
|
*/
|
|
7049
|
-
async function
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7358
|
+
async function flushAndExit(code) {
|
|
7359
|
+
await flushSentry();
|
|
7360
|
+
await new Promise((resolve) => {
|
|
7361
|
+
logger$1.flush();
|
|
7362
|
+
setTimeout(resolve, 500);
|
|
7363
|
+
});
|
|
7364
|
+
process.exit(code);
|
|
7054
7365
|
}
|
|
7055
|
-
|
|
7056
|
-
|
|
7366
|
+
/**
|
|
7367
|
+
* One-shot migration for VMs provisioned before the persona-files-go-in-agent-workspace
|
|
7368
|
+
* fix landed. Old behavior wrote SOUL.md/IDENTITY.md/BOOTSTRAP.md/AGENTS.md at the
|
|
7369
|
+
* OpenClaw home (e.g. `~/.openclaw/`); the agent reads from the agent workspace
|
|
7370
|
+
* (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
|
|
7371
|
+
* workspace already has the file. Idempotent and safe to run on every daemon start.
|
|
7372
|
+
*/
|
|
7373
|
+
const PERSONA_FILES = [
|
|
7374
|
+
"SOUL.md",
|
|
7375
|
+
"IDENTITY.md",
|
|
7376
|
+
"BOOTSTRAP.md",
|
|
7377
|
+
"AGENTS.md"
|
|
7378
|
+
];
|
|
7379
|
+
async function migrateLegacyPersonaFiles(home, agentWorkspace) {
|
|
7380
|
+
if (home === agentWorkspace) return;
|
|
7381
|
+
let moved = 0;
|
|
7382
|
+
let skipped = 0;
|
|
7383
|
+
for (const filename of PERSONA_FILES) {
|
|
7384
|
+
const src = join(home, filename);
|
|
7385
|
+
const dst = join(agentWorkspace, filename);
|
|
7386
|
+
try {
|
|
7387
|
+
await stat(src);
|
|
7388
|
+
} catch {
|
|
7389
|
+
continue;
|
|
7390
|
+
}
|
|
7391
|
+
try {
|
|
7392
|
+
await stat(dst);
|
|
7393
|
+
skipped++;
|
|
7394
|
+
continue;
|
|
7395
|
+
} catch {}
|
|
7396
|
+
try {
|
|
7397
|
+
await mkdir(agentWorkspace, { recursive: true });
|
|
7398
|
+
await rename(src, dst);
|
|
7399
|
+
moved++;
|
|
7400
|
+
} catch (err) {
|
|
7401
|
+
logger$1.warn({
|
|
7402
|
+
src,
|
|
7403
|
+
dst,
|
|
7404
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7405
|
+
}, "Persona file migration: rename failed");
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
if (moved > 0 || skipped > 0) logger$1.info({
|
|
7409
|
+
home,
|
|
7410
|
+
agentWorkspace,
|
|
7411
|
+
moved,
|
|
7412
|
+
skipped
|
|
7413
|
+
}, "Persona file backfill migration complete");
|
|
7057
7414
|
}
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
}
|
|
7415
|
+
/**
|
|
7416
|
+
* Start the local AI proxy (before the runtime so LLM requests work
|
|
7417
|
+
* immediately). Failure is non-fatal — the daemon starts anyway and LLM
|
|
7418
|
+
* requests fail until the proxy is fixed.
|
|
7419
|
+
*/
|
|
7420
|
+
async function startAiProxy(apiKey) {
|
|
7421
|
+
logger$1.debug("Starting AI proxy...");
|
|
7422
|
+
const handle = {
|
|
7423
|
+
server: null,
|
|
7424
|
+
url: null,
|
|
7425
|
+
running: false
|
|
7426
|
+
};
|
|
7070
7427
|
try {
|
|
7071
|
-
|
|
7428
|
+
const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
|
|
7429
|
+
const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
|
|
7430
|
+
const proxyUrl = getAiServiceUrlFromToken(apiKey);
|
|
7431
|
+
const port = DEFAULT_AI_PROXY_PORT ?? 18193;
|
|
7432
|
+
handle.server = createProxyServer({
|
|
7433
|
+
port,
|
|
7434
|
+
apiKey,
|
|
7435
|
+
proxyUrl
|
|
7436
|
+
});
|
|
7437
|
+
const server = handle.server;
|
|
7438
|
+
await new Promise((resolve, reject) => {
|
|
7439
|
+
server.listen(port, "127.0.0.1", () => {
|
|
7440
|
+
handle.running = true;
|
|
7441
|
+
handle.url = `http://127.0.0.1:${String(port)}`;
|
|
7442
|
+
logger$1.info({
|
|
7443
|
+
port,
|
|
7444
|
+
upstream: proxyUrl
|
|
7445
|
+
}, "AI proxy started");
|
|
7446
|
+
resolve();
|
|
7447
|
+
});
|
|
7448
|
+
server.on("error", reject);
|
|
7449
|
+
});
|
|
7072
7450
|
} catch (err) {
|
|
7073
|
-
const
|
|
7451
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7452
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
7074
7453
|
logger$1.error({
|
|
7075
|
-
err:
|
|
7076
|
-
|
|
7077
|
-
}, "
|
|
7078
|
-
return `Installed plist at ${plistPath}, but failed to bootstrap service: ${msg}`;
|
|
7454
|
+
err: message,
|
|
7455
|
+
stack
|
|
7456
|
+
}, "Failed to start AI proxy — LLM requests will fail");
|
|
7079
7457
|
}
|
|
7080
|
-
return
|
|
7458
|
+
return handle;
|
|
7081
7459
|
}
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7460
|
+
/**
|
|
7461
|
+
* Start the IPC server (runtime plugins connect via this socket). A failure
|
|
7462
|
+
* here is fatal — plugins have no other path to the daemon — so this flushes
|
|
7463
|
+
* logs and exits instead of returning.
|
|
7464
|
+
*/
|
|
7465
|
+
async function startIpcServer(socketPath, requestHandler) {
|
|
7466
|
+
logger$1.debug({ socketPath }, "Starting IPC server...");
|
|
7467
|
+
const ipcServer = new IPCServer(socketPath);
|
|
7468
|
+
ipcServer.setRequestHandler(requestHandler);
|
|
7088
7469
|
try {
|
|
7089
|
-
await
|
|
7090
|
-
|
|
7091
|
-
|
|
7470
|
+
await ipcServer.start();
|
|
7471
|
+
logger$1.debug("IPC server started");
|
|
7472
|
+
} catch (err) {
|
|
7473
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7474
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
7475
|
+
logger$1.error({
|
|
7476
|
+
err: message,
|
|
7477
|
+
stack
|
|
7478
|
+
}, "Failed to start IPC server");
|
|
7479
|
+
await flushAndExit(1);
|
|
7480
|
+
}
|
|
7481
|
+
return ipcServer;
|
|
7092
7482
|
}
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7483
|
+
//#endregion
|
|
7484
|
+
//#region src/mcp-handlers.ts
|
|
7485
|
+
/**
|
|
7486
|
+
* Return the daemon-hosted MCP bundler's current namespaced tool catalog.
|
|
7487
|
+
* Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
|
|
7488
|
+
* cheap (in-memory snapshot, no I/O).
|
|
7489
|
+
*/
|
|
7490
|
+
function handleMcpListTools(bundler) {
|
|
7491
|
+
if (!bundler) return {
|
|
7492
|
+
ok: false,
|
|
7493
|
+
error: {
|
|
7494
|
+
code: "MCP_BUNDLER_UNAVAILABLE",
|
|
7495
|
+
message: "MCP bundler not initialized"
|
|
7496
|
+
}
|
|
7497
|
+
};
|
|
7498
|
+
return {
|
|
7499
|
+
ok: true,
|
|
7500
|
+
payload: { tools: bundler.listTools() }
|
|
7501
|
+
};
|
|
7110
7502
|
}
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7503
|
+
/**
|
|
7504
|
+
* Observability backstop for the INTEGRATION warm path. `applyForIntegration`
|
|
7505
|
+
* registers MCP servers into the store and the daemon warms them silently via
|
|
7506
|
+
* the store-change `onChange` hook — so a server that fails its connect (e.g.
|
|
7507
|
+
* an MCP server whose backing account/credential wasn't resolvable at warm
|
|
7508
|
+
* time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
|
|
7509
|
+
* making the black hole undiagnosable. After a warm, read `bundler.statuses()`
|
|
7510
|
+
* and warn once per still-failed server so the failure is visible. These
|
|
7511
|
+
* self-heal on the bundler's background retry sweep once the dependency
|
|
7512
|
+
* appears. Returns the servers it warned about (for tests / callers).
|
|
7513
|
+
*/
|
|
7514
|
+
function warnFailedMcpServers(bundler, log, reason) {
|
|
7515
|
+
if (!bundler) return [];
|
|
7516
|
+
const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
|
|
7517
|
+
for (const status of failed) log.warn({
|
|
7518
|
+
server: status.name,
|
|
7519
|
+
reason,
|
|
7520
|
+
consecutiveFailures: status.consecutiveFailures,
|
|
7521
|
+
lastError: status.lastError
|
|
7522
|
+
}, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
|
|
7523
|
+
return failed;
|
|
7129
7524
|
}
|
|
7130
7525
|
/**
|
|
7131
|
-
*
|
|
7132
|
-
*
|
|
7133
|
-
*
|
|
7526
|
+
* Route a tool call to the appropriate MCP child via the daemon-hosted
|
|
7527
|
+
* bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
|
|
7528
|
+
* the raw JSON object the LLM produced.
|
|
7134
7529
|
*/
|
|
7135
|
-
function ensureLaunchdLoaded(uid) {
|
|
7136
|
-
try {
|
|
7137
|
-
execSync(`launchctl bootstrap gui/${uid} ${getLaunchdPlistPath()}`, { stdio: "pipe" });
|
|
7138
|
-
} catch {}
|
|
7139
|
-
}
|
|
7140
7530
|
/**
|
|
7141
|
-
*
|
|
7142
|
-
*
|
|
7531
|
+
* List every server entry in the alfe bundler store. Lets agents inspect
|
|
7532
|
+
* what they've already registered before adding a new one.
|
|
7143
7533
|
*/
|
|
7144
|
-
function
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7534
|
+
function handleMcpListServers(manager) {
|
|
7535
|
+
if (!manager) return {
|
|
7536
|
+
ok: false,
|
|
7537
|
+
error: {
|
|
7538
|
+
code: "MCP_MANAGER_UNAVAILABLE",
|
|
7539
|
+
message: "MCP manager not initialized"
|
|
7540
|
+
}
|
|
7541
|
+
};
|
|
7542
|
+
const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
|
|
7543
|
+
const byName = new Map(statuses.map((s) => [s.name, s]));
|
|
7544
|
+
return {
|
|
7545
|
+
ok: true,
|
|
7546
|
+
payload: { servers: manager.listServers().map(({ id, entry }) => ({
|
|
7547
|
+
id,
|
|
7548
|
+
entry,
|
|
7549
|
+
status: byName.get(id) ?? {
|
|
7550
|
+
name: id,
|
|
7551
|
+
connected: false,
|
|
7552
|
+
toolCount: 0,
|
|
7553
|
+
consecutiveFailures: 0
|
|
7554
|
+
}
|
|
7555
|
+
})) }
|
|
7556
|
+
};
|
|
7159
7557
|
}
|
|
7160
7558
|
/**
|
|
7161
|
-
*
|
|
7162
|
-
*
|
|
7163
|
-
*
|
|
7164
|
-
*
|
|
7559
|
+
* How long the add-confirm probe waits for the freshly-added server to
|
|
7560
|
+
* connect before replying. This is effectively the whole budget for
|
|
7561
|
+
* `bundler.warmServer`: `Manager.addServer` now reconciles the bundler
|
|
7562
|
+
* BEFORE resolving, so the Connection object already exists and
|
|
7563
|
+
* `warmServer`'s pre-reconcile is a no-diff cheap pass. Kept well under the
|
|
7564
|
+
* plugin caller's 30s IPC timeout. A slower server still connects in the
|
|
7565
|
+
* background and surfaces on the next `alfe_mcp_list_tools` — the probe just
|
|
7566
|
+
* reports what it saw within the window; a missed connect is re-attempted by
|
|
7567
|
+
* the bundler's `retryNeverConnected` sweep.
|
|
7165
7568
|
*/
|
|
7166
|
-
|
|
7167
|
-
const platform = process.platform;
|
|
7168
|
-
if (platform === "darwin") {
|
|
7169
|
-
const uid = getLaunchdUid();
|
|
7170
|
-
ensureLaunchdLoaded(uid);
|
|
7171
|
-
execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "pipe" });
|
|
7172
|
-
logger$1.info("Restarted launchd service");
|
|
7173
|
-
return;
|
|
7174
|
-
}
|
|
7175
|
-
if (platform === "linux") {
|
|
7176
|
-
execSync(`${isRootUser() ? "systemctl" : "systemctl --user"} restart ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
7177
|
-
logger$1.info("Restarted systemd service");
|
|
7178
|
-
return;
|
|
7179
|
-
}
|
|
7180
|
-
throw new Error(`Unsupported platform: ${platform}`);
|
|
7181
|
-
}
|
|
7569
|
+
const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
|
|
7182
7570
|
/**
|
|
7183
|
-
*
|
|
7184
|
-
*
|
|
7185
|
-
*
|
|
7186
|
-
*
|
|
7571
|
+
* Register a new MCP server in the alfe bundler store on behalf of the agent,
|
|
7572
|
+
* then CONFIRM the connect before replying so the agent learns whether the
|
|
7573
|
+
* server actually works. Owned as `'manual'` so the agent can later remove it
|
|
7574
|
+
* without an expectedOwner conflict — matches what `alfe mcp add` does from the
|
|
7575
|
+
* CLI. Registration is durable regardless of the probe outcome: a probe
|
|
7576
|
+
* failure (or a runtime with no daemon bundler, e.g. hermes) still returns
|
|
7577
|
+
* `ok` with `connected: false`; the store watcher / runtime picks the entry up.
|
|
7187
7578
|
*/
|
|
7188
|
-
function
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7579
|
+
async function handleMcpAddServer(params, manager) {
|
|
7580
|
+
if (!manager) return {
|
|
7581
|
+
ok: false,
|
|
7582
|
+
error: {
|
|
7583
|
+
code: "MCP_MANAGER_UNAVAILABLE",
|
|
7584
|
+
message: "MCP manager not initialized"
|
|
7585
|
+
}
|
|
7586
|
+
};
|
|
7587
|
+
const p = params;
|
|
7588
|
+
if (typeof p.id !== "string" || p.id.length === 0) return {
|
|
7589
|
+
ok: false,
|
|
7590
|
+
error: {
|
|
7591
|
+
code: "INVALID_PARAMS",
|
|
7592
|
+
message: "id is required (string)"
|
|
7593
|
+
}
|
|
7594
|
+
};
|
|
7595
|
+
const config = buildServerConfig(p);
|
|
7596
|
+
if (!config) return {
|
|
7597
|
+
ok: false,
|
|
7598
|
+
error: {
|
|
7599
|
+
code: "INVALID_PARAMS",
|
|
7600
|
+
message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
|
|
7601
|
+
}
|
|
7602
|
+
};
|
|
7603
|
+
try {
|
|
7604
|
+
await manager.addServer(config, {
|
|
7605
|
+
id: p.id,
|
|
7606
|
+
owner: "manual"
|
|
7607
|
+
});
|
|
7608
|
+
} catch (err) {
|
|
7609
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7610
|
+
logger$1.warn({
|
|
7611
|
+
id: p.id,
|
|
7612
|
+
err: message
|
|
7613
|
+
}, "mcp.add_server failed");
|
|
7614
|
+
return {
|
|
7615
|
+
ok: false,
|
|
7616
|
+
error: {
|
|
7617
|
+
code: "MCP_ADD_FAILED",
|
|
7618
|
+
message
|
|
7619
|
+
}
|
|
7620
|
+
};
|
|
7194
7621
|
}
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7622
|
+
let status = null;
|
|
7623
|
+
if (manager.warmServer) try {
|
|
7624
|
+
status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
|
|
7625
|
+
} catch (err) {
|
|
7626
|
+
logger$1.warn({
|
|
7627
|
+
id: p.id,
|
|
7628
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7629
|
+
}, "mcp.add_server warm probe threw");
|
|
7199
7630
|
}
|
|
7200
|
-
|
|
7631
|
+
return {
|
|
7632
|
+
ok: true,
|
|
7633
|
+
payload: {
|
|
7634
|
+
id: p.id,
|
|
7635
|
+
connected: status?.connected ?? false,
|
|
7636
|
+
toolCount: status?.toolCount ?? 0,
|
|
7637
|
+
...status?.lastError !== void 0 ? { error: status.lastError } : {}
|
|
7638
|
+
}
|
|
7639
|
+
};
|
|
7201
7640
|
}
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7641
|
+
function buildServerConfig(p) {
|
|
7642
|
+
if (typeof p.command === "string" && p.command.length > 0) {
|
|
7643
|
+
const cfg = { command: p.command };
|
|
7644
|
+
if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
|
|
7645
|
+
if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
|
|
7646
|
+
const env = {};
|
|
7647
|
+
for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
|
|
7648
|
+
cfg.env = env;
|
|
7649
|
+
}
|
|
7650
|
+
if (typeof p.cwd === "string") cfg.cwd = p.cwd;
|
|
7651
|
+
return cfg;
|
|
7652
|
+
}
|
|
7653
|
+
if (typeof p.url === "string" && p.url.length > 0) {
|
|
7654
|
+
const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
|
|
7655
|
+
const cfg = {
|
|
7656
|
+
url: p.url,
|
|
7657
|
+
transport
|
|
7658
|
+
};
|
|
7659
|
+
if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
|
|
7660
|
+
const headers = {};
|
|
7661
|
+
for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
|
|
7662
|
+
cfg.headers = headers;
|
|
7663
|
+
}
|
|
7664
|
+
return cfg;
|
|
7665
|
+
}
|
|
7666
|
+
return null;
|
|
7212
7667
|
}
|
|
7213
7668
|
/**
|
|
7214
|
-
*
|
|
7669
|
+
* Drop a server entry the agent previously registered. Restricted to
|
|
7670
|
+
* `manual`-owned entries so the agent can't accidentally clobber
|
|
7671
|
+
* integration-installed or CLI-installed servers (the daemon owns
|
|
7672
|
+
* those; the agent can ask the user to uninstall an integration via
|
|
7673
|
+
* the dashboard).
|
|
7215
7674
|
*/
|
|
7216
|
-
async function
|
|
7217
|
-
|
|
7675
|
+
async function handleMcpRemoveServer(params, manager) {
|
|
7676
|
+
if (!manager) return {
|
|
7677
|
+
ok: false,
|
|
7678
|
+
error: {
|
|
7679
|
+
code: "MCP_MANAGER_UNAVAILABLE",
|
|
7680
|
+
message: "MCP manager not initialized"
|
|
7681
|
+
}
|
|
7682
|
+
};
|
|
7683
|
+
const { id } = params;
|
|
7684
|
+
if (typeof id !== "string" || id.length === 0) return {
|
|
7685
|
+
ok: false,
|
|
7686
|
+
error: {
|
|
7687
|
+
code: "INVALID_PARAMS",
|
|
7688
|
+
message: "id is required (string)"
|
|
7689
|
+
}
|
|
7690
|
+
};
|
|
7691
|
+
try {
|
|
7692
|
+
return {
|
|
7693
|
+
ok: true,
|
|
7694
|
+
payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
|
|
7695
|
+
};
|
|
7696
|
+
} catch (err) {
|
|
7697
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7698
|
+
if (message.includes("owned by")) return {
|
|
7699
|
+
ok: false,
|
|
7700
|
+
error: {
|
|
7701
|
+
code: "MCP_OWNER_MISMATCH",
|
|
7702
|
+
message
|
|
7703
|
+
}
|
|
7704
|
+
};
|
|
7705
|
+
logger$1.warn({
|
|
7706
|
+
id,
|
|
7707
|
+
err: message
|
|
7708
|
+
}, "mcp.remove_server failed");
|
|
7709
|
+
return {
|
|
7710
|
+
ok: false,
|
|
7711
|
+
error: {
|
|
7712
|
+
code: "MCP_REMOVE_FAILED",
|
|
7713
|
+
message
|
|
7714
|
+
}
|
|
7715
|
+
};
|
|
7716
|
+
}
|
|
7218
7717
|
}
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7718
|
+
async function handleMcpCallTool(bundler, params) {
|
|
7719
|
+
if (!bundler) return {
|
|
7720
|
+
ok: false,
|
|
7721
|
+
error: {
|
|
7722
|
+
code: "MCP_BUNDLER_UNAVAILABLE",
|
|
7723
|
+
message: "MCP bundler not initialized"
|
|
7724
|
+
}
|
|
7725
|
+
};
|
|
7726
|
+
const { name, args } = params;
|
|
7727
|
+
if (typeof name !== "string" || name.length === 0) return {
|
|
7728
|
+
ok: false,
|
|
7729
|
+
error: {
|
|
7730
|
+
code: "INVALID_PARAMS",
|
|
7731
|
+
message: "name is required (string)"
|
|
7732
|
+
}
|
|
7733
|
+
};
|
|
7223
7734
|
try {
|
|
7224
|
-
|
|
7225
|
-
|
|
7735
|
+
return {
|
|
7736
|
+
ok: true,
|
|
7737
|
+
payload: await bundler.callTool(name, args)
|
|
7738
|
+
};
|
|
7739
|
+
} catch (err) {
|
|
7740
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7741
|
+
logger$1.warn({
|
|
7742
|
+
tool: name,
|
|
7743
|
+
err: message
|
|
7744
|
+
}, "mcp.call_tool failed");
|
|
7745
|
+
return {
|
|
7746
|
+
ok: false,
|
|
7747
|
+
error: {
|
|
7748
|
+
code: "MCP_CALL_FAILED",
|
|
7749
|
+
message
|
|
7750
|
+
}
|
|
7751
|
+
};
|
|
7752
|
+
}
|
|
7226
7753
|
}
|
|
7754
|
+
//#endregion
|
|
7755
|
+
//#region src/integration-bootstrap.ts
|
|
7227
7756
|
/**
|
|
7228
|
-
*
|
|
7229
|
-
*
|
|
7757
|
+
* Adapter from `AgentApiClient`'s per-provider methods to the
|
|
7758
|
+
* `CredentialsResolver` shape the MCP applier expects. Returns
|
|
7759
|
+
* `undefined` for unknown providers and on 404/network error so the
|
|
7760
|
+
* applier can skip registration silently (its documented contract).
|
|
7230
7761
|
*/
|
|
7231
|
-
async function
|
|
7232
|
-
if (
|
|
7233
|
-
|
|
7234
|
-
const
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7762
|
+
async function fetchProviderCredentials(agentApi, provider, connectionId) {
|
|
7763
|
+
if (connectionId) try {
|
|
7764
|
+
const raw = await agentApi.getConnectionCredentials(connectionId);
|
|
7765
|
+
const fields = { ...raw.providerMetadata ?? {} };
|
|
7766
|
+
if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
|
|
7767
|
+
const bundle = JSON.parse(raw.accessToken);
|
|
7768
|
+
if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
|
|
7769
|
+
} catch (err) {
|
|
7770
|
+
logger$1.warn({
|
|
7771
|
+
connectionId,
|
|
7772
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7773
|
+
}, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
|
|
7774
|
+
fields.accessToken = raw.accessToken;
|
|
7239
7775
|
}
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7776
|
+
return fields;
|
|
7777
|
+
} catch (err) {
|
|
7778
|
+
logger$1.warn({
|
|
7779
|
+
connectionId,
|
|
7780
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7781
|
+
}, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
|
|
7782
|
+
return;
|
|
7783
|
+
}
|
|
7784
|
+
const key = provider.toLowerCase();
|
|
7785
|
+
try {
|
|
7786
|
+
switch (key) {
|
|
7787
|
+
case "atlassian": return await agentApi.getAtlassianCredentials();
|
|
7788
|
+
case "github": return await agentApi.getGithubCredentials();
|
|
7789
|
+
case "xero": return await agentApi.getXeroCredentials();
|
|
7790
|
+
case "notion": return await agentApi.getNotionCredentials();
|
|
7791
|
+
case "myob": return await agentApi.getMYOBCredentials();
|
|
7792
|
+
case "google": return await agentApi.getGoogleCredentials();
|
|
7793
|
+
default:
|
|
7794
|
+
logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
|
|
7795
|
+
return;
|
|
7246
7796
|
}
|
|
7247
7797
|
} catch {
|
|
7248
|
-
return
|
|
7798
|
+
return;
|
|
7249
7799
|
}
|
|
7250
7800
|
}
|
|
7251
7801
|
/**
|
|
7252
|
-
*
|
|
7802
|
+
* Post-reconcile trigger: rebuild the command registry from the manager's
|
|
7803
|
+
* active integrations and report the available commands to the cloud
|
|
7804
|
+
* gateway. Wired by daemon.ts as the cloud client's
|
|
7805
|
+
* `onReconciliationComplete` callback.
|
|
7253
7806
|
*/
|
|
7254
|
-
|
|
7255
|
-
const
|
|
7256
|
-
if (!pid) return false;
|
|
7807
|
+
function reportAvailableCommands(deps) {
|
|
7808
|
+
const { integrationManager, commandRegistry, cloudClient } = deps;
|
|
7257
7809
|
try {
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
await removePidFile();
|
|
7270
|
-
return true;
|
|
7271
|
-
} catch {
|
|
7272
|
-
await removePidFile();
|
|
7273
|
-
return false;
|
|
7810
|
+
const activeCommands = integrationManager.getActiveCommands();
|
|
7811
|
+
commandRegistry.clear();
|
|
7812
|
+
for (const { integrationId, commands } of activeCommands) for (const cmd of commands) commandRegistry.register(integrationId, cmd.name, cmd.resolvedPath, cmd.method, cmd.timeoutMs);
|
|
7813
|
+
const commandsMsg = {
|
|
7814
|
+
type: "COMMANDS_AVAILABLE",
|
|
7815
|
+
commands: commandRegistry.listCommands()
|
|
7816
|
+
};
|
|
7817
|
+
cloudClient.sendMessage(commandsMsg);
|
|
7818
|
+
logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
|
|
7819
|
+
} catch (err) {
|
|
7820
|
+
logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
|
|
7274
7821
|
}
|
|
7275
7822
|
}
|
|
7276
7823
|
//#endregion
|
|
@@ -23150,7 +23697,7 @@ var CommandDedupe = class {
|
|
|
23150
23697
|
* the gateway when needed and uses `callerScopes: ["operator.admin"]`.
|
|
23151
23698
|
* 3. Skip iteration if `pending.json` mtime hasn't changed (cheap fast path)
|
|
23152
23699
|
*/
|
|
23153
|
-
const execFileAsync$
|
|
23700
|
+
const execFileAsync$1 = promisify(execFile);
|
|
23154
23701
|
const APPROVE_TIMEOUT_MS = 1e4;
|
|
23155
23702
|
function resolveStateDir(override) {
|
|
23156
23703
|
if (override) return override;
|
|
@@ -23249,7 +23796,7 @@ function startPairingApprovalPoller(opts) {
|
|
|
23249
23796
|
const intervalMs = opts.intervalMs ?? 3e4;
|
|
23250
23797
|
const cliLock = opts.cliLock ?? new NoopOpenClawCliLock();
|
|
23251
23798
|
const rawExec = opts.exec ?? (async (file, args, { timeout }) => {
|
|
23252
|
-
const { stdout, stderr } = await execFileAsync$
|
|
23799
|
+
const { stdout, stderr } = await execFileAsync$1(file, args, { timeout });
|
|
23253
23800
|
return {
|
|
23254
23801
|
stdout,
|
|
23255
23802
|
stderr
|
|
@@ -23323,12 +23870,12 @@ function startPairingApprovalPoller(opts) {
|
|
|
23323
23870
|
* Sentinel-gated at `~/.alfe/.openclaw-mirror-migrated` — runs exactly
|
|
23324
23871
|
* once per agent.
|
|
23325
23872
|
*/
|
|
23326
|
-
const execFileAsync
|
|
23873
|
+
const execFileAsync = promisify(execFile);
|
|
23327
23874
|
const DEFAULT_SENTINEL_PATH = join(homedir(), ".alfe", ".openclaw-mirror-migrated");
|
|
23328
23875
|
const defaultOpenclaw = {
|
|
23329
23876
|
async listServers() {
|
|
23330
23877
|
try {
|
|
23331
|
-
const { stdout } = await execFileAsync
|
|
23878
|
+
const { stdout } = await execFileAsync("openclaw", [
|
|
23332
23879
|
"config",
|
|
23333
23880
|
"get",
|
|
23334
23881
|
"mcp.servers"
|
|
@@ -23345,7 +23892,7 @@ const defaultOpenclaw = {
|
|
|
23345
23892
|
}
|
|
23346
23893
|
},
|
|
23347
23894
|
async unsetServer(key) {
|
|
23348
|
-
await execFileAsync
|
|
23895
|
+
await execFileAsync("openclaw", [
|
|
23349
23896
|
"config",
|
|
23350
23897
|
"unset",
|
|
23351
23898
|
`mcp.servers.${key}`
|
|
@@ -23527,8 +24074,12 @@ function createMcpErrorHooks() {
|
|
|
23527
24074
|
* 7. Start agent runtime (managed mode only — e.g. OpenClaw)
|
|
23528
24075
|
* 8. Write PID file (non-managed only)
|
|
23529
24076
|
* 9. Handle graceful shutdown
|
|
24077
|
+
*
|
|
24078
|
+
* Split along seams (this file stays the orchestrator):
|
|
24079
|
+
* - daemon-bootstrap.ts — version resolution, persona migration, AI-proxy + IPC start
|
|
24080
|
+
* - mcp-handlers.ts — `mcp.*` IPC request handlers
|
|
24081
|
+
* - integration-bootstrap.ts — provider-credential fetch + post-reconcile command reporting
|
|
23530
24082
|
*/
|
|
23531
|
-
const execFileAsync = promisify(execFile);
|
|
23532
24083
|
let config;
|
|
23533
24084
|
let cloudClient;
|
|
23534
24085
|
let ipcServer = null;
|
|
@@ -23560,202 +24111,21 @@ let cloudConnected = false;
|
|
|
23560
24111
|
* Dedupe for at-least-once durable command delivery (Phase B). The cloud may
|
|
23561
24112
|
* push a command over the live WS AND re-drain the same commandId on the next
|
|
23562
24113
|
* SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
|
|
23563
|
-
* so a duplicate is ACKed with the prior result without re-executing.
|
|
23564
|
-
*/
|
|
23565
|
-
const commandDedupe = new CommandDedupe();
|
|
23566
|
-
let shuttingDown = false;
|
|
23567
|
-
let commandRegistry;
|
|
23568
|
-
let resolvedCliVersion;
|
|
23569
|
-
let resolvedRuntimeVersion;
|
|
23570
|
-
let upgradingRuntime = false;
|
|
23571
|
-
let stopPairingApprovalPoller = null;
|
|
23572
|
-
/**
|
|
23573
|
-
* Module-level handle on the runtime appliers built during start(), so the
|
|
23574
|
-
* module-scoped command handler can route `alfe.config_set` to the active
|
|
23575
|
-
* runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
|
|
23576
|
-
*/
|
|
23577
|
-
let runtimeAppliersRef = null;
|
|
23578
|
-
/**
|
|
23579
|
-
* Resolve the installed @alfe.ai/cli version.
|
|
23580
|
-
*
|
|
23581
|
-
* Strategy (in order):
|
|
23582
|
-
* 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
|
|
23583
|
-
* 2. Walk up from this file to find @alfe.ai/cli/package.json
|
|
23584
|
-
* (works when systemd runs the gateway binary directly, since
|
|
23585
|
-
* the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
|
|
23586
|
-
*/
|
|
23587
|
-
/**
|
|
23588
|
-
* Adapter from `AgentApiClient`'s per-provider methods to the
|
|
23589
|
-
* `CredentialsResolver` shape the MCP applier expects. Returns
|
|
23590
|
-
* `undefined` for unknown providers and on 404/network error so the
|
|
23591
|
-
* applier can skip registration silently (its documented contract).
|
|
23592
|
-
*/
|
|
23593
|
-
async function fetchProviderCredentials(agentApi, provider, connectionId) {
|
|
23594
|
-
if (connectionId) try {
|
|
23595
|
-
const raw = await agentApi.getConnectionCredentials(connectionId);
|
|
23596
|
-
const fields = { ...raw.providerMetadata ?? {} };
|
|
23597
|
-
if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
|
|
23598
|
-
const bundle = JSON.parse(raw.accessToken);
|
|
23599
|
-
if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
|
|
23600
|
-
} catch (err) {
|
|
23601
|
-
logger$1.warn({
|
|
23602
|
-
connectionId,
|
|
23603
|
-
err: err instanceof Error ? err.message : String(err)
|
|
23604
|
-
}, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
|
|
23605
|
-
fields.accessToken = raw.accessToken;
|
|
23606
|
-
}
|
|
23607
|
-
return fields;
|
|
23608
|
-
} catch (err) {
|
|
23609
|
-
logger$1.warn({
|
|
23610
|
-
connectionId,
|
|
23611
|
-
err: err instanceof Error ? err.message : String(err)
|
|
23612
|
-
}, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
|
|
23613
|
-
return;
|
|
23614
|
-
}
|
|
23615
|
-
const key = provider.toLowerCase();
|
|
23616
|
-
try {
|
|
23617
|
-
switch (key) {
|
|
23618
|
-
case "atlassian": return await agentApi.getAtlassianCredentials();
|
|
23619
|
-
case "github": return await agentApi.getGithubCredentials();
|
|
23620
|
-
case "xero": return await agentApi.getXeroCredentials();
|
|
23621
|
-
case "notion": return await agentApi.getNotionCredentials();
|
|
23622
|
-
case "myob": return await agentApi.getMYOBCredentials();
|
|
23623
|
-
case "google": return await agentApi.getGoogleCredentials();
|
|
23624
|
-
default:
|
|
23625
|
-
logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
|
|
23626
|
-
return;
|
|
23627
|
-
}
|
|
23628
|
-
} catch {
|
|
23629
|
-
return;
|
|
23630
|
-
}
|
|
23631
|
-
}
|
|
23632
|
-
async function getCliVersion() {
|
|
23633
|
-
if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
|
|
23634
|
-
try {
|
|
23635
|
-
const { fileURLToPath } = await import("node:url");
|
|
23636
|
-
const { dirname } = await import("node:path");
|
|
23637
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
23638
|
-
for (let i = 0; i < 10; i++) {
|
|
23639
|
-
const candidate = join(dir, "package.json");
|
|
23640
|
-
try {
|
|
23641
|
-
const raw = await readFile(candidate, "utf-8");
|
|
23642
|
-
const pkg = JSON.parse(raw);
|
|
23643
|
-
if (pkg.name === "@alfe.ai/cli") return pkg.version;
|
|
23644
|
-
} catch {}
|
|
23645
|
-
const parent = dirname(dir);
|
|
23646
|
-
if (parent === dir) break;
|
|
23647
|
-
dir = parent;
|
|
23648
|
-
}
|
|
23649
|
-
} catch {}
|
|
23650
|
-
logger$1.debug("Could not resolve @alfe.ai/cli version");
|
|
23651
|
-
}
|
|
23652
|
-
/**
|
|
23653
|
-
* Per-runtime "print installed version" commands. Local to the gateway — we do
|
|
23654
|
-
* NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
|
|
23655
|
-
* `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
|
|
23656
|
-
* connection-status report degrades gracefully instead of crashing.
|
|
23657
|
-
*/
|
|
23658
|
-
const RUNTIME_VERSION_COMMANDS = {
|
|
23659
|
-
openclaw: {
|
|
23660
|
-
command: "openclaw",
|
|
23661
|
-
args: ["--version"]
|
|
23662
|
-
},
|
|
23663
|
-
hermes: {
|
|
23664
|
-
command: "hermes",
|
|
23665
|
-
args: ["version"]
|
|
23666
|
-
},
|
|
23667
|
-
"claude-code": {
|
|
23668
|
-
command: "alfe-claude-host",
|
|
23669
|
-
args: ["--version"]
|
|
23670
|
-
}
|
|
23671
|
-
};
|
|
23672
|
-
/**
|
|
23673
|
-
* Pure resolver (exported for tests) — the per-runtime version command, or
|
|
23674
|
-
* `undefined` for an unknown runtime.
|
|
23675
|
-
*/
|
|
23676
|
-
function resolveRuntimeVersionCommand(runtime) {
|
|
23677
|
-
return RUNTIME_VERSION_COMMANDS[runtime];
|
|
23678
|
-
}
|
|
23679
|
-
/**
|
|
23680
|
-
* Resolve the installed runtime version by running the per-runtime version
|
|
23681
|
-
* command and returning the trimmed stdout. An unknown runtime (no command in
|
|
23682
|
-
* the map) returns `undefined` without spawning — it must never throw.
|
|
23683
|
-
*/
|
|
23684
|
-
async function getRuntimeVersion(runtime) {
|
|
23685
|
-
const cmd = resolveRuntimeVersionCommand(runtime);
|
|
23686
|
-
if (!cmd) {
|
|
23687
|
-
logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
|
|
23688
|
-
return;
|
|
23689
|
-
}
|
|
23690
|
-
try {
|
|
23691
|
-
const { stdout } = await execFileAsync(cmd.command, cmd.args);
|
|
23692
|
-
return stdout.trim() || void 0;
|
|
23693
|
-
} catch {
|
|
23694
|
-
logger$1.debug({ runtime }, "Could not resolve runtime version");
|
|
23695
|
-
return;
|
|
23696
|
-
}
|
|
23697
|
-
}
|
|
23698
|
-
/**
|
|
23699
|
-
* Flush pino's async transport and exit.
|
|
23700
|
-
* process.exit() can drop buffered log lines — this ensures they're written first.
|
|
23701
|
-
*/
|
|
23702
|
-
async function flushAndExit(code) {
|
|
23703
|
-
await flushSentry();
|
|
23704
|
-
await new Promise((resolve) => {
|
|
23705
|
-
logger$1.flush();
|
|
23706
|
-
setTimeout(resolve, 500);
|
|
23707
|
-
});
|
|
23708
|
-
process.exit(code);
|
|
23709
|
-
}
|
|
24114
|
+
* so a duplicate is ACKed with the prior result without re-executing.
|
|
24115
|
+
*/
|
|
24116
|
+
const commandDedupe = new CommandDedupe();
|
|
24117
|
+
let shuttingDown = false;
|
|
24118
|
+
let commandRegistry;
|
|
24119
|
+
let resolvedCliVersion;
|
|
24120
|
+
let resolvedRuntimeVersion;
|
|
24121
|
+
let upgradingRuntime = false;
|
|
24122
|
+
let stopPairingApprovalPoller = null;
|
|
23710
24123
|
/**
|
|
23711
|
-
*
|
|
23712
|
-
*
|
|
23713
|
-
*
|
|
23714
|
-
* (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
|
|
23715
|
-
* workspace already has the file. Idempotent and safe to run on every daemon start.
|
|
24124
|
+
* Module-level handle on the runtime appliers built during start(), so the
|
|
24125
|
+
* module-scoped command handler can route `alfe.config_set` to the active
|
|
24126
|
+
* runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
|
|
23716
24127
|
*/
|
|
23717
|
-
|
|
23718
|
-
"SOUL.md",
|
|
23719
|
-
"IDENTITY.md",
|
|
23720
|
-
"BOOTSTRAP.md",
|
|
23721
|
-
"AGENTS.md"
|
|
23722
|
-
];
|
|
23723
|
-
async function migrateLegacyPersonaFiles(home, agentWorkspace) {
|
|
23724
|
-
if (home === agentWorkspace) return;
|
|
23725
|
-
let moved = 0;
|
|
23726
|
-
let skipped = 0;
|
|
23727
|
-
for (const filename of PERSONA_FILES) {
|
|
23728
|
-
const src = join(home, filename);
|
|
23729
|
-
const dst = join(agentWorkspace, filename);
|
|
23730
|
-
try {
|
|
23731
|
-
await stat(src);
|
|
23732
|
-
} catch {
|
|
23733
|
-
continue;
|
|
23734
|
-
}
|
|
23735
|
-
try {
|
|
23736
|
-
await stat(dst);
|
|
23737
|
-
skipped++;
|
|
23738
|
-
continue;
|
|
23739
|
-
} catch {}
|
|
23740
|
-
try {
|
|
23741
|
-
await mkdir(agentWorkspace, { recursive: true });
|
|
23742
|
-
await rename(src, dst);
|
|
23743
|
-
moved++;
|
|
23744
|
-
} catch (err) {
|
|
23745
|
-
logger$1.warn({
|
|
23746
|
-
src,
|
|
23747
|
-
dst,
|
|
23748
|
-
err: err instanceof Error ? err.message : String(err)
|
|
23749
|
-
}, "Persona file migration: rename failed");
|
|
23750
|
-
}
|
|
23751
|
-
}
|
|
23752
|
-
if (moved > 0 || skipped > 0) logger$1.info({
|
|
23753
|
-
home,
|
|
23754
|
-
agentWorkspace,
|
|
23755
|
-
moved,
|
|
23756
|
-
skipped
|
|
23757
|
-
}, "Persona file backfill migration complete");
|
|
23758
|
-
}
|
|
24128
|
+
let runtimeAppliersRef = null;
|
|
23759
24129
|
async function startDaemon() {
|
|
23760
24130
|
startedAt = Date.now();
|
|
23761
24131
|
const managed = isManagedMode();
|
|
@@ -23815,54 +24185,12 @@ async function startDaemon() {
|
|
|
23815
24185
|
commandQueue = new CommandQueue();
|
|
23816
24186
|
commandQueue.startGC();
|
|
23817
24187
|
commandRegistry = new CommandRegistry();
|
|
23818
|
-
|
|
23819
|
-
|
|
23820
|
-
|
|
23821
|
-
|
|
23822
|
-
|
|
23823
|
-
const port = DEFAULT_AI_PROXY_PORT ?? 18193;
|
|
23824
|
-
aiProxyServer = createProxyServer({
|
|
23825
|
-
port,
|
|
23826
|
-
apiKey: config.apiKey,
|
|
23827
|
-
proxyUrl
|
|
23828
|
-
});
|
|
23829
|
-
const server = aiProxyServer;
|
|
23830
|
-
await new Promise((resolve, reject) => {
|
|
23831
|
-
server.listen(port, "127.0.0.1", () => {
|
|
23832
|
-
aiProxyRunning = true;
|
|
23833
|
-
aiProxyUrl = `http://127.0.0.1:${String(port)}`;
|
|
23834
|
-
logger$1.info({
|
|
23835
|
-
port,
|
|
23836
|
-
upstream: proxyUrl
|
|
23837
|
-
}, "AI proxy started");
|
|
23838
|
-
resolve();
|
|
23839
|
-
});
|
|
23840
|
-
server.on("error", reject);
|
|
23841
|
-
});
|
|
23842
|
-
} catch (err) {
|
|
23843
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
23844
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
23845
|
-
logger$1.error({
|
|
23846
|
-
err: message,
|
|
23847
|
-
stack
|
|
23848
|
-
}, "Failed to start AI proxy — LLM requests will fail");
|
|
23849
|
-
}
|
|
23850
|
-
logger$1.debug({ socketPath: config.socketPath }, "Starting IPC server...");
|
|
23851
|
-
ipcServer = new IPCServer(config.socketPath);
|
|
23852
|
-
ipcServer.setRequestHandler(handlePluginRequest);
|
|
24188
|
+
const aiProxy = await startAiProxy(config.apiKey);
|
|
24189
|
+
aiProxyServer = aiProxy.server;
|
|
24190
|
+
aiProxyUrl = aiProxy.url;
|
|
24191
|
+
aiProxyRunning = aiProxy.running;
|
|
24192
|
+
ipcServer = await startIpcServer(config.socketPath, handlePluginRequest);
|
|
23853
24193
|
turnActivityProbe = new IpcTurnActivityProbe(() => ipcServer);
|
|
23854
|
-
try {
|
|
23855
|
-
await ipcServer.start();
|
|
23856
|
-
logger$1.debug("IPC server started");
|
|
23857
|
-
} catch (err) {
|
|
23858
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
23859
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
23860
|
-
logger$1.error({
|
|
23861
|
-
err: message,
|
|
23862
|
-
stack
|
|
23863
|
-
}, "Failed to start IPC server");
|
|
23864
|
-
await flushAndExit(1);
|
|
23865
|
-
}
|
|
23866
24194
|
resolvedCliVersion = await getCliVersion();
|
|
23867
24195
|
resolvedRuntimeVersion = await getRuntimeVersion(config.runtime);
|
|
23868
24196
|
logger$1.info({
|
|
@@ -23928,6 +24256,7 @@ async function startDaemon() {
|
|
|
23928
24256
|
mcpBundler = new McpBundler({
|
|
23929
24257
|
logger: logger$1,
|
|
23930
24258
|
idleTtlMs: 0,
|
|
24259
|
+
retryNeverConnected: true,
|
|
23931
24260
|
...mcpErrorHooks
|
|
23932
24261
|
}, { connect: defaultConnect });
|
|
23933
24262
|
if (config.runtime === "hermes" || config.runtime === "claude-code") logger$1.info({ runtime: config.runtime }, "MCP store mirrored into runtime config by the runtime MCP sync — daemon MCP bundler left idle");
|
|
@@ -24005,19 +24334,11 @@ async function startDaemon() {
|
|
|
24005
24334
|
}
|
|
24006
24335
|
}
|
|
24007
24336
|
cloudClient.setOnReconciliationComplete(() => {
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
commandRegistry
|
|
24011
|
-
|
|
24012
|
-
|
|
24013
|
-
type: "COMMANDS_AVAILABLE",
|
|
24014
|
-
commands: commandRegistry.listCommands()
|
|
24015
|
-
};
|
|
24016
|
-
cloudClient.sendMessage(commandsMsg);
|
|
24017
|
-
logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
|
|
24018
|
-
} catch (err) {
|
|
24019
|
-
logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
|
|
24020
|
-
}
|
|
24337
|
+
reportAvailableCommands({
|
|
24338
|
+
integrationManager,
|
|
24339
|
+
commandRegistry,
|
|
24340
|
+
cloudClient
|
|
24341
|
+
});
|
|
24021
24342
|
});
|
|
24022
24343
|
cloudClient.start();
|
|
24023
24344
|
logger$1.debug("Cloud client started");
|
|
@@ -24302,519 +24623,251 @@ async function executeCloudCommand(command) {
|
|
|
24302
24623
|
};
|
|
24303
24624
|
return {
|
|
24304
24625
|
type: "COMMAND_ACK",
|
|
24305
|
-
commandId: command.commandId,
|
|
24306
|
-
status: "ok",
|
|
24307
|
-
result: { integrations: [info] }
|
|
24308
|
-
};
|
|
24309
|
-
}
|
|
24310
|
-
const integrations = integrationManager.list();
|
|
24311
|
-
return {
|
|
24312
|
-
type: "COMMAND_ACK",
|
|
24313
|
-
commandId: command.commandId,
|
|
24314
|
-
status: "ok",
|
|
24315
|
-
result: { integrations }
|
|
24316
|
-
};
|
|
24317
|
-
} catch (err) {
|
|
24318
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
24319
|
-
logger$1.error({ err: message }, "Integration status failed");
|
|
24320
|
-
return {
|
|
24321
|
-
type: "COMMAND_ACK",
|
|
24322
|
-
commandId: command.commandId,
|
|
24323
|
-
status: "error",
|
|
24324
|
-
result: {
|
|
24325
|
-
code: "STATUS_FAILED",
|
|
24326
|
-
message
|
|
24327
|
-
}
|
|
24328
|
-
};
|
|
24329
|
-
}
|
|
24330
|
-
}
|
|
24331
|
-
if (command.command === "alfe.config_set") {
|
|
24332
|
-
const payload = command.payload;
|
|
24333
|
-
const key = payload?.key;
|
|
24334
|
-
const value = payload?.value;
|
|
24335
|
-
if (!key || value === void 0) return {
|
|
24336
|
-
type: "COMMAND_ACK",
|
|
24337
|
-
commandId: command.commandId,
|
|
24338
|
-
status: "error",
|
|
24339
|
-
result: {
|
|
24340
|
-
code: "INVALID_PAYLOAD",
|
|
24341
|
-
message: "alfe.config_set requires key and value"
|
|
24342
|
-
}
|
|
24343
|
-
};
|
|
24344
|
-
const runtime = config.runtime;
|
|
24345
|
-
const applier = runtimeAppliersRef?.get(runtime);
|
|
24346
|
-
if (!applier || typeof applier.setConfigRaw !== "function") {
|
|
24347
|
-
const message = `No runtime applier supports config_set for runtime "${runtime}"`;
|
|
24348
|
-
logger$1.warn({
|
|
24349
|
-
runtime,
|
|
24350
|
-
key
|
|
24351
|
-
}, message);
|
|
24352
|
-
return {
|
|
24353
|
-
type: "COMMAND_ACK",
|
|
24354
|
-
commandId: command.commandId,
|
|
24355
|
-
status: "error",
|
|
24356
|
-
result: {
|
|
24357
|
-
code: "CONFIG_SET_UNSUPPORTED",
|
|
24358
|
-
message
|
|
24359
|
-
}
|
|
24360
|
-
};
|
|
24361
|
-
}
|
|
24362
|
-
try {
|
|
24363
|
-
await applier.setConfigRaw(key, value);
|
|
24364
|
-
logger$1.info({
|
|
24365
|
-
runtime,
|
|
24366
|
-
key
|
|
24367
|
-
}, "Applied config via runtime applier setConfigRaw");
|
|
24368
|
-
return {
|
|
24369
|
-
type: "COMMAND_ACK",
|
|
24370
|
-
commandId: command.commandId,
|
|
24371
|
-
status: "ok",
|
|
24372
|
-
result: {
|
|
24373
|
-
key,
|
|
24374
|
-
value
|
|
24375
|
-
}
|
|
24376
|
-
};
|
|
24377
|
-
} catch (err) {
|
|
24378
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
24379
|
-
logger$1.error({
|
|
24380
|
-
err: message,
|
|
24381
|
-
runtime,
|
|
24382
|
-
key
|
|
24383
|
-
}, "Failed to apply config via runtime applier");
|
|
24384
|
-
return {
|
|
24385
|
-
type: "COMMAND_ACK",
|
|
24386
|
-
commandId: command.commandId,
|
|
24387
|
-
status: "error",
|
|
24388
|
-
result: {
|
|
24389
|
-
code: "CONFIG_SET_FAILED",
|
|
24390
|
-
message
|
|
24391
|
-
}
|
|
24392
|
-
};
|
|
24393
|
-
}
|
|
24394
|
-
}
|
|
24395
|
-
if (commandRegistry.has(command.command)) try {
|
|
24396
|
-
const ctx = buildCommandContext();
|
|
24397
|
-
const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
|
|
24398
|
-
return {
|
|
24399
|
-
type: "COMMAND_ACK",
|
|
24400
|
-
commandId: command.commandId,
|
|
24401
|
-
status: result.status,
|
|
24402
|
-
result: result.result
|
|
24403
|
-
};
|
|
24404
|
-
} catch (err) {
|
|
24405
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
24406
|
-
logger$1.error({
|
|
24407
|
-
err: message,
|
|
24408
|
-
command: command.command
|
|
24409
|
-
}, "Command registry execution failed");
|
|
24410
|
-
return {
|
|
24411
|
-
type: "COMMAND_ACK",
|
|
24412
|
-
commandId: command.commandId,
|
|
24413
|
-
status: "error",
|
|
24414
|
-
result: {
|
|
24415
|
-
code: "REGISTRY_ERROR",
|
|
24416
|
-
message
|
|
24417
|
-
}
|
|
24418
|
-
};
|
|
24419
|
-
}
|
|
24420
|
-
const ipcRequest = cloudCommandToIPCRequest(command);
|
|
24421
|
-
if (!ipcRequest) {
|
|
24422
|
-
logger$1.warn({ command: command.command }, "Unrecognized cloud command");
|
|
24423
|
-
return {
|
|
24424
|
-
type: "COMMAND_ACK",
|
|
24425
|
-
commandId: command.commandId,
|
|
24426
|
-
status: "error",
|
|
24427
|
-
result: {
|
|
24428
|
-
code: "UNKNOWN_COMMAND",
|
|
24429
|
-
message: `Unrecognized command: ${command.command}`
|
|
24430
|
-
}
|
|
24431
|
-
};
|
|
24432
|
-
}
|
|
24433
|
-
const plugins = ipcServer?.getRegisteredPlugins() ?? [];
|
|
24434
|
-
if (plugins.length === 0) {
|
|
24435
|
-
logger$1.info({
|
|
24436
|
-
commandId: command.commandId,
|
|
24437
|
-
command: command.command
|
|
24438
|
-
}, "No plugins connected — queuing command");
|
|
24439
|
-
commandQueue.enqueue("_default", ipcRequest, command.commandId);
|
|
24440
|
-
return {
|
|
24441
|
-
type: "COMMAND_ACK",
|
|
24442
|
-
commandId: command.commandId,
|
|
24443
|
-
status: "ok",
|
|
24444
|
-
result: {
|
|
24445
|
-
queued: true,
|
|
24446
|
-
message: "Command queued — no plugins connected"
|
|
24447
|
-
}
|
|
24448
|
-
};
|
|
24449
|
-
}
|
|
24450
|
-
const [pluginId] = plugins[0];
|
|
24451
|
-
if (!ipcServer) return {
|
|
24452
|
-
type: "COMMAND_ACK",
|
|
24453
|
-
commandId: command.commandId,
|
|
24454
|
-
status: "error",
|
|
24455
|
-
result: {
|
|
24456
|
-
code: "NO_IPC",
|
|
24457
|
-
message: "IPC server not available"
|
|
24626
|
+
commandId: command.commandId,
|
|
24627
|
+
status: "ok",
|
|
24628
|
+
result: { integrations: [info] }
|
|
24629
|
+
};
|
|
24630
|
+
}
|
|
24631
|
+
const integrations = integrationManager.list();
|
|
24632
|
+
return {
|
|
24633
|
+
type: "COMMAND_ACK",
|
|
24634
|
+
commandId: command.commandId,
|
|
24635
|
+
status: "ok",
|
|
24636
|
+
result: { integrations }
|
|
24637
|
+
};
|
|
24638
|
+
} catch (err) {
|
|
24639
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24640
|
+
logger$1.error({ err: message }, "Integration status failed");
|
|
24641
|
+
return {
|
|
24642
|
+
type: "COMMAND_ACK",
|
|
24643
|
+
commandId: command.commandId,
|
|
24644
|
+
status: "error",
|
|
24645
|
+
result: {
|
|
24646
|
+
code: "STATUS_FAILED",
|
|
24647
|
+
message
|
|
24648
|
+
}
|
|
24649
|
+
};
|
|
24458
24650
|
}
|
|
24459
|
-
}
|
|
24460
|
-
|
|
24461
|
-
const
|
|
24462
|
-
|
|
24463
|
-
|
|
24464
|
-
|
|
24465
|
-
return {
|
|
24651
|
+
}
|
|
24652
|
+
if (command.command === "alfe.config_set") {
|
|
24653
|
+
const payload = command.payload;
|
|
24654
|
+
const key = payload?.key;
|
|
24655
|
+
const value = payload?.value;
|
|
24656
|
+
if (!key || value === void 0) return {
|
|
24466
24657
|
type: "COMMAND_ACK",
|
|
24467
24658
|
commandId: command.commandId,
|
|
24468
24659
|
status: "error",
|
|
24469
24660
|
result: {
|
|
24470
|
-
code: "
|
|
24471
|
-
message
|
|
24661
|
+
code: "INVALID_PAYLOAD",
|
|
24662
|
+
message: "alfe.config_set requires key and value"
|
|
24472
24663
|
}
|
|
24473
24664
|
};
|
|
24474
|
-
|
|
24475
|
-
|
|
24476
|
-
|
|
24477
|
-
|
|
24478
|
-
|
|
24479
|
-
|
|
24480
|
-
|
|
24481
|
-
|
|
24482
|
-
apiKey: config.apiKey,
|
|
24483
|
-
async exec(cmd, opts) {
|
|
24484
|
-
const { exec: execCb } = await import("child_process");
|
|
24485
|
-
const { promisify } = await import("util");
|
|
24486
|
-
const { stdout, stderr } = await promisify(execCb)(cmd, {
|
|
24487
|
-
cwd: workspacePath,
|
|
24488
|
-
timeout: opts?.timeoutMs ?? 25e3,
|
|
24489
|
-
maxBuffer: opts?.maxBuffer ?? 512 * 1024
|
|
24490
|
-
});
|
|
24665
|
+
const runtime = config.runtime;
|
|
24666
|
+
const applier = runtimeAppliersRef?.get(runtime);
|
|
24667
|
+
if (!applier || typeof applier.setConfigRaw !== "function") {
|
|
24668
|
+
const message = `No runtime applier supports config_set for runtime "${runtime}"`;
|
|
24669
|
+
logger$1.warn({
|
|
24670
|
+
runtime,
|
|
24671
|
+
key
|
|
24672
|
+
}, message);
|
|
24491
24673
|
return {
|
|
24492
|
-
|
|
24493
|
-
|
|
24674
|
+
type: "COMMAND_ACK",
|
|
24675
|
+
commandId: command.commandId,
|
|
24676
|
+
status: "error",
|
|
24677
|
+
result: {
|
|
24678
|
+
code: "CONFIG_SET_UNSUPPORTED",
|
|
24679
|
+
message
|
|
24680
|
+
}
|
|
24494
24681
|
};
|
|
24495
24682
|
}
|
|
24496
|
-
|
|
24497
|
-
|
|
24498
|
-
|
|
24499
|
-
|
|
24500
|
-
|
|
24501
|
-
|
|
24502
|
-
|
|
24503
|
-
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
|
|
24509
|
-
|
|
24510
|
-
|
|
24511
|
-
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
|
|
24518
|
-
|
|
24519
|
-
|
|
24520
|
-
|
|
24521
|
-
|
|
24522
|
-
|
|
24523
|
-
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
|
|
24527
|
-
},
|
|
24528
|
-
cloud: {
|
|
24529
|
-
status: cloudConnected ? "connected" : "disconnected",
|
|
24530
|
-
latencyMs: cloudClient.getLatencyMs()
|
|
24531
|
-
},
|
|
24532
|
-
aiProxy: { status: aiProxyRunning ? "running" : "stopped" },
|
|
24533
|
-
plugins: (ipcServer?.getRegisteredPlugins() ?? []).map(([, info]) => ({
|
|
24534
|
-
name: info.name,
|
|
24535
|
-
version: info.version,
|
|
24536
|
-
capabilities: info.capabilities,
|
|
24537
|
-
connectedAt: info.connectedAt,
|
|
24538
|
-
lastSeen: info.lastSeen
|
|
24539
|
-
})),
|
|
24540
|
-
commandQueue: { totalPending: commandQueue.totalPending() }
|
|
24541
|
-
}
|
|
24542
|
-
};
|
|
24543
|
-
}
|
|
24544
|
-
function handleIntegrationList() {
|
|
24545
|
-
return {
|
|
24546
|
-
ok: true,
|
|
24547
|
-
payload: { integrations: integrationManager.list() }
|
|
24548
|
-
};
|
|
24549
|
-
}
|
|
24550
|
-
/**
|
|
24551
|
-
* Return the daemon-hosted MCP bundler's current namespaced tool catalog.
|
|
24552
|
-
* Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
|
|
24553
|
-
* cheap (in-memory snapshot, no I/O).
|
|
24554
|
-
*/
|
|
24555
|
-
function handleMcpListTools(bundler) {
|
|
24556
|
-
if (!bundler) return {
|
|
24557
|
-
ok: false,
|
|
24558
|
-
error: {
|
|
24559
|
-
code: "MCP_BUNDLER_UNAVAILABLE",
|
|
24560
|
-
message: "MCP bundler not initialized"
|
|
24561
|
-
}
|
|
24562
|
-
};
|
|
24563
|
-
return {
|
|
24564
|
-
ok: true,
|
|
24565
|
-
payload: { tools: bundler.listTools() }
|
|
24566
|
-
};
|
|
24567
|
-
}
|
|
24568
|
-
/**
|
|
24569
|
-
* Observability backstop for the INTEGRATION warm path. `applyForIntegration`
|
|
24570
|
-
* registers MCP servers into the store and the daemon warms them silently via
|
|
24571
|
-
* the store-change `onChange` hook — so a server that fails its connect (e.g.
|
|
24572
|
-
* an MCP server whose backing account/credential wasn't resolvable at warm
|
|
24573
|
-
* time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
|
|
24574
|
-
* making the black hole undiagnosable. After a warm, read `bundler.statuses()`
|
|
24575
|
-
* and warn once per still-failed server so the failure is visible. These
|
|
24576
|
-
* self-heal on the bundler's background retry sweep once the dependency
|
|
24577
|
-
* appears. Returns the servers it warned about (for tests / callers).
|
|
24578
|
-
*/
|
|
24579
|
-
function warnFailedMcpServers(bundler, log, reason) {
|
|
24580
|
-
if (!bundler) return [];
|
|
24581
|
-
const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
|
|
24582
|
-
for (const status of failed) log.warn({
|
|
24583
|
-
server: status.name,
|
|
24584
|
-
reason,
|
|
24585
|
-
consecutiveFailures: status.consecutiveFailures,
|
|
24586
|
-
lastError: status.lastError
|
|
24587
|
-
}, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
|
|
24588
|
-
return failed;
|
|
24589
|
-
}
|
|
24590
|
-
/**
|
|
24591
|
-
* Route a tool call to the appropriate MCP child via the daemon-hosted
|
|
24592
|
-
* bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
|
|
24593
|
-
* the raw JSON object the LLM produced.
|
|
24594
|
-
*/
|
|
24595
|
-
/**
|
|
24596
|
-
* List every server entry in the alfe bundler store. Lets agents inspect
|
|
24597
|
-
* what they've already registered before adding a new one.
|
|
24598
|
-
*/
|
|
24599
|
-
function handleMcpListServers(manager = mcpManagerRef) {
|
|
24600
|
-
if (!manager) return {
|
|
24601
|
-
ok: false,
|
|
24602
|
-
error: {
|
|
24603
|
-
code: "MCP_MANAGER_UNAVAILABLE",
|
|
24604
|
-
message: "MCP manager not initialized"
|
|
24605
|
-
}
|
|
24606
|
-
};
|
|
24607
|
-
const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
|
|
24608
|
-
const byName = new Map(statuses.map((s) => [s.name, s]));
|
|
24609
|
-
return {
|
|
24610
|
-
ok: true,
|
|
24611
|
-
payload: { servers: manager.listServers().map(({ id, entry }) => ({
|
|
24612
|
-
id,
|
|
24613
|
-
entry,
|
|
24614
|
-
status: byName.get(id) ?? {
|
|
24615
|
-
name: id,
|
|
24616
|
-
connected: false,
|
|
24617
|
-
toolCount: 0,
|
|
24618
|
-
consecutiveFailures: 0
|
|
24619
|
-
}
|
|
24620
|
-
})) }
|
|
24621
|
-
};
|
|
24622
|
-
}
|
|
24623
|
-
/**
|
|
24624
|
-
* How long the add-confirm probe waits for the freshly-added server to
|
|
24625
|
-
* connect before replying. This is effectively the whole budget for
|
|
24626
|
-
* `bundler.warmServer`: the `Manager.warmServer` pre-reconcile is cheap here
|
|
24627
|
-
* because `addServer` already queued the Connection object into the store +
|
|
24628
|
-
* bundler before we probe, so `reconcileBundler()` finds no diff and returns
|
|
24629
|
-
* fast. Kept well under the plugin caller's 30s IPC timeout. A slower server
|
|
24630
|
-
* still connects in the background and surfaces on the next
|
|
24631
|
-
* `alfe_mcp_list_tools` — the probe just reports what it saw within the window.
|
|
24632
|
-
*/
|
|
24633
|
-
const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
|
|
24634
|
-
/**
|
|
24635
|
-
* Register a new MCP server in the alfe bundler store on behalf of the agent,
|
|
24636
|
-
* then CONFIRM the connect before replying so the agent learns whether the
|
|
24637
|
-
* server actually works. Owned as `'manual'` so the agent can later remove it
|
|
24638
|
-
* without an expectedOwner conflict — matches what `alfe mcp add` does from the
|
|
24639
|
-
* CLI. Registration is durable regardless of the probe outcome: a probe
|
|
24640
|
-
* failure (or a runtime with no daemon bundler, e.g. hermes) still returns
|
|
24641
|
-
* `ok` with `connected: false`; the store watcher / runtime picks the entry up.
|
|
24642
|
-
*/
|
|
24643
|
-
async function handleMcpAddServer(params, manager = mcpManagerRef) {
|
|
24644
|
-
if (!manager) return {
|
|
24645
|
-
ok: false,
|
|
24646
|
-
error: {
|
|
24647
|
-
code: "MCP_MANAGER_UNAVAILABLE",
|
|
24648
|
-
message: "MCP manager not initialized"
|
|
24649
|
-
}
|
|
24650
|
-
};
|
|
24651
|
-
const p = params;
|
|
24652
|
-
if (typeof p.id !== "string" || p.id.length === 0) return {
|
|
24653
|
-
ok: false,
|
|
24654
|
-
error: {
|
|
24655
|
-
code: "INVALID_PARAMS",
|
|
24656
|
-
message: "id is required (string)"
|
|
24657
|
-
}
|
|
24658
|
-
};
|
|
24659
|
-
const config = buildServerConfig(p);
|
|
24660
|
-
if (!config) return {
|
|
24661
|
-
ok: false,
|
|
24662
|
-
error: {
|
|
24663
|
-
code: "INVALID_PARAMS",
|
|
24664
|
-
message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
|
|
24683
|
+
try {
|
|
24684
|
+
await applier.setConfigRaw(key, value);
|
|
24685
|
+
logger$1.info({
|
|
24686
|
+
runtime,
|
|
24687
|
+
key
|
|
24688
|
+
}, "Applied config via runtime applier setConfigRaw");
|
|
24689
|
+
return {
|
|
24690
|
+
type: "COMMAND_ACK",
|
|
24691
|
+
commandId: command.commandId,
|
|
24692
|
+
status: "ok",
|
|
24693
|
+
result: {
|
|
24694
|
+
key,
|
|
24695
|
+
value
|
|
24696
|
+
}
|
|
24697
|
+
};
|
|
24698
|
+
} catch (err) {
|
|
24699
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24700
|
+
logger$1.error({
|
|
24701
|
+
err: message,
|
|
24702
|
+
runtime,
|
|
24703
|
+
key
|
|
24704
|
+
}, "Failed to apply config via runtime applier");
|
|
24705
|
+
return {
|
|
24706
|
+
type: "COMMAND_ACK",
|
|
24707
|
+
commandId: command.commandId,
|
|
24708
|
+
status: "error",
|
|
24709
|
+
result: {
|
|
24710
|
+
code: "CONFIG_SET_FAILED",
|
|
24711
|
+
message
|
|
24712
|
+
}
|
|
24713
|
+
};
|
|
24665
24714
|
}
|
|
24666
|
-
}
|
|
24667
|
-
try {
|
|
24668
|
-
|
|
24669
|
-
|
|
24670
|
-
|
|
24671
|
-
|
|
24715
|
+
}
|
|
24716
|
+
if (commandRegistry.has(command.command)) try {
|
|
24717
|
+
const ctx = buildCommandContext();
|
|
24718
|
+
const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
|
|
24719
|
+
return {
|
|
24720
|
+
type: "COMMAND_ACK",
|
|
24721
|
+
commandId: command.commandId,
|
|
24722
|
+
status: result.status,
|
|
24723
|
+
result: result.result
|
|
24724
|
+
};
|
|
24672
24725
|
} catch (err) {
|
|
24673
24726
|
const message = err instanceof Error ? err.message : String(err);
|
|
24674
|
-
logger$1.
|
|
24675
|
-
|
|
24676
|
-
|
|
24677
|
-
}, "
|
|
24727
|
+
logger$1.error({
|
|
24728
|
+
err: message,
|
|
24729
|
+
command: command.command
|
|
24730
|
+
}, "Command registry execution failed");
|
|
24678
24731
|
return {
|
|
24679
|
-
|
|
24680
|
-
|
|
24681
|
-
|
|
24732
|
+
type: "COMMAND_ACK",
|
|
24733
|
+
commandId: command.commandId,
|
|
24734
|
+
status: "error",
|
|
24735
|
+
result: {
|
|
24736
|
+
code: "REGISTRY_ERROR",
|
|
24682
24737
|
message
|
|
24683
24738
|
}
|
|
24684
24739
|
};
|
|
24685
24740
|
}
|
|
24686
|
-
|
|
24687
|
-
if (
|
|
24688
|
-
|
|
24689
|
-
|
|
24690
|
-
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
|
|
24694
|
-
|
|
24695
|
-
|
|
24696
|
-
|
|
24697
|
-
|
|
24698
|
-
id: p.id,
|
|
24699
|
-
connected: status?.connected ?? false,
|
|
24700
|
-
toolCount: status?.toolCount ?? 0,
|
|
24701
|
-
...status?.lastError !== void 0 ? { error: status.lastError } : {}
|
|
24702
|
-
}
|
|
24703
|
-
};
|
|
24704
|
-
}
|
|
24705
|
-
function buildServerConfig(p) {
|
|
24706
|
-
if (typeof p.command === "string" && p.command.length > 0) {
|
|
24707
|
-
const cfg = { command: p.command };
|
|
24708
|
-
if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
|
|
24709
|
-
if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
|
|
24710
|
-
const env = {};
|
|
24711
|
-
for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
|
|
24712
|
-
cfg.env = env;
|
|
24713
|
-
}
|
|
24714
|
-
if (typeof p.cwd === "string") cfg.cwd = p.cwd;
|
|
24715
|
-
return cfg;
|
|
24741
|
+
const ipcRequest = cloudCommandToIPCRequest(command);
|
|
24742
|
+
if (!ipcRequest) {
|
|
24743
|
+
logger$1.warn({ command: command.command }, "Unrecognized cloud command");
|
|
24744
|
+
return {
|
|
24745
|
+
type: "COMMAND_ACK",
|
|
24746
|
+
commandId: command.commandId,
|
|
24747
|
+
status: "error",
|
|
24748
|
+
result: {
|
|
24749
|
+
code: "UNKNOWN_COMMAND",
|
|
24750
|
+
message: `Unrecognized command: ${command.command}`
|
|
24751
|
+
}
|
|
24752
|
+
};
|
|
24716
24753
|
}
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
|
|
24721
|
-
|
|
24754
|
+
const plugins = ipcServer?.getRegisteredPlugins() ?? [];
|
|
24755
|
+
if (plugins.length === 0) {
|
|
24756
|
+
logger$1.info({
|
|
24757
|
+
commandId: command.commandId,
|
|
24758
|
+
command: command.command
|
|
24759
|
+
}, "No plugins connected — queuing command");
|
|
24760
|
+
commandQueue.enqueue("_default", ipcRequest, command.commandId);
|
|
24761
|
+
return {
|
|
24762
|
+
type: "COMMAND_ACK",
|
|
24763
|
+
commandId: command.commandId,
|
|
24764
|
+
status: "ok",
|
|
24765
|
+
result: {
|
|
24766
|
+
queued: true,
|
|
24767
|
+
message: "Command queued — no plugins connected"
|
|
24768
|
+
}
|
|
24722
24769
|
};
|
|
24723
|
-
if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
|
|
24724
|
-
const headers = {};
|
|
24725
|
-
for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
|
|
24726
|
-
cfg.headers = headers;
|
|
24727
|
-
}
|
|
24728
|
-
return cfg;
|
|
24729
24770
|
}
|
|
24730
|
-
|
|
24731
|
-
|
|
24732
|
-
|
|
24733
|
-
|
|
24734
|
-
|
|
24735
|
-
|
|
24736
|
-
|
|
24737
|
-
|
|
24738
|
-
*/
|
|
24739
|
-
async function handleMcpRemoveServer(params, manager = mcpManagerRef) {
|
|
24740
|
-
if (!manager) return {
|
|
24741
|
-
ok: false,
|
|
24742
|
-
error: {
|
|
24743
|
-
code: "MCP_MANAGER_UNAVAILABLE",
|
|
24744
|
-
message: "MCP manager not initialized"
|
|
24745
|
-
}
|
|
24746
|
-
};
|
|
24747
|
-
const { id } = params;
|
|
24748
|
-
if (typeof id !== "string" || id.length === 0) return {
|
|
24749
|
-
ok: false,
|
|
24750
|
-
error: {
|
|
24751
|
-
code: "INVALID_PARAMS",
|
|
24752
|
-
message: "id is required (string)"
|
|
24771
|
+
const [pluginId] = plugins[0];
|
|
24772
|
+
if (!ipcServer) return {
|
|
24773
|
+
type: "COMMAND_ACK",
|
|
24774
|
+
commandId: command.commandId,
|
|
24775
|
+
status: "error",
|
|
24776
|
+
result: {
|
|
24777
|
+
code: "NO_IPC",
|
|
24778
|
+
message: "IPC server not available"
|
|
24753
24779
|
}
|
|
24754
24780
|
};
|
|
24755
24781
|
try {
|
|
24756
|
-
|
|
24757
|
-
|
|
24758
|
-
payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
|
|
24759
|
-
};
|
|
24782
|
+
const response = await ipcServer.sendRequest(pluginId, ipcRequest.method, ipcRequest.params, 3e4);
|
|
24783
|
+
return ipcResponseToCloudAck(command.commandId, response);
|
|
24760
24784
|
} catch (err) {
|
|
24761
24785
|
const message = err instanceof Error ? err.message : String(err);
|
|
24762
|
-
if (message.includes("owned by")) return {
|
|
24763
|
-
ok: false,
|
|
24764
|
-
error: {
|
|
24765
|
-
code: "MCP_OWNER_MISMATCH",
|
|
24766
|
-
message
|
|
24767
|
-
}
|
|
24768
|
-
};
|
|
24769
|
-
logger$1.warn({
|
|
24770
|
-
id,
|
|
24771
|
-
err: message
|
|
24772
|
-
}, "mcp.remove_server failed");
|
|
24773
24786
|
return {
|
|
24774
|
-
|
|
24775
|
-
|
|
24776
|
-
|
|
24787
|
+
type: "COMMAND_ACK",
|
|
24788
|
+
commandId: command.commandId,
|
|
24789
|
+
status: "error",
|
|
24790
|
+
result: {
|
|
24791
|
+
code: "PLUGIN_ERROR",
|
|
24777
24792
|
message
|
|
24778
24793
|
}
|
|
24779
24794
|
};
|
|
24780
24795
|
}
|
|
24781
24796
|
}
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24797
|
+
function buildCommandContext() {
|
|
24798
|
+
const workspacePath = Object.values(config.runtimes)[0]?.workspace ?? "~/.openclaw";
|
|
24799
|
+
return {
|
|
24800
|
+
workspacePath,
|
|
24801
|
+
aiProxyUrl: aiProxyUrl ?? void 0,
|
|
24802
|
+
aiProxyRunning,
|
|
24803
|
+
apiKey: config.apiKey,
|
|
24804
|
+
async exec(cmd, opts) {
|
|
24805
|
+
const { exec: execCb } = await import("child_process");
|
|
24806
|
+
const { promisify } = await import("util");
|
|
24807
|
+
const { stdout, stderr } = await promisify(execCb)(cmd, {
|
|
24808
|
+
cwd: workspacePath,
|
|
24809
|
+
timeout: opts?.timeoutMs ?? 25e3,
|
|
24810
|
+
maxBuffer: opts?.maxBuffer ?? 512 * 1024
|
|
24811
|
+
});
|
|
24812
|
+
return {
|
|
24813
|
+
stdout: stdout.trim(),
|
|
24814
|
+
stderr: stderr.trim()
|
|
24815
|
+
};
|
|
24796
24816
|
}
|
|
24797
24817
|
};
|
|
24798
|
-
|
|
24799
|
-
|
|
24800
|
-
|
|
24801
|
-
|
|
24802
|
-
|
|
24803
|
-
|
|
24804
|
-
|
|
24805
|
-
|
|
24806
|
-
|
|
24807
|
-
|
|
24808
|
-
|
|
24809
|
-
return {
|
|
24818
|
+
}
|
|
24819
|
+
function handlePluginRequest(method, params, pluginId) {
|
|
24820
|
+
switch (method) {
|
|
24821
|
+
case "status": return Promise.resolve(handleStatus());
|
|
24822
|
+
case "integration.list": return Promise.resolve(handleIntegrationList());
|
|
24823
|
+
case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
|
|
24824
|
+
case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
|
|
24825
|
+
case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
|
|
24826
|
+
case "mcp.list_servers": return Promise.resolve(handleMcpListServers(mcpManagerRef));
|
|
24827
|
+
case "mcp.add_server": return handleMcpAddServer(params, mcpManagerRef);
|
|
24828
|
+
case "mcp.remove_server": return handleMcpRemoveServer(params, mcpManagerRef);
|
|
24829
|
+
default: return Promise.resolve({
|
|
24810
24830
|
ok: false,
|
|
24811
24831
|
error: {
|
|
24812
|
-
code: "
|
|
24813
|
-
message
|
|
24832
|
+
code: "UNKNOWN_METHOD",
|
|
24833
|
+
message: `Unknown method: ${method}`
|
|
24814
24834
|
}
|
|
24815
|
-
};
|
|
24835
|
+
});
|
|
24816
24836
|
}
|
|
24817
24837
|
}
|
|
24838
|
+
function handleStatus() {
|
|
24839
|
+
return {
|
|
24840
|
+
ok: true,
|
|
24841
|
+
payload: {
|
|
24842
|
+
daemon: {
|
|
24843
|
+
status: "running",
|
|
24844
|
+
pid: process.pid,
|
|
24845
|
+
uptime: (Date.now() - startedAt) / 1e3,
|
|
24846
|
+
version: "0.1.0",
|
|
24847
|
+
runtimeVersion: resolvedRuntimeVersion
|
|
24848
|
+
},
|
|
24849
|
+
cloud: {
|
|
24850
|
+
status: cloudConnected ? "connected" : "disconnected",
|
|
24851
|
+
latencyMs: cloudClient.getLatencyMs()
|
|
24852
|
+
},
|
|
24853
|
+
aiProxy: { status: aiProxyRunning ? "running" : "stopped" },
|
|
24854
|
+
plugins: (ipcServer?.getRegisteredPlugins() ?? []).map(([, info]) => ({
|
|
24855
|
+
name: info.name,
|
|
24856
|
+
version: info.version,
|
|
24857
|
+
capabilities: info.capabilities,
|
|
24858
|
+
connectedAt: info.connectedAt,
|
|
24859
|
+
lastSeen: info.lastSeen
|
|
24860
|
+
})),
|
|
24861
|
+
commandQueue: { totalPending: commandQueue.totalPending() }
|
|
24862
|
+
}
|
|
24863
|
+
};
|
|
24864
|
+
}
|
|
24865
|
+
function handleIntegrationList() {
|
|
24866
|
+
return {
|
|
24867
|
+
ok: true,
|
|
24868
|
+
payload: { integrations: integrationManager.list() }
|
|
24869
|
+
};
|
|
24870
|
+
}
|
|
24818
24871
|
function handleIntegrationReport(params, pluginId) {
|
|
24819
24872
|
const { name, status, detail } = params;
|
|
24820
24873
|
if (!name || !status) return {
|