@alfe.ai/gateway 0.7.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/gateway.js +1 -1
- package/dist/health.js +215 -401
- package/dist/sentry.js +386 -0
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +2 -1
- package/dist/upgrade.js +154 -5
- package/package.json +4 -4
package/dist/health.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { n as logger$1 } from "./logger.js";
|
|
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";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
5
|
import { execFile, execSync, spawn } from "node:child_process";
|
|
@@ -7,12 +8,12 @@ import { dirname, join } from "node:path";
|
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
9
|
import pino from "pino";
|
|
9
10
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { getEndpointFromToken, readConfig
|
|
11
|
+
import { getEndpointFromToken, readConfig } from "@alfe.ai/config";
|
|
11
12
|
import crypto from "crypto";
|
|
12
13
|
import { parse } from "smol-toml";
|
|
13
14
|
import WebSocket from "ws";
|
|
14
15
|
import { createConnection, createServer } from "node:net";
|
|
15
|
-
import { HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, NoopOpenClawCliLock, OpenClawApplier, SerialOpenClawCliLock } from "@alfe.ai/integrations";
|
|
16
|
+
import { ClaudeCodeApplier, ClaudeCodeMcpSync, HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, NoopOpenClawCliLock, OpenClawApplier, SerialOpenClawCliLock } from "@alfe.ai/integrations";
|
|
16
17
|
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
17
18
|
import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
|
|
18
19
|
import stream, { Readable } from "stream";
|
|
@@ -563,6 +564,17 @@ var AuthService = class {
|
|
|
563
564
|
body: JSON.stringify(input)
|
|
564
565
|
});
|
|
565
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* Preview the proration cost for changing a personal (individual-tier)
|
|
569
|
+
* subscriber's platform plan — read-only companion to `changePlatformPlan`.
|
|
570
|
+
* Nothing is charged.
|
|
571
|
+
*/
|
|
572
|
+
previewPlatformPlanChange(input) {
|
|
573
|
+
return this.client.request(`${this.prefix}/subscription/change/preview`, {
|
|
574
|
+
method: "POST",
|
|
575
|
+
body: JSON.stringify(input)
|
|
576
|
+
});
|
|
577
|
+
}
|
|
566
578
|
updateSeats(seats) {
|
|
567
579
|
return this.client.request(`${this.prefix}/subscription/seats`, {
|
|
568
580
|
method: "POST",
|
|
@@ -699,6 +711,15 @@ var IntegrationsService = class {
|
|
|
699
711
|
reinstallIntegration(agentId, integrationId) {
|
|
700
712
|
return this.client.request(`/integrations/agents/${agentId}/${integrationId}/reinstall`, { method: "POST" });
|
|
701
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Upgrade an integration to the registry's latest version via the fast,
|
|
716
|
+
* diff-based daemon path (bumps `version` without `reinstallRequestedAt`).
|
|
717
|
+
* The agent stays online — no destructive teardown. Distinct from
|
|
718
|
+
* {@link reinstallIntegration}, which is the destructive repair path.
|
|
719
|
+
*/
|
|
720
|
+
upgradeIntegration(agentId, integrationId) {
|
|
721
|
+
return this.client.request(`/integrations/agents/${agentId}/${integrationId}/upgrade`, { method: "POST" });
|
|
722
|
+
}
|
|
702
723
|
getRegistry() {
|
|
703
724
|
return this.client.request("/integrations/registry");
|
|
704
725
|
}
|
|
@@ -4499,6 +4520,7 @@ enumValues({
|
|
|
4499
4520
|
IndividualPro: "individual_pro",
|
|
4500
4521
|
IndividualMax: "individual_max",
|
|
4501
4522
|
OrgFree: "org_free",
|
|
4523
|
+
OrgStartup: "org_startup",
|
|
4502
4524
|
OrgProfessional: "org_professional",
|
|
4503
4525
|
OrgEnterprise: "org_enterprise"
|
|
4504
4526
|
});
|
|
@@ -4535,7 +4557,8 @@ enumValues({
|
|
|
4535
4557
|
enumValues({
|
|
4536
4558
|
OpenClaw: "openclaw",
|
|
4537
4559
|
NanoClaw: "nanoclaw",
|
|
4538
|
-
Hermes: "hermes"
|
|
4560
|
+
Hermes: "hermes",
|
|
4561
|
+
ClaudeCode: "claude-code"
|
|
4539
4562
|
});
|
|
4540
4563
|
const TTS_MODELS = enumValues({
|
|
4541
4564
|
ElevenLabsTurbo: "elevenlabs-turbo",
|
|
@@ -4676,9 +4699,10 @@ enumValues({
|
|
|
4676
4699
|
const AnthropicModel = {
|
|
4677
4700
|
Opus48: "claude-opus-4-8",
|
|
4678
4701
|
Opus47: "claude-opus-4-7",
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4702
|
+
Opus46: "claude-opus-4-6",
|
|
4703
|
+
Sonnet5: "claude-sonnet-5",
|
|
4704
|
+
Sonnet46: "claude-sonnet-4-6",
|
|
4705
|
+
Haiku45: "claude-haiku-4-5"
|
|
4682
4706
|
};
|
|
4683
4707
|
const ANTHROPIC_MODELS = enumValues(AnthropicModel);
|
|
4684
4708
|
const OpenAIModel = {
|
|
@@ -4693,9 +4717,14 @@ const OpenAIModel = {
|
|
|
4693
4717
|
GPT54Nano: "gpt-5.4-nano",
|
|
4694
4718
|
GPT54Pro: "gpt-5.4-pro",
|
|
4695
4719
|
GPT55: "gpt-5.5",
|
|
4720
|
+
GPT55Pro: "gpt-5.5-pro",
|
|
4721
|
+
GPT56Sol: "gpt-5.6-sol",
|
|
4722
|
+
GPT56Terra: "gpt-5.6-terra",
|
|
4723
|
+
GPT56Luna: "gpt-5.6-luna",
|
|
4696
4724
|
O3Mini: "o3-mini",
|
|
4697
4725
|
O4Mini: "o4-mini",
|
|
4698
|
-
TextEmbedding3Small: "text-embedding-3-small"
|
|
4726
|
+
TextEmbedding3Small: "text-embedding-3-small",
|
|
4727
|
+
TextEmbedding3Large: "text-embedding-3-large"
|
|
4699
4728
|
};
|
|
4700
4729
|
const OPENAI_MODELS = enumValues(OpenAIModel);
|
|
4701
4730
|
const DeepSeekModel = {
|
|
@@ -4719,16 +4748,25 @@ const MiniMaxModel = {
|
|
|
4719
4748
|
M3: "MiniMax-M3",
|
|
4720
4749
|
M27: "MiniMax-M2.7",
|
|
4721
4750
|
M27HighSpeed: "MiniMax-M2.7-highspeed",
|
|
4722
|
-
M25: "MiniMax-M2.5"
|
|
4751
|
+
M25: "MiniMax-M2.5",
|
|
4752
|
+
M21: "MiniMax-M2.1",
|
|
4753
|
+
M2: "MiniMax-M2"
|
|
4723
4754
|
};
|
|
4724
4755
|
const MINIMAX_MODELS = enumValues(MiniMaxModel);
|
|
4725
4756
|
const MistralModel = {
|
|
4726
4757
|
Large: "mistral-large-latest",
|
|
4758
|
+
Medium: "mistral-medium-latest",
|
|
4727
4759
|
Small: "mistral-small-latest",
|
|
4728
|
-
Codestral: "codestral-latest"
|
|
4760
|
+
Codestral: "codestral-latest",
|
|
4761
|
+
Ministral8b: "ministral-8b-latest",
|
|
4762
|
+
Ministral3b: "ministral-3b-latest",
|
|
4763
|
+
MagistralMedium: "magistral-medium-latest",
|
|
4764
|
+
MagistralSmall: "magistral-small-latest",
|
|
4765
|
+
DevstralMedium: "devstral-medium-latest"
|
|
4729
4766
|
};
|
|
4730
4767
|
const MISTRAL_MODELS = enumValues(MistralModel);
|
|
4731
4768
|
const XAIModel = {
|
|
4769
|
+
Grok45: "grok-4.5",
|
|
4732
4770
|
Grok43: "grok-4.3",
|
|
4733
4771
|
Grok4: "grok-4",
|
|
4734
4772
|
Grok41Fast: "grok-4.1-fast"
|
|
@@ -4737,10 +4775,12 @@ const XAI_MODELS = enumValues(XAIModel);
|
|
|
4737
4775
|
const ZhipuModel = {
|
|
4738
4776
|
GLM52: "glm-5.2",
|
|
4739
4777
|
GLM51: "glm-5.1",
|
|
4740
|
-
|
|
4778
|
+
GLM46: "glm-4.6",
|
|
4779
|
+
GLM45: "glm-4.5",
|
|
4780
|
+
GLM45Air: "glm-4.5-air"
|
|
4741
4781
|
};
|
|
4742
4782
|
const ZHIPU_MODELS = enumValues(ZhipuModel);
|
|
4743
|
-
AnthropicModel.
|
|
4783
|
+
AnthropicModel.Sonnet5;
|
|
4744
4784
|
[
|
|
4745
4785
|
...ANTHROPIC_MODELS,
|
|
4746
4786
|
...OPENAI_MODELS,
|
|
@@ -4760,8 +4800,8 @@ _enum(MISTRAL_MODELS);
|
|
|
4760
4800
|
_enum(XAI_MODELS);
|
|
4761
4801
|
_enum(ZHIPU_MODELS);
|
|
4762
4802
|
string().min(1);
|
|
4763
|
-
AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.
|
|
4764
|
-
AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.
|
|
4803
|
+
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;
|
|
4804
|
+
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;
|
|
4765
4805
|
enumValues({
|
|
4766
4806
|
PendingChallenge: "pending_challenge",
|
|
4767
4807
|
Creating: "creating",
|
|
@@ -4837,6 +4877,7 @@ const NotificationType = {
|
|
|
4837
4877
|
AutoRechargeFailed: "auto-recharge.failed",
|
|
4838
4878
|
SubscriptionCreated: "subscription.created",
|
|
4839
4879
|
SubscriptionCancelled: "subscription.cancelled",
|
|
4880
|
+
StartupGrantEnded: "startup.grant_ended",
|
|
4840
4881
|
SubscriptionPastDue: "subscription.past_due",
|
|
4841
4882
|
BalanceThresholdWarning: "balance.threshold.warning",
|
|
4842
4883
|
PlatformTierPriceIncrease: "platform.tier_price_increase",
|
|
@@ -4861,7 +4902,7 @@ const RecipientStrategy = {
|
|
|
4861
4902
|
TenantAdmins: "tenant_admins",
|
|
4862
4903
|
SpecificUser: "specific_user"
|
|
4863
4904
|
};
|
|
4864
|
-
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.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PlatformTierPriceIncrease, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.BrowserTakeoverRequested, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
|
|
4905
|
+
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.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PlatformTierPriceIncrease, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.BrowserTakeoverRequested, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
|
|
4865
4906
|
//#endregion
|
|
4866
4907
|
//#region src/config.ts
|
|
4867
4908
|
/**
|
|
@@ -4977,6 +5018,7 @@ function deriveGatewayWsUrl(apiEndpoint) {
|
|
|
4977
5018
|
*/
|
|
4978
5019
|
function deriveAgentWorkspace(runtime, home) {
|
|
4979
5020
|
if (runtime === "openclaw") return join(home, "workspace");
|
|
5021
|
+
if (runtime === "claude-code") return join(home, "workspace");
|
|
4980
5022
|
return home;
|
|
4981
5023
|
}
|
|
4982
5024
|
async function loadRuntimeConfigs() {
|
|
@@ -5397,390 +5439,6 @@ var NoopRuntimeGate = class {
|
|
|
5397
5439
|
requestRestart() {}
|
|
5398
5440
|
};
|
|
5399
5441
|
//#endregion
|
|
5400
|
-
//#region src/sentry.ts
|
|
5401
|
-
/**
|
|
5402
|
-
* Agent-side error reporting (Sentry) for the Alfe CLI + gateway daemon.
|
|
5403
|
-
*
|
|
5404
|
-
* This module is the single Sentry bootstrap shared by both published
|
|
5405
|
-
* agent-side packages — `@alfe.ai/gateway` (the daemon) and `@alfe.ai/cli`
|
|
5406
|
-
* (which re-imports these helpers from `@alfe.ai/gateway`). It exists so
|
|
5407
|
-
* integration-installation failures on customer/managed VMs surface centrally
|
|
5408
|
-
* instead of silently rotting until someone SSHes into the box.
|
|
5409
|
-
*
|
|
5410
|
-
* Design constraints (see packages/gateway/DEVELOPING.md → "Error reporting"):
|
|
5411
|
-
* - Errors only. No tracing/profiling (`tracesSampleRate: 0`, `sampleRate: 1`).
|
|
5412
|
-
* - `@sentry/node` is lazy-imported so `alfe --version` doesn't pay for it.
|
|
5413
|
-
* - Sentry can NEVER break the CLI/daemon — every entry point is try/catch'd.
|
|
5414
|
-
* - The DSNs are baked as constants: published tarballs run on agent VMs and
|
|
5415
|
-
* cannot read the repo's `config/config.ts`. That file's `SENTRY_DSNS`
|
|
5416
|
-
* registry (keys `agentDaemon` + `cli`) mirrors them as the human reference.
|
|
5417
|
-
* - Secrets are scrubbed in `beforeSend`; request/env payloads are dropped.
|
|
5418
|
-
* - Opt-out via `ALFE_ERROR_REPORTING=0|false` or `errorReporting = false`
|
|
5419
|
-
* in `~/.alfe/config.toml`. Default ON.
|
|
5420
|
-
*/
|
|
5421
|
-
/**
|
|
5422
|
-
* Baked per-surface Sentry DSNs (org `alfe-ai`). DSNs are public by design
|
|
5423
|
-
* (they only permit event ingestion). Mirrors of `config/config.ts` →
|
|
5424
|
-
* `SENTRY_DSNS.agentDaemon` / `SENTRY_DSNS.cli`. If you rotate a DSN, update
|
|
5425
|
-
* both places.
|
|
5426
|
-
*/
|
|
5427
|
-
const AGENT_DAEMON_SENTRY_DSN = "https://a47f010d8d39fb4350f913492189f283@o4511008239452160.ingest.us.sentry.io/4511679448547328";
|
|
5428
|
-
const CLI_SENTRY_DSN = "https://82dde4631336561f2fcc89d7531623de@o4511008239452160.ingest.us.sentry.io/4511679411191808";
|
|
5429
|
-
/**
|
|
5430
|
-
* Dedicated project for errors emitted BY the agent runtime child process
|
|
5431
|
-
* (OpenClaw/Hermes) — crashes, spawn failures, and error-looking output. Kept
|
|
5432
|
-
* separate from `agent-daemon` so runtime noise never drowns daemon issues.
|
|
5433
|
-
* Mirrors `SENTRY_DSNS.agentRuntime` in `config/config.ts`.
|
|
5434
|
-
*/
|
|
5435
|
-
const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
|
|
5436
|
-
/**
|
|
5437
|
-
* MCP tool/server failures on agent VMs — reported by the daemon-hosted MCP
|
|
5438
|
-
* bundler (tool errors, child crashes, stderr output). Dedicated `local-mcp`
|
|
5439
|
-
* project for the locally-installed MCP surface: the cloud MCP Fly service
|
|
5440
|
-
* keeps the `mcp` project (`SENTRY_DSNS.mcp`), so planned daemon restarts
|
|
5441
|
-
* tearing down MCP children never pollute the remote service's signal.
|
|
5442
|
-
* The `source: agent-daemon` + `agentId` tags are kept for continuity.
|
|
5443
|
-
*/
|
|
5444
|
-
const AGENT_MCP_SENTRY_DSN = "https://b4c4b4e19135692206bacabcd12a7fd3@o4511008239452160.ingest.us.sentry.io/4511697094377472";
|
|
5445
|
-
/** Surface → Sentry project: `cli` and `agent-daemon` respectively. */
|
|
5446
|
-
const SURFACE_DSNS = {
|
|
5447
|
-
cli: CLI_SENTRY_DSN,
|
|
5448
|
-
daemon: AGENT_DAEMON_SENTRY_DSN
|
|
5449
|
-
};
|
|
5450
|
-
/** Held after a successful init so the capture helpers can run synchronously. */
|
|
5451
|
-
let sentry = null;
|
|
5452
|
-
/**
|
|
5453
|
-
* Additional bound clients (daemon surface only). The default client keeps the
|
|
5454
|
-
* daemon's own DSN; runtime child-process events and MCP failures are routed
|
|
5455
|
-
* to their own projects via explicitly-bound Scopes — the documented
|
|
5456
|
-
* multi-client pattern. `null` until `initAgentSentry({ surface: "daemon" })`
|
|
5457
|
-
* succeeds.
|
|
5458
|
-
*/
|
|
5459
|
-
let runtimeClient = null;
|
|
5460
|
-
let runtimeScope = null;
|
|
5461
|
-
let mcpClient = null;
|
|
5462
|
-
let mcpScope = null;
|
|
5463
|
-
/**
|
|
5464
|
-
* Map the configured API URL to a coarse environment tag. Best-effort — returns
|
|
5465
|
-
* `"unknown"` when no config is present yet (e.g. `alfe login` pre-setup).
|
|
5466
|
-
*/
|
|
5467
|
-
function deriveEnvironment() {
|
|
5468
|
-
try {
|
|
5469
|
-
const { apiUrl } = resolveConfig();
|
|
5470
|
-
if (apiUrl.includes("api.dev.alfe.ai") || apiUrl.includes("dev.alfe.ai")) return "dev";
|
|
5471
|
-
if (apiUrl.includes("api.test.alfe.ai")) return "test";
|
|
5472
|
-
if (apiUrl.includes("api.demo.alfe.ai")) return "demo";
|
|
5473
|
-
if (apiUrl.includes("api.alfe.ai")) return "prod";
|
|
5474
|
-
} catch {}
|
|
5475
|
-
return "unknown";
|
|
5476
|
-
}
|
|
5477
|
-
/** Active runtime (`openclaw`/`hermes`) from config, or undefined if unknown. */
|
|
5478
|
-
function deriveRuntime() {
|
|
5479
|
-
try {
|
|
5480
|
-
return resolveConfig().runtime;
|
|
5481
|
-
} catch {
|
|
5482
|
-
return;
|
|
5483
|
-
}
|
|
5484
|
-
}
|
|
5485
|
-
/** True when the operator has opted out of error reporting. */
|
|
5486
|
-
function errorReportingDisabled() {
|
|
5487
|
-
const env = process.env.ALFE_ERROR_REPORTING?.trim().toLowerCase();
|
|
5488
|
-
if (env === "0" || env === "false") return true;
|
|
5489
|
-
try {
|
|
5490
|
-
if (readConfig().errorReporting === false) return true;
|
|
5491
|
-
} catch {}
|
|
5492
|
-
return false;
|
|
5493
|
-
}
|
|
5494
|
-
const KV_SECRET_RE = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:authorization|api[_-]?key|apikey|token|secret|password|passwd|bearer|credential)s?)(\s*[:=]\s*|\s+)("?)([^\s"']+)\3/gi;
|
|
5495
|
-
const URL_QUERY_SECRET_RE = /([?&](?:[A-Za-z0-9_-]*(?:token|secret|key|signature|credential|password)|sig|code|x-amz-[a-z-]+)=)[^&\s"']+/gi;
|
|
5496
|
-
const MIN_BARE_SECRET_LEN = 16;
|
|
5497
|
-
const ALFE_TOKEN_RE = /alfe_(?:dev|test|demo|live)_[A-Za-z0-9._-]+/g;
|
|
5498
|
-
const BEARER_RE = /\bbearer\s+[A-Za-z0-9._\-+/=]+/gi;
|
|
5499
|
-
function scrubString(value) {
|
|
5500
|
-
return value.replace(BEARER_RE, "Bearer [REDACTED]").replace(URL_QUERY_SECRET_RE, "$1[REDACTED]").replace(KV_SECRET_RE, (match, label, sep, _q, secret) => {
|
|
5501
|
-
if (!/[:=]/.test(sep) && secret.length < MIN_BARE_SECRET_LEN) return match;
|
|
5502
|
-
return `${label}${sep}[REDACTED]`;
|
|
5503
|
-
}).replace(ALFE_TOKEN_RE, "alfe_[REDACTED]");
|
|
5504
|
-
}
|
|
5505
|
-
/** Scrub secrets from a breadcrumb's message + string data values in place. */
|
|
5506
|
-
function scrubBreadcrumb(b) {
|
|
5507
|
-
if (typeof b.message === "string") b.message = scrubString(b.message);
|
|
5508
|
-
const data = b.data;
|
|
5509
|
-
if (data) for (const k of Object.keys(data)) {
|
|
5510
|
-
const dv = data[k];
|
|
5511
|
-
if (typeof dv === "string") data[k] = scrubString(dv);
|
|
5512
|
-
}
|
|
5513
|
-
return b;
|
|
5514
|
-
}
|
|
5515
|
-
/**
|
|
5516
|
-
* Scrub secrets and drop request/env payloads before an event is sent.
|
|
5517
|
-
* Generic so it preserves the caller's exact event subtype (`beforeSend`
|
|
5518
|
-
* receives — and must return — an `ErrorEvent`, not a widened `Event`).
|
|
5519
|
-
*/
|
|
5520
|
-
function scrubEvent(event) {
|
|
5521
|
-
delete event.request;
|
|
5522
|
-
if (event.contexts) delete event.contexts.runtime;
|
|
5523
|
-
delete event.extra;
|
|
5524
|
-
delete event.server_name;
|
|
5525
|
-
if (typeof event.message === "string") event.message = scrubString(event.message);
|
|
5526
|
-
const values = event.exception?.values;
|
|
5527
|
-
if (values) {
|
|
5528
|
-
for (const v of values) if (typeof v.value === "string") v.value = scrubString(v.value);
|
|
5529
|
-
}
|
|
5530
|
-
if (event.breadcrumbs) for (const b of event.breadcrumbs) scrubBreadcrumb(b);
|
|
5531
|
-
return event;
|
|
5532
|
-
}
|
|
5533
|
-
/**
|
|
5534
|
-
* Initialise Sentry for the agent-side CLI/daemon. Idempotent, best-effort, and
|
|
5535
|
-
* a no-op when opted out or when the baked DSN is empty. Never throws.
|
|
5536
|
-
*/
|
|
5537
|
-
async function initAgentSentry(options) {
|
|
5538
|
-
try {
|
|
5539
|
-
if (sentry) return;
|
|
5540
|
-
if (errorReportingDisabled()) return;
|
|
5541
|
-
const dsn = SURFACE_DSNS[options.surface].trim();
|
|
5542
|
-
if (!dsn) return;
|
|
5543
|
-
const mod = await import("@sentry/node");
|
|
5544
|
-
if (options.surface === "daemon") {
|
|
5545
|
-
const buildBoundScope = (dsn, extraTags) => {
|
|
5546
|
-
try {
|
|
5547
|
-
if (!dsn.trim()) return null;
|
|
5548
|
-
const client = new mod.NodeClient({
|
|
5549
|
-
dsn: dsn.trim(),
|
|
5550
|
-
environment: deriveEnvironment(),
|
|
5551
|
-
...options.release ? { release: options.release } : {},
|
|
5552
|
-
sampleRate: 1,
|
|
5553
|
-
tracesSampleRate: 0,
|
|
5554
|
-
sendDefaultPii: false,
|
|
5555
|
-
transport: mod.makeNodeTransport,
|
|
5556
|
-
stackParser: mod.defaultStackParser,
|
|
5557
|
-
integrations: [],
|
|
5558
|
-
beforeSend: (event) => scrubEvent(event)
|
|
5559
|
-
});
|
|
5560
|
-
client.init();
|
|
5561
|
-
const scope = new mod.Scope();
|
|
5562
|
-
scope.setClient(client);
|
|
5563
|
-
const runtime = deriveRuntime();
|
|
5564
|
-
if (runtime) scope.setTag("runtime", runtime);
|
|
5565
|
-
for (const [k, v] of Object.entries(extraTags)) scope.setTag(k, v);
|
|
5566
|
-
return {
|
|
5567
|
-
client,
|
|
5568
|
-
scope
|
|
5569
|
-
};
|
|
5570
|
-
} catch {
|
|
5571
|
-
return null;
|
|
5572
|
-
}
|
|
5573
|
-
};
|
|
5574
|
-
if (!runtimeScope) {
|
|
5575
|
-
const bound = buildBoundScope(AGENT_RUNTIME_SENTRY_DSN, {});
|
|
5576
|
-
runtimeClient = bound?.client ?? null;
|
|
5577
|
-
runtimeScope = bound?.scope ?? null;
|
|
5578
|
-
}
|
|
5579
|
-
if (!mcpScope) {
|
|
5580
|
-
const bound = buildBoundScope(AGENT_MCP_SENTRY_DSN, { source: "agent-daemon" });
|
|
5581
|
-
mcpClient = bound?.client ?? null;
|
|
5582
|
-
mcpScope = bound?.scope ?? null;
|
|
5583
|
-
}
|
|
5584
|
-
}
|
|
5585
|
-
if (mod.getClient()) {
|
|
5586
|
-
sentry = mod;
|
|
5587
|
-
return;
|
|
5588
|
-
}
|
|
5589
|
-
mod.init({
|
|
5590
|
-
dsn,
|
|
5591
|
-
environment: deriveEnvironment(),
|
|
5592
|
-
...options.release ? { release: options.release } : {},
|
|
5593
|
-
sampleRate: 1,
|
|
5594
|
-
tracesSampleRate: 0,
|
|
5595
|
-
sendDefaultPii: false,
|
|
5596
|
-
...options.surface === "daemon" ? { integrations: (defaults) => defaults.filter((i) => i.name !== "OnUncaughtException" && i.name !== "OnUnhandledRejection") } : {},
|
|
5597
|
-
beforeSend: (event) => scrubEvent(event),
|
|
5598
|
-
beforeBreadcrumb: (breadcrumb) => scrubBreadcrumb(breadcrumb)
|
|
5599
|
-
});
|
|
5600
|
-
sentry = mod;
|
|
5601
|
-
mod.setTag("surface", options.surface);
|
|
5602
|
-
const runtime = deriveRuntime();
|
|
5603
|
-
if (runtime) mod.setTag("runtime", runtime);
|
|
5604
|
-
} catch {
|
|
5605
|
-
sentry = null;
|
|
5606
|
-
}
|
|
5607
|
-
}
|
|
5608
|
-
/**
|
|
5609
|
-
* Attach the resolved agent identity to all subsequent events. Called by the
|
|
5610
|
-
* daemon once `loadDaemonConfig()` has resolved `agentId`/`runtime`.
|
|
5611
|
-
*/
|
|
5612
|
-
function setAgentContext(context) {
|
|
5613
|
-
if (!sentry) return;
|
|
5614
|
-
try {
|
|
5615
|
-
for (const scope of [
|
|
5616
|
-
sentry,
|
|
5617
|
-
runtimeScope,
|
|
5618
|
-
mcpScope
|
|
5619
|
-
]) {
|
|
5620
|
-
if (!scope) continue;
|
|
5621
|
-
if (context.agentId) scope.setTag("agentId", context.agentId);
|
|
5622
|
-
if (context.runtime) scope.setTag("runtime", context.runtime);
|
|
5623
|
-
}
|
|
5624
|
-
} catch {}
|
|
5625
|
-
}
|
|
5626
|
-
/**
|
|
5627
|
-
* Capture an integration lifecycle failure (install/activate/reinstall/remove)
|
|
5628
|
-
* with `{ integration, phase }` tags. Accepts an `Error` (captured with stack)
|
|
5629
|
-
* or a plain message string (captured as an error-level message). No-op when
|
|
5630
|
-
* Sentry is not initialised.
|
|
5631
|
-
*/
|
|
5632
|
-
function captureIntegrationFailure(integration, phase, cause) {
|
|
5633
|
-
if (!sentry) return;
|
|
5634
|
-
try {
|
|
5635
|
-
if (cause instanceof Error) sentry.captureException(cause, { tags: {
|
|
5636
|
-
integration,
|
|
5637
|
-
phase
|
|
5638
|
-
} });
|
|
5639
|
-
else sentry.captureMessage(String(cause), {
|
|
5640
|
-
level: "error",
|
|
5641
|
-
tags: {
|
|
5642
|
-
integration,
|
|
5643
|
-
phase
|
|
5644
|
-
}
|
|
5645
|
-
});
|
|
5646
|
-
} catch {}
|
|
5647
|
-
}
|
|
5648
|
-
/**
|
|
5649
|
-
* Capture a generic CLI failure with a `{ context }` tag. The CLI counterpart
|
|
5650
|
-
* to `captureIntegrationFailure` — used by `exitWithError` at the handled-error
|
|
5651
|
-
* `process.exit()` sites in `@alfe.ai/cli`'s commands, which otherwise report
|
|
5652
|
-
* nothing (the error was caught, never thrown). Accepts an `Error` (captured
|
|
5653
|
-
* with stack), a defined non-Error value (captured as an error-level message),
|
|
5654
|
-
* or nothing (the `context` string itself becomes the message). No-op when
|
|
5655
|
-
* Sentry is not initialised. Never throws.
|
|
5656
|
-
*/
|
|
5657
|
-
function captureCliFailure(context, cause) {
|
|
5658
|
-
if (!sentry) return;
|
|
5659
|
-
try {
|
|
5660
|
-
if (cause instanceof Error) sentry.captureException(cause, { tags: { context } });
|
|
5661
|
-
else if (cause === void 0) sentry.captureMessage(context, { level: "error" });
|
|
5662
|
-
else sentry.captureMessage(String(cause), {
|
|
5663
|
-
level: "error",
|
|
5664
|
-
tags: { context }
|
|
5665
|
-
});
|
|
5666
|
-
} catch {}
|
|
5667
|
-
}
|
|
5668
|
-
/**
|
|
5669
|
-
* Capture a runtime child-process crash (non-graceful exit) or spawn failure
|
|
5670
|
-
* into the dedicated `agent-runtime` project. Recent output travels as
|
|
5671
|
-
* breadcrumbs — NEVER `extra`, which `scrubEvent` (the runtime client's
|
|
5672
|
-
* `beforeSend`) deletes. Throttling is the caller's job (`CaptureThrottle` in
|
|
5673
|
-
* `runtime-output-monitor.ts`). No-op when the runtime client is not
|
|
5674
|
-
* initialised. Never throws.
|
|
5675
|
-
*/
|
|
5676
|
-
function captureRuntimeCrash(opts) {
|
|
5677
|
-
if (!runtimeScope) return;
|
|
5678
|
-
try {
|
|
5679
|
-
const scope = runtimeScope.clone();
|
|
5680
|
-
for (const { stream, line } of opts.recentOutput) scope.addBreadcrumb({
|
|
5681
|
-
category: `runtime.${stream}`,
|
|
5682
|
-
level: stream === "stderr" ? "warning" : "info",
|
|
5683
|
-
message: line
|
|
5684
|
-
});
|
|
5685
|
-
const crashKey = opts.spawnError ? `spawn:${opts.spawnError.code ?? "error"}` : String(opts.code ?? opts.signal ?? "unknown");
|
|
5686
|
-
scope.setFingerprint([
|
|
5687
|
-
"runtime-crash",
|
|
5688
|
-
opts.runtime,
|
|
5689
|
-
crashKey
|
|
5690
|
-
]);
|
|
5691
|
-
scope.setTags({
|
|
5692
|
-
runtime: opts.runtime,
|
|
5693
|
-
exitCode: String(opts.code),
|
|
5694
|
-
signal: String(opts.signal),
|
|
5695
|
-
crashesSuppressed: String(opts.crashesSuppressed ?? 0)
|
|
5696
|
-
});
|
|
5697
|
-
if (opts.spawnError) scope.captureException(opts.spawnError);
|
|
5698
|
-
else scope.captureMessage(`Runtime ${opts.runtime} crashed (code=${String(opts.code)}, signal=${String(opts.signal)})`, "error");
|
|
5699
|
-
} catch {}
|
|
5700
|
-
}
|
|
5701
|
-
/**
|
|
5702
|
-
* Capture an error-looking block of runtime output (stack trace, Python
|
|
5703
|
-
* traceback, or ERROR-level log line) detected while the child is running.
|
|
5704
|
-
* The block lines travel as breadcrumbs; the head line is the message.
|
|
5705
|
-
* Throttling/dedupe is the caller's job. No-op when Sentry is not initialised.
|
|
5706
|
-
* Never throws.
|
|
5707
|
-
*/
|
|
5708
|
-
function captureRuntimeErrorOutput(opts) {
|
|
5709
|
-
if (!runtimeScope) return;
|
|
5710
|
-
try {
|
|
5711
|
-
const scope = runtimeScope.clone();
|
|
5712
|
-
for (const line of opts.lines) scope.addBreadcrumb({
|
|
5713
|
-
category: `runtime.${opts.stream}`,
|
|
5714
|
-
level: "error",
|
|
5715
|
-
message: line
|
|
5716
|
-
});
|
|
5717
|
-
scope.setFingerprint([
|
|
5718
|
-
"runtime-output",
|
|
5719
|
-
opts.runtime,
|
|
5720
|
-
opts.kind,
|
|
5721
|
-
opts.fingerprintKey
|
|
5722
|
-
]);
|
|
5723
|
-
scope.setTags({
|
|
5724
|
-
runtime: opts.runtime,
|
|
5725
|
-
stream: opts.stream,
|
|
5726
|
-
kind: opts.kind,
|
|
5727
|
-
suppressedCount: String(opts.suppressedCount ?? 0)
|
|
5728
|
-
});
|
|
5729
|
-
scope.captureMessage(opts.lines[0].slice(0, 300), "error");
|
|
5730
|
-
} catch {}
|
|
5731
|
-
}
|
|
5732
|
-
/**
|
|
5733
|
-
* Capture an MCP failure (tool error, server crash, or error-looking stderr
|
|
5734
|
-
* output) into the `mcp` project via the daemon's bound MCP client. Throttling
|
|
5735
|
-
* is the caller's job (`mcp-error-capture.ts`). No-op when the MCP client is
|
|
5736
|
-
* not initialised. Never throws.
|
|
5737
|
-
*/
|
|
5738
|
-
function captureMcpFailure(opts) {
|
|
5739
|
-
if (!mcpScope) return;
|
|
5740
|
-
try {
|
|
5741
|
-
const scope = mcpScope.clone();
|
|
5742
|
-
for (const line of opts.lines ?? []) scope.addBreadcrumb({
|
|
5743
|
-
category: `mcp.${opts.server}`,
|
|
5744
|
-
level: "error",
|
|
5745
|
-
message: line
|
|
5746
|
-
});
|
|
5747
|
-
scope.setFingerprint([
|
|
5748
|
-
"agent-mcp",
|
|
5749
|
-
opts.server,
|
|
5750
|
-
opts.kind,
|
|
5751
|
-
...opts.tool ? [opts.tool] : []
|
|
5752
|
-
]);
|
|
5753
|
-
scope.setTags({
|
|
5754
|
-
server: opts.server,
|
|
5755
|
-
kind: opts.kind,
|
|
5756
|
-
...opts.tool ? { tool: opts.tool } : {},
|
|
5757
|
-
suppressedCount: String(opts.suppressedCount ?? 0)
|
|
5758
|
-
});
|
|
5759
|
-
scope.captureMessage(`MCP ${opts.server}${opts.tool ? `.${opts.tool}` : ""} ${opts.kind}: ${opts.message.slice(0, 300)}`, "error");
|
|
5760
|
-
} catch {}
|
|
5761
|
-
}
|
|
5762
|
-
/** Flush queued events (small timeout) so short-lived commands deliver them. */
|
|
5763
|
-
async function flushSentry(timeoutMs = 2e3) {
|
|
5764
|
-
for (const client of [runtimeClient, mcpClient]) try {
|
|
5765
|
-
if (client) await client.flush(timeoutMs);
|
|
5766
|
-
} catch {}
|
|
5767
|
-
if (!sentry) return;
|
|
5768
|
-
try {
|
|
5769
|
-
await sentry.flush(timeoutMs);
|
|
5770
|
-
} catch {}
|
|
5771
|
-
}
|
|
5772
|
-
/**
|
|
5773
|
-
* Capture a fatal error — for the CLI's top-level catch. Does NOT flush: the
|
|
5774
|
-
* caller's `finally { await flushSentry() }` delivers it (flushing here too
|
|
5775
|
-
* doubled the worst-case exit delay on a failing command).
|
|
5776
|
-
*/
|
|
5777
|
-
function captureFatal(cause) {
|
|
5778
|
-
if (!sentry) return;
|
|
5779
|
-
try {
|
|
5780
|
-
sentry.captureException(cause);
|
|
5781
|
-
} catch {}
|
|
5782
|
-
}
|
|
5783
|
-
//#endregion
|
|
5784
5442
|
//#region src/reconciliation.ts
|
|
5785
5443
|
const log$5 = logger$1.child({ component: "Reconciliation" });
|
|
5786
5444
|
/**
|
|
@@ -7099,6 +6757,14 @@ var CommandQueue = class {
|
|
|
7099
6757
|
*/
|
|
7100
6758
|
const LAUNCHD_LABEL = "ai.alfe.gateway";
|
|
7101
6759
|
const SYSTEMD_SERVICE = "alfe-gateway";
|
|
6760
|
+
/**
|
|
6761
|
+
* On-disk path for the boot-time self-heal guard script (Linux only).
|
|
6762
|
+
* Written next to the systemd unit at setup time and invoked via
|
|
6763
|
+
* `ExecStartPre=` — see `writeGuardScript` and `generateSystemdUnit`.
|
|
6764
|
+
*/
|
|
6765
|
+
function getGuardScriptPath() {
|
|
6766
|
+
return isRootUser() ? "/usr/local/lib/alfe/alfe-cli-guard.sh" : join(homedir(), ".alfe", "alfe-cli-guard.sh");
|
|
6767
|
+
}
|
|
7102
6768
|
function isRootUser() {
|
|
7103
6769
|
return process.getuid?.() === 0;
|
|
7104
6770
|
}
|
|
@@ -7174,16 +6840,108 @@ function generateLaunchdPlist() {
|
|
|
7174
6840
|
</plist>`;
|
|
7175
6841
|
}
|
|
7176
6842
|
/**
|
|
6843
|
+
* Boot-time self-heal guard (Linux only).
|
|
6844
|
+
*
|
|
6845
|
+
* Runs as an `ExecStartPre=` BEFORE the daemon launches, OUTSIDE the
|
|
6846
|
+
* (possibly-broken) `alfe` binary. It exists for the case defense #1
|
|
6847
|
+
* (verify-before-exit in `upgrade.ts`) structurally cannot cover: the daemon
|
|
6848
|
+
* is KILLED during `npm install -g @alfe.ai/cli` (reboot / OOM), leaving the
|
|
6849
|
+
* package with deps but no `dist/` and a dangling `/usr/bin/alfe` symlink.
|
|
6850
|
+
* systemd then execs a dangling binary forever → 203/EXEC crash-loop, a
|
|
6851
|
+
* permanent brick with no automatic recovery (real prod incident,
|
|
6852
|
+
* ~13,570 restarts over 2 days).
|
|
6853
|
+
*
|
|
6854
|
+
* The guard:
|
|
6855
|
+
* - checks the CLI is intact: the symlink target (`dist/index.js`) exists AND
|
|
6856
|
+
* `alfe --version` exits 0;
|
|
6857
|
+
* - if broken, reinstalls `@alfe.ai/cli@$ALFE_CLI_VERSION` (the exact version
|
|
6858
|
+
* baked into the unit's `Environment=`; falls back to `@latest` when
|
|
6859
|
+
* unset), so the daemon then execs a working binary;
|
|
6860
|
+
* - is idempotent + fast (no-op when healthy) and NEVER wedges boot: any of
|
|
6861
|
+
* its own failures log + `exit 0` so a human can still SSH in.
|
|
6862
|
+
*
|
|
6863
|
+
* POSIX sh (dash-safe): the box may not have bash. Keep the two intactness
|
|
6864
|
+
* checks in lockstep with `verifyCliInstall` in `verify-cli-install.ts`.
|
|
6865
|
+
*/
|
|
6866
|
+
/** @internal exported for unit tests; not re-exported from the barrel. */
|
|
6867
|
+
function generateGuardScript() {
|
|
6868
|
+
const version = process.env.ALFE_CLI_VERSION;
|
|
6869
|
+
return `#!/bin/sh
|
|
6870
|
+
# Alfe CLI boot-time self-heal guard. Auto-generated by 'alfe setup' — do not edit.
|
|
6871
|
+
# Repairs an interrupted 'npm install -g @alfe.ai/cli' before the daemon starts,
|
|
6872
|
+
# so a mid-install reboot can't brick the agent with a 203/EXEC crash-loop.
|
|
6873
|
+
# Must never wedge boot: every failure path logs and exits 0.
|
|
6874
|
+
set -u
|
|
6875
|
+
|
|
6876
|
+
TARGET='${version && version.length > 0 ? `@alfe.ai/cli@${version}` : "@alfe.ai/cli@latest"}'
|
|
6877
|
+
log() { echo "[alfe-cli-guard] $*" >&2; }
|
|
6878
|
+
|
|
6879
|
+
# Resolve the alfe bin and its real target (dist/index.js). readlink -f follows
|
|
6880
|
+
# the whole symlink chain; fall back to the bin path itself if unavailable.
|
|
6881
|
+
BIN="$(command -v alfe 2>/dev/null || true)"
|
|
6882
|
+
if [ -n "$BIN" ]; then
|
|
6883
|
+
ENTRY="$(readlink -f "$BIN" 2>/dev/null || echo "$BIN")"
|
|
6884
|
+
else
|
|
6885
|
+
ENTRY=""
|
|
6886
|
+
fi
|
|
6887
|
+
|
|
6888
|
+
healthy=1
|
|
6889
|
+
if [ -z "$ENTRY" ] || [ ! -f "$ENTRY" ]; then
|
|
6890
|
+
healthy=0
|
|
6891
|
+
log "CLI entry missing (bin=\${BIN:-<none>} entry=\${ENTRY:-<none>})"
|
|
6892
|
+
elif ! alfe --version >/dev/null 2>&1; then
|
|
6893
|
+
healthy=0
|
|
6894
|
+
log "alfe --version failed"
|
|
6895
|
+
fi
|
|
6896
|
+
|
|
6897
|
+
if [ "$healthy" -eq 1 ]; then
|
|
6898
|
+
exit 0
|
|
6899
|
+
fi
|
|
6900
|
+
|
|
6901
|
+
log "CLI install looks broken — reinstalling $TARGET"
|
|
6902
|
+
# Blow away a partial tree first so npm re-materialises dist/ cleanly. Best-effort.
|
|
6903
|
+
GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
6904
|
+
if [ -n "$GLOBAL_ROOT" ] && [ -d "$GLOBAL_ROOT/@alfe.ai/cli" ]; then
|
|
6905
|
+
rm -rf "$GLOBAL_ROOT/@alfe.ai/cli" 2>/dev/null || true
|
|
6906
|
+
fi
|
|
6907
|
+
|
|
6908
|
+
# Bound the reinstall so a slow/unreachable registry at boot can't stall the
|
|
6909
|
+
# ExecStartPre up to systemd's DefaultTimeoutStartSec (~90s). Two layers:
|
|
6910
|
+
# - npm --fetch-timeout/--fetch-retries cap per-request/hung-socket time;
|
|
6911
|
+
# - a wall-clock 'timeout' wrapper caps the whole install when present
|
|
6912
|
+
# ('timeout' isn't guaranteed, so fall back to a bare install).
|
|
6913
|
+
# stdout->stderr ('>&2') so npm's chatter lands in the journal, not on fd1.
|
|
6914
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
6915
|
+
timeout 120 npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
|
|
6916
|
+
rc=$?
|
|
6917
|
+
else
|
|
6918
|
+
npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
|
|
6919
|
+
rc=$?
|
|
6920
|
+
fi
|
|
6921
|
+
if [ "$rc" -eq 0 ]; then
|
|
6922
|
+
log "reinstall succeeded"
|
|
6923
|
+
else
|
|
6924
|
+
log "reinstall FAILED (rc=$rc) — daemon start may still fail; a human can SSH in to repair"
|
|
6925
|
+
fi
|
|
6926
|
+
|
|
6927
|
+
# Always exit 0: even a failed repair must not block the unit from attempting
|
|
6928
|
+
# ExecStart (which then fails visibly) rather than wedging silently in pre-start.
|
|
6929
|
+
exit 0
|
|
6930
|
+
`;
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
7177
6933
|
* Generate a systemd unit for Linux.
|
|
7178
6934
|
* Root users get a system-level unit; non-root get a user-level unit.
|
|
7179
6935
|
*/
|
|
6936
|
+
/** @internal exported for unit tests; not re-exported from the barrel. */
|
|
7180
6937
|
function generateSystemdUnit() {
|
|
7181
6938
|
const alfeBin = getAlfeBinPath();
|
|
7182
6939
|
const root = isRootUser();
|
|
7183
6940
|
const envLines = [
|
|
7184
6941
|
"ALFE_MANAGED",
|
|
7185
6942
|
"ALFE_API_KEY",
|
|
7186
|
-
"LOG_LEVEL"
|
|
6943
|
+
"LOG_LEVEL",
|
|
6944
|
+
"ALFE_CLI_VERSION"
|
|
7187
6945
|
].filter((key) => process.env[key]).map((key) => `Environment=${key}=${process.env[key] ?? ""}`).join("\n");
|
|
7188
6946
|
return `[Unit]
|
|
7189
6947
|
Description=Alfe Gateway Daemon
|
|
@@ -7192,6 +6950,7 @@ Wants=network-online.target
|
|
|
7192
6950
|
|
|
7193
6951
|
[Service]
|
|
7194
6952
|
Type=simple
|
|
6953
|
+
ExecStartPre=-/bin/sh ${getGuardScriptPath()}
|
|
7195
6954
|
ExecStart=${alfeBin} gateway daemon
|
|
7196
6955
|
Restart=always
|
|
7197
6956
|
RestartSec=10
|
|
@@ -7208,6 +6967,19 @@ ${envLines}
|
|
|
7208
6967
|
WantedBy=${root ? "multi-user.target" : "default.target"}`;
|
|
7209
6968
|
}
|
|
7210
6969
|
/**
|
|
6970
|
+
* Write the boot-time self-heal guard script to disk (Linux only) and make it
|
|
6971
|
+
* executable. Called from `installSystemd` before the unit is written.
|
|
6972
|
+
*/
|
|
6973
|
+
async function writeGuardScript() {
|
|
6974
|
+
const guardPath = getGuardScriptPath();
|
|
6975
|
+
await mkdir(dirname(guardPath), { recursive: true });
|
|
6976
|
+
await writeFile(guardPath, generateGuardScript(), {
|
|
6977
|
+
encoding: "utf-8",
|
|
6978
|
+
mode: 493
|
|
6979
|
+
});
|
|
6980
|
+
logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
|
|
6981
|
+
}
|
|
6982
|
+
/**
|
|
7211
6983
|
* Install the service unit for the current platform.
|
|
7212
6984
|
*/
|
|
7213
6985
|
async function installService() {
|
|
@@ -7269,6 +7041,7 @@ async function installSystemd() {
|
|
|
7269
7041
|
const dir = root ? "/etc/systemd/system" : join(homedir(), ".config", "systemd", "user");
|
|
7270
7042
|
const ctl = root ? "systemctl" : "systemctl --user";
|
|
7271
7043
|
if (!root) await mkdir(dir, { recursive: true });
|
|
7044
|
+
await writeGuardScript();
|
|
7272
7045
|
await writeFile(unitPath, generateSystemdUnit(), "utf-8");
|
|
7273
7046
|
logger$1.info({
|
|
7274
7047
|
path: unitPath,
|
|
@@ -7291,6 +7064,9 @@ async function uninstallSystemd() {
|
|
|
7291
7064
|
try {
|
|
7292
7065
|
await unlink(unitPath);
|
|
7293
7066
|
} catch {}
|
|
7067
|
+
try {
|
|
7068
|
+
await unlink(getGuardScriptPath());
|
|
7069
|
+
} catch {}
|
|
7294
7070
|
try {
|
|
7295
7071
|
execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
|
|
7296
7072
|
} catch {}
|
|
@@ -22659,6 +22435,10 @@ var RuntimeProcess = class {
|
|
|
22659
22435
|
command: "hermes",
|
|
22660
22436
|
args: ["gateway", "run"]
|
|
22661
22437
|
};
|
|
22438
|
+
case "claude-code": return {
|
|
22439
|
+
command: "alfe-claude-host",
|
|
22440
|
+
args: ["run"]
|
|
22441
|
+
};
|
|
22662
22442
|
default: throw new Error(`Unsupported runtime: ${this.options.runtime}`);
|
|
22663
22443
|
}
|
|
22664
22444
|
}
|
|
@@ -23642,6 +23422,13 @@ let mcpManagerRef = null;
|
|
|
23642
23422
|
* and for managed/self-hosted runtimes other than hermes.
|
|
23643
23423
|
*/
|
|
23644
23424
|
let hermesMcpSync = null;
|
|
23425
|
+
/**
|
|
23426
|
+
* claude-code-only MCP store consumer. Mirrors the runtime-agnostic MCP store
|
|
23427
|
+
* into the host's `--mcp-config` JSON (~/.claude-code/mcp-config.json), injecting
|
|
23428
|
+
* ALFE_API_KEY into every stdio server's env. Null for every other runtime
|
|
23429
|
+
* (never constructed) — same shape/role as {@link hermesMcpSync}.
|
|
23430
|
+
*/
|
|
23431
|
+
let claudeCodeMcpSync = null;
|
|
23645
23432
|
let aiProxyServer = null;
|
|
23646
23433
|
let runtimeProcess = null;
|
|
23647
23434
|
let turnActivityProbe = null;
|
|
@@ -23755,6 +23542,10 @@ const RUNTIME_VERSION_COMMANDS = {
|
|
|
23755
23542
|
hermes: {
|
|
23756
23543
|
command: "hermes",
|
|
23757
23544
|
args: ["version"]
|
|
23545
|
+
},
|
|
23546
|
+
"claude-code": {
|
|
23547
|
+
command: "alfe-claude-host",
|
|
23548
|
+
args: ["--version"]
|
|
23758
23549
|
}
|
|
23759
23550
|
};
|
|
23760
23551
|
/**
|
|
@@ -23993,6 +23784,13 @@ async function startDaemon() {
|
|
|
23993
23784
|
runtime: name,
|
|
23994
23785
|
home: runtimeCfg.workspace
|
|
23995
23786
|
}, "Registered Hermes runtime applier");
|
|
23787
|
+
} else if (name === "claude-code") {
|
|
23788
|
+
runtimeAppliers.set(name, new ClaudeCodeApplier({ home: runtimeCfg.workspace }));
|
|
23789
|
+
logger$1.info({
|
|
23790
|
+
runtime: name,
|
|
23791
|
+
home: runtimeCfg.workspace,
|
|
23792
|
+
agentWorkspace: runtimeCfg.agentWorkspace
|
|
23793
|
+
}, "Registered Claude Code runtime applier");
|
|
23996
23794
|
} else logger$1.warn({ runtime: name }, "Unknown runtime type — skipping");
|
|
23997
23795
|
runtimeAppliersRef = runtimeAppliers;
|
|
23998
23796
|
const integrationsService = new IntegrationsService(new AlfeApiClient({
|
|
@@ -24011,7 +23809,7 @@ async function startDaemon() {
|
|
|
24011
23809
|
idleTtlMs: 0,
|
|
24012
23810
|
...mcpErrorHooks
|
|
24013
23811
|
}, { connect: defaultConnect });
|
|
24014
|
-
if (config.runtime === "hermes") logger$1.info(
|
|
23812
|
+
if (config.runtime === "hermes" || config.runtime === "claude-code") logger$1.info({ runtime: config.runtime }, "MCP store mirrored into runtime config by the runtime MCP sync — daemon MCP bundler left idle");
|
|
24015
23813
|
else {
|
|
24016
23814
|
await mcpManager.loadIntoBundler(mcpBundler);
|
|
24017
23815
|
const warmBundler = (reason) => {
|
|
@@ -24070,6 +23868,18 @@ async function startDaemon() {
|
|
|
24070
23868
|
hermesMcpSync.start();
|
|
24071
23869
|
}
|
|
24072
23870
|
}
|
|
23871
|
+
if (config.runtime === "claude-code") {
|
|
23872
|
+
const ccCfg = config.runtimes[config.runtime];
|
|
23873
|
+
if (ccCfg) {
|
|
23874
|
+
claudeCodeMcpSync = new ClaudeCodeMcpSync({
|
|
23875
|
+
manager: mcpManager,
|
|
23876
|
+
home: ccCfg.workspace,
|
|
23877
|
+
apiKey: config.apiKey,
|
|
23878
|
+
requestRestart: requestRuntimeRestart
|
|
23879
|
+
});
|
|
23880
|
+
claudeCodeMcpSync.start();
|
|
23881
|
+
}
|
|
23882
|
+
}
|
|
24073
23883
|
cloudClient.setOnReconciliationComplete(() => {
|
|
24074
23884
|
try {
|
|
24075
23885
|
const activeCommands = integrationManager.getActiveCommands();
|
|
@@ -24133,6 +23943,10 @@ async function startDaemon() {
|
|
|
24133
23943
|
hermesMcpSync.stop();
|
|
24134
23944
|
hermesMcpSync = null;
|
|
24135
23945
|
}
|
|
23946
|
+
if (claudeCodeMcpSync) {
|
|
23947
|
+
claudeCodeMcpSync.stop();
|
|
23948
|
+
claudeCodeMcpSync = null;
|
|
23949
|
+
}
|
|
24136
23950
|
if (mcpBundler) {
|
|
24137
23951
|
logger$1.debug("Stopping MCP bundler...");
|
|
24138
23952
|
await mcpBundler.dispose();
|
|
@@ -24966,4 +24780,4 @@ function formatDuration(ms) {
|
|
|
24966
24780
|
return `${String(Math.round(seconds / 3600))}h`;
|
|
24967
24781
|
}
|
|
24968
24782
|
//#endregion
|
|
24969
|
-
export {
|
|
24783
|
+
export { installService as a, uninstallService as c, PID_PATH as d, SOCKET_PATH as f, PINNED_OPENCLAW_VERSION as g, resolveAgentIdentity as h, checkExistingDaemon as i, PROTOCOL_VERSION as l, loadDaemonConfig as m, queryDaemonHealth as n, startService as o, fetchAgentConfig as p, startDaemon as r, stopExistingDaemon as s, formatHealthReport as t, ALFE_DIR as u };
|