@alfe.ai/gateway 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -37
- package/dist/health.js +1771 -1462
- package/dist/src/index.d.ts +4 -11
- package/dist/upgrade.js +9 -6
- package/package.json +7 -7
package/dist/health.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
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
|
-
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";
|
|
4
|
+
import { chmod, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
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";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
16
|
import { ClaudeCodeApplier, ClaudeCodeMcpSync, HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, NoopOpenClawCliLock, OpenClawApplier, SerialOpenClawCliLock } from "@alfe.ai/integrations";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { Manager, McpBundler, defaultConnect, serverLaunchFingerprint } from "@alfe.ai/mcp-bundler";
|
|
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";
|
|
@@ -94,7 +94,7 @@ const PINNED_OPENCLAW_VERSION = "2026.6.11";
|
|
|
94
94
|
* `PINNED_OPENCLAW_VERSION` — there is no `services/compute/Dockerfile` mirror to
|
|
95
95
|
* keep in sync.
|
|
96
96
|
*/
|
|
97
|
-
const PINNED_CLAUDE_CODE_HOST_VERSION = "0.1.
|
|
97
|
+
const PINNED_CLAUDE_CODE_HOST_VERSION = "0.1.6";
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region ../../packages-internal/ids/dist/prefixes.js
|
|
100
100
|
const ID_PREFIXES = {
|
|
@@ -110,6 +110,7 @@ const ID_PREFIXES = {
|
|
|
110
110
|
promoCode: "prc",
|
|
111
111
|
promoRedemption: "prr",
|
|
112
112
|
pendingPromo: "pdp",
|
|
113
|
+
remoteSession: "rms",
|
|
113
114
|
webhook: "whk",
|
|
114
115
|
webhookDelivery: "wdl",
|
|
115
116
|
onboardingSession: "obs",
|
|
@@ -298,7 +299,7 @@ var AlfeApiClient = class {
|
|
|
298
299
|
getToken;
|
|
299
300
|
onAuthFailure;
|
|
300
301
|
constructor(options) {
|
|
301
|
-
this.apiBaseUrl = options.apiBaseUrl;
|
|
302
|
+
this.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
302
303
|
this.getToken = options.getToken;
|
|
303
304
|
this.onAuthFailure = options.onAuthFailure;
|
|
304
305
|
}
|
|
@@ -315,7 +316,7 @@ var AlfeApiClient = class {
|
|
|
315
316
|
try {
|
|
316
317
|
const url = `${this.apiBaseUrl}${path}`;
|
|
317
318
|
const headers = new Headers(options?.headers);
|
|
318
|
-
headers.set("Content-Type", "application/json");
|
|
319
|
+
if (typeof options?.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
319
320
|
headers.set("x-correlation-id", correlationId());
|
|
320
321
|
if (!skipAuth) {
|
|
321
322
|
const token = await this.getToken();
|
|
@@ -347,15 +348,24 @@ var AlfeApiClient = class {
|
|
|
347
348
|
}
|
|
348
349
|
};
|
|
349
350
|
}
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
const text = res.status === 204 ? "" : await res.text();
|
|
352
|
+
let body;
|
|
353
|
+
if (text) try {
|
|
354
|
+
body = JSON.parse(text);
|
|
355
|
+
} catch {
|
|
356
|
+
body = text;
|
|
357
|
+
}
|
|
358
|
+
if (!res.ok) {
|
|
359
|
+
const errBody = body;
|
|
360
|
+
return {
|
|
354
361
|
ok: false,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
362
|
+
result: {
|
|
363
|
+
ok: false,
|
|
364
|
+
error: (typeof errBody === "object" ? errBody.message : void 0) ?? (typeof body === "string" ? body : `API error: ${String(res.status)}`),
|
|
365
|
+
status: res.status
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
359
369
|
return {
|
|
360
370
|
ok: true,
|
|
361
371
|
res,
|
|
@@ -378,6 +388,10 @@ var AlfeApiClient = class {
|
|
|
378
388
|
async request(path, options) {
|
|
379
389
|
const result = await this._fetch(path, options);
|
|
380
390
|
if (!result.ok) return result.result;
|
|
391
|
+
if (result.body === void 0) return {
|
|
392
|
+
ok: true,
|
|
393
|
+
data: void 0
|
|
394
|
+
};
|
|
381
395
|
return {
|
|
382
396
|
ok: true,
|
|
383
397
|
data: result.body.data
|
|
@@ -392,6 +406,10 @@ var AlfeApiClient = class {
|
|
|
392
406
|
async publicRequest(path, options) {
|
|
393
407
|
const result = await this._fetch(path, options, true);
|
|
394
408
|
if (!result.ok) return result.result;
|
|
409
|
+
if (result.body === void 0) return {
|
|
410
|
+
ok: true,
|
|
411
|
+
data: void 0
|
|
412
|
+
};
|
|
395
413
|
return {
|
|
396
414
|
ok: true,
|
|
397
415
|
data: result.body.data
|
|
@@ -422,7 +440,9 @@ var AlfeApiClient = class {
|
|
|
422
440
|
* (both need `readonly [string, ...string[]]`).
|
|
423
441
|
*/
|
|
424
442
|
function enumValues(obj) {
|
|
425
|
-
|
|
443
|
+
const values = Object.values(obj);
|
|
444
|
+
if (values.length === 0) throw new TypeError("enumValues requires at least one value");
|
|
445
|
+
return values;
|
|
426
446
|
}
|
|
427
447
|
Object.freeze({ status: "aborted" });
|
|
428
448
|
function $constructor(name, initializer, params) {
|
|
@@ -4105,6 +4125,12 @@ function refine(fn, _params = {}) {
|
|
|
4105
4125
|
function superRefine(fn) {
|
|
4106
4126
|
return /* @__PURE__ */ _superRefine(fn);
|
|
4107
4127
|
}
|
|
4128
|
+
//#endregion
|
|
4129
|
+
//#region ../../packages-internal/types/dist/lib/usd.js
|
|
4130
|
+
function isWholeUsdCentAmount(amountUsd) {
|
|
4131
|
+
const roundedCents = Math.round(amountUsd * 100);
|
|
4132
|
+
return Math.abs(amountUsd - roundedCents / 100) < 1e-9;
|
|
4133
|
+
}
|
|
4108
4134
|
enumValues({
|
|
4109
4135
|
Active: "active",
|
|
4110
4136
|
PastDue: "past_due",
|
|
@@ -4133,6 +4159,7 @@ enumValues({
|
|
|
4133
4159
|
Month: "month",
|
|
4134
4160
|
Year: "year"
|
|
4135
4161
|
});
|
|
4162
|
+
number().min(0).max(1e6).refine(isWholeUsdCentAmount, "Stripe price amounts cannot contain fractional cents");
|
|
4136
4163
|
object({
|
|
4137
4164
|
gracePeriodDays: number().int().positive().optional(),
|
|
4138
4165
|
launchDiscountEnabled: boolean().optional()
|
|
@@ -4268,6 +4295,7 @@ enumValues({
|
|
|
4268
4295
|
Completed: "completed",
|
|
4269
4296
|
Failed: "failed"
|
|
4270
4297
|
});
|
|
4298
|
+
number().min(.01).max(1e6).refine(isWholeUsdCentAmount, "discountAmountUsd cannot contain fractional cents");
|
|
4271
4299
|
enumValues({
|
|
4272
4300
|
BalanceCredit: "balance_credit",
|
|
4273
4301
|
SubscriptionDiscount: "subscription_discount",
|
|
@@ -4448,7 +4476,7 @@ _enum(XAI_MODELS);
|
|
|
4448
4476
|
_enum(ZHIPU_MODELS);
|
|
4449
4477
|
_enum(MOONSHOT_MODELS);
|
|
4450
4478
|
_enum(QWEN_MODELS);
|
|
4451
|
-
string().min(1);
|
|
4479
|
+
string().trim().min(1);
|
|
4452
4480
|
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
4481
|
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
4482
|
enumValues({
|
|
@@ -4553,7 +4581,7 @@ const RecipientStrategy = {
|
|
|
4553
4581
|
TenantAdmins: "tenant_admins",
|
|
4554
4582
|
SpecificUser: "specific_user"
|
|
4555
4583
|
};
|
|
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.
|
|
4584
|
+
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.TenantAdmins, 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;
|
|
4557
4585
|
//#endregion
|
|
4558
4586
|
//#region ../../packages-internal/api-client/dist/services/auth.js
|
|
4559
4587
|
var AuthService = class {
|
|
@@ -4578,7 +4606,7 @@ var AuthService = class {
|
|
|
4578
4606
|
});
|
|
4579
4607
|
}
|
|
4580
4608
|
deleteToken(tokenId) {
|
|
4581
|
-
return this.client.request(`${this.prefix}/tokens/${tokenId}`, { method: "DELETE" });
|
|
4609
|
+
return this.client.request(`${this.prefix}/tokens/${encodeURIComponent(tokenId)}`, { method: "DELETE" });
|
|
4582
4610
|
}
|
|
4583
4611
|
/**
|
|
4584
4612
|
* Start a device-code flow. Called by the CLI when the user runs
|
|
@@ -4798,6 +4826,12 @@ var IntegrationsService = class {
|
|
|
4798
4826
|
constructor(client) {
|
|
4799
4827
|
this.client = client;
|
|
4800
4828
|
}
|
|
4829
|
+
agentIntegrationsPath(agentId) {
|
|
4830
|
+
return `/integrations/agents/${encodeURIComponent(agentId)}`;
|
|
4831
|
+
}
|
|
4832
|
+
agentIntegrationPath(agentId, integrationId) {
|
|
4833
|
+
return `${this.agentIntegrationsPath(agentId)}/${encodeURIComponent(integrationId)}`;
|
|
4834
|
+
}
|
|
4801
4835
|
listScopedInstalls(scope, scopeId) {
|
|
4802
4836
|
const params = new URLSearchParams({
|
|
4803
4837
|
scope,
|
|
@@ -4846,31 +4880,31 @@ var IntegrationsService = class {
|
|
|
4846
4880
|
}
|
|
4847
4881
|
listIntegrations(agentId, options) {
|
|
4848
4882
|
const params = new URLSearchParams();
|
|
4849
|
-
if (options?.includeInherited) params.set("includeInherited",
|
|
4850
|
-
if (options?.effective) params.set("effective",
|
|
4883
|
+
if (options?.includeInherited !== void 0) params.set("includeInherited", String(options.includeInherited));
|
|
4884
|
+
if (options?.effective !== void 0) params.set("effective", String(options.effective));
|
|
4851
4885
|
const qs = params.toString();
|
|
4852
|
-
return this.client.request(
|
|
4886
|
+
return this.client.request(`${this.agentIntegrationsPath(agentId)}${qs ? `?${qs}` : ""}`);
|
|
4853
4887
|
}
|
|
4854
4888
|
installIntegration(agentId, data) {
|
|
4855
|
-
return this.client.request(
|
|
4889
|
+
return this.client.request(this.agentIntegrationsPath(agentId), {
|
|
4856
4890
|
method: "POST",
|
|
4857
4891
|
body: JSON.stringify(data)
|
|
4858
4892
|
});
|
|
4859
4893
|
}
|
|
4860
4894
|
getIntegrationConfig(agentId, integrationId) {
|
|
4861
|
-
return this.client.request(
|
|
4895
|
+
return this.client.request(`${this.agentIntegrationPath(agentId, integrationId)}/config`);
|
|
4862
4896
|
}
|
|
4863
4897
|
updateIntegration(agentId, integrationId, data) {
|
|
4864
|
-
return this.client.request(
|
|
4898
|
+
return this.client.request(this.agentIntegrationPath(agentId, integrationId), {
|
|
4865
4899
|
method: "PATCH",
|
|
4866
4900
|
body: JSON.stringify(data)
|
|
4867
4901
|
});
|
|
4868
4902
|
}
|
|
4869
4903
|
removeIntegration(agentId, integrationId) {
|
|
4870
|
-
return this.client.request(
|
|
4904
|
+
return this.client.request(this.agentIntegrationPath(agentId, integrationId), { method: "DELETE" });
|
|
4871
4905
|
}
|
|
4872
4906
|
reinstallIntegration(agentId, integrationId) {
|
|
4873
|
-
return this.client.request(
|
|
4907
|
+
return this.client.request(`${this.agentIntegrationPath(agentId, integrationId)}/reinstall`, { method: "POST" });
|
|
4874
4908
|
}
|
|
4875
4909
|
/**
|
|
4876
4910
|
* Upgrade an integration to the registry's latest version via the fast,
|
|
@@ -4879,7 +4913,7 @@ var IntegrationsService = class {
|
|
|
4879
4913
|
* {@link reinstallIntegration}, which is the destructive repair path.
|
|
4880
4914
|
*/
|
|
4881
4915
|
upgradeIntegration(agentId, integrationId) {
|
|
4882
|
-
return this.client.request(
|
|
4916
|
+
return this.client.request(`${this.agentIntegrationPath(agentId, integrationId)}/upgrade`, { method: "POST" });
|
|
4883
4917
|
}
|
|
4884
4918
|
getRegistry() {
|
|
4885
4919
|
return this.client.request("/integrations/registry");
|
|
@@ -4890,17 +4924,16 @@ var IntegrationsService = class {
|
|
|
4890
4924
|
body: JSON.stringify({ agentId })
|
|
4891
4925
|
});
|
|
4892
4926
|
}
|
|
4893
|
-
getDiscordGuildChannels(guildId) {
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
listMobileNumbers() {
|
|
4897
|
-
return this.client.request("/mobile/numbers");
|
|
4927
|
+
getDiscordGuildChannels(agentId, guildId) {
|
|
4928
|
+
const query = new URLSearchParams({ agentId });
|
|
4929
|
+
return this.client.request(`/discord/guilds/${encodeURIComponent(guildId)}/channels?${query}`);
|
|
4898
4930
|
}
|
|
4899
4931
|
searchMobileNumbers(country, query) {
|
|
4900
4932
|
const params = new URLSearchParams();
|
|
4901
|
-
if (country) params.set("country", country);
|
|
4902
|
-
if (query) params.set("query", query);
|
|
4903
|
-
|
|
4933
|
+
if (country !== void 0) params.set("country", country);
|
|
4934
|
+
if (query !== void 0) params.set("query", query);
|
|
4935
|
+
const qs = params.toString();
|
|
4936
|
+
return this.client.request(`/mobile/numbers/search${qs ? `?${qs}` : ""}`);
|
|
4904
4937
|
}
|
|
4905
4938
|
assignMobileNumber(agentId, phoneNumber, countryCode) {
|
|
4906
4939
|
return this.client.request("/mobile/numbers/assign", {
|
|
@@ -4921,10 +4954,6 @@ var IntegrationsService = class {
|
|
|
4921
4954
|
getMobileNumber(agentId) {
|
|
4922
4955
|
return this.client.request(`/mobile/numbers?agentId=${encodeURIComponent(agentId)}`);
|
|
4923
4956
|
}
|
|
4924
|
-
disconnectGoogle(agentId, email) {
|
|
4925
|
-
const query = email ? `?email=${encodeURIComponent(email)}` : "";
|
|
4926
|
-
return this.client.request(`/google/agents/${encodeURIComponent(agentId)}/account${query}`, { method: "DELETE" });
|
|
4927
|
-
}
|
|
4928
4957
|
getAtlassianSites(agentId) {
|
|
4929
4958
|
return this.client.request(`/atlassian/agents/${encodeURIComponent(agentId)}/sites`);
|
|
4930
4959
|
}
|
|
@@ -4966,10 +4995,7 @@ const PID_PATH = join(ALFE_DIR, "gateway.pid");
|
|
|
4966
4995
|
* Returns agentId (derived from tokenId) and orgId (tenantId).
|
|
4967
4996
|
*/
|
|
4968
4997
|
async function resolveAgentIdentity(apiKey, apiEndpoint) {
|
|
4969
|
-
logger$1.debug({
|
|
4970
|
-
apiEndpoint,
|
|
4971
|
-
keyPrefix: apiKey.slice(0, 8) + "..."
|
|
4972
|
-
}, "Resolving agent identity...");
|
|
4998
|
+
logger$1.debug({ apiEndpoint }, "Resolving agent identity...");
|
|
4973
4999
|
const auth = new AuthService(new AlfeApiClient({
|
|
4974
5000
|
apiBaseUrl: apiEndpoint,
|
|
4975
5001
|
getToken: () => Promise.resolve(apiKey)
|
|
@@ -5065,21 +5091,32 @@ function deriveAgentWorkspace(runtime, home) {
|
|
|
5065
5091
|
if (runtime === "claude-code") return join(home, "workspace");
|
|
5066
5092
|
return home;
|
|
5067
5093
|
}
|
|
5094
|
+
const UNSAFE_RECORD_KEYS = new Set([
|
|
5095
|
+
"__proto__",
|
|
5096
|
+
"constructor",
|
|
5097
|
+
"prototype"
|
|
5098
|
+
]);
|
|
5099
|
+
function isRecord$1(value) {
|
|
5100
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5101
|
+
}
|
|
5102
|
+
function parseRuntimeConfigs(parsed) {
|
|
5103
|
+
if (!isRecord$1(parsed) || !isRecord$1(parsed.runtimes)) return {};
|
|
5104
|
+
const result = {};
|
|
5105
|
+
for (const [name, candidate] of Object.entries(parsed.runtimes)) {
|
|
5106
|
+
if (name.length === 0 || name.length > 128 || UNSAFE_RECORD_KEYS.has(name) || !isRecord$1(candidate) || typeof candidate.workspace !== "string" || candidate.workspace.length === 0 || candidate.workspace.length > 4096) continue;
|
|
5107
|
+
const workspace = candidate.workspace.startsWith("~/") ? join(homedir(), candidate.workspace.slice(2)) : candidate.workspace;
|
|
5108
|
+
result[name] = {
|
|
5109
|
+
workspace,
|
|
5110
|
+
agentWorkspace: deriveAgentWorkspace(name, workspace)
|
|
5111
|
+
};
|
|
5112
|
+
}
|
|
5113
|
+
return result;
|
|
5114
|
+
}
|
|
5068
5115
|
async function loadRuntimeConfigs() {
|
|
5069
5116
|
const configPath = join(ALFE_DIR, "config.toml");
|
|
5070
5117
|
if (!existsSync(configPath)) return {};
|
|
5071
5118
|
try {
|
|
5072
|
-
|
|
5073
|
-
if (!runtimes) return {};
|
|
5074
|
-
const result = {};
|
|
5075
|
-
for (const [name, cfg] of Object.entries(runtimes)) if (typeof cfg.workspace === "string") {
|
|
5076
|
-
const workspace = cfg.workspace.startsWith("~/") ? join(homedir(), cfg.workspace.slice(2)) : cfg.workspace;
|
|
5077
|
-
result[name] = {
|
|
5078
|
-
workspace,
|
|
5079
|
-
agentWorkspace: deriveAgentWorkspace(name, workspace)
|
|
5080
|
-
};
|
|
5081
|
-
}
|
|
5082
|
-
return result;
|
|
5119
|
+
return parseRuntimeConfigs(parse(await readFile(configPath, "utf-8")));
|
|
5083
5120
|
} catch {
|
|
5084
5121
|
return {};
|
|
5085
5122
|
}
|
|
@@ -5204,66 +5241,59 @@ async function loadDaemonConfig() {
|
|
|
5204
5241
|
* Returns null if the agent has no template assigned or the fetch fails.
|
|
5205
5242
|
*/
|
|
5206
5243
|
async function fetchAgentConfig(apiKey, apiEndpoint) {
|
|
5244
|
+
const client = new AgentApiClient({
|
|
5245
|
+
apiKey,
|
|
5246
|
+
apiUrl: apiEndpoint
|
|
5247
|
+
});
|
|
5248
|
+
logger$1.debug({ apiEndpoint }, "Fetching agent workspace config...");
|
|
5249
|
+
let workspace;
|
|
5207
5250
|
try {
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
logger$1.debug({
|
|
5232
|
-
templateKey,
|
|
5233
|
-
version: pinnedVersion
|
|
5234
|
-
}, "Fetching template files...");
|
|
5235
|
-
const filesResponse = await fetch(filesUrl.toString(), {
|
|
5236
|
-
method: "GET",
|
|
5237
|
-
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
5238
|
-
});
|
|
5239
|
-
if (!filesResponse.ok) {
|
|
5240
|
-
logger$1.debug({ status: filesResponse.status }, "Template files fetch failed");
|
|
5241
|
-
return {
|
|
5242
|
-
templateKey,
|
|
5243
|
-
defaultModel,
|
|
5244
|
-
files: {}
|
|
5245
|
-
};
|
|
5246
|
-
}
|
|
5247
|
-
const files = (await filesResponse.json()).data?.files ?? {};
|
|
5248
|
-
const fileCount = Object.keys(files).length;
|
|
5249
|
-
logger$1.debug({
|
|
5250
|
-
templateKey,
|
|
5251
|
-
version: pinnedVersion,
|
|
5252
|
-
fileCount
|
|
5253
|
-
}, "Template files fetched");
|
|
5251
|
+
workspace = await client.getWorkspace();
|
|
5252
|
+
} catch (err) {
|
|
5253
|
+
logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Workspace config fetch failed");
|
|
5254
|
+
return null;
|
|
5255
|
+
}
|
|
5256
|
+
const { templateKey, installedFrom } = workspace;
|
|
5257
|
+
const defaultModel = workspace.defaultModel ?? void 0;
|
|
5258
|
+
if (!templateKey) {
|
|
5259
|
+
logger$1.debug("No templateKey in workspace response");
|
|
5260
|
+
return defaultModel ? {
|
|
5261
|
+
defaultModel,
|
|
5262
|
+
files: {}
|
|
5263
|
+
} : null;
|
|
5264
|
+
}
|
|
5265
|
+
const pinnedVersion = installedFrom?.templateKey === templateKey ? installedFrom.version : void 0;
|
|
5266
|
+
logger$1.debug({
|
|
5267
|
+
templateKey,
|
|
5268
|
+
version: pinnedVersion
|
|
5269
|
+
}, "Fetching template files...");
|
|
5270
|
+
let files;
|
|
5271
|
+
try {
|
|
5272
|
+
files = (await client.getTemplateFiles(templateKey, { version: pinnedVersion })).files;
|
|
5273
|
+
} catch (err) {
|
|
5274
|
+
logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "Template files fetch failed");
|
|
5254
5275
|
return {
|
|
5255
5276
|
templateKey,
|
|
5256
5277
|
defaultModel,
|
|
5257
|
-
files
|
|
5258
|
-
personaFiles: Object.keys(files)
|
|
5278
|
+
files: {}
|
|
5259
5279
|
};
|
|
5260
|
-
} catch (err) {
|
|
5261
|
-
logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "fetchAgentConfig failed");
|
|
5262
|
-
return null;
|
|
5263
5280
|
}
|
|
5281
|
+
logger$1.debug({
|
|
5282
|
+
templateKey,
|
|
5283
|
+
version: pinnedVersion,
|
|
5284
|
+
fileCount: Object.keys(files).length
|
|
5285
|
+
}, "Template files fetched");
|
|
5286
|
+
return {
|
|
5287
|
+
templateKey,
|
|
5288
|
+
defaultModel,
|
|
5289
|
+
files,
|
|
5290
|
+
personaFiles: Object.keys(files)
|
|
5291
|
+
};
|
|
5264
5292
|
}
|
|
5265
5293
|
//#endregion
|
|
5266
5294
|
//#region src/protocol.ts
|
|
5295
|
+
/** Maximum size of one newline-delimited local IPC message. */
|
|
5296
|
+
const MAX_IPC_MESSAGE_BYTES = 1024 * 1024;
|
|
5267
5297
|
/**
|
|
5268
5298
|
* Map a cloud command name to an IPC method name.
|
|
5269
5299
|
* Cloud commands use dot-notation matching IPC methods.
|
|
@@ -5362,23 +5392,56 @@ function parseMessage(raw) {
|
|
|
5362
5392
|
return null;
|
|
5363
5393
|
}
|
|
5364
5394
|
}
|
|
5395
|
+
const MAX_DESIRED_INTEGRATIONS = 4096;
|
|
5396
|
+
const MAX_CONFIG_VALUES = 512;
|
|
5397
|
+
function isRecord(value) {
|
|
5398
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5399
|
+
}
|
|
5400
|
+
function isNonEmptyString(value) {
|
|
5401
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
5402
|
+
}
|
|
5403
|
+
function isDesiredCustomSource(value) {
|
|
5404
|
+
if (!isRecord(value)) return false;
|
|
5405
|
+
return isNonEmptyString(value.connectionId) && isNonEmptyString(value.manifestRepo) && isNonEmptyString(value.manifestPath) && isNonEmptyString(value.manifestRef) && isNonEmptyString(value.manifestSha) && isNonEmptyString(value.manifestCommit) && isRecord(value.manifest);
|
|
5406
|
+
}
|
|
5407
|
+
function isDesiredConfig(value) {
|
|
5408
|
+
if (!isRecord(value)) return false;
|
|
5409
|
+
if (!Number.isSafeInteger(value.version) || value.version < 0) return false;
|
|
5410
|
+
if (!isRecord(value.values)) return false;
|
|
5411
|
+
const entries = Object.entries(value.values);
|
|
5412
|
+
return entries.length <= MAX_CONFIG_VALUES && entries.every(([key, configValue]) => key.trim().length > 0 && (typeof configValue === "string" || configValue === null));
|
|
5413
|
+
}
|
|
5414
|
+
function isDesiredIntegration(value) {
|
|
5415
|
+
if (!isRecord(value)) return false;
|
|
5416
|
+
return isNonEmptyString(value.integrationId) && typeof value.version === "string" && (value.desiredStatus === "active" || value.desiredStatus === "removed") && (value.reinstallRequestedAt === void 0 || typeof value.reinstallRequestedAt === "string") && (value.customSource === void 0 || isDesiredCustomSource(value.customSource));
|
|
5417
|
+
}
|
|
5365
5418
|
/**
|
|
5366
5419
|
* Type guard: is this a cloud COMMAND message?
|
|
5367
5420
|
*/
|
|
5368
5421
|
function isCloudCommand(msg) {
|
|
5369
|
-
|
|
5422
|
+
if (!isRecord(msg)) return false;
|
|
5423
|
+
return msg.type === "COMMAND" && isNonEmptyString(msg.commandId) && isNonEmptyString(msg.agentId) && isNonEmptyString(msg.command);
|
|
5370
5424
|
}
|
|
5371
5425
|
/**
|
|
5372
5426
|
* Type guard: is this a cloud SERVICE_ACK message?
|
|
5373
5427
|
*/
|
|
5374
5428
|
function isCloudServiceAck(msg) {
|
|
5375
|
-
return
|
|
5429
|
+
return isRecord(msg) && msg.type === "SERVICE_ACK" && (msg.status === "ok" || msg.status === "error");
|
|
5376
5430
|
}
|
|
5377
5431
|
/**
|
|
5378
5432
|
* Type guard: is this a cloud DESIRED_STATE message?
|
|
5379
5433
|
*/
|
|
5380
5434
|
function isCloudDesiredState(msg) {
|
|
5381
|
-
|
|
5435
|
+
if (!isRecord(msg) || msg.type !== "DESIRED_STATE" || !Array.isArray(msg.integrations)) return false;
|
|
5436
|
+
if (msg.integrations.length > MAX_DESIRED_INTEGRATIONS) return false;
|
|
5437
|
+
if (msg.config !== void 0 && !isDesiredConfig(msg.config)) return false;
|
|
5438
|
+
const integrationIds = /* @__PURE__ */ new Set();
|
|
5439
|
+
for (const integration of msg.integrations) {
|
|
5440
|
+
if (!isDesiredIntegration(integration)) return false;
|
|
5441
|
+
if (integrationIds.has(integration.integrationId)) return false;
|
|
5442
|
+
integrationIds.add(integration.integrationId);
|
|
5443
|
+
}
|
|
5444
|
+
return true;
|
|
5382
5445
|
}
|
|
5383
5446
|
/**
|
|
5384
5447
|
* Create a RECONCILIATION_REPORT message.
|
|
@@ -5411,7 +5474,7 @@ function isCloudPing(msg) {
|
|
|
5411
5474
|
* Type guard: is this an IPC request?
|
|
5412
5475
|
*/
|
|
5413
5476
|
function isIPCRequest(msg) {
|
|
5414
|
-
return
|
|
5477
|
+
return isRecord(msg) && msg.type === "req" && isNonEmptyString(msg.id) && isNonEmptyString(msg.method) && isRecord(msg.params);
|
|
5415
5478
|
}
|
|
5416
5479
|
/**
|
|
5417
5480
|
* Type guard: is this an IPC response?
|
|
@@ -5567,7 +5630,20 @@ var ReconciliationEngine = class {
|
|
|
5567
5630
|
localIntegrations = await this.manager.getInstalledIntegrations();
|
|
5568
5631
|
} catch (err) {
|
|
5569
5632
|
log$5.error({ err }, "Failed to get local integrations");
|
|
5570
|
-
|
|
5633
|
+
captureIntegrationFailure("_reconciliation", "read_local_state", err);
|
|
5634
|
+
const errorMessage = "Failed to read local integration state; reconciliation skipped";
|
|
5635
|
+
const affectedIds = desiredIntegrations.length > 0 ? desiredIntegrations.map((desired) => desired.integrationId) : ["*"];
|
|
5636
|
+
report.errors.push(...affectedIds.map((integrationId) => ({
|
|
5637
|
+
integrationId,
|
|
5638
|
+
error: errorMessage
|
|
5639
|
+
})));
|
|
5640
|
+
report.results.push(...desiredIntegrations.map((desired) => ({
|
|
5641
|
+
integrationId: desired.integrationId,
|
|
5642
|
+
action: "error",
|
|
5643
|
+
actualStatus: "unknown",
|
|
5644
|
+
errorMessage
|
|
5645
|
+
})));
|
|
5646
|
+
return report;
|
|
5571
5647
|
}
|
|
5572
5648
|
const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
|
|
5573
5649
|
const desiredMap = new Map(desiredIntegrations.map((d) => [d.integrationId, d]));
|
|
@@ -5922,6 +5998,7 @@ var CloudClient = class {
|
|
|
5922
5998
|
closed = false;
|
|
5923
5999
|
registered = false;
|
|
5924
6000
|
pingTimer = null;
|
|
6001
|
+
reconnectTimer = null;
|
|
5925
6002
|
lastPong = 0;
|
|
5926
6003
|
config;
|
|
5927
6004
|
onCommand = null;
|
|
@@ -5994,6 +6071,7 @@ var CloudClient = class {
|
|
|
5994
6071
|
agentId: this.config.agentId
|
|
5995
6072
|
}, "Cloud client starting...");
|
|
5996
6073
|
this.closed = false;
|
|
6074
|
+
if (this.ws || this.reconnectTimer) return;
|
|
5997
6075
|
this.doConnect();
|
|
5998
6076
|
}
|
|
5999
6077
|
/**
|
|
@@ -6003,12 +6081,14 @@ var CloudClient = class {
|
|
|
6003
6081
|
logger$1.debug("Cloud client stopping...");
|
|
6004
6082
|
this.closed = true;
|
|
6005
6083
|
this.stopPingTimer();
|
|
6084
|
+
this.stopReconnectTimer();
|
|
6006
6085
|
if (this.ws) {
|
|
6007
|
-
|
|
6086
|
+
const ws = this.ws;
|
|
6087
|
+
this.ws = null;
|
|
6088
|
+
logger$1.debug({ readyState: ws.readyState }, "Cloud: closing WebSocket");
|
|
6008
6089
|
try {
|
|
6009
|
-
|
|
6090
|
+
ws.close(1e3, "Daemon shutting down");
|
|
6010
6091
|
} catch {}
|
|
6011
|
-
this.ws = null;
|
|
6012
6092
|
}
|
|
6013
6093
|
this.registered = false;
|
|
6014
6094
|
this.reconciling = false;
|
|
@@ -6043,35 +6123,45 @@ var CloudClient = class {
|
|
|
6043
6123
|
logger$1.debug("Cloud: doConnect skipped — client is closed");
|
|
6044
6124
|
return;
|
|
6045
6125
|
}
|
|
6126
|
+
if (this.ws) {
|
|
6127
|
+
logger$1.debug("Cloud: doConnect skipped — connection already exists");
|
|
6128
|
+
return;
|
|
6129
|
+
}
|
|
6046
6130
|
logger$1.info({
|
|
6047
6131
|
url: this.config.wsUrl,
|
|
6048
6132
|
backoffMs: this.backoffMs
|
|
6049
6133
|
}, "Connecting to cloud gateway...");
|
|
6050
|
-
logger$1.debug({
|
|
6051
|
-
|
|
6052
|
-
keyPrefix: this.config.apiKey.slice(0, 12) + "..."
|
|
6053
|
-
}, "Cloud: connection details");
|
|
6054
|
-
this.ws = new WebSocket(this.config.wsUrl, {
|
|
6134
|
+
logger$1.debug({ agentId: this.config.agentId }, "Cloud: connection details");
|
|
6135
|
+
const ws = new WebSocket(this.config.wsUrl, {
|
|
6055
6136
|
headers: { authorization: `Bearer ${this.config.apiKey}` },
|
|
6056
6137
|
maxPayload: 10 * 1024 * 1024,
|
|
6057
6138
|
handshakeTimeout: 1e4
|
|
6058
6139
|
});
|
|
6059
|
-
this.ws
|
|
6140
|
+
this.ws = ws;
|
|
6141
|
+
ws.on("open", () => {
|
|
6142
|
+
if (this.ws !== ws) return;
|
|
6060
6143
|
logger$1.info("Cloud WebSocket connected");
|
|
6061
|
-
logger$1.debug({ readyState:
|
|
6144
|
+
logger$1.debug({ readyState: ws.readyState }, "Cloud: WebSocket open, sending registration...");
|
|
6062
6145
|
this.backoffMs = 1e3;
|
|
6063
6146
|
this.sendRegister();
|
|
6064
6147
|
});
|
|
6065
|
-
|
|
6148
|
+
ws.on("message", (data) => {
|
|
6149
|
+
if (this.ws !== ws) return;
|
|
6066
6150
|
const text = Buffer.isBuffer(data) ? data.toString("utf-8") : Buffer.from(data).toString("utf-8");
|
|
6067
6151
|
logger$1.debug({ size: text.length }, "Cloud: received message");
|
|
6068
6152
|
this.handleMessage(text);
|
|
6069
6153
|
});
|
|
6070
|
-
|
|
6154
|
+
ws.on("ping", () => {
|
|
6155
|
+
if (this.ws !== ws) return;
|
|
6071
6156
|
logger$1.debug("Cloud: received ping, sending pong");
|
|
6072
|
-
|
|
6157
|
+
ws.pong();
|
|
6073
6158
|
});
|
|
6074
|
-
|
|
6159
|
+
ws.on("close", (code, reason) => {
|
|
6160
|
+
if (this.ws !== ws) {
|
|
6161
|
+
logger$1.debug("Cloud: ignoring close from superseded WebSocket");
|
|
6162
|
+
return;
|
|
6163
|
+
}
|
|
6164
|
+
this.ws = null;
|
|
6075
6165
|
logger$1.warn({
|
|
6076
6166
|
code,
|
|
6077
6167
|
reason: reason.toString()
|
|
@@ -6081,7 +6171,8 @@ var CloudClient = class {
|
|
|
6081
6171
|
this.onConnectionChange?.(false);
|
|
6082
6172
|
this.scheduleReconnect();
|
|
6083
6173
|
});
|
|
6084
|
-
|
|
6174
|
+
ws.on("error", (err) => {
|
|
6175
|
+
if (this.ws !== ws) return;
|
|
6085
6176
|
logger$1.error({
|
|
6086
6177
|
err: err.message,
|
|
6087
6178
|
url: this.config.wsUrl
|
|
@@ -6128,10 +6219,16 @@ var CloudClient = class {
|
|
|
6128
6219
|
if (ack.status === "ok") {
|
|
6129
6220
|
logger$1.info("Cloud: registered successfully ✅");
|
|
6130
6221
|
this.registered = true;
|
|
6222
|
+
this.lastPong = Date.now();
|
|
6131
6223
|
this.startPingTimer();
|
|
6132
6224
|
this.onConnectionChange?.(true);
|
|
6225
|
+
} else if (ack.retryable === false) {
|
|
6226
|
+
logger$1.error({ message: ack.message }, "Cloud: registration rejected — not retrying");
|
|
6227
|
+
this.closed = true;
|
|
6228
|
+
this.stopReconnectTimer();
|
|
6229
|
+
this.ws?.close(1008, "Registration rejected");
|
|
6133
6230
|
} else {
|
|
6134
|
-
logger$1.error({ message: ack.message }, "Cloud: registration failed");
|
|
6231
|
+
logger$1.error({ message: ack.message }, "Cloud: registration failed — will retry");
|
|
6135
6232
|
this.ws?.close(1008, "Registration rejected");
|
|
6136
6233
|
}
|
|
6137
6234
|
}
|
|
@@ -6261,16 +6358,27 @@ var CloudClient = class {
|
|
|
6261
6358
|
logger$1.debug("Cloud: reconnect skipped — client is closed");
|
|
6262
6359
|
return;
|
|
6263
6360
|
}
|
|
6361
|
+
if (this.reconnectTimer) {
|
|
6362
|
+
logger$1.debug("Cloud: reconnect already scheduled");
|
|
6363
|
+
return;
|
|
6364
|
+
}
|
|
6264
6365
|
const delay = this.backoffMs;
|
|
6265
6366
|
this.backoffMs = Math.min(this.backoffMs * 2, 3e4);
|
|
6266
6367
|
logger$1.info({
|
|
6267
6368
|
delayMs: delay,
|
|
6268
6369
|
nextBackoffMs: this.backoffMs
|
|
6269
6370
|
}, "Cloud: scheduling reconnect...");
|
|
6270
|
-
setTimeout(() => {
|
|
6371
|
+
this.reconnectTimer = setTimeout(() => {
|
|
6372
|
+
this.reconnectTimer = null;
|
|
6271
6373
|
this.doConnect();
|
|
6272
6374
|
}, delay);
|
|
6273
6375
|
}
|
|
6376
|
+
stopReconnectTimer() {
|
|
6377
|
+
if (this.reconnectTimer) {
|
|
6378
|
+
clearTimeout(this.reconnectTimer);
|
|
6379
|
+
this.reconnectTimer = null;
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6274
6382
|
};
|
|
6275
6383
|
//#endregion
|
|
6276
6384
|
//#region src/config-reconciler.ts
|
|
@@ -6360,406 +6468,70 @@ var ConfigReconciler = class {
|
|
|
6360
6468
|
}
|
|
6361
6469
|
};
|
|
6362
6470
|
//#endregion
|
|
6363
|
-
//#region src/
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
var IPCServer = class {
|
|
6376
|
-
server = null;
|
|
6377
|
-
connections = /* @__PURE__ */ new Map();
|
|
6378
|
-
socketPath;
|
|
6379
|
-
requestHandler = null;
|
|
6380
|
-
constructor(socketPath) {
|
|
6381
|
-
this.socketPath = socketPath;
|
|
6471
|
+
//#region src/command-queue.ts
|
|
6472
|
+
const DEFAULT_TTL_MS$1 = 300 * 1e3;
|
|
6473
|
+
const DEFAULT_MAX_PER_SERVICE = 100;
|
|
6474
|
+
const DEFAULT_MAX_TOTAL = 1e3;
|
|
6475
|
+
var CommandQueue = class {
|
|
6476
|
+
queues = /* @__PURE__ */ new Map();
|
|
6477
|
+
ttlMs;
|
|
6478
|
+
gcTimer = null;
|
|
6479
|
+
constructor(ttlMs = DEFAULT_TTL_MS$1, maxPerService = DEFAULT_MAX_PER_SERVICE, maxTotal = DEFAULT_MAX_TOTAL) {
|
|
6480
|
+
this.maxPerService = maxPerService;
|
|
6481
|
+
this.maxTotal = maxTotal;
|
|
6482
|
+
this.ttlMs = ttlMs;
|
|
6382
6483
|
}
|
|
6383
6484
|
/**
|
|
6384
|
-
*
|
|
6485
|
+
* Start periodic garbage collection of expired commands.
|
|
6385
6486
|
*/
|
|
6386
|
-
|
|
6387
|
-
this.
|
|
6487
|
+
startGC(intervalMs = 3e4) {
|
|
6488
|
+
this.stopGC();
|
|
6489
|
+
this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
|
|
6388
6490
|
}
|
|
6389
6491
|
/**
|
|
6390
|
-
*
|
|
6492
|
+
* Stop periodic garbage collection.
|
|
6391
6493
|
*/
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
this.server = createServer((socket) => {
|
|
6398
|
-
this.handleConnection(socket);
|
|
6399
|
-
});
|
|
6400
|
-
this.server.on("error", (err) => {
|
|
6401
|
-
logger$1.error({ err: err.message }, "IPC server error");
|
|
6402
|
-
reject(err);
|
|
6403
|
-
});
|
|
6404
|
-
this.server.listen(this.socketPath, () => {
|
|
6405
|
-
try {
|
|
6406
|
-
chmodSync(this.socketPath, 384);
|
|
6407
|
-
} catch (err) {
|
|
6408
|
-
logger$1.warn({ err }, "Failed to chmod socket");
|
|
6409
|
-
}
|
|
6410
|
-
logger$1.info({ path: this.socketPath }, "IPC server listening");
|
|
6411
|
-
resolve();
|
|
6412
|
-
});
|
|
6413
|
-
});
|
|
6494
|
+
stopGC() {
|
|
6495
|
+
if (this.gcTimer) {
|
|
6496
|
+
clearInterval(this.gcTimer);
|
|
6497
|
+
this.gcTimer = null;
|
|
6498
|
+
}
|
|
6414
6499
|
}
|
|
6415
6500
|
/**
|
|
6416
|
-
*
|
|
6501
|
+
* Enqueue a command for a specific service.
|
|
6417
6502
|
*/
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
resolve();
|
|
6434
|
-
});
|
|
6435
|
-
for (const conn of this.connections.values()) conn.socket.destroy();
|
|
6436
|
-
this.connections.clear();
|
|
6503
|
+
enqueue(serviceId, request, commandId) {
|
|
6504
|
+
this.purgeExpired();
|
|
6505
|
+
let queue = this.queues.get(serviceId);
|
|
6506
|
+
if (!queue) {
|
|
6507
|
+
queue = [];
|
|
6508
|
+
this.queues.set(serviceId, queue);
|
|
6509
|
+
}
|
|
6510
|
+
if (queue.length >= this.maxPerService || this.totalPending() >= this.maxTotal) {
|
|
6511
|
+
if (queue.length === 0) this.queues.delete(serviceId);
|
|
6512
|
+
return false;
|
|
6513
|
+
}
|
|
6514
|
+
queue.push({
|
|
6515
|
+
request,
|
|
6516
|
+
queuedAt: Date.now(),
|
|
6517
|
+
commandId
|
|
6437
6518
|
});
|
|
6519
|
+
return true;
|
|
6438
6520
|
}
|
|
6439
6521
|
/**
|
|
6440
|
-
*
|
|
6522
|
+
* Drain all pending (non-expired) commands for a service.
|
|
6523
|
+
* Returns them in order and removes them from the queue.
|
|
6441
6524
|
*/
|
|
6442
|
-
|
|
6443
|
-
const
|
|
6444
|
-
if (!
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
message: `Plugin ${pluginId} not connected`
|
|
6450
|
-
}
|
|
6451
|
-
};
|
|
6452
|
-
const id = pluginConnectionId();
|
|
6453
|
-
const request = {
|
|
6454
|
-
type: "req",
|
|
6455
|
-
id,
|
|
6456
|
-
method,
|
|
6457
|
-
params
|
|
6458
|
-
};
|
|
6459
|
-
return new Promise((resolve, reject) => {
|
|
6460
|
-
const timer = setTimeout(() => {
|
|
6461
|
-
conn.pending.delete(id);
|
|
6462
|
-
resolve({
|
|
6463
|
-
id,
|
|
6464
|
-
ok: false,
|
|
6465
|
-
error: {
|
|
6466
|
-
code: "TIMEOUT",
|
|
6467
|
-
message: `Request ${method} timed out after ${String(timeoutMs)}ms`
|
|
6468
|
-
}
|
|
6469
|
-
});
|
|
6470
|
-
}, timeoutMs);
|
|
6471
|
-
conn.pending.set(id, {
|
|
6472
|
-
resolve,
|
|
6473
|
-
reject,
|
|
6474
|
-
timer
|
|
6475
|
-
});
|
|
6476
|
-
try {
|
|
6477
|
-
conn.socket.write(JSON.stringify(request) + "\n");
|
|
6478
|
-
} catch (err) {
|
|
6479
|
-
clearTimeout(timer);
|
|
6480
|
-
conn.pending.delete(id);
|
|
6481
|
-
resolve({
|
|
6482
|
-
id,
|
|
6483
|
-
ok: false,
|
|
6484
|
-
error: {
|
|
6485
|
-
code: "SEND_FAILED",
|
|
6486
|
-
message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
|
|
6487
|
-
}
|
|
6488
|
-
});
|
|
6489
|
-
}
|
|
6490
|
-
});
|
|
6525
|
+
drain(serviceId) {
|
|
6526
|
+
const queue = this.queues.get(serviceId);
|
|
6527
|
+
if (!queue || queue.length === 0) return [];
|
|
6528
|
+
const now = Date.now();
|
|
6529
|
+
const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
|
|
6530
|
+
this.queues.delete(serviceId);
|
|
6531
|
+
return valid;
|
|
6491
6532
|
}
|
|
6492
6533
|
/**
|
|
6493
|
-
*
|
|
6494
|
-
* Returns responses keyed by plugin ID.
|
|
6495
|
-
*/
|
|
6496
|
-
async broadcastRequest(method, params, timeoutMs = 3e4) {
|
|
6497
|
-
const results = /* @__PURE__ */ new Map();
|
|
6498
|
-
const registered = this.getRegisteredPlugins();
|
|
6499
|
-
await Promise.all(registered.map(async ([pluginId]) => {
|
|
6500
|
-
const response = await this.sendRequest(pluginId, method, params, timeoutMs);
|
|
6501
|
-
results.set(pluginId, response);
|
|
6502
|
-
}));
|
|
6503
|
-
return results;
|
|
6504
|
-
}
|
|
6505
|
-
/**
|
|
6506
|
-
* Send an event to a specific plugin (fire-and-forget).
|
|
6507
|
-
*/
|
|
6508
|
-
sendEventToPlugin(pluginId, event, payload) {
|
|
6509
|
-
const conn = this.connections.get(pluginId);
|
|
6510
|
-
if (!conn) return false;
|
|
6511
|
-
return this.sendEvent(conn, event, payload);
|
|
6512
|
-
}
|
|
6513
|
-
/**
|
|
6514
|
-
* Broadcast an event to all connected plugins.
|
|
6515
|
-
*/
|
|
6516
|
-
broadcastEvent(event, payload) {
|
|
6517
|
-
for (const conn of this.connections.values()) if (conn.info) this.sendEvent(conn, event, payload);
|
|
6518
|
-
}
|
|
6519
|
-
/**
|
|
6520
|
-
* Get all registered plugins and their info.
|
|
6521
|
-
*/
|
|
6522
|
-
getRegisteredPlugins() {
|
|
6523
|
-
const plugins = [];
|
|
6524
|
-
for (const [id, conn] of this.connections) if (conn.info) plugins.push([id, conn.info]);
|
|
6525
|
-
return plugins;
|
|
6526
|
-
}
|
|
6527
|
-
/**
|
|
6528
|
-
* Get number of connected plugins (including unregistered).
|
|
6529
|
-
*/
|
|
6530
|
-
get connectionCount() {
|
|
6531
|
-
return this.connections.size;
|
|
6532
|
-
}
|
|
6533
|
-
/**
|
|
6534
|
-
* Get number of registered plugins.
|
|
6535
|
-
*/
|
|
6536
|
-
get registeredCount() {
|
|
6537
|
-
let count = 0;
|
|
6538
|
-
for (const conn of this.connections.values()) if (conn.info) count++;
|
|
6539
|
-
return count;
|
|
6540
|
-
}
|
|
6541
|
-
handleConnection(socket) {
|
|
6542
|
-
const connId = pluginConnectionId();
|
|
6543
|
-
const conn = {
|
|
6544
|
-
id: connId,
|
|
6545
|
-
socket,
|
|
6546
|
-
info: null,
|
|
6547
|
-
buffer: "",
|
|
6548
|
-
pending: /* @__PURE__ */ new Map()
|
|
6549
|
-
};
|
|
6550
|
-
this.connections.set(connId, conn);
|
|
6551
|
-
logger$1.info({ connId }, "IPC: new connection");
|
|
6552
|
-
socket.on("data", (data) => {
|
|
6553
|
-
conn.buffer += data.toString();
|
|
6554
|
-
this.processBuffer(conn);
|
|
6555
|
-
});
|
|
6556
|
-
socket.on("close", () => {
|
|
6557
|
-
logger$1.info({
|
|
6558
|
-
connId,
|
|
6559
|
-
plugin: conn.info?.name
|
|
6560
|
-
}, "IPC: connection closed");
|
|
6561
|
-
for (const [, pending] of conn.pending) {
|
|
6562
|
-
clearTimeout(pending.timer);
|
|
6563
|
-
pending.resolve({
|
|
6564
|
-
id: "",
|
|
6565
|
-
ok: false,
|
|
6566
|
-
error: {
|
|
6567
|
-
code: "DISCONNECTED",
|
|
6568
|
-
message: "Plugin disconnected"
|
|
6569
|
-
}
|
|
6570
|
-
});
|
|
6571
|
-
}
|
|
6572
|
-
this.connections.delete(connId);
|
|
6573
|
-
});
|
|
6574
|
-
socket.on("error", (err) => {
|
|
6575
|
-
logger$1.error({
|
|
6576
|
-
connId,
|
|
6577
|
-
err: err.message
|
|
6578
|
-
}, "IPC: socket error");
|
|
6579
|
-
});
|
|
6580
|
-
}
|
|
6581
|
-
processBuffer(conn) {
|
|
6582
|
-
let newlineIdx;
|
|
6583
|
-
while ((newlineIdx = conn.buffer.indexOf("\n")) !== -1) {
|
|
6584
|
-
const line = conn.buffer.slice(0, newlineIdx).trim();
|
|
6585
|
-
conn.buffer = conn.buffer.slice(newlineIdx + 1);
|
|
6586
|
-
if (!line) continue;
|
|
6587
|
-
let parsed;
|
|
6588
|
-
try {
|
|
6589
|
-
parsed = JSON.parse(line);
|
|
6590
|
-
} catch {
|
|
6591
|
-
logger$1.warn({ connId: conn.id }, "IPC: invalid JSON from plugin");
|
|
6592
|
-
continue;
|
|
6593
|
-
}
|
|
6594
|
-
if (isIPCResponse(parsed)) {
|
|
6595
|
-
this.handlePluginResponse(conn, parsed);
|
|
6596
|
-
continue;
|
|
6597
|
-
}
|
|
6598
|
-
if (isIPCRequest(parsed)) {
|
|
6599
|
-
this.handlePluginRequest(conn, parsed);
|
|
6600
|
-
continue;
|
|
6601
|
-
}
|
|
6602
|
-
logger$1.debug({
|
|
6603
|
-
connId: conn.id,
|
|
6604
|
-
msg: parsed
|
|
6605
|
-
}, "IPC: unhandled message");
|
|
6606
|
-
}
|
|
6607
|
-
}
|
|
6608
|
-
handlePluginResponse(conn, response) {
|
|
6609
|
-
const pending = conn.pending.get(response.id);
|
|
6610
|
-
if (!pending) return;
|
|
6611
|
-
clearTimeout(pending.timer);
|
|
6612
|
-
conn.pending.delete(response.id);
|
|
6613
|
-
pending.resolve(response);
|
|
6614
|
-
}
|
|
6615
|
-
async handlePluginRequest(conn, request) {
|
|
6616
|
-
if (conn.info) conn.info.lastSeen = Date.now();
|
|
6617
|
-
if (request.method === "register") {
|
|
6618
|
-
this.handleRegister(conn, request);
|
|
6619
|
-
return;
|
|
6620
|
-
}
|
|
6621
|
-
if (this.requestHandler) try {
|
|
6622
|
-
const result = await this.requestHandler(request.method, request.params, conn.id);
|
|
6623
|
-
const response = result.ok ? createIPCResponse(request.id, result.payload) : createIPCError(request.id, result.error?.code ?? "UNKNOWN", result.error?.message ?? "Unknown error");
|
|
6624
|
-
this.sendResponse(conn, response);
|
|
6625
|
-
} catch (err) {
|
|
6626
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
6627
|
-
this.sendResponse(conn, createIPCError(request.id, "INTERNAL", message));
|
|
6628
|
-
}
|
|
6629
|
-
else this.sendResponse(conn, createIPCError(request.id, "NO_HANDLER", `No handler for method: ${request.method}`));
|
|
6630
|
-
}
|
|
6631
|
-
handleRegister(conn, request) {
|
|
6632
|
-
const { name, version, protocolVersion, capabilities, pid } = request.params;
|
|
6633
|
-
if (!name || !version) {
|
|
6634
|
-
this.sendResponse(conn, createIPCError(request.id, "INVALID_REGISTER", "name and version are required"));
|
|
6635
|
-
return;
|
|
6636
|
-
}
|
|
6637
|
-
if (protocolVersion !== void 0 && protocolVersion !== 1) {
|
|
6638
|
-
this.sendResponse(conn, createIPCError(request.id, "PROTOCOL_MISMATCH", `Unsupported protocol version ${String(protocolVersion)}. Server supports v${String(1)}.`));
|
|
6639
|
-
return;
|
|
6640
|
-
}
|
|
6641
|
-
const staleIds = [];
|
|
6642
|
-
for (const [existingId, existing] of this.connections) if (existingId !== conn.id && existing.info?.name === name) if (pid != null && existing.info.pid === pid) {
|
|
6643
|
-
logger$1.info({
|
|
6644
|
-
staleConnId: existingId,
|
|
6645
|
-
newConnId: conn.id,
|
|
6646
|
-
plugin: name,
|
|
6647
|
-
pid
|
|
6648
|
-
}, "IPC: closing stale duplicate connection (same PID)");
|
|
6649
|
-
existing.socket.destroy();
|
|
6650
|
-
staleIds.push(existingId);
|
|
6651
|
-
} else {
|
|
6652
|
-
logger$1.info({
|
|
6653
|
-
staleConnId: existingId,
|
|
6654
|
-
newConnId: conn.id,
|
|
6655
|
-
plugin: name,
|
|
6656
|
-
oldPid: existing.info.pid,
|
|
6657
|
-
newPid: pid
|
|
6658
|
-
}, "IPC: replacing connection from different process");
|
|
6659
|
-
this.sendEvent(existing, "plugin.replaced", {
|
|
6660
|
-
reason: "Another process registered with the same plugin name",
|
|
6661
|
-
replacedBy: conn.id
|
|
6662
|
-
});
|
|
6663
|
-
existing.socket.end();
|
|
6664
|
-
staleIds.push(existingId);
|
|
6665
|
-
}
|
|
6666
|
-
for (const id of staleIds) this.connections.delete(id);
|
|
6667
|
-
conn.info = {
|
|
6668
|
-
name,
|
|
6669
|
-
version,
|
|
6670
|
-
protocolVersion: protocolVersion ?? 1,
|
|
6671
|
-
capabilities: capabilities ?? [],
|
|
6672
|
-
pid: pid ?? null,
|
|
6673
|
-
connectedAt: Date.now(),
|
|
6674
|
-
lastSeen: Date.now()
|
|
6675
|
-
};
|
|
6676
|
-
logger$1.info({
|
|
6677
|
-
connId: conn.id,
|
|
6678
|
-
plugin: name,
|
|
6679
|
-
version,
|
|
6680
|
-
capabilities
|
|
6681
|
-
}, "IPC: plugin registered");
|
|
6682
|
-
this.sendResponse(conn, createIPCResponse(request.id, {
|
|
6683
|
-
status: "registered",
|
|
6684
|
-
daemonVersion: "0.1.0",
|
|
6685
|
-
protocolVersion: 1
|
|
6686
|
-
}));
|
|
6687
|
-
}
|
|
6688
|
-
sendResponse(conn, response) {
|
|
6689
|
-
try {
|
|
6690
|
-
conn.socket.write(JSON.stringify(response) + "\n");
|
|
6691
|
-
} catch (err) {
|
|
6692
|
-
logger$1.error({
|
|
6693
|
-
connId: conn.id,
|
|
6694
|
-
err
|
|
6695
|
-
}, "IPC: failed to send response");
|
|
6696
|
-
}
|
|
6697
|
-
}
|
|
6698
|
-
sendEvent(conn, event, payload) {
|
|
6699
|
-
try {
|
|
6700
|
-
const msg = createIPCEvent(event, payload);
|
|
6701
|
-
conn.socket.write(JSON.stringify(msg) + "\n");
|
|
6702
|
-
return true;
|
|
6703
|
-
} catch {
|
|
6704
|
-
return false;
|
|
6705
|
-
}
|
|
6706
|
-
}
|
|
6707
|
-
};
|
|
6708
|
-
//#endregion
|
|
6709
|
-
//#region src/command-queue.ts
|
|
6710
|
-
const DEFAULT_TTL_MS$1 = 300 * 1e3;
|
|
6711
|
-
var CommandQueue = class {
|
|
6712
|
-
queues = /* @__PURE__ */ new Map();
|
|
6713
|
-
ttlMs;
|
|
6714
|
-
gcTimer = null;
|
|
6715
|
-
constructor(ttlMs = DEFAULT_TTL_MS$1) {
|
|
6716
|
-
this.ttlMs = ttlMs;
|
|
6717
|
-
}
|
|
6718
|
-
/**
|
|
6719
|
-
* Start periodic garbage collection of expired commands.
|
|
6720
|
-
*/
|
|
6721
|
-
startGC(intervalMs = 3e4) {
|
|
6722
|
-
this.stopGC();
|
|
6723
|
-
this.gcTimer = setInterval(() => this.purgeExpired(), intervalMs);
|
|
6724
|
-
}
|
|
6725
|
-
/**
|
|
6726
|
-
* Stop periodic garbage collection.
|
|
6727
|
-
*/
|
|
6728
|
-
stopGC() {
|
|
6729
|
-
if (this.gcTimer) {
|
|
6730
|
-
clearInterval(this.gcTimer);
|
|
6731
|
-
this.gcTimer = null;
|
|
6732
|
-
}
|
|
6733
|
-
}
|
|
6734
|
-
/**
|
|
6735
|
-
* Enqueue a command for a specific service.
|
|
6736
|
-
*/
|
|
6737
|
-
enqueue(serviceId, request, commandId) {
|
|
6738
|
-
let queue = this.queues.get(serviceId);
|
|
6739
|
-
if (!queue) {
|
|
6740
|
-
queue = [];
|
|
6741
|
-
this.queues.set(serviceId, queue);
|
|
6742
|
-
}
|
|
6743
|
-
queue.push({
|
|
6744
|
-
request,
|
|
6745
|
-
queuedAt: Date.now(),
|
|
6746
|
-
commandId
|
|
6747
|
-
});
|
|
6748
|
-
}
|
|
6749
|
-
/**
|
|
6750
|
-
* Drain all pending (non-expired) commands for a service.
|
|
6751
|
-
* Returns them in order and removes them from the queue.
|
|
6752
|
-
*/
|
|
6753
|
-
drain(serviceId) {
|
|
6754
|
-
const queue = this.queues.get(serviceId);
|
|
6755
|
-
if (!queue || queue.length === 0) return [];
|
|
6756
|
-
const now = Date.now();
|
|
6757
|
-
const valid = queue.filter((cmd) => now - cmd.queuedAt < this.ttlMs);
|
|
6758
|
-
this.queues.delete(serviceId);
|
|
6759
|
-
return valid;
|
|
6760
|
-
}
|
|
6761
|
-
/**
|
|
6762
|
-
* Get the number of pending commands for a service.
|
|
6534
|
+
* Get the number of pending commands for a service.
|
|
6763
6535
|
*/
|
|
6764
6536
|
pendingCount(serviceId) {
|
|
6765
6537
|
const queue = this.queues.get(serviceId);
|
|
@@ -6813,6 +6585,13 @@ var CommandQueue = class {
|
|
|
6813
6585
|
*/
|
|
6814
6586
|
const LAUNCHD_LABEL = "ai.alfe.gateway";
|
|
6815
6587
|
const SYSTEMD_SERVICE = "alfe-gateway";
|
|
6588
|
+
const MANAGED_ENV_VARS = [
|
|
6589
|
+
"ALFE_MANAGED",
|
|
6590
|
+
"ALFE_API_KEY",
|
|
6591
|
+
"LOG_LEVEL",
|
|
6592
|
+
"ALFE_CLI_VERSION"
|
|
6593
|
+
];
|
|
6594
|
+
const execFileAsync$3 = promisify(execFile);
|
|
6816
6595
|
/**
|
|
6817
6596
|
* On-disk path for the boot-time self-heal guard script (Linux only).
|
|
6818
6597
|
* Written next to the systemd unit at setup time and invoked via
|
|
@@ -6827,6 +6606,21 @@ function isRootUser() {
|
|
|
6827
6606
|
function getSystemdSystemServicePath() {
|
|
6828
6607
|
return `/etc/systemd/system/${SYSTEMD_SERVICE}.service`;
|
|
6829
6608
|
}
|
|
6609
|
+
function getManagedEnvironmentPath() {
|
|
6610
|
+
return isRootUser() ? "/etc/alfe/gateway.env" : join(homedir(), ".alfe", "gateway.env");
|
|
6611
|
+
}
|
|
6612
|
+
function escapeEnvironmentFileValue(value) {
|
|
6613
|
+
if (/[\0\r\n]/.test(value)) throw new Error("Managed environment values cannot contain NUL or newlines");
|
|
6614
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
6615
|
+
}
|
|
6616
|
+
/** @internal exported for unit tests; not re-exported from the barrel. */
|
|
6617
|
+
function generateManagedEnvironmentFile() {
|
|
6618
|
+
const lines = MANAGED_ENV_VARS.flatMap((key) => {
|
|
6619
|
+
const value = process.env[key];
|
|
6620
|
+
return value === void 0 ? [] : [`${key}="${escapeEnvironmentFileValue(value)}"`];
|
|
6621
|
+
});
|
|
6622
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
6623
|
+
}
|
|
6830
6624
|
function getLaunchdPlistPath() {
|
|
6831
6625
|
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
6832
6626
|
}
|
|
@@ -6921,7 +6715,7 @@ function generateLaunchdPlist() {
|
|
|
6921
6715
|
*/
|
|
6922
6716
|
/** @internal exported for unit tests; not re-exported from the barrel. */
|
|
6923
6717
|
function generateGuardScript() {
|
|
6924
|
-
const
|
|
6718
|
+
const exactVersion = process.env.ALFE_CLI_VERSION?.match(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/)?.[0];
|
|
6925
6719
|
return `#!/bin/sh
|
|
6926
6720
|
# Alfe CLI boot-time self-heal guard. Auto-generated by 'alfe setup' — do not edit.
|
|
6927
6721
|
# Repairs an interrupted 'npm install -g @alfe.ai/cli' before the daemon starts,
|
|
@@ -6929,7 +6723,7 @@ function generateGuardScript() {
|
|
|
6929
6723
|
# Must never wedge boot: every failure path logs and exits 0.
|
|
6930
6724
|
set -u
|
|
6931
6725
|
|
|
6932
|
-
TARGET='${
|
|
6726
|
+
TARGET='${exactVersion ? `@alfe.ai/cli@${exactVersion}` : "@alfe.ai/cli@latest"}'
|
|
6933
6727
|
log() { echo "[alfe-cli-guard] $*" >&2; }
|
|
6934
6728
|
|
|
6935
6729
|
# Resolve the alfe bin and its real target (dist/index.js). readlink -f follows
|
|
@@ -6993,12 +6787,7 @@ exit 0
|
|
|
6993
6787
|
function generateSystemdUnit() {
|
|
6994
6788
|
const alfeBin = getAlfeBinPath();
|
|
6995
6789
|
const root = isRootUser();
|
|
6996
|
-
const
|
|
6997
|
-
"ALFE_MANAGED",
|
|
6998
|
-
"ALFE_API_KEY",
|
|
6999
|
-
"LOG_LEVEL",
|
|
7000
|
-
"ALFE_CLI_VERSION"
|
|
7001
|
-
].filter((key) => process.env[key]).map((key) => `Environment=${key}=${process.env[key] ?? ""}`).join("\n");
|
|
6790
|
+
const managedEnvironmentPath = getManagedEnvironmentPath();
|
|
7002
6791
|
return `[Unit]
|
|
7003
6792
|
Description=Alfe Gateway Daemon
|
|
7004
6793
|
After=network-online.target
|
|
@@ -7017,7 +6806,7 @@ RestartSec=10
|
|
|
7017
6806
|
# SIGKILL when the stop timeout expires.
|
|
7018
6807
|
KillMode=mixed
|
|
7019
6808
|
Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
|
|
7020
|
-
|
|
6809
|
+
EnvironmentFile=-${managedEnvironmentPath}
|
|
7021
6810
|
|
|
7022
6811
|
[Install]
|
|
7023
6812
|
WantedBy=${root ? "multi-user.target" : "default.target"}`;
|
|
@@ -7035,6 +6824,19 @@ async function writeGuardScript() {
|
|
|
7035
6824
|
});
|
|
7036
6825
|
logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
|
|
7037
6826
|
}
|
|
6827
|
+
async function writeManagedEnvironmentFile() {
|
|
6828
|
+
const environmentPath = getManagedEnvironmentPath();
|
|
6829
|
+
await mkdir(dirname(environmentPath), {
|
|
6830
|
+
recursive: true,
|
|
6831
|
+
mode: 448
|
|
6832
|
+
});
|
|
6833
|
+
await writeFile(environmentPath, generateManagedEnvironmentFile(), {
|
|
6834
|
+
encoding: "utf-8",
|
|
6835
|
+
mode: 384
|
|
6836
|
+
});
|
|
6837
|
+
await chmod(environmentPath, 384);
|
|
6838
|
+
logger$1.info({ path: environmentPath }, "Wrote managed environment file");
|
|
6839
|
+
}
|
|
7038
6840
|
/**
|
|
7039
6841
|
* Install the service unit for the current platform.
|
|
7040
6842
|
*/
|
|
@@ -7098,6 +6900,7 @@ async function installSystemd() {
|
|
|
7098
6900
|
const ctl = root ? "systemctl" : "systemctl --user";
|
|
7099
6901
|
if (!root) await mkdir(dir, { recursive: true });
|
|
7100
6902
|
await writeGuardScript();
|
|
6903
|
+
await writeManagedEnvironmentFile();
|
|
7101
6904
|
await writeFile(unitPath, generateSystemdUnit(), "utf-8");
|
|
7102
6905
|
logger$1.info({
|
|
7103
6906
|
path: unitPath,
|
|
@@ -7123,6 +6926,9 @@ async function uninstallSystemd() {
|
|
|
7123
6926
|
try {
|
|
7124
6927
|
await unlink(getGuardScriptPath());
|
|
7125
6928
|
} catch {}
|
|
6929
|
+
try {
|
|
6930
|
+
await unlink(getManagedEnvironmentPath());
|
|
6931
|
+
} catch {}
|
|
7126
6932
|
try {
|
|
7127
6933
|
execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
|
|
7128
6934
|
} catch {}
|
|
@@ -7217,6 +7023,26 @@ function isServiceInstalled() {
|
|
|
7217
7023
|
async function writePidFile() {
|
|
7218
7024
|
await writeFile(PID_PATH, String(process.pid), "utf-8");
|
|
7219
7025
|
}
|
|
7026
|
+
/** @internal exported for unit tests; not re-exported from the barrel. */
|
|
7027
|
+
function isGatewayDaemonCommand(command) {
|
|
7028
|
+
const normalized = command.replaceAll("\0", " ").replace(/\s+/g, " ").trim();
|
|
7029
|
+
const hasGatewayEntrypoint = /(?:^|[\s/])(?:alfe-gateway|gateway(?:\.js)?)(?:\s|$)/.test(normalized) || /(?:^|\s)alfe(?:\s|$)/.test(normalized) && /(?:^|\s)gateway(?:\s|$)/.test(normalized);
|
|
7030
|
+
const hasDaemonVerb = /(?:^|\s)(?:daemon|start|restart)(?:\s|$)/.test(normalized);
|
|
7031
|
+
return hasGatewayEntrypoint && hasDaemonVerb;
|
|
7032
|
+
}
|
|
7033
|
+
async function isGatewayDaemonProcess(pid) {
|
|
7034
|
+
try {
|
|
7035
|
+
const { stdout } = await execFileAsync$3("ps", [
|
|
7036
|
+
"-p",
|
|
7037
|
+
String(pid),
|
|
7038
|
+
"-o",
|
|
7039
|
+
"command="
|
|
7040
|
+
], { timeout: 2e3 });
|
|
7041
|
+
return isGatewayDaemonCommand(stdout);
|
|
7042
|
+
} catch {
|
|
7043
|
+
return false;
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7220
7046
|
/**
|
|
7221
7047
|
* Remove the PID file.
|
|
7222
7048
|
*/
|
|
@@ -7232,15 +7058,18 @@ async function removePidFile() {
|
|
|
7232
7058
|
async function checkExistingDaemon() {
|
|
7233
7059
|
if (!existsSync(PID_PATH)) return null;
|
|
7234
7060
|
try {
|
|
7235
|
-
const
|
|
7236
|
-
const pid =
|
|
7237
|
-
if (
|
|
7061
|
+
const trimmedPid = (await readFile(PID_PATH, "utf-8")).trim();
|
|
7062
|
+
const pid = /^\d+$/.test(trimmedPid) ? Number(trimmedPid) : NaN;
|
|
7063
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) {
|
|
7238
7064
|
await removePidFile();
|
|
7239
7065
|
return null;
|
|
7240
7066
|
}
|
|
7241
7067
|
try {
|
|
7242
7068
|
process.kill(pid, 0);
|
|
7243
|
-
return pid;
|
|
7069
|
+
if (await isGatewayDaemonProcess(pid)) return pid;
|
|
7070
|
+
logger$1.warn({ pid }, "Ignoring stale daemon PID owned by another process");
|
|
7071
|
+
await removePidFile();
|
|
7072
|
+
return null;
|
|
7244
7073
|
} catch {
|
|
7245
7074
|
await removePidFile();
|
|
7246
7075
|
return null;
|
|
@@ -7266,7 +7095,8 @@ async function stopExistingDaemon() {
|
|
|
7266
7095
|
return true;
|
|
7267
7096
|
}
|
|
7268
7097
|
}
|
|
7269
|
-
process.kill(pid, "SIGKILL");
|
|
7098
|
+
if (await isGatewayDaemonProcess(pid)) process.kill(pid, "SIGKILL");
|
|
7099
|
+
else logger$1.warn({ pid }, "Skipping SIGKILL because daemon PID ownership changed");
|
|
7270
7100
|
await removePidFile();
|
|
7271
7101
|
return true;
|
|
7272
7102
|
} catch {
|
|
@@ -7275,176 +7105,1104 @@ async function stopExistingDaemon() {
|
|
|
7275
7105
|
}
|
|
7276
7106
|
}
|
|
7277
7107
|
//#endregion
|
|
7278
|
-
//#region
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
"./package.json": "./package.json"
|
|
7299
|
-
},
|
|
7300
|
-
"scripts": {
|
|
7301
|
-
"dts-check": "tsc --project tests/types/tsconfig.json",
|
|
7302
|
-
"lint": "standard",
|
|
7303
|
-
"pretest": "npm run lint && npm run dts-check",
|
|
7304
|
-
"test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
|
|
7305
|
-
"test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
|
|
7306
|
-
"prerelease": "npm test",
|
|
7307
|
-
"release": "standard-version"
|
|
7308
|
-
},
|
|
7309
|
-
"repository": {
|
|
7310
|
-
"type": "git",
|
|
7311
|
-
"url": "git://github.com/motdotla/dotenv.git"
|
|
7312
|
-
},
|
|
7313
|
-
"homepage": "https://github.com/motdotla/dotenv#readme",
|
|
7314
|
-
"funding": "https://dotenvx.com",
|
|
7315
|
-
"keywords": [
|
|
7316
|
-
"dotenv",
|
|
7317
|
-
"env",
|
|
7318
|
-
".env",
|
|
7319
|
-
"environment",
|
|
7320
|
-
"variables",
|
|
7321
|
-
"config",
|
|
7322
|
-
"settings"
|
|
7323
|
-
],
|
|
7324
|
-
"readmeFilename": "README.md",
|
|
7325
|
-
"license": "BSD-2-Clause",
|
|
7326
|
-
"devDependencies": {
|
|
7327
|
-
"@types/node": "^18.11.3",
|
|
7328
|
-
"decache": "^4.6.2",
|
|
7329
|
-
"sinon": "^14.0.1",
|
|
7330
|
-
"standard": "^17.0.0",
|
|
7331
|
-
"standard-version": "^9.5.0",
|
|
7332
|
-
"tap": "^19.2.0",
|
|
7333
|
-
"typescript": "^4.8.4"
|
|
7334
|
-
},
|
|
7335
|
-
"engines": { "node": ">=12" },
|
|
7336
|
-
"browser": { "fs": false }
|
|
7337
|
-
};
|
|
7338
|
-
}));
|
|
7339
|
-
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
7340
|
-
const fs$1 = __require("fs");
|
|
7341
|
-
const path$1 = __require("path");
|
|
7342
|
-
const os$1 = __require("os");
|
|
7343
|
-
const crypto$2 = __require("crypto");
|
|
7344
|
-
const version = require_package().version;
|
|
7345
|
-
const TIPS = [
|
|
7346
|
-
"🔐 encrypt with Dotenvx: https://dotenvx.com",
|
|
7347
|
-
"🔐 prevent committing .env to code: https://dotenvx.com/precommit",
|
|
7348
|
-
"🔐 prevent building .env in docker: https://dotenvx.com/prebuild",
|
|
7349
|
-
"📡 observe env with Radar: https://dotenvx.com/radar",
|
|
7350
|
-
"📡 auto-backup env with Radar: https://dotenvx.com/radar",
|
|
7351
|
-
"📡 version env with Radar: https://dotenvx.com/radar",
|
|
7352
|
-
"🛠️ run anywhere with `dotenvx run -- yourcommand`",
|
|
7353
|
-
"⚙️ specify custom .env file path with { path: '/custom/path/.env' }",
|
|
7354
|
-
"⚙️ enable debug logging with { debug: true }",
|
|
7355
|
-
"⚙️ override existing env vars with { override: true }",
|
|
7356
|
-
"⚙️ suppress all logs with { quiet: true }",
|
|
7357
|
-
"⚙️ write to custom object with { processEnv: myObject }",
|
|
7358
|
-
"⚙️ load multiple .env files with { path: ['.env.local', '.env'] }"
|
|
7359
|
-
];
|
|
7360
|
-
function _getRandomTip() {
|
|
7361
|
-
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
7362
|
-
}
|
|
7363
|
-
function parseBoolean(value) {
|
|
7364
|
-
if (typeof value === "string") return ![
|
|
7365
|
-
"false",
|
|
7366
|
-
"0",
|
|
7367
|
-
"no",
|
|
7368
|
-
"off",
|
|
7369
|
-
""
|
|
7370
|
-
].includes(value.toLowerCase());
|
|
7371
|
-
return Boolean(value);
|
|
7108
|
+
//#region src/ipc-server.ts
|
|
7109
|
+
/**
|
|
7110
|
+
* IPC Server — Unix socket server for local plugin connections.
|
|
7111
|
+
*
|
|
7112
|
+
* Plugins connect to ~/.alfe/gateway.sock and speak the IPC protocol:
|
|
7113
|
+
* Request: { type: 'req', id, method, params }
|
|
7114
|
+
* Response: { id, ok, payload?, error? }
|
|
7115
|
+
* Event: { type: 'event', event, payload }
|
|
7116
|
+
*
|
|
7117
|
+
* Each connected plugin registers with its name/version and receives
|
|
7118
|
+
* commands from the daemon (forwarded from cloud).
|
|
7119
|
+
*/
|
|
7120
|
+
var IPCServer = class {
|
|
7121
|
+
server = null;
|
|
7122
|
+
connections = /* @__PURE__ */ new Map();
|
|
7123
|
+
socketPath;
|
|
7124
|
+
requestHandler = null;
|
|
7125
|
+
constructor(socketPath, daemonVersion = "unknown") {
|
|
7126
|
+
this.daemonVersion = daemonVersion;
|
|
7127
|
+
this.socketPath = socketPath;
|
|
7372
7128
|
}
|
|
7373
|
-
|
|
7374
|
-
|
|
7129
|
+
/**
|
|
7130
|
+
* Set the handler for incoming IPC requests from plugins.
|
|
7131
|
+
*/
|
|
7132
|
+
setRequestHandler(handler) {
|
|
7133
|
+
this.requestHandler = handler;
|
|
7375
7134
|
}
|
|
7376
|
-
|
|
7377
|
-
|
|
7135
|
+
/**
|
|
7136
|
+
* Start listening on the Unix socket.
|
|
7137
|
+
*/
|
|
7138
|
+
async start() {
|
|
7139
|
+
try {
|
|
7140
|
+
unlinkSync(this.socketPath);
|
|
7141
|
+
} catch {}
|
|
7142
|
+
return new Promise((resolve, reject) => {
|
|
7143
|
+
this.server = createServer((socket) => {
|
|
7144
|
+
this.handleConnection(socket);
|
|
7145
|
+
});
|
|
7146
|
+
this.server.on("error", (err) => {
|
|
7147
|
+
logger$1.error({ err: err.message }, "IPC server error");
|
|
7148
|
+
reject(err);
|
|
7149
|
+
});
|
|
7150
|
+
this.server.listen(this.socketPath, () => {
|
|
7151
|
+
try {
|
|
7152
|
+
chmodSync(this.socketPath, 384);
|
|
7153
|
+
} catch (err) {
|
|
7154
|
+
logger$1.warn({ err }, "Failed to chmod socket");
|
|
7155
|
+
}
|
|
7156
|
+
logger$1.info({ path: this.socketPath }, "IPC server listening");
|
|
7157
|
+
resolve();
|
|
7158
|
+
});
|
|
7159
|
+
});
|
|
7378
7160
|
}
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
if (maybeQuote === "\"") {
|
|
7392
|
-
value = value.replace(/\\n/g, "\n");
|
|
7393
|
-
value = value.replace(/\\r/g, "\r");
|
|
7161
|
+
/**
|
|
7162
|
+
* Stop the IPC server and disconnect all plugins.
|
|
7163
|
+
*/
|
|
7164
|
+
async stop() {
|
|
7165
|
+
for (const conn of this.connections.values()) try {
|
|
7166
|
+
this.sendEvent(conn, "daemon.shutdown", { reason: "daemon stopping" });
|
|
7167
|
+
conn.socket.end();
|
|
7168
|
+
} catch {}
|
|
7169
|
+
return new Promise((resolve) => {
|
|
7170
|
+
if (!this.server) {
|
|
7171
|
+
resolve();
|
|
7172
|
+
return;
|
|
7394
7173
|
}
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
const err = /* @__PURE__ */ new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
|
|
7406
|
-
err.code = "MISSING_DATA";
|
|
7407
|
-
throw err;
|
|
7408
|
-
}
|
|
7409
|
-
const keys = _dotenvKey(options).split(",");
|
|
7410
|
-
const length = keys.length;
|
|
7411
|
-
let decrypted;
|
|
7412
|
-
for (let i = 0; i < length; i++) try {
|
|
7413
|
-
const attrs = _instructions(result, keys[i].trim());
|
|
7414
|
-
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
|
|
7415
|
-
break;
|
|
7416
|
-
} catch (error) {
|
|
7417
|
-
if (i + 1 >= length) throw error;
|
|
7418
|
-
}
|
|
7419
|
-
return DotenvModule.parse(decrypted);
|
|
7420
|
-
}
|
|
7421
|
-
function _warn(message) {
|
|
7422
|
-
console.error(`[dotenv@${version}][WARN] ${message}`);
|
|
7423
|
-
}
|
|
7424
|
-
function _debug(message) {
|
|
7425
|
-
console.log(`[dotenv@${version}][DEBUG] ${message}`);
|
|
7426
|
-
}
|
|
7427
|
-
function _log(message) {
|
|
7428
|
-
console.log(`[dotenv@${version}] ${message}`);
|
|
7429
|
-
}
|
|
7430
|
-
function _dotenvKey(options) {
|
|
7431
|
-
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
|
|
7432
|
-
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY;
|
|
7433
|
-
return "";
|
|
7174
|
+
this.server.close(() => {
|
|
7175
|
+
try {
|
|
7176
|
+
unlinkSync(this.socketPath);
|
|
7177
|
+
} catch {}
|
|
7178
|
+
logger$1.info("IPC server stopped");
|
|
7179
|
+
resolve();
|
|
7180
|
+
});
|
|
7181
|
+
for (const conn of this.connections.values()) conn.socket.destroy();
|
|
7182
|
+
this.connections.clear();
|
|
7183
|
+
});
|
|
7434
7184
|
}
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7185
|
+
/**
|
|
7186
|
+
* Send an IPC request to a specific plugin and wait for response.
|
|
7187
|
+
*/
|
|
7188
|
+
async sendRequest(pluginId, method, params, timeoutMs = 3e4) {
|
|
7189
|
+
const conn = this.connections.get(pluginId);
|
|
7190
|
+
if (!conn?.info) return {
|
|
7191
|
+
id: "",
|
|
7192
|
+
ok: false,
|
|
7193
|
+
error: {
|
|
7194
|
+
code: "PLUGIN_NOT_CONNECTED",
|
|
7195
|
+
message: `Plugin ${pluginId} not connected`
|
|
7196
|
+
}
|
|
7197
|
+
};
|
|
7198
|
+
const id = pluginConnectionId();
|
|
7199
|
+
const request = {
|
|
7200
|
+
type: "req",
|
|
7201
|
+
id,
|
|
7202
|
+
method,
|
|
7203
|
+
params
|
|
7204
|
+
};
|
|
7205
|
+
return new Promise((resolve, reject) => {
|
|
7206
|
+
const timer = setTimeout(() => {
|
|
7207
|
+
conn.pending.delete(id);
|
|
7208
|
+
resolve({
|
|
7209
|
+
id,
|
|
7210
|
+
ok: false,
|
|
7211
|
+
error: {
|
|
7212
|
+
code: "TIMEOUT",
|
|
7213
|
+
message: `Request ${method} timed out after ${String(timeoutMs)}ms`
|
|
7214
|
+
}
|
|
7215
|
+
});
|
|
7216
|
+
}, timeoutMs);
|
|
7217
|
+
conn.pending.set(id, {
|
|
7218
|
+
resolve,
|
|
7219
|
+
reject,
|
|
7220
|
+
timer
|
|
7221
|
+
});
|
|
7222
|
+
try {
|
|
7223
|
+
conn.socket.write(JSON.stringify(request) + "\n");
|
|
7224
|
+
} catch (err) {
|
|
7225
|
+
clearTimeout(timer);
|
|
7226
|
+
conn.pending.delete(id);
|
|
7227
|
+
resolve({
|
|
7228
|
+
id,
|
|
7229
|
+
ok: false,
|
|
7230
|
+
error: {
|
|
7231
|
+
code: "SEND_FAILED",
|
|
7232
|
+
message: `Failed to send to plugin: ${err instanceof Error ? err.message : String(err)}`
|
|
7233
|
+
}
|
|
7234
|
+
});
|
|
7235
|
+
}
|
|
7236
|
+
});
|
|
7237
|
+
}
|
|
7238
|
+
/**
|
|
7239
|
+
* Send an IPC request to ALL registered plugins.
|
|
7240
|
+
* Returns responses keyed by plugin ID.
|
|
7241
|
+
*/
|
|
7242
|
+
async broadcastRequest(method, params, timeoutMs = 3e4) {
|
|
7243
|
+
const results = /* @__PURE__ */ new Map();
|
|
7244
|
+
const registered = this.getRegisteredPlugins();
|
|
7245
|
+
await Promise.all(registered.map(async ([pluginId]) => {
|
|
7246
|
+
const response = await this.sendRequest(pluginId, method, params, timeoutMs);
|
|
7247
|
+
results.set(pluginId, response);
|
|
7248
|
+
}));
|
|
7249
|
+
return results;
|
|
7250
|
+
}
|
|
7251
|
+
/**
|
|
7252
|
+
* Send an event to a specific plugin (fire-and-forget).
|
|
7253
|
+
*/
|
|
7254
|
+
sendEventToPlugin(pluginId, event, payload) {
|
|
7255
|
+
const conn = this.connections.get(pluginId);
|
|
7256
|
+
if (!conn) return false;
|
|
7257
|
+
return this.sendEvent(conn, event, payload);
|
|
7258
|
+
}
|
|
7259
|
+
/**
|
|
7260
|
+
* Broadcast an event to all connected plugins.
|
|
7261
|
+
*/
|
|
7262
|
+
broadcastEvent(event, payload) {
|
|
7263
|
+
for (const conn of this.connections.values()) if (conn.info) this.sendEvent(conn, event, payload);
|
|
7264
|
+
}
|
|
7265
|
+
/**
|
|
7266
|
+
* Get all registered plugins and their info.
|
|
7267
|
+
*/
|
|
7268
|
+
getRegisteredPlugins() {
|
|
7269
|
+
const plugins = [];
|
|
7270
|
+
for (const [id, conn] of this.connections) if (conn.info) plugins.push([id, conn.info]);
|
|
7271
|
+
return plugins;
|
|
7272
|
+
}
|
|
7273
|
+
/**
|
|
7274
|
+
* Get number of connected plugins (including unregistered).
|
|
7275
|
+
*/
|
|
7276
|
+
get connectionCount() {
|
|
7277
|
+
return this.connections.size;
|
|
7278
|
+
}
|
|
7279
|
+
/**
|
|
7280
|
+
* Get number of registered plugins.
|
|
7281
|
+
*/
|
|
7282
|
+
get registeredCount() {
|
|
7283
|
+
let count = 0;
|
|
7284
|
+
for (const conn of this.connections.values()) if (conn.info) count++;
|
|
7285
|
+
return count;
|
|
7286
|
+
}
|
|
7287
|
+
handleConnection(socket) {
|
|
7288
|
+
const connId = pluginConnectionId();
|
|
7289
|
+
const conn = {
|
|
7290
|
+
id: connId,
|
|
7291
|
+
socket,
|
|
7292
|
+
info: null,
|
|
7293
|
+
buffer: "",
|
|
7294
|
+
pending: /* @__PURE__ */ new Map()
|
|
7295
|
+
};
|
|
7296
|
+
this.connections.set(connId, conn);
|
|
7297
|
+
logger$1.info({ connId }, "IPC: new connection");
|
|
7298
|
+
socket.on("data", (data) => {
|
|
7299
|
+
conn.buffer += data.toString();
|
|
7300
|
+
this.processBuffer(conn);
|
|
7301
|
+
if (Buffer.byteLength(conn.buffer, "utf8") > 1048576) this.closeOversizedConnection(conn);
|
|
7302
|
+
});
|
|
7303
|
+
socket.on("close", () => {
|
|
7304
|
+
logger$1.info({
|
|
7305
|
+
connId,
|
|
7306
|
+
plugin: conn.info?.name
|
|
7307
|
+
}, "IPC: connection closed");
|
|
7308
|
+
for (const [, pending] of conn.pending) {
|
|
7309
|
+
clearTimeout(pending.timer);
|
|
7310
|
+
pending.resolve({
|
|
7311
|
+
id: "",
|
|
7312
|
+
ok: false,
|
|
7313
|
+
error: {
|
|
7314
|
+
code: "DISCONNECTED",
|
|
7315
|
+
message: "Plugin disconnected"
|
|
7316
|
+
}
|
|
7317
|
+
});
|
|
7318
|
+
}
|
|
7319
|
+
this.connections.delete(connId);
|
|
7320
|
+
});
|
|
7321
|
+
socket.on("error", (err) => {
|
|
7322
|
+
logger$1.error({
|
|
7323
|
+
connId,
|
|
7324
|
+
err: err.message
|
|
7325
|
+
}, "IPC: socket error");
|
|
7326
|
+
});
|
|
7327
|
+
}
|
|
7328
|
+
processBuffer(conn) {
|
|
7329
|
+
let newlineIdx;
|
|
7330
|
+
while ((newlineIdx = conn.buffer.indexOf("\n")) !== -1) {
|
|
7331
|
+
const rawLine = conn.buffer.slice(0, newlineIdx);
|
|
7332
|
+
conn.buffer = conn.buffer.slice(newlineIdx + 1);
|
|
7333
|
+
if (Buffer.byteLength(rawLine, "utf8") > 1048576) {
|
|
7334
|
+
this.closeOversizedConnection(conn);
|
|
7335
|
+
return;
|
|
7336
|
+
}
|
|
7337
|
+
const line = rawLine.trim();
|
|
7338
|
+
if (!line) continue;
|
|
7339
|
+
let parsed;
|
|
7340
|
+
try {
|
|
7341
|
+
parsed = JSON.parse(line);
|
|
7342
|
+
} catch {
|
|
7343
|
+
logger$1.warn({ connId: conn.id }, "IPC: invalid JSON from plugin");
|
|
7344
|
+
continue;
|
|
7345
|
+
}
|
|
7346
|
+
if (isIPCResponse(parsed)) {
|
|
7347
|
+
this.handlePluginResponse(conn, parsed);
|
|
7348
|
+
continue;
|
|
7349
|
+
}
|
|
7350
|
+
if (isIPCRequest(parsed)) {
|
|
7351
|
+
this.handlePluginRequest(conn, parsed);
|
|
7352
|
+
continue;
|
|
7353
|
+
}
|
|
7354
|
+
logger$1.debug({
|
|
7355
|
+
connId: conn.id,
|
|
7356
|
+
msg: parsed
|
|
7357
|
+
}, "IPC: unhandled message");
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
closeOversizedConnection(conn) {
|
|
7361
|
+
logger$1.warn({
|
|
7362
|
+
connId: conn.id,
|
|
7363
|
+
maxBytes: MAX_IPC_MESSAGE_BYTES
|
|
7364
|
+
}, "IPC: message exceeded size limit — closing connection");
|
|
7365
|
+
conn.buffer = "";
|
|
7366
|
+
conn.socket.destroy();
|
|
7367
|
+
}
|
|
7368
|
+
handlePluginResponse(conn, response) {
|
|
7369
|
+
const pending = conn.pending.get(response.id);
|
|
7370
|
+
if (!pending) return;
|
|
7371
|
+
clearTimeout(pending.timer);
|
|
7372
|
+
conn.pending.delete(response.id);
|
|
7373
|
+
pending.resolve(response);
|
|
7374
|
+
}
|
|
7375
|
+
async handlePluginRequest(conn, request) {
|
|
7376
|
+
if (conn.info) conn.info.lastSeen = Date.now();
|
|
7377
|
+
if (request.method === "register") {
|
|
7378
|
+
this.handleRegister(conn, request);
|
|
7379
|
+
return;
|
|
7380
|
+
}
|
|
7381
|
+
if (this.requestHandler) try {
|
|
7382
|
+
const result = await this.requestHandler(request.method, request.params, conn.id);
|
|
7383
|
+
const response = result.ok ? createIPCResponse(request.id, result.payload) : createIPCError(request.id, result.error?.code ?? "UNKNOWN", result.error?.message ?? "Unknown error");
|
|
7384
|
+
this.sendResponse(conn, response);
|
|
7385
|
+
} catch (err) {
|
|
7386
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7387
|
+
this.sendResponse(conn, createIPCError(request.id, "INTERNAL", message));
|
|
7388
|
+
}
|
|
7389
|
+
else this.sendResponse(conn, createIPCError(request.id, "NO_HANDLER", `No handler for method: ${request.method}`));
|
|
7390
|
+
}
|
|
7391
|
+
handleRegister(conn, request) {
|
|
7392
|
+
const { name, version, protocolVersion, capabilities, pid } = request.params;
|
|
7393
|
+
const validCapabilities = capabilities === void 0 || Array.isArray(capabilities) && capabilities.length <= 256 && capabilities.every((capability) => typeof capability === "string" && capability.trim().length > 0 && capability.length <= 256);
|
|
7394
|
+
const validPid = pid === void 0 || Number.isSafeInteger(pid) && pid > 0;
|
|
7395
|
+
const validProtocolVersion = protocolVersion === void 0 || Number.isSafeInteger(protocolVersion);
|
|
7396
|
+
if (typeof name !== "string" || name.trim().length === 0 || name.length > 256 || typeof version !== "string" || version.trim().length === 0 || version.length > 256 || !validCapabilities || !validPid || !validProtocolVersion) {
|
|
7397
|
+
this.sendResponse(conn, createIPCError(request.id, "INVALID_REGISTER", "Invalid plugin registration fields"));
|
|
7398
|
+
return;
|
|
7399
|
+
}
|
|
7400
|
+
if (protocolVersion !== void 0 && protocolVersion !== 1) {
|
|
7401
|
+
this.sendResponse(conn, createIPCError(request.id, "PROTOCOL_MISMATCH", `Unsupported protocol version ${String(protocolVersion)}. Server supports v${String(1)}.`));
|
|
7402
|
+
return;
|
|
7403
|
+
}
|
|
7404
|
+
const staleIds = [];
|
|
7405
|
+
for (const [existingId, existing] of this.connections) if (existingId !== conn.id && existing.info?.name === name) if (pid != null && existing.info.pid === pid) {
|
|
7406
|
+
logger$1.info({
|
|
7407
|
+
staleConnId: existingId,
|
|
7408
|
+
newConnId: conn.id,
|
|
7409
|
+
plugin: name,
|
|
7410
|
+
pid
|
|
7411
|
+
}, "IPC: closing stale duplicate connection (same PID)");
|
|
7412
|
+
existing.socket.destroy();
|
|
7413
|
+
staleIds.push(existingId);
|
|
7414
|
+
} else {
|
|
7415
|
+
logger$1.info({
|
|
7416
|
+
staleConnId: existingId,
|
|
7417
|
+
newConnId: conn.id,
|
|
7418
|
+
plugin: name,
|
|
7419
|
+
oldPid: existing.info.pid,
|
|
7420
|
+
newPid: pid
|
|
7421
|
+
}, "IPC: replacing connection from different process");
|
|
7422
|
+
this.sendEvent(existing, "plugin.replaced", {
|
|
7423
|
+
reason: "Another process registered with the same plugin name",
|
|
7424
|
+
replacedBy: conn.id
|
|
7425
|
+
});
|
|
7426
|
+
existing.socket.end();
|
|
7427
|
+
staleIds.push(existingId);
|
|
7428
|
+
}
|
|
7429
|
+
for (const id of staleIds) this.connections.delete(id);
|
|
7430
|
+
conn.info = {
|
|
7431
|
+
name,
|
|
7432
|
+
version,
|
|
7433
|
+
protocolVersion: protocolVersion ?? 1,
|
|
7434
|
+
capabilities: capabilities ? [...capabilities] : [],
|
|
7435
|
+
pid: pid ?? null,
|
|
7436
|
+
connectedAt: Date.now(),
|
|
7437
|
+
lastSeen: Date.now()
|
|
7438
|
+
};
|
|
7439
|
+
logger$1.info({
|
|
7440
|
+
connId: conn.id,
|
|
7441
|
+
plugin: name,
|
|
7442
|
+
version,
|
|
7443
|
+
capabilities
|
|
7444
|
+
}, "IPC: plugin registered");
|
|
7445
|
+
this.sendResponse(conn, createIPCResponse(request.id, {
|
|
7446
|
+
status: "registered",
|
|
7447
|
+
daemonVersion: this.daemonVersion,
|
|
7448
|
+
protocolVersion: 1
|
|
7449
|
+
}));
|
|
7450
|
+
}
|
|
7451
|
+
sendResponse(conn, response) {
|
|
7452
|
+
try {
|
|
7453
|
+
conn.socket.write(JSON.stringify(response) + "\n");
|
|
7454
|
+
} catch (err) {
|
|
7455
|
+
logger$1.error({
|
|
7456
|
+
connId: conn.id,
|
|
7457
|
+
err
|
|
7458
|
+
}, "IPC: failed to send response");
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7461
|
+
sendEvent(conn, event, payload) {
|
|
7462
|
+
try {
|
|
7463
|
+
const msg = createIPCEvent(event, payload);
|
|
7464
|
+
conn.socket.write(JSON.stringify(msg) + "\n");
|
|
7465
|
+
return true;
|
|
7466
|
+
} catch {
|
|
7467
|
+
return false;
|
|
7468
|
+
}
|
|
7469
|
+
}
|
|
7470
|
+
};
|
|
7471
|
+
//#endregion
|
|
7472
|
+
//#region src/daemon-bootstrap.ts
|
|
7473
|
+
/**
|
|
7474
|
+
* Daemon bootstrap helpers — startup ordering, config/version resolution,
|
|
7475
|
+
* AI-proxy + IPC server wiring.
|
|
7476
|
+
*
|
|
7477
|
+
* Extracted from daemon.ts (which remains the orchestrator). These are the
|
|
7478
|
+
* standalone pieces of `startDaemon()`: version detection, the one-shot
|
|
7479
|
+
* persona-file migration, the AI proxy listener, and the IPC server start.
|
|
7480
|
+
*/
|
|
7481
|
+
const execFileAsync$2 = promisify(execFile);
|
|
7482
|
+
/**
|
|
7483
|
+
* Resolve the installed @alfe.ai/cli version.
|
|
7484
|
+
*
|
|
7485
|
+
* Strategy (in order):
|
|
7486
|
+
* 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
|
|
7487
|
+
* 2. Walk up from this file to find @alfe.ai/cli/package.json
|
|
7488
|
+
* (works when systemd runs the gateway binary directly, since
|
|
7489
|
+
* the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
|
|
7490
|
+
*/
|
|
7491
|
+
async function getCliVersion() {
|
|
7492
|
+
if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
|
|
7493
|
+
try {
|
|
7494
|
+
const { fileURLToPath } = await import("node:url");
|
|
7495
|
+
const { dirname } = await import("node:path");
|
|
7496
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7497
|
+
for (let i = 0; i < 10; i++) {
|
|
7498
|
+
const candidate = join(dir, "package.json");
|
|
7499
|
+
try {
|
|
7500
|
+
const raw = await readFile(candidate, "utf-8");
|
|
7501
|
+
const pkg = JSON.parse(raw);
|
|
7502
|
+
if (pkg.name === "@alfe.ai/cli") return pkg.version;
|
|
7503
|
+
} catch {}
|
|
7504
|
+
const parent = dirname(dir);
|
|
7505
|
+
if (parent === dir) break;
|
|
7506
|
+
dir = parent;
|
|
7507
|
+
}
|
|
7508
|
+
} catch {}
|
|
7509
|
+
logger$1.debug("Could not resolve @alfe.ai/cli version");
|
|
7510
|
+
}
|
|
7511
|
+
/**
|
|
7512
|
+
* Per-runtime "print installed version" commands. Local to the gateway — we do
|
|
7513
|
+
* NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
|
|
7514
|
+
* `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
|
|
7515
|
+
* connection-status report degrades gracefully instead of crashing.
|
|
7516
|
+
*/
|
|
7517
|
+
const RUNTIME_VERSION_COMMANDS = {
|
|
7518
|
+
openclaw: {
|
|
7519
|
+
command: "openclaw",
|
|
7520
|
+
args: ["--version"]
|
|
7521
|
+
},
|
|
7522
|
+
hermes: {
|
|
7523
|
+
command: "hermes",
|
|
7524
|
+
args: ["version"]
|
|
7525
|
+
},
|
|
7526
|
+
"claude-code": {
|
|
7527
|
+
command: "alfe-claude-host",
|
|
7528
|
+
args: ["--version"]
|
|
7529
|
+
}
|
|
7530
|
+
};
|
|
7531
|
+
/**
|
|
7532
|
+
* Pure resolver (exported for tests) — the per-runtime version command, or
|
|
7533
|
+
* `undefined` for an unknown runtime.
|
|
7534
|
+
*/
|
|
7535
|
+
function resolveRuntimeVersionCommand(runtime) {
|
|
7536
|
+
return RUNTIME_VERSION_COMMANDS[runtime];
|
|
7537
|
+
}
|
|
7538
|
+
/**
|
|
7539
|
+
* Resolve the installed runtime version by running the per-runtime version
|
|
7540
|
+
* command and returning the trimmed stdout. An unknown runtime (no command in
|
|
7541
|
+
* the map) returns `undefined` without spawning — it must never throw.
|
|
7542
|
+
*/
|
|
7543
|
+
async function getRuntimeVersion(runtime) {
|
|
7544
|
+
const cmd = resolveRuntimeVersionCommand(runtime);
|
|
7545
|
+
if (!cmd) {
|
|
7546
|
+
logger$1.debug({ runtime }, "No version command for runtime — skipping version detection");
|
|
7547
|
+
return;
|
|
7548
|
+
}
|
|
7549
|
+
try {
|
|
7550
|
+
const { stdout } = await execFileAsync$2(cmd.command, cmd.args);
|
|
7551
|
+
return stdout.trim() || void 0;
|
|
7552
|
+
} catch {
|
|
7553
|
+
logger$1.debug({ runtime }, "Could not resolve runtime version");
|
|
7554
|
+
return;
|
|
7555
|
+
}
|
|
7556
|
+
}
|
|
7557
|
+
/**
|
|
7558
|
+
* Flush pino's async transport and exit.
|
|
7559
|
+
* process.exit() can drop buffered log lines — this ensures they're written first.
|
|
7560
|
+
*/
|
|
7561
|
+
async function flushAndExit(code) {
|
|
7562
|
+
await flushSentry();
|
|
7563
|
+
await new Promise((resolve) => {
|
|
7564
|
+
logger$1.flush();
|
|
7565
|
+
setTimeout(resolve, 500);
|
|
7566
|
+
});
|
|
7567
|
+
process.exit(code);
|
|
7568
|
+
}
|
|
7569
|
+
/**
|
|
7570
|
+
* One-shot migration for VMs provisioned before the persona-files-go-in-agent-workspace
|
|
7571
|
+
* fix landed. Old behavior wrote SOUL.md/IDENTITY.md/BOOTSTRAP.md/AGENTS.md at the
|
|
7572
|
+
* OpenClaw home (e.g. `~/.openclaw/`); the agent reads from the agent workspace
|
|
7573
|
+
* (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
|
|
7574
|
+
* workspace already has the file. Idempotent and safe to run on every daemon start.
|
|
7575
|
+
*/
|
|
7576
|
+
const PERSONA_FILES = [
|
|
7577
|
+
"SOUL.md",
|
|
7578
|
+
"IDENTITY.md",
|
|
7579
|
+
"BOOTSTRAP.md",
|
|
7580
|
+
"AGENTS.md"
|
|
7581
|
+
];
|
|
7582
|
+
async function migrateLegacyPersonaFiles(home, agentWorkspace) {
|
|
7583
|
+
if (home === agentWorkspace) return;
|
|
7584
|
+
let moved = 0;
|
|
7585
|
+
let skipped = 0;
|
|
7586
|
+
for (const filename of PERSONA_FILES) {
|
|
7587
|
+
const src = join(home, filename);
|
|
7588
|
+
const dst = join(agentWorkspace, filename);
|
|
7589
|
+
try {
|
|
7590
|
+
await stat(src);
|
|
7591
|
+
} catch {
|
|
7592
|
+
continue;
|
|
7593
|
+
}
|
|
7594
|
+
try {
|
|
7595
|
+
await stat(dst);
|
|
7596
|
+
skipped++;
|
|
7597
|
+
continue;
|
|
7598
|
+
} catch {}
|
|
7599
|
+
try {
|
|
7600
|
+
await mkdir(agentWorkspace, { recursive: true });
|
|
7601
|
+
await rename(src, dst);
|
|
7602
|
+
moved++;
|
|
7603
|
+
} catch (err) {
|
|
7604
|
+
logger$1.warn({
|
|
7605
|
+
src,
|
|
7606
|
+
dst,
|
|
7607
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7608
|
+
}, "Persona file migration: rename failed");
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
if (moved > 0 || skipped > 0) logger$1.info({
|
|
7612
|
+
home,
|
|
7613
|
+
agentWorkspace,
|
|
7614
|
+
moved,
|
|
7615
|
+
skipped
|
|
7616
|
+
}, "Persona file backfill migration complete");
|
|
7617
|
+
}
|
|
7618
|
+
/**
|
|
7619
|
+
* Start the local AI proxy (before the runtime so LLM requests work
|
|
7620
|
+
* immediately). Failure is non-fatal — the daemon starts anyway and LLM
|
|
7621
|
+
* requests fail until the proxy is fixed.
|
|
7622
|
+
*/
|
|
7623
|
+
async function startAiProxy(apiKey) {
|
|
7624
|
+
logger$1.debug("Starting AI proxy...");
|
|
7625
|
+
const handle = {
|
|
7626
|
+
server: null,
|
|
7627
|
+
url: null,
|
|
7628
|
+
running: false
|
|
7629
|
+
};
|
|
7630
|
+
try {
|
|
7631
|
+
const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
|
|
7632
|
+
const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
|
|
7633
|
+
const proxyUrl = getAiServiceUrlFromToken(apiKey);
|
|
7634
|
+
const port = DEFAULT_AI_PROXY_PORT ?? 18193;
|
|
7635
|
+
handle.server = createProxyServer({
|
|
7636
|
+
port,
|
|
7637
|
+
apiKey,
|
|
7638
|
+
proxyUrl
|
|
7639
|
+
});
|
|
7640
|
+
const server = handle.server;
|
|
7641
|
+
await new Promise((resolve, reject) => {
|
|
7642
|
+
server.listen(port, "127.0.0.1", () => {
|
|
7643
|
+
handle.running = true;
|
|
7644
|
+
handle.url = `http://127.0.0.1:${String(port)}`;
|
|
7645
|
+
logger$1.info({
|
|
7646
|
+
port,
|
|
7647
|
+
upstream: proxyUrl
|
|
7648
|
+
}, "AI proxy started");
|
|
7649
|
+
resolve();
|
|
7650
|
+
});
|
|
7651
|
+
server.on("error", reject);
|
|
7652
|
+
});
|
|
7653
|
+
} catch (err) {
|
|
7654
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7655
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
7656
|
+
logger$1.error({
|
|
7657
|
+
err: message,
|
|
7658
|
+
stack
|
|
7659
|
+
}, "Failed to start AI proxy — LLM requests will fail");
|
|
7660
|
+
}
|
|
7661
|
+
return handle;
|
|
7662
|
+
}
|
|
7663
|
+
/**
|
|
7664
|
+
* Start the IPC server (runtime plugins connect via this socket). A failure
|
|
7665
|
+
* here is fatal — plugins have no other path to the daemon — so this flushes
|
|
7666
|
+
* logs and exits instead of returning.
|
|
7667
|
+
*/
|
|
7668
|
+
async function startIpcServer(socketPath, requestHandler, daemonVersion) {
|
|
7669
|
+
logger$1.debug({ socketPath }, "Starting IPC server...");
|
|
7670
|
+
const ipcServer = new IPCServer(socketPath, daemonVersion);
|
|
7671
|
+
ipcServer.setRequestHandler(requestHandler);
|
|
7672
|
+
try {
|
|
7673
|
+
await ipcServer.start();
|
|
7674
|
+
logger$1.debug("IPC server started");
|
|
7675
|
+
} catch (err) {
|
|
7676
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7677
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
7678
|
+
logger$1.error({
|
|
7679
|
+
err: message,
|
|
7680
|
+
stack
|
|
7681
|
+
}, "Failed to start IPC server");
|
|
7682
|
+
await flushAndExit(1);
|
|
7683
|
+
}
|
|
7684
|
+
return ipcServer;
|
|
7685
|
+
}
|
|
7686
|
+
//#endregion
|
|
7687
|
+
//#region src/mcp-handlers.ts
|
|
7688
|
+
/**
|
|
7689
|
+
* Return the daemon-hosted MCP bundler's current namespaced tool catalog.
|
|
7690
|
+
* Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
|
|
7691
|
+
* cheap (in-memory snapshot, no I/O).
|
|
7692
|
+
*/
|
|
7693
|
+
function handleMcpListTools(bundler) {
|
|
7694
|
+
if (!bundler) return {
|
|
7695
|
+
ok: false,
|
|
7696
|
+
error: {
|
|
7697
|
+
code: "MCP_BUNDLER_UNAVAILABLE",
|
|
7698
|
+
message: "MCP bundler not initialized"
|
|
7699
|
+
}
|
|
7700
|
+
};
|
|
7701
|
+
return {
|
|
7702
|
+
ok: true,
|
|
7703
|
+
payload: { tools: bundler.listTools() }
|
|
7704
|
+
};
|
|
7705
|
+
}
|
|
7706
|
+
/**
|
|
7707
|
+
* Observability backstop for the INTEGRATION warm path. `applyForIntegration`
|
|
7708
|
+
* registers MCP servers into the store and the daemon warms them silently via
|
|
7709
|
+
* the store-change `onChange` hook — so a server that fails its connect (e.g.
|
|
7710
|
+
* an MCP server whose backing account/credential wasn't resolvable at warm
|
|
7711
|
+
* time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
|
|
7712
|
+
* making the black hole undiagnosable. After a warm, read `bundler.statuses()`
|
|
7713
|
+
* and warn once per still-failed server so the failure is visible. These
|
|
7714
|
+
* self-heal on the bundler's background retry sweep once the dependency
|
|
7715
|
+
* appears. Returns the servers it warned about (for tests / callers).
|
|
7716
|
+
*/
|
|
7717
|
+
function warnFailedMcpServers(bundler, log, reason) {
|
|
7718
|
+
if (!bundler) return [];
|
|
7719
|
+
const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
|
|
7720
|
+
for (const status of failed) log.warn({
|
|
7721
|
+
server: status.name,
|
|
7722
|
+
reason,
|
|
7723
|
+
consecutiveFailures: status.consecutiveFailures,
|
|
7724
|
+
lastError: status.lastError
|
|
7725
|
+
}, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
|
|
7726
|
+
return failed;
|
|
7727
|
+
}
|
|
7728
|
+
/**
|
|
7729
|
+
* Route a tool call to the appropriate MCP child via the daemon-hosted
|
|
7730
|
+
* bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
|
|
7731
|
+
* the raw JSON object the LLM produced.
|
|
7732
|
+
*/
|
|
7733
|
+
/**
|
|
7734
|
+
* List every server entry in the alfe bundler store. Lets agents inspect
|
|
7735
|
+
* what they've already registered before adding a new one.
|
|
7736
|
+
*/
|
|
7737
|
+
function handleMcpListServers(manager) {
|
|
7738
|
+
if (!manager) return {
|
|
7739
|
+
ok: false,
|
|
7740
|
+
error: {
|
|
7741
|
+
code: "MCP_MANAGER_UNAVAILABLE",
|
|
7742
|
+
message: "MCP manager not initialized"
|
|
7743
|
+
}
|
|
7744
|
+
};
|
|
7745
|
+
const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
|
|
7746
|
+
const byName = new Map(statuses.map((s) => [s.name, s]));
|
|
7747
|
+
return {
|
|
7748
|
+
ok: true,
|
|
7749
|
+
payload: { servers: manager.listServers().map(({ id, entry }) => ({
|
|
7750
|
+
id,
|
|
7751
|
+
entry: toPublicServerEntry(entry),
|
|
7752
|
+
fingerprint: serverLaunchFingerprint(entry),
|
|
7753
|
+
status: byName.get(id) ?? {
|
|
7754
|
+
name: id,
|
|
7755
|
+
connected: false,
|
|
7756
|
+
toolCount: 0,
|
|
7757
|
+
consecutiveFailures: 0
|
|
7758
|
+
}
|
|
7759
|
+
})) }
|
|
7760
|
+
};
|
|
7761
|
+
}
|
|
7762
|
+
function toPublicServerEntry(entry) {
|
|
7763
|
+
return {
|
|
7764
|
+
owner: entry.owner,
|
|
7765
|
+
addedAt: entry.addedAt,
|
|
7766
|
+
transport: entry.transport,
|
|
7767
|
+
...entry.version !== void 0 ? { version: entry.version } : {}
|
|
7768
|
+
};
|
|
7769
|
+
}
|
|
7770
|
+
/**
|
|
7771
|
+
* How long the add-confirm probe waits for the freshly-added server to
|
|
7772
|
+
* connect before replying. This is effectively the whole budget for
|
|
7773
|
+
* `bundler.warmServer`: `Manager.addServer` now reconciles the bundler
|
|
7774
|
+
* BEFORE resolving, so the Connection object already exists and
|
|
7775
|
+
* `warmServer`'s pre-reconcile is a no-diff cheap pass. Kept well under the
|
|
7776
|
+
* plugin caller's 30s IPC timeout. A slower server still connects in the
|
|
7777
|
+
* background and surfaces on the next `alfe_mcp_list_tools` — the probe just
|
|
7778
|
+
* reports what it saw within the window; a missed connect is re-attempted by
|
|
7779
|
+
* the bundler's `retryNeverConnected` sweep.
|
|
7780
|
+
*/
|
|
7781
|
+
const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
|
|
7782
|
+
/**
|
|
7783
|
+
* Register a new MCP server in the alfe bundler store on behalf of the agent,
|
|
7784
|
+
* then CONFIRM the connect before replying so the agent learns whether the
|
|
7785
|
+
* server actually works. Owned as `'manual'` so the agent can later remove it
|
|
7786
|
+
* without an expectedOwner conflict — matches what `alfe mcp add` does from the
|
|
7787
|
+
* CLI. Registration is durable regardless of the probe outcome: a probe
|
|
7788
|
+
* failure (or a runtime with no daemon bundler, e.g. hermes) still returns
|
|
7789
|
+
* `ok` with `connected: false`; the store watcher / runtime picks the entry up.
|
|
7790
|
+
*/
|
|
7791
|
+
async function handleMcpAddServer(params, manager) {
|
|
7792
|
+
if (!manager) return {
|
|
7793
|
+
ok: false,
|
|
7794
|
+
error: {
|
|
7795
|
+
code: "MCP_MANAGER_UNAVAILABLE",
|
|
7796
|
+
message: "MCP manager not initialized"
|
|
7797
|
+
}
|
|
7798
|
+
};
|
|
7799
|
+
const p = params;
|
|
7800
|
+
if (typeof p.id !== "string" || p.id.length === 0) return {
|
|
7801
|
+
ok: false,
|
|
7802
|
+
error: {
|
|
7803
|
+
code: "INVALID_PARAMS",
|
|
7804
|
+
message: "id is required (string)"
|
|
7805
|
+
}
|
|
7806
|
+
};
|
|
7807
|
+
const config = buildServerConfig(p);
|
|
7808
|
+
if (!config) return {
|
|
7809
|
+
ok: false,
|
|
7810
|
+
error: {
|
|
7811
|
+
code: "INVALID_PARAMS",
|
|
7812
|
+
message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
|
|
7813
|
+
}
|
|
7814
|
+
};
|
|
7815
|
+
try {
|
|
7816
|
+
await manager.addServer(config, {
|
|
7817
|
+
id: p.id,
|
|
7818
|
+
owner: "manual"
|
|
7819
|
+
});
|
|
7820
|
+
} catch (err) {
|
|
7821
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7822
|
+
logger$1.warn({
|
|
7823
|
+
id: p.id,
|
|
7824
|
+
err: message
|
|
7825
|
+
}, "mcp.add_server failed");
|
|
7826
|
+
return {
|
|
7827
|
+
ok: false,
|
|
7828
|
+
error: {
|
|
7829
|
+
code: "MCP_ADD_FAILED",
|
|
7830
|
+
message
|
|
7831
|
+
}
|
|
7832
|
+
};
|
|
7833
|
+
}
|
|
7834
|
+
let status = null;
|
|
7835
|
+
if (manager.warmServer) try {
|
|
7836
|
+
status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
|
|
7837
|
+
} catch (err) {
|
|
7838
|
+
logger$1.warn({
|
|
7839
|
+
id: p.id,
|
|
7840
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7841
|
+
}, "mcp.add_server warm probe threw");
|
|
7842
|
+
}
|
|
7843
|
+
return {
|
|
7844
|
+
ok: true,
|
|
7845
|
+
payload: {
|
|
7846
|
+
id: p.id,
|
|
7847
|
+
connected: status?.connected ?? false,
|
|
7848
|
+
toolCount: status?.toolCount ?? 0,
|
|
7849
|
+
...status?.lastError !== void 0 ? { error: status.lastError } : {}
|
|
7850
|
+
}
|
|
7851
|
+
};
|
|
7852
|
+
}
|
|
7853
|
+
function buildServerConfig(p) {
|
|
7854
|
+
if (typeof p.command === "string" && p.command.length > 0) {
|
|
7855
|
+
const cfg = { command: p.command };
|
|
7856
|
+
if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
|
|
7857
|
+
if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
|
|
7858
|
+
const env = {};
|
|
7859
|
+
for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
|
|
7860
|
+
cfg.env = env;
|
|
7861
|
+
}
|
|
7862
|
+
if (typeof p.cwd === "string") cfg.cwd = p.cwd;
|
|
7863
|
+
return cfg;
|
|
7864
|
+
}
|
|
7865
|
+
if (typeof p.url === "string" && p.url.length > 0) {
|
|
7866
|
+
const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
|
|
7867
|
+
const cfg = {
|
|
7868
|
+
url: p.url,
|
|
7869
|
+
transport
|
|
7870
|
+
};
|
|
7871
|
+
if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
|
|
7872
|
+
const headers = {};
|
|
7873
|
+
for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
|
|
7874
|
+
cfg.headers = headers;
|
|
7875
|
+
}
|
|
7876
|
+
return cfg;
|
|
7877
|
+
}
|
|
7878
|
+
return null;
|
|
7879
|
+
}
|
|
7880
|
+
/**
|
|
7881
|
+
* Drop a server entry the agent previously registered. Restricted to
|
|
7882
|
+
* `manual`-owned entries so the agent can't accidentally clobber
|
|
7883
|
+
* integration-installed or CLI-installed servers (the daemon owns
|
|
7884
|
+
* those; the agent can ask the user to uninstall an integration via
|
|
7885
|
+
* the dashboard).
|
|
7886
|
+
*/
|
|
7887
|
+
async function handleMcpRemoveServer(params, manager) {
|
|
7888
|
+
if (!manager) return {
|
|
7889
|
+
ok: false,
|
|
7890
|
+
error: {
|
|
7891
|
+
code: "MCP_MANAGER_UNAVAILABLE",
|
|
7892
|
+
message: "MCP manager not initialized"
|
|
7893
|
+
}
|
|
7894
|
+
};
|
|
7895
|
+
const { id } = params;
|
|
7896
|
+
if (typeof id !== "string" || id.length === 0) return {
|
|
7897
|
+
ok: false,
|
|
7898
|
+
error: {
|
|
7899
|
+
code: "INVALID_PARAMS",
|
|
7900
|
+
message: "id is required (string)"
|
|
7901
|
+
}
|
|
7902
|
+
};
|
|
7903
|
+
try {
|
|
7904
|
+
return {
|
|
7905
|
+
ok: true,
|
|
7906
|
+
payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
|
|
7907
|
+
};
|
|
7908
|
+
} catch (err) {
|
|
7909
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7910
|
+
if (message.includes("owned by")) return {
|
|
7911
|
+
ok: false,
|
|
7912
|
+
error: {
|
|
7913
|
+
code: "MCP_OWNER_MISMATCH",
|
|
7914
|
+
message
|
|
7915
|
+
}
|
|
7916
|
+
};
|
|
7917
|
+
logger$1.warn({
|
|
7918
|
+
id,
|
|
7919
|
+
err: message
|
|
7920
|
+
}, "mcp.remove_server failed");
|
|
7921
|
+
return {
|
|
7922
|
+
ok: false,
|
|
7923
|
+
error: {
|
|
7924
|
+
code: "MCP_REMOVE_FAILED",
|
|
7925
|
+
message
|
|
7926
|
+
}
|
|
7927
|
+
};
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
async function handleMcpCallTool(bundler, params) {
|
|
7931
|
+
if (!bundler) return {
|
|
7932
|
+
ok: false,
|
|
7933
|
+
error: {
|
|
7934
|
+
code: "MCP_BUNDLER_UNAVAILABLE",
|
|
7935
|
+
message: "MCP bundler not initialized"
|
|
7936
|
+
}
|
|
7937
|
+
};
|
|
7938
|
+
const { name, args } = params;
|
|
7939
|
+
if (typeof name !== "string" || name.length === 0) return {
|
|
7940
|
+
ok: false,
|
|
7941
|
+
error: {
|
|
7942
|
+
code: "INVALID_PARAMS",
|
|
7943
|
+
message: "name is required (string)"
|
|
7944
|
+
}
|
|
7945
|
+
};
|
|
7946
|
+
try {
|
|
7947
|
+
return {
|
|
7948
|
+
ok: true,
|
|
7949
|
+
payload: await bundler.callTool(name, args)
|
|
7950
|
+
};
|
|
7951
|
+
} catch (err) {
|
|
7952
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7953
|
+
logger$1.warn({
|
|
7954
|
+
tool: name,
|
|
7955
|
+
err: message
|
|
7956
|
+
}, "mcp.call_tool failed");
|
|
7957
|
+
return {
|
|
7958
|
+
ok: false,
|
|
7959
|
+
error: {
|
|
7960
|
+
code: "MCP_CALL_FAILED",
|
|
7961
|
+
message
|
|
7962
|
+
}
|
|
7963
|
+
};
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
//#endregion
|
|
7967
|
+
//#region src/integration-bootstrap.ts
|
|
7968
|
+
/**
|
|
7969
|
+
* Adapter from `AgentApiClient`'s per-provider methods to the
|
|
7970
|
+
* `CredentialsResolver` shape the MCP applier expects. Returns
|
|
7971
|
+
* `undefined` for unknown providers and on 404/network error so the
|
|
7972
|
+
* applier can skip registration silently (its documented contract).
|
|
7973
|
+
*/
|
|
7974
|
+
async function fetchProviderCredentials(agentApi, provider, connectionId) {
|
|
7975
|
+
if (connectionId) try {
|
|
7976
|
+
const raw = await agentApi.getConnectionCredentials(connectionId);
|
|
7977
|
+
const fields = { ...raw.providerMetadata ?? {} };
|
|
7978
|
+
if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
|
|
7979
|
+
const bundle = JSON.parse(raw.accessToken);
|
|
7980
|
+
if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
|
|
7981
|
+
} catch (err) {
|
|
7982
|
+
logger$1.warn({
|
|
7983
|
+
connectionId,
|
|
7984
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7985
|
+
}, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
|
|
7986
|
+
fields.accessToken = raw.accessToken;
|
|
7987
|
+
}
|
|
7988
|
+
return fields;
|
|
7989
|
+
} catch (err) {
|
|
7990
|
+
logger$1.warn({
|
|
7991
|
+
connectionId,
|
|
7992
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7993
|
+
}, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
|
|
7994
|
+
return;
|
|
7995
|
+
}
|
|
7996
|
+
const key = provider.toLowerCase();
|
|
7997
|
+
try {
|
|
7998
|
+
switch (key) {
|
|
7999
|
+
case "atlassian": return await agentApi.getAtlassianCredentials();
|
|
8000
|
+
case "github": return await agentApi.getGithubCredentials();
|
|
8001
|
+
case "xero": return await agentApi.getXeroCredentials();
|
|
8002
|
+
case "notion": return await agentApi.getNotionCredentials();
|
|
8003
|
+
case "myob": return await agentApi.getMYOBCredentials();
|
|
8004
|
+
case "google": return await agentApi.getGoogleCredentials();
|
|
8005
|
+
default:
|
|
8006
|
+
logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
|
|
8007
|
+
return;
|
|
8008
|
+
}
|
|
8009
|
+
} catch {
|
|
8010
|
+
return;
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
/**
|
|
8014
|
+
* Post-reconcile trigger: rebuild the command registry from the manager's
|
|
8015
|
+
* active integrations and report the available commands to the cloud
|
|
8016
|
+
* gateway. Wired by daemon.ts as the cloud client's
|
|
8017
|
+
* `onReconciliationComplete` callback.
|
|
8018
|
+
*/
|
|
8019
|
+
function reportAvailableCommands(deps) {
|
|
8020
|
+
const { integrationManager, commandRegistry, cloudClient } = deps;
|
|
8021
|
+
try {
|
|
8022
|
+
const activeCommands = integrationManager.getActiveCommands();
|
|
8023
|
+
commandRegistry.clear();
|
|
8024
|
+
for (const { integrationId, commands } of activeCommands) for (const cmd of commands) commandRegistry.register(integrationId, cmd.name, cmd.resolvedPath, cmd.method, cmd.timeoutMs);
|
|
8025
|
+
const commandsMsg = {
|
|
8026
|
+
type: "COMMANDS_AVAILABLE",
|
|
8027
|
+
commands: commandRegistry.listCommands()
|
|
8028
|
+
};
|
|
8029
|
+
cloudClient.sendMessage(commandsMsg);
|
|
8030
|
+
logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
|
|
8031
|
+
} catch (err) {
|
|
8032
|
+
logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
//#endregion
|
|
8036
|
+
//#region ../../node_modules/.pnpm/dotenv@17.2.2/node_modules/dotenv/package.json
|
|
8037
|
+
var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
8038
|
+
module.exports = {
|
|
8039
|
+
"name": "dotenv",
|
|
8040
|
+
"version": "17.2.2",
|
|
8041
|
+
"description": "Loads environment variables from .env file",
|
|
8042
|
+
"main": "lib/main.js",
|
|
8043
|
+
"types": "lib/main.d.ts",
|
|
8044
|
+
"exports": {
|
|
8045
|
+
".": {
|
|
8046
|
+
"types": "./lib/main.d.ts",
|
|
8047
|
+
"require": "./lib/main.js",
|
|
8048
|
+
"default": "./lib/main.js"
|
|
8049
|
+
},
|
|
8050
|
+
"./config": "./config.js",
|
|
8051
|
+
"./config.js": "./config.js",
|
|
8052
|
+
"./lib/env-options": "./lib/env-options.js",
|
|
8053
|
+
"./lib/env-options.js": "./lib/env-options.js",
|
|
8054
|
+
"./lib/cli-options": "./lib/cli-options.js",
|
|
8055
|
+
"./lib/cli-options.js": "./lib/cli-options.js",
|
|
8056
|
+
"./package.json": "./package.json"
|
|
8057
|
+
},
|
|
8058
|
+
"scripts": {
|
|
8059
|
+
"dts-check": "tsc --project tests/types/tsconfig.json",
|
|
8060
|
+
"lint": "standard",
|
|
8061
|
+
"pretest": "npm run lint && npm run dts-check",
|
|
8062
|
+
"test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
|
|
8063
|
+
"test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
|
|
8064
|
+
"prerelease": "npm test",
|
|
8065
|
+
"release": "standard-version"
|
|
8066
|
+
},
|
|
8067
|
+
"repository": {
|
|
8068
|
+
"type": "git",
|
|
8069
|
+
"url": "git://github.com/motdotla/dotenv.git"
|
|
8070
|
+
},
|
|
8071
|
+
"homepage": "https://github.com/motdotla/dotenv#readme",
|
|
8072
|
+
"funding": "https://dotenvx.com",
|
|
8073
|
+
"keywords": [
|
|
8074
|
+
"dotenv",
|
|
8075
|
+
"env",
|
|
8076
|
+
".env",
|
|
8077
|
+
"environment",
|
|
8078
|
+
"variables",
|
|
8079
|
+
"config",
|
|
8080
|
+
"settings"
|
|
8081
|
+
],
|
|
8082
|
+
"readmeFilename": "README.md",
|
|
8083
|
+
"license": "BSD-2-Clause",
|
|
8084
|
+
"devDependencies": {
|
|
8085
|
+
"@types/node": "^18.11.3",
|
|
8086
|
+
"decache": "^4.6.2",
|
|
8087
|
+
"sinon": "^14.0.1",
|
|
8088
|
+
"standard": "^17.0.0",
|
|
8089
|
+
"standard-version": "^9.5.0",
|
|
8090
|
+
"tap": "^19.2.0",
|
|
8091
|
+
"typescript": "^4.8.4"
|
|
8092
|
+
},
|
|
8093
|
+
"engines": { "node": ">=12" },
|
|
8094
|
+
"browser": { "fs": false }
|
|
8095
|
+
};
|
|
8096
|
+
}));
|
|
8097
|
+
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
8098
|
+
const fs$1 = __require("fs");
|
|
8099
|
+
const path$1 = __require("path");
|
|
8100
|
+
const os$1 = __require("os");
|
|
8101
|
+
const crypto$2 = __require("crypto");
|
|
8102
|
+
const version = require_package().version;
|
|
8103
|
+
const TIPS = [
|
|
8104
|
+
"🔐 encrypt with Dotenvx: https://dotenvx.com",
|
|
8105
|
+
"🔐 prevent committing .env to code: https://dotenvx.com/precommit",
|
|
8106
|
+
"🔐 prevent building .env in docker: https://dotenvx.com/prebuild",
|
|
8107
|
+
"📡 observe env with Radar: https://dotenvx.com/radar",
|
|
8108
|
+
"📡 auto-backup env with Radar: https://dotenvx.com/radar",
|
|
8109
|
+
"📡 version env with Radar: https://dotenvx.com/radar",
|
|
8110
|
+
"🛠️ run anywhere with `dotenvx run -- yourcommand`",
|
|
8111
|
+
"⚙️ specify custom .env file path with { path: '/custom/path/.env' }",
|
|
8112
|
+
"⚙️ enable debug logging with { debug: true }",
|
|
8113
|
+
"⚙️ override existing env vars with { override: true }",
|
|
8114
|
+
"⚙️ suppress all logs with { quiet: true }",
|
|
8115
|
+
"⚙️ write to custom object with { processEnv: myObject }",
|
|
8116
|
+
"⚙️ load multiple .env files with { path: ['.env.local', '.env'] }"
|
|
8117
|
+
];
|
|
8118
|
+
function _getRandomTip() {
|
|
8119
|
+
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
8120
|
+
}
|
|
8121
|
+
function parseBoolean(value) {
|
|
8122
|
+
if (typeof value === "string") return ![
|
|
8123
|
+
"false",
|
|
8124
|
+
"0",
|
|
8125
|
+
"no",
|
|
8126
|
+
"off",
|
|
8127
|
+
""
|
|
8128
|
+
].includes(value.toLowerCase());
|
|
8129
|
+
return Boolean(value);
|
|
8130
|
+
}
|
|
8131
|
+
function supportsAnsi() {
|
|
8132
|
+
return process.stdout.isTTY;
|
|
8133
|
+
}
|
|
8134
|
+
function dim(text) {
|
|
8135
|
+
return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text;
|
|
8136
|
+
}
|
|
8137
|
+
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
|
|
8138
|
+
function parse(src) {
|
|
8139
|
+
const obj = {};
|
|
8140
|
+
let lines = src.toString();
|
|
8141
|
+
lines = lines.replace(/\r\n?/gm, "\n");
|
|
8142
|
+
let match;
|
|
8143
|
+
while ((match = LINE.exec(lines)) != null) {
|
|
8144
|
+
const key = match[1];
|
|
8145
|
+
let value = match[2] || "";
|
|
8146
|
+
value = value.trim();
|
|
8147
|
+
const maybeQuote = value[0];
|
|
8148
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
|
|
8149
|
+
if (maybeQuote === "\"") {
|
|
8150
|
+
value = value.replace(/\\n/g, "\n");
|
|
8151
|
+
value = value.replace(/\\r/g, "\r");
|
|
8152
|
+
}
|
|
8153
|
+
obj[key] = value;
|
|
8154
|
+
}
|
|
8155
|
+
return obj;
|
|
8156
|
+
}
|
|
8157
|
+
function _parseVault(options) {
|
|
8158
|
+
options = options || {};
|
|
8159
|
+
const vaultPath = _vaultPath(options);
|
|
8160
|
+
options.path = vaultPath;
|
|
8161
|
+
const result = DotenvModule.configDotenv(options);
|
|
8162
|
+
if (!result.parsed) {
|
|
8163
|
+
const err = /* @__PURE__ */ new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
|
|
8164
|
+
err.code = "MISSING_DATA";
|
|
8165
|
+
throw err;
|
|
8166
|
+
}
|
|
8167
|
+
const keys = _dotenvKey(options).split(",");
|
|
8168
|
+
const length = keys.length;
|
|
8169
|
+
let decrypted;
|
|
8170
|
+
for (let i = 0; i < length; i++) try {
|
|
8171
|
+
const attrs = _instructions(result, keys[i].trim());
|
|
8172
|
+
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
|
|
8173
|
+
break;
|
|
8174
|
+
} catch (error) {
|
|
8175
|
+
if (i + 1 >= length) throw error;
|
|
8176
|
+
}
|
|
8177
|
+
return DotenvModule.parse(decrypted);
|
|
8178
|
+
}
|
|
8179
|
+
function _warn(message) {
|
|
8180
|
+
console.error(`[dotenv@${version}][WARN] ${message}`);
|
|
8181
|
+
}
|
|
8182
|
+
function _debug(message) {
|
|
8183
|
+
console.log(`[dotenv@${version}][DEBUG] ${message}`);
|
|
8184
|
+
}
|
|
8185
|
+
function _log(message) {
|
|
8186
|
+
console.log(`[dotenv@${version}] ${message}`);
|
|
8187
|
+
}
|
|
8188
|
+
function _dotenvKey(options) {
|
|
8189
|
+
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
|
|
8190
|
+
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY;
|
|
8191
|
+
return "";
|
|
8192
|
+
}
|
|
8193
|
+
function _instructions(result, dotenvKey) {
|
|
8194
|
+
let uri;
|
|
8195
|
+
try {
|
|
8196
|
+
uri = new URL(dotenvKey);
|
|
8197
|
+
} catch (error) {
|
|
8198
|
+
if (error.code === "ERR_INVALID_URL") {
|
|
8199
|
+
const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
|
|
8200
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
8201
|
+
throw err;
|
|
8202
|
+
}
|
|
8203
|
+
throw error;
|
|
8204
|
+
}
|
|
8205
|
+
const key = uri.password;
|
|
7448
8206
|
if (!key) {
|
|
7449
8207
|
const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing key part");
|
|
7450
8208
|
err.code = "INVALID_DOTENV_KEY";
|
|
@@ -22568,7 +23326,7 @@ var RuntimeProcess = class {
|
|
|
22568
23326
|
* Start the runtime process.
|
|
22569
23327
|
*/
|
|
22570
23328
|
start() {
|
|
22571
|
-
if (this.stopped) return;
|
|
23329
|
+
if (this.stopped || this.child !== null) return;
|
|
22572
23330
|
const { command, args } = this.resolveCommand();
|
|
22573
23331
|
this.lastStartTime = Date.now();
|
|
22574
23332
|
log$3.info({
|
|
@@ -22577,7 +23335,7 @@ var RuntimeProcess = class {
|
|
|
22577
23335
|
args,
|
|
22578
23336
|
workspace: this.options.workspace
|
|
22579
23337
|
}, "Starting runtime process");
|
|
22580
|
-
|
|
23338
|
+
const child = spawn(command, args, {
|
|
22581
23339
|
cwd: this.options.workspace,
|
|
22582
23340
|
env: {
|
|
22583
23341
|
...process.env,
|
|
@@ -22589,7 +23347,9 @@ var RuntimeProcess = class {
|
|
|
22589
23347
|
"pipe"
|
|
22590
23348
|
]
|
|
22591
23349
|
});
|
|
22592
|
-
this.child
|
|
23350
|
+
this.child = child;
|
|
23351
|
+
let terminalHandled = false;
|
|
23352
|
+
child.stdout.on("data", (data) => {
|
|
22593
23353
|
const lines = data.toString().trim().split("\n");
|
|
22594
23354
|
for (const line of lines) {
|
|
22595
23355
|
log$3.info({
|
|
@@ -22599,7 +23359,7 @@ var RuntimeProcess = class {
|
|
|
22599
23359
|
this.observeLine("stdout", line);
|
|
22600
23360
|
}
|
|
22601
23361
|
});
|
|
22602
|
-
|
|
23362
|
+
child.stderr.on("data", (data) => {
|
|
22603
23363
|
const lines = data.toString().trim().split("\n");
|
|
22604
23364
|
for (const line of lines) {
|
|
22605
23365
|
log$3.warn({
|
|
@@ -22609,8 +23369,14 @@ var RuntimeProcess = class {
|
|
|
22609
23369
|
this.observeLine("stderr", line);
|
|
22610
23370
|
}
|
|
22611
23371
|
});
|
|
22612
|
-
|
|
22613
|
-
|
|
23372
|
+
const handleTermination = (code, signal, spawnError) => {
|
|
23373
|
+
if (terminalHandled) return;
|
|
23374
|
+
terminalHandled = true;
|
|
23375
|
+
if (this.child === child) this.child = null;
|
|
23376
|
+
if (spawnError) log$3.error({
|
|
23377
|
+
runtime: this.options.runtime,
|
|
23378
|
+
err: spawnError.message
|
|
23379
|
+
}, "Runtime process error");
|
|
22614
23380
|
if (this.stopped) {
|
|
22615
23381
|
log$3.info({
|
|
22616
23382
|
runtime: this.options.runtime,
|
|
@@ -22619,7 +23385,7 @@ var RuntimeProcess = class {
|
|
|
22619
23385
|
}, "Runtime stopped (expected)");
|
|
22620
23386
|
return;
|
|
22621
23387
|
}
|
|
22622
|
-
if (code === 0 && signal == null) {
|
|
23388
|
+
if (!spawnError && code === 0 && signal == null) {
|
|
22623
23389
|
log$3.info({
|
|
22624
23390
|
runtime: this.options.runtime,
|
|
22625
23391
|
code
|
|
@@ -22634,6 +23400,7 @@ var RuntimeProcess = class {
|
|
|
22634
23400
|
runtime: this.options.runtime,
|
|
22635
23401
|
code,
|
|
22636
23402
|
signal,
|
|
23403
|
+
spawnError: spawnError?.message,
|
|
22637
23404
|
backoffMs: this.backoffMs
|
|
22638
23405
|
}, "Runtime crashed — scheduling restart with backoff");
|
|
22639
23406
|
const uptime = Date.now() - this.lastStartTime;
|
|
@@ -22649,7 +23416,8 @@ var RuntimeProcess = class {
|
|
|
22649
23416
|
signal,
|
|
22650
23417
|
uptimeMs: uptime,
|
|
22651
23418
|
recentOutput: this.ringBuffer.snapshot(),
|
|
22652
|
-
crashesSuppressed: crash.suppressedCount
|
|
23419
|
+
crashesSuppressed: crash.suppressedCount,
|
|
23420
|
+
...spawnError ? { spawnError } : {}
|
|
22653
23421
|
});
|
|
22654
23422
|
else log$3.debug({
|
|
22655
23423
|
runtime: this.options.runtime,
|
|
@@ -22661,23 +23429,12 @@ var RuntimeProcess = class {
|
|
|
22661
23429
|
this.start();
|
|
22662
23430
|
}, this.backoffMs);
|
|
22663
23431
|
this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
|
|
23432
|
+
};
|
|
23433
|
+
child.on("exit", (code, signal) => {
|
|
23434
|
+
handleTermination(code, signal);
|
|
22664
23435
|
});
|
|
22665
|
-
|
|
22666
|
-
|
|
22667
|
-
runtime: this.options.runtime,
|
|
22668
|
-
err: err.message
|
|
22669
|
-
}, "Runtime process error");
|
|
22670
|
-
if (this.stopped) return;
|
|
22671
|
-
const crash = this.throttle.allowCrashCapture(Date.now() - this.lastStartTime);
|
|
22672
|
-
if (crash.allow) captureRuntimeCrash({
|
|
22673
|
-
runtime: this.options.runtime,
|
|
22674
|
-
code: null,
|
|
22675
|
-
signal: null,
|
|
22676
|
-
uptimeMs: Date.now() - this.lastStartTime,
|
|
22677
|
-
recentOutput: this.ringBuffer.snapshot(),
|
|
22678
|
-
crashesSuppressed: crash.suppressedCount,
|
|
22679
|
-
spawnError: err
|
|
22680
|
-
});
|
|
23436
|
+
child.on("error", (err) => {
|
|
23437
|
+
handleTermination(null, null, err);
|
|
22681
23438
|
});
|
|
22682
23439
|
}
|
|
22683
23440
|
/**
|
|
@@ -22746,14 +23503,19 @@ var RuntimeProcess = class {
|
|
|
22746
23503
|
const child = this.child;
|
|
22747
23504
|
if (!child) return;
|
|
22748
23505
|
return new Promise((resolve) => {
|
|
23506
|
+
let settled = false;
|
|
23507
|
+
const finish = () => {
|
|
23508
|
+
if (settled) return;
|
|
23509
|
+
settled = true;
|
|
23510
|
+
clearTimeout(killTimer);
|
|
23511
|
+
resolve();
|
|
23512
|
+
};
|
|
22749
23513
|
const killTimer = setTimeout(() => {
|
|
22750
23514
|
log$3.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
|
|
22751
23515
|
child.kill("SIGKILL");
|
|
22752
23516
|
}, 5e3);
|
|
22753
|
-
child.
|
|
22754
|
-
|
|
22755
|
-
resolve();
|
|
22756
|
-
});
|
|
23517
|
+
child.once("exit", finish);
|
|
23518
|
+
child.once("error", finish);
|
|
22757
23519
|
child.kill("SIGTERM");
|
|
22758
23520
|
});
|
|
22759
23521
|
}
|
|
@@ -22925,6 +23687,8 @@ var IpcTurnActivityProbe = class {
|
|
|
22925
23687
|
* via ESM dynamic import on first use and cached until the registry is cleared.
|
|
22926
23688
|
*/
|
|
22927
23689
|
const log$1 = createLogger("CommandRegistry");
|
|
23690
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 3e4;
|
|
23691
|
+
const MAX_COMMAND_TIMEOUT_MS = 1800 * 1e3;
|
|
22928
23692
|
var CommandRegistry = class {
|
|
22929
23693
|
commands = /* @__PURE__ */ new Map();
|
|
22930
23694
|
version = 0;
|
|
@@ -22932,7 +23696,7 @@ var CommandRegistry = class {
|
|
|
22932
23696
|
* Register a command from an integration.
|
|
22933
23697
|
* Rejects duplicate command names — first registration wins.
|
|
22934
23698
|
*/
|
|
22935
|
-
register(integrationId, name, handlerPath, method = "handle", timeoutMs =
|
|
23699
|
+
register(integrationId, name, handlerPath, method = "handle", timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS) {
|
|
22936
23700
|
const existing = this.commands.get(name);
|
|
22937
23701
|
if (existing) {
|
|
22938
23702
|
log$1.warn({
|
|
@@ -22950,12 +23714,13 @@ var CommandRegistry = class {
|
|
|
22950
23714
|
}, "Handler file does not exist — skipping registration");
|
|
22951
23715
|
return;
|
|
22952
23716
|
}
|
|
23717
|
+
const boundedTimeoutMs = Number.isSafeInteger(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_COMMAND_TIMEOUT_MS) : DEFAULT_COMMAND_TIMEOUT_MS;
|
|
22953
23718
|
this.commands.set(name, {
|
|
22954
23719
|
commandName: name,
|
|
22955
23720
|
integrationId,
|
|
22956
23721
|
handlerPath,
|
|
22957
23722
|
handlerMethod: method,
|
|
22958
|
-
timeoutMs,
|
|
23723
|
+
timeoutMs: boundedTimeoutMs,
|
|
22959
23724
|
handler: null
|
|
22960
23725
|
});
|
|
22961
23726
|
log$1.info({
|
|
@@ -22963,7 +23728,7 @@ var CommandRegistry = class {
|
|
|
22963
23728
|
integrationId,
|
|
22964
23729
|
handlerPath,
|
|
22965
23730
|
method,
|
|
22966
|
-
timeoutMs
|
|
23731
|
+
timeoutMs: boundedTimeoutMs
|
|
22967
23732
|
}, "Registered command");
|
|
22968
23733
|
}
|
|
22969
23734
|
/**
|
|
@@ -23023,9 +23788,10 @@ var CommandRegistry = class {
|
|
|
23023
23788
|
}
|
|
23024
23789
|
};
|
|
23025
23790
|
}
|
|
23791
|
+
let timeout;
|
|
23026
23792
|
try {
|
|
23027
23793
|
return await Promise.race([entry.handler(payload, context), new Promise((_, reject) => {
|
|
23028
|
-
setTimeout(() => {
|
|
23794
|
+
timeout = setTimeout(() => {
|
|
23029
23795
|
reject(/* @__PURE__ */ new Error(`Command "${name}" timed out after ${String(entry.timeoutMs)}ms`));
|
|
23030
23796
|
}, entry.timeoutMs);
|
|
23031
23797
|
})]);
|
|
@@ -23042,6 +23808,8 @@ var CommandRegistry = class {
|
|
|
23042
23808
|
message
|
|
23043
23809
|
}
|
|
23044
23810
|
};
|
|
23811
|
+
} finally {
|
|
23812
|
+
if (timeout) clearTimeout(timeout);
|
|
23045
23813
|
}
|
|
23046
23814
|
}
|
|
23047
23815
|
/**
|
|
@@ -23091,13 +23859,18 @@ var CommandDedupe = class {
|
|
|
23091
23859
|
duplicate: true
|
|
23092
23860
|
};
|
|
23093
23861
|
const promise = exec();
|
|
23094
|
-
|
|
23862
|
+
const entry = {
|
|
23095
23863
|
insertedAt: this.now(),
|
|
23096
|
-
promise
|
|
23097
|
-
|
|
23864
|
+
promise,
|
|
23865
|
+
settled: false
|
|
23866
|
+
};
|
|
23867
|
+
this.entries.set(commandId, entry);
|
|
23098
23868
|
this.evictOverCap();
|
|
23099
|
-
promise.
|
|
23100
|
-
|
|
23869
|
+
promise.then(() => {
|
|
23870
|
+
entry.settled = true;
|
|
23871
|
+
this.evictOverCap();
|
|
23872
|
+
}, () => {
|
|
23873
|
+
if (this.entries.get(commandId) === entry) this.entries.delete(commandId);
|
|
23101
23874
|
});
|
|
23102
23875
|
return {
|
|
23103
23876
|
ack: await promise,
|
|
@@ -23112,7 +23885,7 @@ var CommandDedupe = class {
|
|
|
23112
23885
|
has(commandId) {
|
|
23113
23886
|
const e = this.entries.get(commandId);
|
|
23114
23887
|
if (!e) return false;
|
|
23115
|
-
if (this.now() - e.insertedAt > this.ttlMs) {
|
|
23888
|
+
if (e.settled && this.now() - e.insertedAt > this.ttlMs) {
|
|
23116
23889
|
this.entries.delete(commandId);
|
|
23117
23890
|
return false;
|
|
23118
23891
|
}
|
|
@@ -23120,13 +23893,13 @@ var CommandDedupe = class {
|
|
|
23120
23893
|
}
|
|
23121
23894
|
evictExpired() {
|
|
23122
23895
|
const cutoff = this.now() - this.ttlMs;
|
|
23123
|
-
for (const [id, entry] of this.entries) if (entry.insertedAt <= cutoff) this.entries.delete(id);
|
|
23896
|
+
for (const [id, entry] of this.entries) if (entry.settled && entry.insertedAt <= cutoff) this.entries.delete(id);
|
|
23124
23897
|
}
|
|
23125
23898
|
evictOverCap() {
|
|
23126
23899
|
while (this.entries.size > this.maxEntries) {
|
|
23127
|
-
const
|
|
23128
|
-
if (
|
|
23129
|
-
this.entries.delete(
|
|
23900
|
+
const oldestSettled = Array.from(this.entries).find(([, entry]) => entry.settled);
|
|
23901
|
+
if (!oldestSettled) break;
|
|
23902
|
+
this.entries.delete(oldestSettled[0]);
|
|
23130
23903
|
}
|
|
23131
23904
|
}
|
|
23132
23905
|
};
|
|
@@ -23151,7 +23924,7 @@ var CommandDedupe = class {
|
|
|
23151
23924
|
* the gateway when needed and uses `callerScopes: ["operator.admin"]`.
|
|
23152
23925
|
* 3. Skip iteration if `pending.json` mtime hasn't changed (cheap fast path)
|
|
23153
23926
|
*/
|
|
23154
|
-
const execFileAsync$
|
|
23927
|
+
const execFileAsync$1 = promisify(execFile);
|
|
23155
23928
|
const APPROVE_TIMEOUT_MS = 1e4;
|
|
23156
23929
|
function resolveStateDir(override) {
|
|
23157
23930
|
if (override) return override;
|
|
@@ -23250,7 +24023,7 @@ function startPairingApprovalPoller(opts) {
|
|
|
23250
24023
|
const intervalMs = opts.intervalMs ?? 3e4;
|
|
23251
24024
|
const cliLock = opts.cliLock ?? new NoopOpenClawCliLock();
|
|
23252
24025
|
const rawExec = opts.exec ?? (async (file, args, { timeout }) => {
|
|
23253
|
-
const { stdout, stderr } = await execFileAsync$
|
|
24026
|
+
const { stdout, stderr } = await execFileAsync$1(file, args, { timeout });
|
|
23254
24027
|
return {
|
|
23255
24028
|
stdout,
|
|
23256
24029
|
stderr
|
|
@@ -23324,12 +24097,12 @@ function startPairingApprovalPoller(opts) {
|
|
|
23324
24097
|
* Sentinel-gated at `~/.alfe/.openclaw-mirror-migrated` — runs exactly
|
|
23325
24098
|
* once per agent.
|
|
23326
24099
|
*/
|
|
23327
|
-
const execFileAsync
|
|
24100
|
+
const execFileAsync = promisify(execFile);
|
|
23328
24101
|
const DEFAULT_SENTINEL_PATH = join(homedir(), ".alfe", ".openclaw-mirror-migrated");
|
|
23329
24102
|
const defaultOpenclaw = {
|
|
23330
24103
|
async listServers() {
|
|
23331
24104
|
try {
|
|
23332
|
-
const { stdout } = await execFileAsync
|
|
24105
|
+
const { stdout } = await execFileAsync("openclaw", [
|
|
23333
24106
|
"config",
|
|
23334
24107
|
"get",
|
|
23335
24108
|
"mcp.servers"
|
|
@@ -23346,7 +24119,7 @@ const defaultOpenclaw = {
|
|
|
23346
24119
|
}
|
|
23347
24120
|
},
|
|
23348
24121
|
async unsetServer(key) {
|
|
23349
|
-
await execFileAsync
|
|
24122
|
+
await execFileAsync("openclaw", [
|
|
23350
24123
|
"config",
|
|
23351
24124
|
"unset",
|
|
23352
24125
|
`mcp.servers.${key}`
|
|
@@ -23528,8 +24301,12 @@ function createMcpErrorHooks() {
|
|
|
23528
24301
|
* 7. Start agent runtime (managed mode only — e.g. OpenClaw)
|
|
23529
24302
|
* 8. Write PID file (non-managed only)
|
|
23530
24303
|
* 9. Handle graceful shutdown
|
|
24304
|
+
*
|
|
24305
|
+
* Split along seams (this file stays the orchestrator):
|
|
24306
|
+
* - daemon-bootstrap.ts — version resolution, persona migration, AI-proxy + IPC start
|
|
24307
|
+
* - mcp-handlers.ts — `mcp.*` IPC request handlers
|
|
24308
|
+
* - integration-bootstrap.ts — provider-credential fetch + post-reconcile command reporting
|
|
23531
24309
|
*/
|
|
23532
|
-
const execFileAsync = promisify(execFile);
|
|
23533
24310
|
let config;
|
|
23534
24311
|
let cloudClient;
|
|
23535
24312
|
let ipcServer = null;
|
|
@@ -23538,225 +24315,46 @@ let startedAt;
|
|
|
23538
24315
|
let integrationManager;
|
|
23539
24316
|
let mcpBundler = null;
|
|
23540
24317
|
let mcpManagerRef = null;
|
|
23541
|
-
/**
|
|
23542
|
-
* Hermes-only MCP store consumer (Approach B). Mirrors the runtime-agnostic MCP
|
|
23543
|
-
* store into ~/.hermes/config.yaml. Null for openclaw agents (never constructed)
|
|
23544
|
-
* and for managed/self-hosted runtimes other than hermes.
|
|
23545
|
-
*/
|
|
23546
|
-
let hermesMcpSync = null;
|
|
23547
|
-
/**
|
|
23548
|
-
* claude-code-only MCP store consumer. Mirrors the runtime-agnostic MCP store
|
|
23549
|
-
* into the host's `--mcp-config` JSON (~/.claude-code/mcp-config.json), injecting
|
|
23550
|
-
* ALFE_API_KEY into every stdio server's env. Null for every other runtime
|
|
23551
|
-
* (never constructed) — same shape/role as {@link hermesMcpSync}.
|
|
23552
|
-
*/
|
|
23553
|
-
let claudeCodeMcpSync = null;
|
|
23554
|
-
let aiProxyServer = null;
|
|
23555
|
-
let runtimeProcess = null;
|
|
23556
|
-
let turnActivityProbe = null;
|
|
23557
|
-
let aiProxyUrl = null;
|
|
23558
|
-
let aiProxyRunning = false;
|
|
23559
|
-
let cloudConnected = false;
|
|
23560
|
-
/**
|
|
23561
|
-
* Dedupe for at-least-once durable command delivery (Phase B). The cloud may
|
|
23562
|
-
* push a command over the live WS AND re-drain the same commandId on the next
|
|
23563
|
-
* SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
|
|
23564
|
-
* so a duplicate is ACKed with the prior result without re-executing.
|
|
23565
|
-
*/
|
|
23566
|
-
const commandDedupe = new CommandDedupe();
|
|
23567
|
-
let shuttingDown = false;
|
|
23568
|
-
let commandRegistry;
|
|
23569
|
-
let resolvedCliVersion;
|
|
23570
|
-
let resolvedRuntimeVersion;
|
|
23571
|
-
let upgradingRuntime = false;
|
|
23572
|
-
let stopPairingApprovalPoller = null;
|
|
23573
|
-
/**
|
|
23574
|
-
* Module-level handle on the runtime appliers built during start(), so the
|
|
23575
|
-
* module-scoped command handler can route `alfe.config_set` to the active
|
|
23576
|
-
* runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
|
|
23577
|
-
*/
|
|
23578
|
-
let runtimeAppliersRef = null;
|
|
23579
|
-
/**
|
|
23580
|
-
* Resolve the installed @alfe.ai/cli version.
|
|
23581
|
-
*
|
|
23582
|
-
* Strategy (in order):
|
|
23583
|
-
* 1. ALFE_CLI_VERSION env var (set by CLI entry point when run directly)
|
|
23584
|
-
* 2. Walk up from this file to find @alfe.ai/cli/package.json
|
|
23585
|
-
* (works when systemd runs the gateway binary directly, since
|
|
23586
|
-
* the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
|
|
23587
|
-
*/
|
|
23588
|
-
/**
|
|
23589
|
-
* Adapter from `AgentApiClient`'s per-provider methods to the
|
|
23590
|
-
* `CredentialsResolver` shape the MCP applier expects. Returns
|
|
23591
|
-
* `undefined` for unknown providers and on 404/network error so the
|
|
23592
|
-
* applier can skip registration silently (its documented contract).
|
|
23593
|
-
*/
|
|
23594
|
-
async function fetchProviderCredentials(agentApi, provider, connectionId) {
|
|
23595
|
-
if (connectionId) try {
|
|
23596
|
-
const raw = await agentApi.getConnectionCredentials(connectionId);
|
|
23597
|
-
const fields = { ...raw.providerMetadata ?? {} };
|
|
23598
|
-
if (typeof raw.accessToken === "string" && raw.accessToken.length > 0) try {
|
|
23599
|
-
const bundle = JSON.parse(raw.accessToken);
|
|
23600
|
-
if (bundle && typeof bundle === "object" && !Array.isArray(bundle)) Object.assign(fields, bundle);
|
|
23601
|
-
} catch (err) {
|
|
23602
|
-
logger$1.warn({
|
|
23603
|
-
connectionId,
|
|
23604
|
-
err: err instanceof Error ? err.message : String(err)
|
|
23605
|
-
}, "Custom connection accessToken is not a JSON bundle — exposing as accessToken field");
|
|
23606
|
-
fields.accessToken = raw.accessToken;
|
|
23607
|
-
}
|
|
23608
|
-
return fields;
|
|
23609
|
-
} catch (err) {
|
|
23610
|
-
logger$1.warn({
|
|
23611
|
-
connectionId,
|
|
23612
|
-
err: err instanceof Error ? err.message : String(err)
|
|
23613
|
-
}, "Failed to resolve connection-scoped credentials — MCP server will be skipped");
|
|
23614
|
-
return;
|
|
23615
|
-
}
|
|
23616
|
-
const key = provider.toLowerCase();
|
|
23617
|
-
try {
|
|
23618
|
-
switch (key) {
|
|
23619
|
-
case "atlassian": return await agentApi.getAtlassianCredentials();
|
|
23620
|
-
case "github": return await agentApi.getGithubCredentials();
|
|
23621
|
-
case "xero": return await agentApi.getXeroCredentials();
|
|
23622
|
-
case "notion": return await agentApi.getNotionCredentials();
|
|
23623
|
-
case "myob": return await agentApi.getMYOBCredentials();
|
|
23624
|
-
case "google": return await agentApi.getGoogleCredentials();
|
|
23625
|
-
default:
|
|
23626
|
-
logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
|
|
23627
|
-
return;
|
|
23628
|
-
}
|
|
23629
|
-
} catch {
|
|
23630
|
-
return;
|
|
23631
|
-
}
|
|
23632
|
-
}
|
|
23633
|
-
async function getCliVersion() {
|
|
23634
|
-
if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
|
|
23635
|
-
try {
|
|
23636
|
-
const { fileURLToPath } = await import("node:url");
|
|
23637
|
-
const { dirname } = await import("node:path");
|
|
23638
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
23639
|
-
for (let i = 0; i < 10; i++) {
|
|
23640
|
-
const candidate = join(dir, "package.json");
|
|
23641
|
-
try {
|
|
23642
|
-
const raw = await readFile(candidate, "utf-8");
|
|
23643
|
-
const pkg = JSON.parse(raw);
|
|
23644
|
-
if (pkg.name === "@alfe.ai/cli") return pkg.version;
|
|
23645
|
-
} catch {}
|
|
23646
|
-
const parent = dirname(dir);
|
|
23647
|
-
if (parent === dir) break;
|
|
23648
|
-
dir = parent;
|
|
23649
|
-
}
|
|
23650
|
-
} catch {}
|
|
23651
|
-
logger$1.debug("Could not resolve @alfe.ai/cli version");
|
|
23652
|
-
}
|
|
23653
|
-
/**
|
|
23654
|
-
* Per-runtime "print installed version" commands. Local to the gateway — we do
|
|
23655
|
-
* NOT import the CLI's `detectRuntime` (`@alfe.ai/gateway` must not depend on
|
|
23656
|
-
* `@alfe.ai/cli`). Unknown runtimes resolve to `undefined` (never throw) so the
|
|
23657
|
-
* connection-status report degrades gracefully instead of crashing.
|
|
23658
|
-
*/
|
|
23659
|
-
const RUNTIME_VERSION_COMMANDS = {
|
|
23660
|
-
openclaw: {
|
|
23661
|
-
command: "openclaw",
|
|
23662
|
-
args: ["--version"]
|
|
23663
|
-
},
|
|
23664
|
-
hermes: {
|
|
23665
|
-
command: "hermes",
|
|
23666
|
-
args: ["version"]
|
|
23667
|
-
},
|
|
23668
|
-
"claude-code": {
|
|
23669
|
-
command: "alfe-claude-host",
|
|
23670
|
-
args: ["--version"]
|
|
23671
|
-
}
|
|
23672
|
-
};
|
|
23673
|
-
/**
|
|
23674
|
-
* Pure resolver (exported for tests) — the per-runtime version command, or
|
|
23675
|
-
* `undefined` for an unknown runtime.
|
|
24318
|
+
/**
|
|
24319
|
+
* Hermes-only MCP store consumer (Approach B). Mirrors the runtime-agnostic MCP
|
|
24320
|
+
* store into ~/.hermes/config.yaml. Null for openclaw agents (never constructed)
|
|
24321
|
+
* and for managed/self-hosted runtimes other than hermes.
|
|
23676
24322
|
*/
|
|
23677
|
-
|
|
23678
|
-
return RUNTIME_VERSION_COMMANDS[runtime];
|
|
23679
|
-
}
|
|
24323
|
+
let hermesMcpSync = null;
|
|
23680
24324
|
/**
|
|
23681
|
-
*
|
|
23682
|
-
*
|
|
23683
|
-
*
|
|
24325
|
+
* claude-code-only MCP store consumer. Mirrors the runtime-agnostic MCP store
|
|
24326
|
+
* into the host's `--mcp-config` JSON (~/.claude-code/mcp-config.json), injecting
|
|
24327
|
+
* ALFE_API_KEY into every stdio server's env. Null for every other runtime
|
|
24328
|
+
* (never constructed) — same shape/role as {@link hermesMcpSync}.
|
|
23684
24329
|
*/
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
|
-
|
|
23688
|
-
|
|
23689
|
-
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
const { stdout } = await execFileAsync(cmd.command, cmd.args);
|
|
23693
|
-
return stdout.trim() || void 0;
|
|
23694
|
-
} catch {
|
|
23695
|
-
logger$1.debug({ runtime }, "Could not resolve runtime version");
|
|
23696
|
-
return;
|
|
23697
|
-
}
|
|
23698
|
-
}
|
|
24330
|
+
let claudeCodeMcpSync = null;
|
|
24331
|
+
let aiProxyServer = null;
|
|
24332
|
+
let runtimeProcess = null;
|
|
24333
|
+
let turnActivityProbe = null;
|
|
24334
|
+
let aiProxyUrl = null;
|
|
24335
|
+
let aiProxyRunning = false;
|
|
24336
|
+
let cloudConnected = false;
|
|
23699
24337
|
/**
|
|
23700
|
-
*
|
|
23701
|
-
*
|
|
24338
|
+
* Dedupe for at-least-once durable command delivery (Phase B). The cloud may
|
|
24339
|
+
* push a command over the live WS AND re-drain the same commandId on the next
|
|
24340
|
+
* SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
|
|
24341
|
+
* so a duplicate is ACKed with the prior result without re-executing.
|
|
23702
24342
|
*/
|
|
23703
|
-
|
|
23704
|
-
|
|
23705
|
-
|
|
23706
|
-
|
|
23707
|
-
|
|
23708
|
-
|
|
23709
|
-
|
|
23710
|
-
}
|
|
24343
|
+
const commandDedupe = new CommandDedupe();
|
|
24344
|
+
let shuttingDown = false;
|
|
24345
|
+
let commandRegistry;
|
|
24346
|
+
let resolvedCliVersion;
|
|
24347
|
+
let resolvedRuntimeVersion;
|
|
24348
|
+
let upgradingRuntime = false;
|
|
24349
|
+
let stopPairingApprovalPoller = null;
|
|
23711
24350
|
/**
|
|
23712
|
-
*
|
|
23713
|
-
*
|
|
23714
|
-
*
|
|
23715
|
-
* (e.g. `~/.openclaw/workspace/`). Move stale copies into place; skip if the
|
|
23716
|
-
* workspace already has the file. Idempotent and safe to run on every daemon start.
|
|
24351
|
+
* Module-level handle on the runtime appliers built during start(), so the
|
|
24352
|
+
* module-scoped command handler can route `alfe.config_set` to the active
|
|
24353
|
+
* runtime's applier (mirrors `mcpManagerRef`). Null until start() builds it.
|
|
23717
24354
|
*/
|
|
23718
|
-
|
|
23719
|
-
|
|
23720
|
-
|
|
23721
|
-
"BOOTSTRAP.md",
|
|
23722
|
-
"AGENTS.md"
|
|
23723
|
-
];
|
|
23724
|
-
async function migrateLegacyPersonaFiles(home, agentWorkspace) {
|
|
23725
|
-
if (home === agentWorkspace) return;
|
|
23726
|
-
let moved = 0;
|
|
23727
|
-
let skipped = 0;
|
|
23728
|
-
for (const filename of PERSONA_FILES) {
|
|
23729
|
-
const src = join(home, filename);
|
|
23730
|
-
const dst = join(agentWorkspace, filename);
|
|
23731
|
-
try {
|
|
23732
|
-
await stat(src);
|
|
23733
|
-
} catch {
|
|
23734
|
-
continue;
|
|
23735
|
-
}
|
|
23736
|
-
try {
|
|
23737
|
-
await stat(dst);
|
|
23738
|
-
skipped++;
|
|
23739
|
-
continue;
|
|
23740
|
-
} catch {}
|
|
23741
|
-
try {
|
|
23742
|
-
await mkdir(agentWorkspace, { recursive: true });
|
|
23743
|
-
await rename(src, dst);
|
|
23744
|
-
moved++;
|
|
23745
|
-
} catch (err) {
|
|
23746
|
-
logger$1.warn({
|
|
23747
|
-
src,
|
|
23748
|
-
dst,
|
|
23749
|
-
err: err instanceof Error ? err.message : String(err)
|
|
23750
|
-
}, "Persona file migration: rename failed");
|
|
23751
|
-
}
|
|
23752
|
-
}
|
|
23753
|
-
if (moved > 0 || skipped > 0) logger$1.info({
|
|
23754
|
-
home,
|
|
23755
|
-
agentWorkspace,
|
|
23756
|
-
moved,
|
|
23757
|
-
skipped
|
|
23758
|
-
}, "Persona file backfill migration complete");
|
|
23759
|
-
}
|
|
24355
|
+
let runtimeAppliersRef = null;
|
|
24356
|
+
const LEGACY_CONFIG_SET_KEYS = new Set(["agents.defaults.model"]);
|
|
24357
|
+
const MAX_LEGACY_CONFIG_VALUE_LENGTH = 16 * 1024;
|
|
23760
24358
|
async function startDaemon() {
|
|
23761
24359
|
startedAt = Date.now();
|
|
23762
24360
|
const managed = isManagedMode();
|
|
@@ -23764,9 +24362,10 @@ async function startDaemon() {
|
|
|
23764
24362
|
managed,
|
|
23765
24363
|
pid: process.pid
|
|
23766
24364
|
}, "Starting Alfe Gateway Daemon...");
|
|
24365
|
+
resolvedCliVersion = await getCliVersion();
|
|
23767
24366
|
await initAgentSentry({
|
|
23768
24367
|
surface: "daemon",
|
|
23769
|
-
release:
|
|
24368
|
+
release: resolvedCliVersion
|
|
23770
24369
|
});
|
|
23771
24370
|
let fatalExiting = false;
|
|
23772
24371
|
process.on("uncaughtException", (err) => {
|
|
@@ -23816,55 +24415,12 @@ async function startDaemon() {
|
|
|
23816
24415
|
commandQueue = new CommandQueue();
|
|
23817
24416
|
commandQueue.startGC();
|
|
23818
24417
|
commandRegistry = new CommandRegistry();
|
|
23819
|
-
|
|
23820
|
-
|
|
23821
|
-
|
|
23822
|
-
|
|
23823
|
-
|
|
23824
|
-
const port = DEFAULT_AI_PROXY_PORT ?? 18193;
|
|
23825
|
-
aiProxyServer = createProxyServer({
|
|
23826
|
-
port,
|
|
23827
|
-
apiKey: config.apiKey,
|
|
23828
|
-
proxyUrl
|
|
23829
|
-
});
|
|
23830
|
-
const server = aiProxyServer;
|
|
23831
|
-
await new Promise((resolve, reject) => {
|
|
23832
|
-
server.listen(port, "127.0.0.1", () => {
|
|
23833
|
-
aiProxyRunning = true;
|
|
23834
|
-
aiProxyUrl = `http://127.0.0.1:${String(port)}`;
|
|
23835
|
-
logger$1.info({
|
|
23836
|
-
port,
|
|
23837
|
-
upstream: proxyUrl
|
|
23838
|
-
}, "AI proxy started");
|
|
23839
|
-
resolve();
|
|
23840
|
-
});
|
|
23841
|
-
server.on("error", reject);
|
|
23842
|
-
});
|
|
23843
|
-
} catch (err) {
|
|
23844
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
23845
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
23846
|
-
logger$1.error({
|
|
23847
|
-
err: message,
|
|
23848
|
-
stack
|
|
23849
|
-
}, "Failed to start AI proxy — LLM requests will fail");
|
|
23850
|
-
}
|
|
23851
|
-
logger$1.debug({ socketPath: config.socketPath }, "Starting IPC server...");
|
|
23852
|
-
ipcServer = new IPCServer(config.socketPath);
|
|
23853
|
-
ipcServer.setRequestHandler(handlePluginRequest);
|
|
24418
|
+
const aiProxy = await startAiProxy(config.apiKey);
|
|
24419
|
+
aiProxyServer = aiProxy.server;
|
|
24420
|
+
aiProxyUrl = aiProxy.url;
|
|
24421
|
+
aiProxyRunning = aiProxy.running;
|
|
24422
|
+
ipcServer = await startIpcServer(config.socketPath, handlePluginRequest, resolvedCliVersion);
|
|
23854
24423
|
turnActivityProbe = new IpcTurnActivityProbe(() => ipcServer);
|
|
23855
|
-
try {
|
|
23856
|
-
await ipcServer.start();
|
|
23857
|
-
logger$1.debug("IPC server started");
|
|
23858
|
-
} catch (err) {
|
|
23859
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
23860
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
23861
|
-
logger$1.error({
|
|
23862
|
-
err: message,
|
|
23863
|
-
stack
|
|
23864
|
-
}, "Failed to start IPC server");
|
|
23865
|
-
await flushAndExit(1);
|
|
23866
|
-
}
|
|
23867
|
-
resolvedCliVersion = await getCliVersion();
|
|
23868
24424
|
resolvedRuntimeVersion = await getRuntimeVersion(config.runtime);
|
|
23869
24425
|
logger$1.info({
|
|
23870
24426
|
cliVersion: resolvedCliVersion,
|
|
@@ -24007,19 +24563,11 @@ async function startDaemon() {
|
|
|
24007
24563
|
}
|
|
24008
24564
|
}
|
|
24009
24565
|
cloudClient.setOnReconciliationComplete(() => {
|
|
24010
|
-
|
|
24011
|
-
|
|
24012
|
-
commandRegistry
|
|
24013
|
-
|
|
24014
|
-
|
|
24015
|
-
type: "COMMANDS_AVAILABLE",
|
|
24016
|
-
commands: commandRegistry.listCommands()
|
|
24017
|
-
};
|
|
24018
|
-
cloudClient.sendMessage(commandsMsg);
|
|
24019
|
-
logger$1.info({ commandCount: commandsMsg.commands.length }, "Reported available commands to cloud");
|
|
24020
|
-
} catch (err) {
|
|
24021
|
-
logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to rebuild command registry");
|
|
24022
|
-
}
|
|
24566
|
+
reportAvailableCommands({
|
|
24567
|
+
integrationManager,
|
|
24568
|
+
commandRegistry,
|
|
24569
|
+
cloudClient
|
|
24570
|
+
});
|
|
24023
24571
|
});
|
|
24024
24572
|
cloudClient.start();
|
|
24025
24573
|
logger$1.debug("Cloud client started");
|
|
@@ -24228,7 +24776,7 @@ async function executeCloudCommand(command) {
|
|
|
24228
24776
|
};
|
|
24229
24777
|
const payload = command.payload;
|
|
24230
24778
|
const runtime = config.runtime;
|
|
24231
|
-
const version = payload?.version ?? (runtime === "openclaw" ? "2026.6.11" : runtime === "claude-code" ? "0.1.
|
|
24779
|
+
const version = payload?.version ?? (runtime === "openclaw" ? "2026.6.11" : runtime === "claude-code" ? "0.1.6" : void 0);
|
|
24232
24780
|
upgradingRuntime = true;
|
|
24233
24781
|
setTimeout(() => {
|
|
24234
24782
|
(async () => {
|
|
@@ -24334,504 +24882,258 @@ async function executeCloudCommand(command) {
|
|
|
24334
24882
|
const payload = command.payload;
|
|
24335
24883
|
const key = payload?.key;
|
|
24336
24884
|
const value = payload?.value;
|
|
24337
|
-
if (!key || value === void 0) return {
|
|
24338
|
-
type: "COMMAND_ACK",
|
|
24339
|
-
commandId: command.commandId,
|
|
24340
|
-
status: "error",
|
|
24341
|
-
result: {
|
|
24342
|
-
code: "INVALID_PAYLOAD",
|
|
24343
|
-
message: "alfe.config_set requires key and value"
|
|
24344
|
-
}
|
|
24345
|
-
};
|
|
24346
|
-
const runtime = config.runtime;
|
|
24347
|
-
const applier = runtimeAppliersRef?.get(runtime);
|
|
24348
|
-
if (!applier || typeof applier.setConfigRaw !== "function") {
|
|
24349
|
-
const message = `No runtime applier supports config_set for runtime "${runtime}"`;
|
|
24350
|
-
logger$1.warn({
|
|
24351
|
-
runtime,
|
|
24352
|
-
key
|
|
24353
|
-
}, message);
|
|
24354
|
-
return {
|
|
24355
|
-
type: "COMMAND_ACK",
|
|
24356
|
-
commandId: command.commandId,
|
|
24357
|
-
status: "error",
|
|
24358
|
-
result: {
|
|
24359
|
-
code: "CONFIG_SET_UNSUPPORTED",
|
|
24360
|
-
message
|
|
24361
|
-
}
|
|
24362
|
-
};
|
|
24363
|
-
}
|
|
24364
|
-
try {
|
|
24365
|
-
await applier.setConfigRaw(key, value);
|
|
24366
|
-
logger$1.info({
|
|
24367
|
-
runtime,
|
|
24368
|
-
key
|
|
24369
|
-
}, "Applied config via runtime applier setConfigRaw");
|
|
24370
|
-
return {
|
|
24371
|
-
type: "COMMAND_ACK",
|
|
24372
|
-
commandId: command.commandId,
|
|
24373
|
-
status: "ok",
|
|
24374
|
-
result: {
|
|
24375
|
-
key,
|
|
24376
|
-
value
|
|
24377
|
-
}
|
|
24378
|
-
};
|
|
24379
|
-
} catch (err) {
|
|
24380
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
24381
|
-
logger$1.error({
|
|
24382
|
-
err: message,
|
|
24383
|
-
runtime,
|
|
24384
|
-
key
|
|
24385
|
-
}, "Failed to apply config via runtime applier");
|
|
24386
|
-
return {
|
|
24387
|
-
type: "COMMAND_ACK",
|
|
24388
|
-
commandId: command.commandId,
|
|
24389
|
-
status: "error",
|
|
24390
|
-
result: {
|
|
24391
|
-
code: "CONFIG_SET_FAILED",
|
|
24392
|
-
message
|
|
24393
|
-
}
|
|
24394
|
-
};
|
|
24395
|
-
}
|
|
24396
|
-
}
|
|
24397
|
-
if (commandRegistry.has(command.command)) try {
|
|
24398
|
-
const ctx = buildCommandContext();
|
|
24399
|
-
const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
|
|
24400
|
-
return {
|
|
24401
|
-
type: "COMMAND_ACK",
|
|
24402
|
-
commandId: command.commandId,
|
|
24403
|
-
status: result.status,
|
|
24404
|
-
result: result.result
|
|
24405
|
-
};
|
|
24406
|
-
} catch (err) {
|
|
24407
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
24408
|
-
logger$1.error({
|
|
24409
|
-
err: message,
|
|
24410
|
-
command: command.command
|
|
24411
|
-
}, "Command registry execution failed");
|
|
24412
|
-
return {
|
|
24413
|
-
type: "COMMAND_ACK",
|
|
24414
|
-
commandId: command.commandId,
|
|
24415
|
-
status: "error",
|
|
24416
|
-
result: {
|
|
24417
|
-
code: "REGISTRY_ERROR",
|
|
24418
|
-
message
|
|
24419
|
-
}
|
|
24420
|
-
};
|
|
24421
|
-
}
|
|
24422
|
-
const ipcRequest = cloudCommandToIPCRequest(command);
|
|
24423
|
-
if (!ipcRequest) {
|
|
24424
|
-
logger$1.warn({ command: command.command }, "Unrecognized cloud command");
|
|
24425
|
-
return {
|
|
24426
|
-
type: "COMMAND_ACK",
|
|
24427
|
-
commandId: command.commandId,
|
|
24428
|
-
status: "error",
|
|
24429
|
-
result: {
|
|
24430
|
-
code: "UNKNOWN_COMMAND",
|
|
24431
|
-
message: `Unrecognized command: ${command.command}`
|
|
24432
|
-
}
|
|
24433
|
-
};
|
|
24434
|
-
}
|
|
24435
|
-
const plugins = ipcServer?.getRegisteredPlugins() ?? [];
|
|
24436
|
-
if (plugins.length === 0) {
|
|
24437
|
-
logger$1.info({
|
|
24438
|
-
commandId: command.commandId,
|
|
24439
|
-
command: command.command
|
|
24440
|
-
}, "No plugins connected — queuing command");
|
|
24441
|
-
commandQueue.enqueue("_default", ipcRequest, command.commandId);
|
|
24442
|
-
return {
|
|
24885
|
+
if (!key || value === void 0) return {
|
|
24443
24886
|
type: "COMMAND_ACK",
|
|
24444
24887
|
commandId: command.commandId,
|
|
24445
|
-
status: "
|
|
24888
|
+
status: "error",
|
|
24446
24889
|
result: {
|
|
24447
|
-
|
|
24448
|
-
message: "
|
|
24890
|
+
code: "INVALID_PAYLOAD",
|
|
24891
|
+
message: "alfe.config_set requires key and value"
|
|
24449
24892
|
}
|
|
24450
24893
|
};
|
|
24451
|
-
|
|
24452
|
-
const [pluginId] = plugins[0];
|
|
24453
|
-
if (!ipcServer) return {
|
|
24454
|
-
type: "COMMAND_ACK",
|
|
24455
|
-
commandId: command.commandId,
|
|
24456
|
-
status: "error",
|
|
24457
|
-
result: {
|
|
24458
|
-
code: "NO_IPC",
|
|
24459
|
-
message: "IPC server not available"
|
|
24460
|
-
}
|
|
24461
|
-
};
|
|
24462
|
-
try {
|
|
24463
|
-
const response = await ipcServer.sendRequest(pluginId, ipcRequest.method, ipcRequest.params, 3e4);
|
|
24464
|
-
return ipcResponseToCloudAck(command.commandId, response);
|
|
24465
|
-
} catch (err) {
|
|
24466
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
24467
|
-
return {
|
|
24894
|
+
if (!LEGACY_CONFIG_SET_KEYS.has(key) || value.length > MAX_LEGACY_CONFIG_VALUE_LENGTH) return {
|
|
24468
24895
|
type: "COMMAND_ACK",
|
|
24469
24896
|
commandId: command.commandId,
|
|
24470
24897
|
status: "error",
|
|
24471
24898
|
result: {
|
|
24472
|
-
code: "
|
|
24473
|
-
message
|
|
24899
|
+
code: "CONFIG_KEY_UNSUPPORTED",
|
|
24900
|
+
message: "Config key is not supported by the legacy command path"
|
|
24474
24901
|
}
|
|
24475
24902
|
};
|
|
24476
|
-
|
|
24477
|
-
|
|
24478
|
-
|
|
24479
|
-
|
|
24480
|
-
|
|
24481
|
-
|
|
24482
|
-
|
|
24483
|
-
|
|
24484
|
-
apiKey: config.apiKey,
|
|
24485
|
-
async exec(cmd, opts) {
|
|
24486
|
-
const { exec: execCb } = await import("child_process");
|
|
24487
|
-
const { promisify } = await import("util");
|
|
24488
|
-
const { stdout, stderr } = await promisify(execCb)(cmd, {
|
|
24489
|
-
cwd: workspacePath,
|
|
24490
|
-
timeout: opts?.timeoutMs ?? 25e3,
|
|
24491
|
-
maxBuffer: opts?.maxBuffer ?? 512 * 1024
|
|
24492
|
-
});
|
|
24903
|
+
const runtime = config.runtime;
|
|
24904
|
+
const applier = runtimeAppliersRef?.get(runtime);
|
|
24905
|
+
if (!applier || typeof applier.setConfigRaw !== "function") {
|
|
24906
|
+
const message = `No runtime applier supports config_set for runtime "${runtime}"`;
|
|
24907
|
+
logger$1.warn({
|
|
24908
|
+
runtime,
|
|
24909
|
+
key
|
|
24910
|
+
}, message);
|
|
24493
24911
|
return {
|
|
24494
|
-
|
|
24495
|
-
|
|
24912
|
+
type: "COMMAND_ACK",
|
|
24913
|
+
commandId: command.commandId,
|
|
24914
|
+
status: "error",
|
|
24915
|
+
result: {
|
|
24916
|
+
code: "CONFIG_SET_UNSUPPORTED",
|
|
24917
|
+
message
|
|
24918
|
+
}
|
|
24496
24919
|
};
|
|
24497
24920
|
}
|
|
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
|
-
|
|
24529
|
-
|
|
24530
|
-
|
|
24531
|
-
|
|
24532
|
-
|
|
24533
|
-
|
|
24534
|
-
|
|
24535
|
-
|
|
24536
|
-
|
|
24537
|
-
|
|
24538
|
-
|
|
24539
|
-
|
|
24540
|
-
lastSeen: info.lastSeen
|
|
24541
|
-
})),
|
|
24542
|
-
commandQueue: { totalPending: commandQueue.totalPending() }
|
|
24543
|
-
}
|
|
24544
|
-
};
|
|
24545
|
-
}
|
|
24546
|
-
function handleIntegrationList() {
|
|
24547
|
-
return {
|
|
24548
|
-
ok: true,
|
|
24549
|
-
payload: { integrations: integrationManager.list() }
|
|
24550
|
-
};
|
|
24551
|
-
}
|
|
24552
|
-
/**
|
|
24553
|
-
* Return the daemon-hosted MCP bundler's current namespaced tool catalog.
|
|
24554
|
-
* Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
|
|
24555
|
-
* cheap (in-memory snapshot, no I/O).
|
|
24556
|
-
*/
|
|
24557
|
-
function handleMcpListTools(bundler) {
|
|
24558
|
-
if (!bundler) return {
|
|
24559
|
-
ok: false,
|
|
24560
|
-
error: {
|
|
24561
|
-
code: "MCP_BUNDLER_UNAVAILABLE",
|
|
24562
|
-
message: "MCP bundler not initialized"
|
|
24563
|
-
}
|
|
24564
|
-
};
|
|
24565
|
-
return {
|
|
24566
|
-
ok: true,
|
|
24567
|
-
payload: { tools: bundler.listTools() }
|
|
24568
|
-
};
|
|
24569
|
-
}
|
|
24570
|
-
/**
|
|
24571
|
-
* Observability backstop for the INTEGRATION warm path. `applyForIntegration`
|
|
24572
|
-
* registers MCP servers into the store and the daemon warms them silently via
|
|
24573
|
-
* the store-change `onChange` hook — so a server that fails its connect (e.g.
|
|
24574
|
-
* an MCP server whose backing account/credential wasn't resolvable at warm
|
|
24575
|
-
* time, like ctrader-mcp `exit(1)`-ing on empty accounts) leaves NO trace,
|
|
24576
|
-
* making the black hole undiagnosable. After a warm, read `bundler.statuses()`
|
|
24577
|
-
* and warn once per still-failed server so the failure is visible. These
|
|
24578
|
-
* self-heal on the bundler's background retry sweep once the dependency
|
|
24579
|
-
* appears. Returns the servers it warned about (for tests / callers).
|
|
24580
|
-
*/
|
|
24581
|
-
function warnFailedMcpServers(bundler, log, reason) {
|
|
24582
|
-
if (!bundler) return [];
|
|
24583
|
-
const failed = bundler.statuses().filter((s) => !s.connected && s.lastError !== void 0);
|
|
24584
|
-
for (const status of failed) log.warn({
|
|
24585
|
-
server: status.name,
|
|
24586
|
-
reason,
|
|
24587
|
-
consecutiveFailures: status.consecutiveFailures,
|
|
24588
|
-
lastError: status.lastError
|
|
24589
|
-
}, `MCP server "${status.name}" failed to connect: ${status.lastError ?? ""}`);
|
|
24590
|
-
return failed;
|
|
24591
|
-
}
|
|
24592
|
-
/**
|
|
24593
|
-
* Route a tool call to the appropriate MCP child via the daemon-hosted
|
|
24594
|
-
* bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
|
|
24595
|
-
* the raw JSON object the LLM produced.
|
|
24596
|
-
*/
|
|
24597
|
-
/**
|
|
24598
|
-
* List every server entry in the alfe bundler store. Lets agents inspect
|
|
24599
|
-
* what they've already registered before adding a new one.
|
|
24600
|
-
*/
|
|
24601
|
-
function handleMcpListServers(manager = mcpManagerRef) {
|
|
24602
|
-
if (!manager) return {
|
|
24603
|
-
ok: false,
|
|
24604
|
-
error: {
|
|
24605
|
-
code: "MCP_MANAGER_UNAVAILABLE",
|
|
24606
|
-
message: "MCP manager not initialized"
|
|
24607
|
-
}
|
|
24608
|
-
};
|
|
24609
|
-
const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
|
|
24610
|
-
const byName = new Map(statuses.map((s) => [s.name, s]));
|
|
24611
|
-
return {
|
|
24612
|
-
ok: true,
|
|
24613
|
-
payload: { servers: manager.listServers().map(({ id, entry }) => ({
|
|
24614
|
-
id,
|
|
24615
|
-
entry,
|
|
24616
|
-
status: byName.get(id) ?? {
|
|
24617
|
-
name: id,
|
|
24618
|
-
connected: false,
|
|
24619
|
-
toolCount: 0,
|
|
24620
|
-
consecutiveFailures: 0
|
|
24621
|
-
}
|
|
24622
|
-
})) }
|
|
24623
|
-
};
|
|
24624
|
-
}
|
|
24625
|
-
/**
|
|
24626
|
-
* How long the add-confirm probe waits for the freshly-added server to
|
|
24627
|
-
* connect before replying. This is effectively the whole budget for
|
|
24628
|
-
* `bundler.warmServer`: `Manager.addServer` now reconciles the bundler
|
|
24629
|
-
* BEFORE resolving, so the Connection object already exists and
|
|
24630
|
-
* `warmServer`'s pre-reconcile is a no-diff cheap pass. Kept well under the
|
|
24631
|
-
* plugin caller's 30s IPC timeout. A slower server still connects in the
|
|
24632
|
-
* background and surfaces on the next `alfe_mcp_list_tools` — the probe just
|
|
24633
|
-
* reports what it saw within the window; a missed connect is re-attempted by
|
|
24634
|
-
* the bundler's `retryNeverConnected` sweep.
|
|
24635
|
-
*/
|
|
24636
|
-
const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
|
|
24637
|
-
/**
|
|
24638
|
-
* Register a new MCP server in the alfe bundler store on behalf of the agent,
|
|
24639
|
-
* then CONFIRM the connect before replying so the agent learns whether the
|
|
24640
|
-
* server actually works. Owned as `'manual'` so the agent can later remove it
|
|
24641
|
-
* without an expectedOwner conflict — matches what `alfe mcp add` does from the
|
|
24642
|
-
* CLI. Registration is durable regardless of the probe outcome: a probe
|
|
24643
|
-
* failure (or a runtime with no daemon bundler, e.g. hermes) still returns
|
|
24644
|
-
* `ok` with `connected: false`; the store watcher / runtime picks the entry up.
|
|
24645
|
-
*/
|
|
24646
|
-
async function handleMcpAddServer(params, manager = mcpManagerRef) {
|
|
24647
|
-
if (!manager) return {
|
|
24648
|
-
ok: false,
|
|
24649
|
-
error: {
|
|
24650
|
-
code: "MCP_MANAGER_UNAVAILABLE",
|
|
24651
|
-
message: "MCP manager not initialized"
|
|
24652
|
-
}
|
|
24653
|
-
};
|
|
24654
|
-
const p = params;
|
|
24655
|
-
if (typeof p.id !== "string" || p.id.length === 0) return {
|
|
24656
|
-
ok: false,
|
|
24657
|
-
error: {
|
|
24658
|
-
code: "INVALID_PARAMS",
|
|
24659
|
-
message: "id is required (string)"
|
|
24660
|
-
}
|
|
24661
|
-
};
|
|
24662
|
-
const config = buildServerConfig(p);
|
|
24663
|
-
if (!config) return {
|
|
24664
|
-
ok: false,
|
|
24665
|
-
error: {
|
|
24666
|
-
code: "INVALID_PARAMS",
|
|
24667
|
-
message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
|
|
24668
|
-
}
|
|
24669
|
-
};
|
|
24670
|
-
try {
|
|
24671
|
-
await manager.addServer(config, {
|
|
24672
|
-
id: p.id,
|
|
24673
|
-
owner: "manual"
|
|
24674
|
-
});
|
|
24921
|
+
try {
|
|
24922
|
+
await applier.setConfigRaw(key, value);
|
|
24923
|
+
logger$1.info({
|
|
24924
|
+
runtime,
|
|
24925
|
+
key
|
|
24926
|
+
}, "Applied config via runtime applier setConfigRaw");
|
|
24927
|
+
return {
|
|
24928
|
+
type: "COMMAND_ACK",
|
|
24929
|
+
commandId: command.commandId,
|
|
24930
|
+
status: "ok",
|
|
24931
|
+
result: {
|
|
24932
|
+
key,
|
|
24933
|
+
applied: true
|
|
24934
|
+
}
|
|
24935
|
+
};
|
|
24936
|
+
} catch (err) {
|
|
24937
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24938
|
+
logger$1.error({
|
|
24939
|
+
err: message,
|
|
24940
|
+
runtime,
|
|
24941
|
+
key
|
|
24942
|
+
}, "Failed to apply config via runtime applier");
|
|
24943
|
+
return {
|
|
24944
|
+
type: "COMMAND_ACK",
|
|
24945
|
+
commandId: command.commandId,
|
|
24946
|
+
status: "error",
|
|
24947
|
+
result: {
|
|
24948
|
+
code: "CONFIG_SET_FAILED",
|
|
24949
|
+
message
|
|
24950
|
+
}
|
|
24951
|
+
};
|
|
24952
|
+
}
|
|
24953
|
+
}
|
|
24954
|
+
if (commandRegistry.has(command.command)) try {
|
|
24955
|
+
const ctx = buildCommandContext();
|
|
24956
|
+
const result = await commandRegistry.execute(command.command, typeof command.payload === "object" && command.payload !== null ? command.payload : {}, ctx);
|
|
24957
|
+
return {
|
|
24958
|
+
type: "COMMAND_ACK",
|
|
24959
|
+
commandId: command.commandId,
|
|
24960
|
+
status: result.status,
|
|
24961
|
+
result: result.result
|
|
24962
|
+
};
|
|
24675
24963
|
} catch (err) {
|
|
24676
24964
|
const message = err instanceof Error ? err.message : String(err);
|
|
24677
|
-
logger$1.
|
|
24678
|
-
|
|
24679
|
-
|
|
24680
|
-
}, "
|
|
24965
|
+
logger$1.error({
|
|
24966
|
+
err: message,
|
|
24967
|
+
command: command.command
|
|
24968
|
+
}, "Command registry execution failed");
|
|
24681
24969
|
return {
|
|
24682
|
-
|
|
24683
|
-
|
|
24684
|
-
|
|
24970
|
+
type: "COMMAND_ACK",
|
|
24971
|
+
commandId: command.commandId,
|
|
24972
|
+
status: "error",
|
|
24973
|
+
result: {
|
|
24974
|
+
code: "REGISTRY_ERROR",
|
|
24685
24975
|
message
|
|
24686
24976
|
}
|
|
24687
24977
|
};
|
|
24688
24978
|
}
|
|
24689
|
-
|
|
24690
|
-
if (
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
|
|
24694
|
-
|
|
24695
|
-
|
|
24696
|
-
|
|
24979
|
+
const ipcRequest = cloudCommandToIPCRequest(command);
|
|
24980
|
+
if (!ipcRequest) {
|
|
24981
|
+
logger$1.warn({ command: command.command }, "Unrecognized cloud command");
|
|
24982
|
+
return {
|
|
24983
|
+
type: "COMMAND_ACK",
|
|
24984
|
+
commandId: command.commandId,
|
|
24985
|
+
status: "error",
|
|
24986
|
+
result: {
|
|
24987
|
+
code: "UNKNOWN_COMMAND",
|
|
24988
|
+
message: `Unrecognized command: ${command.command}`
|
|
24989
|
+
}
|
|
24990
|
+
};
|
|
24697
24991
|
}
|
|
24698
|
-
|
|
24699
|
-
|
|
24700
|
-
|
|
24701
|
-
|
|
24702
|
-
|
|
24703
|
-
|
|
24704
|
-
|
|
24705
|
-
|
|
24706
|
-
|
|
24707
|
-
|
|
24708
|
-
|
|
24709
|
-
|
|
24710
|
-
|
|
24711
|
-
|
|
24712
|
-
|
|
24713
|
-
|
|
24714
|
-
|
|
24715
|
-
|
|
24992
|
+
const plugins = ipcServer?.getRegisteredPlugins() ?? [];
|
|
24993
|
+
if (plugins.length === 0) {
|
|
24994
|
+
logger$1.info({
|
|
24995
|
+
commandId: command.commandId,
|
|
24996
|
+
command: command.command
|
|
24997
|
+
}, "No plugins connected — queuing command");
|
|
24998
|
+
if (!commandQueue.enqueue("_default", ipcRequest, command.commandId)) {
|
|
24999
|
+
logger$1.warn({
|
|
25000
|
+
commandId: command.commandId,
|
|
25001
|
+
command: command.command
|
|
25002
|
+
}, "Plugin command queue is full");
|
|
25003
|
+
return {
|
|
25004
|
+
type: "COMMAND_ACK",
|
|
25005
|
+
commandId: command.commandId,
|
|
25006
|
+
status: "error",
|
|
25007
|
+
result: {
|
|
25008
|
+
code: "QUEUE_FULL",
|
|
25009
|
+
message: "Plugin command queue is at capacity"
|
|
25010
|
+
}
|
|
25011
|
+
};
|
|
24716
25012
|
}
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
|
|
24721
|
-
|
|
24722
|
-
|
|
24723
|
-
|
|
24724
|
-
|
|
25013
|
+
return {
|
|
25014
|
+
type: "COMMAND_ACK",
|
|
25015
|
+
commandId: command.commandId,
|
|
25016
|
+
status: "ok",
|
|
25017
|
+
result: {
|
|
25018
|
+
queued: true,
|
|
25019
|
+
message: "Command queued — no plugins connected"
|
|
25020
|
+
}
|
|
24725
25021
|
};
|
|
24726
|
-
if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
|
|
24727
|
-
const headers = {};
|
|
24728
|
-
for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
|
|
24729
|
-
cfg.headers = headers;
|
|
24730
|
-
}
|
|
24731
|
-
return cfg;
|
|
24732
25022
|
}
|
|
24733
|
-
|
|
24734
|
-
|
|
24735
|
-
|
|
24736
|
-
|
|
24737
|
-
|
|
24738
|
-
|
|
24739
|
-
|
|
24740
|
-
|
|
24741
|
-
*/
|
|
24742
|
-
async function handleMcpRemoveServer(params, manager = mcpManagerRef) {
|
|
24743
|
-
if (!manager) return {
|
|
24744
|
-
ok: false,
|
|
24745
|
-
error: {
|
|
24746
|
-
code: "MCP_MANAGER_UNAVAILABLE",
|
|
24747
|
-
message: "MCP manager not initialized"
|
|
24748
|
-
}
|
|
24749
|
-
};
|
|
24750
|
-
const { id } = params;
|
|
24751
|
-
if (typeof id !== "string" || id.length === 0) return {
|
|
24752
|
-
ok: false,
|
|
24753
|
-
error: {
|
|
24754
|
-
code: "INVALID_PARAMS",
|
|
24755
|
-
message: "id is required (string)"
|
|
25023
|
+
const [pluginId] = plugins[0];
|
|
25024
|
+
if (!ipcServer) return {
|
|
25025
|
+
type: "COMMAND_ACK",
|
|
25026
|
+
commandId: command.commandId,
|
|
25027
|
+
status: "error",
|
|
25028
|
+
result: {
|
|
25029
|
+
code: "NO_IPC",
|
|
25030
|
+
message: "IPC server not available"
|
|
24756
25031
|
}
|
|
24757
25032
|
};
|
|
24758
25033
|
try {
|
|
24759
|
-
|
|
24760
|
-
|
|
24761
|
-
payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
|
|
24762
|
-
};
|
|
25034
|
+
const response = await ipcServer.sendRequest(pluginId, ipcRequest.method, ipcRequest.params, 3e4);
|
|
25035
|
+
return ipcResponseToCloudAck(command.commandId, response);
|
|
24763
25036
|
} catch (err) {
|
|
24764
25037
|
const message = err instanceof Error ? err.message : String(err);
|
|
24765
|
-
if (message.includes("owned by")) return {
|
|
24766
|
-
ok: false,
|
|
24767
|
-
error: {
|
|
24768
|
-
code: "MCP_OWNER_MISMATCH",
|
|
24769
|
-
message
|
|
24770
|
-
}
|
|
24771
|
-
};
|
|
24772
|
-
logger$1.warn({
|
|
24773
|
-
id,
|
|
24774
|
-
err: message
|
|
24775
|
-
}, "mcp.remove_server failed");
|
|
24776
25038
|
return {
|
|
24777
|
-
|
|
24778
|
-
|
|
24779
|
-
|
|
25039
|
+
type: "COMMAND_ACK",
|
|
25040
|
+
commandId: command.commandId,
|
|
25041
|
+
status: "error",
|
|
25042
|
+
result: {
|
|
25043
|
+
code: "PLUGIN_ERROR",
|
|
24780
25044
|
message
|
|
24781
25045
|
}
|
|
24782
25046
|
};
|
|
24783
25047
|
}
|
|
24784
25048
|
}
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24796
|
-
|
|
24797
|
-
|
|
24798
|
-
|
|
25049
|
+
function buildCommandContext() {
|
|
25050
|
+
const workspacePath = Object.values(config.runtimes)[0]?.workspace ?? "~/.openclaw";
|
|
25051
|
+
return {
|
|
25052
|
+
workspacePath,
|
|
25053
|
+
aiProxyUrl: aiProxyUrl ?? void 0,
|
|
25054
|
+
aiProxyRunning,
|
|
25055
|
+
apiKey: config.apiKey,
|
|
25056
|
+
async exec(cmd, opts) {
|
|
25057
|
+
const { exec: execCb } = await import("child_process");
|
|
25058
|
+
const { promisify } = await import("util");
|
|
25059
|
+
const { stdout, stderr } = await promisify(execCb)(cmd, {
|
|
25060
|
+
cwd: workspacePath,
|
|
25061
|
+
timeout: opts?.timeoutMs ?? 25e3,
|
|
25062
|
+
maxBuffer: opts?.maxBuffer ?? 512 * 1024
|
|
25063
|
+
});
|
|
25064
|
+
return {
|
|
25065
|
+
stdout: stdout.trim(),
|
|
25066
|
+
stderr: stderr.trim()
|
|
25067
|
+
};
|
|
24799
25068
|
}
|
|
24800
25069
|
};
|
|
24801
|
-
|
|
24802
|
-
|
|
24803
|
-
|
|
24804
|
-
|
|
24805
|
-
|
|
24806
|
-
|
|
24807
|
-
|
|
24808
|
-
|
|
24809
|
-
|
|
24810
|
-
|
|
24811
|
-
|
|
24812
|
-
return {
|
|
25070
|
+
}
|
|
25071
|
+
function handlePluginRequest(method, params, pluginId) {
|
|
25072
|
+
switch (method) {
|
|
25073
|
+
case "status": return Promise.resolve(handleStatus());
|
|
25074
|
+
case "integration.list": return Promise.resolve(handleIntegrationList());
|
|
25075
|
+
case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
|
|
25076
|
+
case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
|
|
25077
|
+
case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
|
|
25078
|
+
case "mcp.list_servers": return Promise.resolve(handleMcpListServers(mcpManagerRef));
|
|
25079
|
+
case "mcp.add_server": return handleMcpAddServer(params, mcpManagerRef);
|
|
25080
|
+
case "mcp.remove_server": return handleMcpRemoveServer(params, mcpManagerRef);
|
|
25081
|
+
default: return Promise.resolve({
|
|
24813
25082
|
ok: false,
|
|
24814
25083
|
error: {
|
|
24815
|
-
code: "
|
|
24816
|
-
message
|
|
25084
|
+
code: "UNKNOWN_METHOD",
|
|
25085
|
+
message: `Unknown method: ${method}`
|
|
24817
25086
|
}
|
|
24818
|
-
};
|
|
25087
|
+
});
|
|
24819
25088
|
}
|
|
24820
25089
|
}
|
|
25090
|
+
function handleStatus() {
|
|
25091
|
+
return {
|
|
25092
|
+
ok: true,
|
|
25093
|
+
payload: {
|
|
25094
|
+
daemon: {
|
|
25095
|
+
status: "running",
|
|
25096
|
+
pid: process.pid,
|
|
25097
|
+
uptime: (Date.now() - startedAt) / 1e3,
|
|
25098
|
+
version: resolvedCliVersion,
|
|
25099
|
+
runtimeVersion: resolvedRuntimeVersion
|
|
25100
|
+
},
|
|
25101
|
+
cloud: {
|
|
25102
|
+
status: cloudConnected ? "connected" : "disconnected",
|
|
25103
|
+
latencyMs: cloudClient.getLatencyMs()
|
|
25104
|
+
},
|
|
25105
|
+
aiProxy: { status: aiProxyRunning ? "running" : "stopped" },
|
|
25106
|
+
plugins: (ipcServer?.getRegisteredPlugins() ?? []).map(([, info]) => ({
|
|
25107
|
+
name: info.name,
|
|
25108
|
+
version: info.version,
|
|
25109
|
+
capabilities: info.capabilities,
|
|
25110
|
+
connectedAt: info.connectedAt,
|
|
25111
|
+
lastSeen: info.lastSeen
|
|
25112
|
+
})),
|
|
25113
|
+
commandQueue: { totalPending: commandQueue.totalPending() }
|
|
25114
|
+
}
|
|
25115
|
+
};
|
|
25116
|
+
}
|
|
25117
|
+
function handleIntegrationList() {
|
|
25118
|
+
return {
|
|
25119
|
+
ok: true,
|
|
25120
|
+
payload: { integrations: integrationManager.list() }
|
|
25121
|
+
};
|
|
25122
|
+
}
|
|
24821
25123
|
function handleIntegrationReport(params, pluginId) {
|
|
24822
25124
|
const { name, status, detail } = params;
|
|
24823
|
-
if (
|
|
25125
|
+
if (typeof name !== "string" || name.length === 0 || name.length > 256 || typeof status !== "string" || status.length === 0 || status.length > 64) return {
|
|
24824
25126
|
ok: false,
|
|
24825
25127
|
error: {
|
|
24826
25128
|
code: "INVALID_PARAMS",
|
|
24827
|
-
message: "name and status
|
|
25129
|
+
message: "name and status must be non-empty bounded strings"
|
|
24828
25130
|
}
|
|
24829
25131
|
};
|
|
24830
25132
|
logger$1.info({
|
|
24831
25133
|
pluginId,
|
|
24832
25134
|
integration: name,
|
|
24833
25135
|
status,
|
|
24834
|
-
detail
|
|
25136
|
+
hasDetail: detail !== void 0
|
|
24835
25137
|
}, "Integration status report");
|
|
24836
25138
|
return {
|
|
24837
25139
|
ok: true,
|
|
@@ -24869,6 +25171,12 @@ async function queryDaemonHealth(socketPath, timeoutMs = 5e3) {
|
|
|
24869
25171
|
}, timeoutMs);
|
|
24870
25172
|
socket.on("data", (data) => {
|
|
24871
25173
|
buffer += data.toString();
|
|
25174
|
+
if (Buffer.byteLength(buffer, "utf8") > 1048576) {
|
|
25175
|
+
clearTimeout(timer);
|
|
25176
|
+
socket.destroy();
|
|
25177
|
+
reject(/* @__PURE__ */ new Error("Health response exceeded IPC size limit"));
|
|
25178
|
+
return;
|
|
25179
|
+
}
|
|
24872
25180
|
const newlineIdx = buffer.indexOf("\n");
|
|
24873
25181
|
if (newlineIdx === -1) return;
|
|
24874
25182
|
const line = buffer.slice(0, newlineIdx).trim();
|
|
@@ -24877,7 +25185,8 @@ async function queryDaemonHealth(socketPath, timeoutMs = 5e3) {
|
|
|
24877
25185
|
socket.end();
|
|
24878
25186
|
try {
|
|
24879
25187
|
const response = JSON.parse(line);
|
|
24880
|
-
if (response
|
|
25188
|
+
if (!isIPCResponse(response)) reject(/* @__PURE__ */ new Error("Invalid health response"));
|
|
25189
|
+
else if (response.ok && response.payload) resolve(response.payload);
|
|
24881
25190
|
else reject(new Error(response.error?.message ?? "Health check failed"));
|
|
24882
25191
|
} catch {
|
|
24883
25192
|
reject(/* @__PURE__ */ new Error("Invalid health response"));
|