@alfe.ai/gateway 0.0.49 → 0.1.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.
Files changed (2) hide show
  1. package/dist/health.js +555 -30
  2. package/package.json +7 -5
package/dist/health.js CHANGED
@@ -6,13 +6,15 @@ import { promisify } from "node:util";
6
6
  import { dirname, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import pino from "pino";
9
- import { chmodSync, existsSync, readFileSync, unlinkSync } from "node:fs";
9
+ import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import { getEndpointFromToken, readConfig } from "@alfe.ai/config";
11
11
  import crypto from "crypto";
12
12
  import { parse } from "smol-toml";
13
13
  import WebSocket from "ws";
14
14
  import { createConnection, createServer } from "node:net";
15
- import { IntegrationManager, IntegrationManagerAdapter, OpenClawApplier } from "@alfe.ai/integrations";
15
+ import { IntegrationManager, IntegrationManagerAdapter, McpApplier, OpenClawApplier } from "@alfe.ai/integrations";
16
+ import { AgentApiClient } from "@alfe.ai/agent-api-client";
17
+ import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
16
18
  import stream, { Readable } from "stream";
17
19
  import util, { format } from "util";
18
20
  import http from "http";
@@ -90,6 +92,9 @@ const ID_PREFIXES = {
90
92
  identityVerification: "ivf",
91
93
  role: "role",
92
94
  directGrant: "dgr",
95
+ oauthConnection: "con",
96
+ oauthConnectionRequest: "crq",
97
+ deviceCode: "dvc",
93
98
  attachment: "att",
94
99
  run: "run",
95
100
  request: "req",
@@ -255,31 +260,41 @@ var AlfeApiClient = class {
255
260
  this.getToken = options.getToken;
256
261
  this.onAuthFailure = options.onAuthFailure;
257
262
  }
258
- /** Shared fetch logic — handles auth, 401, and network errors. */
259
- async _fetch(path, options) {
263
+ /**
264
+ * Shared fetch logic — handles auth, 401, and network errors.
265
+ *
266
+ * `skipAuth` callers (public endpoints like OAuth device-code) opt out of
267
+ * the auth-header injection AND the synthetic 401 short-circuit. Without
268
+ * this opt-out, a CLI calling /auth/device-code (no token yet by design)
269
+ * would never reach the network — the `getToken: () => null` path would
270
+ * synthesize a 401 and fire onAuthFailure, breaking the entire flow.
271
+ */
272
+ async _fetch(path, options, skipAuth = false) {
260
273
  try {
261
- const token = await this.getToken();
262
- if (!token) {
263
- this.onAuthFailure?.();
264
- return {
265
- ok: false,
266
- result: {
267
- ok: false,
268
- error: "No auth token available",
269
- status: 401
270
- }
271
- };
272
- }
273
274
  const url = `${this.apiBaseUrl}${path}`;
274
275
  const headers = new Headers(options?.headers);
275
- headers.set("Authorization", `Bearer ${token}`);
276
276
  headers.set("Content-Type", "application/json");
277
277
  headers.set("x-correlation-id", correlationId());
278
+ if (!skipAuth) {
279
+ const token = await this.getToken();
280
+ if (!token) {
281
+ this.onAuthFailure?.();
282
+ return {
283
+ ok: false,
284
+ result: {
285
+ ok: false,
286
+ error: "No auth token available",
287
+ status: 401
288
+ }
289
+ };
290
+ }
291
+ headers.set("Authorization", `Bearer ${token}`);
292
+ }
278
293
  const res = await fetch(url, {
279
294
  ...options,
280
295
  headers
281
296
  });
282
- if (res.status === 401) {
297
+ if (res.status === 401 && !skipAuth) {
283
298
  this.onAuthFailure?.();
284
299
  return {
285
300
  ok: false,
@@ -327,6 +342,20 @@ var AlfeApiClient = class {
327
342
  };
328
343
  }
329
344
  /**
345
+ * Make a request to a PUBLIC endpoint that does not require authentication.
346
+ * Skips both the Authorization header injection AND the onAuthFailure
347
+ * callback. Use for endpoints like /auth/device-code that the CLI hits
348
+ * before it has a token.
349
+ */
350
+ async publicRequest(path, options) {
351
+ const result = await this._fetch(path, options, true);
352
+ if (!result.ok) return result.result;
353
+ return {
354
+ ok: true,
355
+ data: result.body.data
356
+ };
357
+ }
358
+ /**
330
359
  * Make an authenticated request that returns the body directly (no envelope unwrap).
331
360
  * Use for APIs that don't use the @auriclabs/api-core response format (e.g. gateway).
332
361
  */
@@ -368,6 +397,55 @@ var AuthService = class {
368
397
  deleteToken(tokenId) {
369
398
  return this.client.request(`${this.prefix}/tokens/${tokenId}`, { method: "DELETE" });
370
399
  }
400
+ /**
401
+ * Start a device-code flow. Called by the CLI when the user runs
402
+ * `alfe login` and picks the browser path. The returned `device_code`
403
+ * is the CLI's bearer secret for polling /auth/device-token; the
404
+ * `user_code` is what the user types/sees on the dashboard.
405
+ *
406
+ * Uses `publicRequest` because the CLI has no token yet — the standard
407
+ * `request` path would short-circuit with a synthetic 401.
408
+ */
409
+ startDeviceCode(input) {
410
+ return this.client.publicRequest(`${this.prefix}/device-code`, {
411
+ method: "POST",
412
+ body: JSON.stringify(input)
413
+ });
414
+ }
415
+ /**
416
+ * Poll for the minted API key. Public endpoint. Returns:
417
+ * 200 → { api_key, tenantId, tokenExpiresAt } (success — write to config)
418
+ * 428 → still pending (caller should sleep `interval` and retry)
419
+ * 429 → polling too fast; caller should back off
420
+ * 410 → device-code expired; user must run `alfe login` again
421
+ * 400 → already redeemed (security: do not retry)
422
+ *
423
+ * Surface the HTTP-status code via the standard ApiResult error shape
424
+ * so callers can branch.
425
+ */
426
+ pollDeviceToken(deviceCode) {
427
+ return this.client.publicRequest(`${this.prefix}/device-token`, {
428
+ method: "POST",
429
+ body: JSON.stringify({ device_code: deviceCode })
430
+ });
431
+ }
432
+ /** Dashboard-side. Fetch the device fingerprint for an approval card. */
433
+ lookupDeviceCode(userCode) {
434
+ return this.client.request(`${this.prefix}/device-code/lookup`, {
435
+ method: "POST",
436
+ body: JSON.stringify({ user_code: userCode })
437
+ });
438
+ }
439
+ /** Dashboard-side. Approve the pending device-code; CLI's next poll gets the key. */
440
+ approveDeviceCode(input) {
441
+ return this.client.request(`${this.prefix}/device-code/approve`, {
442
+ method: "POST",
443
+ body: JSON.stringify({
444
+ user_code: input.userCode,
445
+ expiresIn: input.expiresIn
446
+ })
447
+ });
448
+ }
371
449
  getOnboardingStatus() {
372
450
  return this.client.request(`${this.prefix}/onboarding/status`);
373
451
  }
@@ -421,6 +499,48 @@ var AuthService = class {
421
499
  body: JSON.stringify({ seats })
422
500
  });
423
501
  }
502
+ listOrgMembers() {
503
+ return this.client.request(`${this.prefix}/org/members`);
504
+ }
505
+ patchOrgMember(userId, body) {
506
+ return this.client.request(`${this.prefix}/org/members/${encodeURIComponent(userId)}`, {
507
+ method: "PATCH",
508
+ body: JSON.stringify(body)
509
+ });
510
+ }
511
+ removeOrgMember(userId) {
512
+ return this.client.request(`${this.prefix}/org/members/${encodeURIComponent(userId)}`, { method: "DELETE" });
513
+ }
514
+ listOrgInvitations() {
515
+ return this.client.request(`${this.prefix}/org/invitations`);
516
+ }
517
+ createOrgInvitation(input) {
518
+ return this.client.request(`${this.prefix}/org/invitations`, {
519
+ method: "POST",
520
+ body: JSON.stringify(input)
521
+ });
522
+ }
523
+ revokeOrgInvitation(invitationId) {
524
+ return this.client.request(`${this.prefix}/org/invitations/${encodeURIComponent(invitationId)}`, { method: "DELETE" });
525
+ }
526
+ listOrgDomains() {
527
+ return this.client.request(`${this.prefix}/org/domains`);
528
+ }
529
+ createOrgDomain(input) {
530
+ return this.client.request(`${this.prefix}/org/domains`, {
531
+ method: "POST",
532
+ body: JSON.stringify(input)
533
+ });
534
+ }
535
+ deleteOrgDomain(domainId) {
536
+ return this.client.request(`${this.prefix}/org/domains/${encodeURIComponent(domainId)}`, { method: "DELETE" });
537
+ }
538
+ updateOrgSettings(input) {
539
+ return this.client.request(`${this.prefix}/org/settings`, {
540
+ method: "PATCH",
541
+ body: JSON.stringify(input)
542
+ });
543
+ }
424
544
  };
425
545
  //#endregion
426
546
  //#region ../../packages-internal/api-client/dist/services/integrations.js
@@ -536,12 +656,6 @@ var IntegrationsService = class {
536
656
  const query = email ? `?email=${encodeURIComponent(email)}` : "";
537
657
  return this.client.request(`/google/agents/${encodeURIComponent(agentId)}/account${query}`, { method: "DELETE" });
538
658
  }
539
- setDefaultGoogleAccount(agentId, email) {
540
- return this.client.request(`/google/agents/${encodeURIComponent(agentId)}/account/default`, {
541
- method: "PUT",
542
- body: JSON.stringify({ email })
543
- });
544
- }
545
659
  getAtlassianSites(agentId) {
546
660
  return this.client.request(`/atlassian/agents/${encodeURIComponent(agentId)}/sites`);
547
661
  }
@@ -4236,7 +4350,9 @@ enumValues({
4236
4350
  Zhipu: "zhipu",
4237
4351
  OpenRouter: "openrouter",
4238
4352
  ElevenLabs: "elevenlabs",
4239
- Twilio: "twilio"
4353
+ Twilio: "twilio",
4354
+ ClaudeMax: "claude-max",
4355
+ OpenAICodexMax: "openai-codex-max"
4240
4356
  });
4241
4357
  enumValues({
4242
4358
  Month: "month",
@@ -4452,6 +4568,7 @@ const GoogleModel = {
4452
4568
  const GOOGLE_MODELS = enumValues(GoogleModel);
4453
4569
  const MiniMaxModel = {
4454
4570
  M27: "MiniMax-M2.7",
4571
+ M27HighSpeed: "MiniMax-M2.7-highspeed",
4455
4572
  M25: "MiniMax-M2.5"
4456
4573
  };
4457
4574
  const MINIMAX_MODELS = enumValues(MiniMaxModel);
@@ -4491,8 +4608,8 @@ _enum(MISTRAL_MODELS);
4491
4608
  _enum(XAI_MODELS);
4492
4609
  _enum(ZHIPU_MODELS);
4493
4610
  string().min(1);
4494
- AnthropicModel.Opus, AnthropicModel.Sonnet, AnthropicModel.Haiku, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M27, MiniMaxModel.M25, MistralModel.Large, MistralModel.Small, MistralModel.Codestral, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM51, ZhipuModel.GLM51Air;
4495
- AnthropicModel.Opus, AnthropicModel.Sonnet, AnthropicModel.Haiku, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M27, MiniMaxModel.M25, MistralModel.Large, MistralModel.Small, MistralModel.Codestral, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM51, ZhipuModel.GLM51Air;
4611
+ AnthropicModel.Opus, AnthropicModel.Sonnet, AnthropicModel.Haiku, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MistralModel.Large, MistralModel.Small, MistralModel.Codestral, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM51, ZhipuModel.GLM51Air;
4612
+ AnthropicModel.Opus, AnthropicModel.Sonnet, AnthropicModel.Haiku, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MistralModel.Large, MistralModel.Small, MistralModel.Codestral, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM51, ZhipuModel.GLM51Air;
4496
4613
  enumValues({
4497
4614
  PendingChallenge: "pending_challenge",
4498
4615
  Creating: "creating",
@@ -4799,7 +4916,7 @@ async function loadDaemonConfig() {
4799
4916
  orgId: identity.orgId,
4800
4917
  runtime: identity.runtime,
4801
4918
  runtimes,
4802
- autoStartRuntime: alfeConfig.auto_start_runtime ?? false
4919
+ autoStartRuntime: alfeConfig.auto_start_runtime ?? true
4803
4920
  };
4804
4921
  }
4805
4922
  /**
@@ -21659,7 +21776,7 @@ var CommandRegistry = class {
21659
21776
  * the gateway when needed and uses `callerScopes: ["operator.admin"]`.
21660
21777
  * 3. Skip iteration if `pending.json` mtime hasn't changed (cheap fast path)
21661
21778
  */
21662
- const execFileAsync$1 = promisify(execFile);
21779
+ const execFileAsync$2 = promisify(execFile);
21663
21780
  const APPROVE_TIMEOUT_MS = 1e4;
21664
21781
  function resolveStateDir(override) {
21665
21782
  if (override) return override;
@@ -21757,7 +21874,7 @@ function startPairingApprovalPoller(opts) {
21757
21874
  const stateDir = resolveStateDir(opts.stateDir);
21758
21875
  const intervalMs = opts.intervalMs ?? 3e4;
21759
21876
  const exec = opts.exec ?? (async (file, args, { timeout }) => {
21760
- const { stdout, stderr } = await execFileAsync$1(file, args, { timeout });
21877
+ const { stdout, stderr } = await execFileAsync$2(file, args, { timeout });
21761
21878
  return {
21762
21879
  stdout,
21763
21880
  stderr
@@ -21799,6 +21916,128 @@ function startPairingApprovalPoller(opts) {
21799
21916
  };
21800
21917
  }
21801
21918
  //#endregion
21919
+ //#region src/openclaw-mcp-cleanup.ts
21920
+ /**
21921
+ * One-shot startup migration that drops stale `mcp.servers.*` entries
21922
+ * from openclaw.json on agents set up before the single-source-of-truth
21923
+ * refactor. Pre-cutover agents had two writers:
21924
+ *
21925
+ * - The old `post_activate.mjs` hook wrote `mcp.servers.<integrationId>`
21926
+ * directly (legacy style — e.g. QA Tester's `mcp.servers.atlassian`).
21927
+ * - The bundler manager mirror-wrote `mcp.servers.<integrationId>-<serverId>`
21928
+ * for entries it owned (newer style).
21929
+ *
21930
+ * Post-cutover the alfe store owns everything and the openclaw.json
21931
+ * mirror is gone. If the legacy entries linger they cause:
21932
+ * 1. claude-cli / codex-cli to native-spawn from openclaw.json AND the
21933
+ * daemon's bundler to spawn from the store → two mcp-atlassian
21934
+ * children, the second one likely missing credentials.
21935
+ * 2. The openclaw-mcp-bundler plugin's `hasNativeOpenclawMcpServers`
21936
+ * check returns true (mcp.servers non-empty) → plugin no-ops on
21937
+ * every backend → MiniMax-backed agents lose tools entirely.
21938
+ *
21939
+ * Strategy: on first daemon start after the upgrade, drop every key in
21940
+ * openclaw.json#mcp.servers that the alfe store ALSO holds (direct id
21941
+ * match) OR that maps to an alfe-store entry by integration owner
21942
+ * (catches the legacy `mcp.servers.<integrationId>` shape where the
21943
+ * new store uses `<integrationId>-<serverId>`). User-added private MCPs
21944
+ * (e.g. `mcp.servers.my-private-thing` with no matching alfe entry) are
21945
+ * preserved.
21946
+ *
21947
+ * Sentinel-gated at `~/.alfe/.openclaw-mirror-migrated` — runs exactly
21948
+ * once per agent.
21949
+ */
21950
+ const execFileAsync$1 = promisify(execFile);
21951
+ const DEFAULT_SENTINEL_PATH = join(homedir(), ".alfe", ".openclaw-mirror-migrated");
21952
+ const defaultOpenclaw = {
21953
+ async listServers() {
21954
+ try {
21955
+ const { stdout } = await execFileAsync$1("openclaw", [
21956
+ "config",
21957
+ "get",
21958
+ "mcp.servers"
21959
+ ], { timeout: 1e4 });
21960
+ const trimmed = stdout.trim();
21961
+ if (!trimmed) return [];
21962
+ const parsed = JSON.parse(trimmed);
21963
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
21964
+ return Object.keys(parsed);
21965
+ } catch (err) {
21966
+ const message = err instanceof Error ? err.message : String(err);
21967
+ if (/not\s+set|not\s+found|undefined|no such key/i.test(message)) return [];
21968
+ throw err;
21969
+ }
21970
+ },
21971
+ async unsetServer(key) {
21972
+ await execFileAsync$1("openclaw", [
21973
+ "config",
21974
+ "unset",
21975
+ `mcp.servers.${key}`
21976
+ ], { timeout: 1e4 });
21977
+ }
21978
+ };
21979
+ /**
21980
+ * Run the cleanup. Idempotent — second call after the sentinel lands
21981
+ * returns immediately. Errors are caught and logged; the daemon must
21982
+ * not abort startup over a best-effort migration.
21983
+ */
21984
+ async function runOpenclawMcpCleanup(opts) {
21985
+ const sentinelPath = opts.sentinelPath ?? DEFAULT_SENTINEL_PATH;
21986
+ const openclaw = opts.openclaw ?? defaultOpenclaw;
21987
+ if (existsSync(sentinelPath)) {
21988
+ opts.logger.debug({ sentinel: sentinelPath }, "openclaw.json mcp.servers cleanup already ran — skipping");
21989
+ return;
21990
+ }
21991
+ let cleanupErr;
21992
+ try {
21993
+ const existing = await openclaw.listServers();
21994
+ if (existing.length === 0) {
21995
+ opts.logger.debug({}, "openclaw.json#mcp.servers is empty — nothing to clean up");
21996
+ writeSentinel(sentinelPath);
21997
+ return;
21998
+ }
21999
+ const stored = opts.manager.listServers();
22000
+ const storeIds = new Set(stored.map((s) => s.id));
22001
+ const integrationOwners = /* @__PURE__ */ new Set();
22002
+ for (const s of stored) if (s.entry.owner.startsWith("integration:")) integrationOwners.add(s.entry.owner.slice(12));
22003
+ const toUnset = [];
22004
+ for (const key of existing) {
22005
+ if (storeIds.has(key)) {
22006
+ toUnset.push(key);
22007
+ continue;
22008
+ }
22009
+ if (integrationOwners.has(key)) toUnset.push(key);
22010
+ }
22011
+ if (toUnset.length === 0) {
22012
+ opts.logger.info({ existingKeys: existing }, "openclaw.json#mcp.servers has entries but none match alfe-managed integrations — preserving as user customizations");
22013
+ writeSentinel(sentinelPath);
22014
+ return;
22015
+ }
22016
+ for (const key of toUnset) try {
22017
+ await openclaw.unsetServer(key);
22018
+ opts.logger.info({ key }, "Cleaned up stale openclaw.json#mcp.servers entry");
22019
+ } catch (err) {
22020
+ opts.logger.warn({
22021
+ key,
22022
+ err: err instanceof Error ? err.message : String(err)
22023
+ }, "Failed to unset stale mcp.servers entry — continuing");
22024
+ }
22025
+ } catch (err) {
22026
+ cleanupErr = err;
22027
+ opts.logger.warn({ err: err instanceof Error ? err.message : String(err) }, "openclaw.json#mcp.servers cleanup hit an error — leaving sentinel un-touched so next start retries");
22028
+ }
22029
+ if (cleanupErr === void 0) writeSentinel(sentinelPath);
22030
+ }
22031
+ function writeSentinel(path) {
22032
+ try {
22033
+ mkdirSync(dirname(path), { recursive: true });
22034
+ writeFileSync(path, `${(/* @__PURE__ */ new Date()).toISOString()}\n`, {
22035
+ encoding: "utf8",
22036
+ mode: 384
22037
+ });
22038
+ } catch {}
22039
+ }
22040
+ //#endregion
21802
22041
  //#region src/daemon.ts
21803
22042
  /**
21804
22043
  * Alfe Gateway Daemon — main entry point.
@@ -21821,6 +22060,8 @@ let ipcServer = null;
21821
22060
  let commandQueue;
21822
22061
  let startedAt;
21823
22062
  let integrationManager;
22063
+ let mcpBundler = null;
22064
+ let mcpManagerRef = null;
21824
22065
  let aiProxyServer = null;
21825
22066
  let runtimeProcess = null;
21826
22067
  let aiProxyUrl = null;
@@ -21841,6 +22082,30 @@ let stopPairingApprovalPoller = null;
21841
22082
  * (works when systemd runs the gateway binary directly, since
21842
22083
  * the path is .../cli/node_modules/@alfe.ai/gateway/dist/...)
21843
22084
  */
22085
+ /**
22086
+ * Adapter from `AgentApiClient`'s per-provider methods to the
22087
+ * `CredentialsResolver` shape the MCP applier expects. Returns
22088
+ * `undefined` for unknown providers and on 404/network error so the
22089
+ * applier can skip registration silently (its documented contract).
22090
+ */
22091
+ async function fetchProviderCredentials(agentApi, provider) {
22092
+ const key = provider.toLowerCase();
22093
+ try {
22094
+ switch (key) {
22095
+ case "atlassian": return await agentApi.getAtlassianCredentials();
22096
+ case "github": return await agentApi.getGithubCredentials();
22097
+ case "xero": return await agentApi.getXeroCredentials();
22098
+ case "notion": return await agentApi.getNotionCredentials();
22099
+ case "myob": return await agentApi.getMYOBCredentials();
22100
+ case "google": return await agentApi.getGoogleCredentials();
22101
+ default:
22102
+ logger$1.warn({ provider }, "Unknown OAuth provider for requires_credentials — MCP server will be skipped");
22103
+ return;
22104
+ }
22105
+ } catch {
22106
+ return;
22107
+ }
22108
+ }
21844
22109
  async function getCliVersion() {
21845
22110
  if (process.env.ALFE_CLI_VERSION) return process.env.ALFE_CLI_VERSION;
21846
22111
  try {
@@ -22056,8 +22321,44 @@ async function startDaemon() {
22056
22321
  apiBaseUrl: config.apiEndpoint,
22057
22322
  getToken: () => Promise.resolve(config.apiKey)
22058
22323
  }));
22324
+ const agentApi = new AgentApiClient({
22325
+ apiKey: config.apiKey,
22326
+ apiUrl: config.apiEndpoint
22327
+ });
22328
+ const mcpManager = new Manager({ logger: logger$1 });
22329
+ mcpManagerRef = mcpManager;
22330
+ mcpBundler = new McpBundler({
22331
+ logger: logger$1,
22332
+ idleTtlMs: 0
22333
+ }, { connect: defaultConnect });
22334
+ await mcpManager.loadIntoBundler(mcpBundler);
22335
+ const warmBundler = (reason) => {
22336
+ if (!mcpBundler) return;
22337
+ mcpBundler.warmup().catch((err) => {
22338
+ logger$1.warn({
22339
+ err: err instanceof Error ? err.message : String(err),
22340
+ reason
22341
+ }, "MCP bundler warmup failed");
22342
+ });
22343
+ };
22344
+ warmBundler("startup");
22345
+ mcpManager.onChange(() => {
22346
+ warmBundler("store change");
22347
+ });
22348
+ logger$1.info("MCP bundler attached to manager — warming children");
22349
+ await runOpenclawMcpCleanup({
22350
+ manager: mcpManager,
22351
+ logger: logger$1
22352
+ }).catch((err) => {
22353
+ logger$1.warn({ err: err instanceof Error ? err.message : String(err) }, "openclaw.json mcp.servers cleanup threw — startup continues");
22354
+ });
22059
22355
  integrationManager = new IntegrationManager({
22060
22356
  runtimeAppliers,
22357
+ mcpApplier: new McpApplier({
22358
+ manager: mcpManager,
22359
+ credentials: { getCredentials: (provider) => fetchProviderCredentials(agentApi, provider) },
22360
+ platform: { apiUrl: config.apiEndpoint }
22361
+ }),
22061
22362
  registryFetcher: async () => {
22062
22363
  const result = await integrationsService.getRegistry();
22063
22364
  if (!result.ok) throw new Error(result.error);
@@ -22091,6 +22392,7 @@ async function startDaemon() {
22091
22392
  });
22092
22393
  cloudClient.start();
22093
22394
  logger$1.debug("Cloud client started");
22395
+ if (!config.autoStartRuntime && config.runtime) logger$1.warn({ runtime: config.runtime }, "Runtime configured but auto_start_runtime=false — runtime will not be spawned; in-runtime plugins (chat/console/sync) will be offline until you start it manually");
22094
22396
  if (config.autoStartRuntime && config.runtime) {
22095
22397
  logger$1.debug({ runtime: config.runtime }, "Starting agent runtime...");
22096
22398
  const runtimeCfg = config.runtimes[config.runtime];
@@ -22127,6 +22429,16 @@ async function startDaemon() {
22127
22429
  await runtimeProcess.stop();
22128
22430
  logger$1.debug("Runtime process stopped");
22129
22431
  }
22432
+ if (mcpBundler) {
22433
+ logger$1.debug("Stopping MCP bundler...");
22434
+ await mcpBundler.dispose();
22435
+ mcpBundler = null;
22436
+ logger$1.debug("MCP bundler stopped");
22437
+ }
22438
+ logger$1.debug("Stopping MCP bundler manager...");
22439
+ await mcpManager.dispose();
22440
+ mcpManagerRef = null;
22441
+ logger$1.debug("MCP bundler manager stopped");
22130
22442
  if (ipcServer) {
22131
22443
  logger$1.debug("Stopping IPC server...");
22132
22444
  await ipcServer.stop();
@@ -22444,6 +22756,11 @@ function handlePluginRequest(method, params, pluginId) {
22444
22756
  case "status": return Promise.resolve(handleStatus());
22445
22757
  case "integration.list": return Promise.resolve(handleIntegrationList());
22446
22758
  case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
22759
+ case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
22760
+ case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
22761
+ case "mcp.list_servers": return Promise.resolve(handleMcpListServers());
22762
+ case "mcp.add_server": return handleMcpAddServer(params);
22763
+ case "mcp.remove_server": return handleMcpRemoveServer(params);
22447
22764
  default: return Promise.resolve({
22448
22765
  ok: false,
22449
22766
  error: {
@@ -22486,6 +22803,214 @@ function handleIntegrationList() {
22486
22803
  payload: { integrations: integrationManager.list() }
22487
22804
  };
22488
22805
  }
22806
+ /**
22807
+ * Return the daemon-hosted MCP bundler's current namespaced tool catalog.
22808
+ * Called by the openclaw-mcp-bundler plugin on every tool-factory invocation;
22809
+ * cheap (in-memory snapshot, no I/O).
22810
+ */
22811
+ function handleMcpListTools(bundler) {
22812
+ if (!bundler) return {
22813
+ ok: false,
22814
+ error: {
22815
+ code: "MCP_BUNDLER_UNAVAILABLE",
22816
+ message: "MCP bundler not initialized"
22817
+ }
22818
+ };
22819
+ return {
22820
+ ok: true,
22821
+ payload: { tools: bundler.listTools() }
22822
+ };
22823
+ }
22824
+ /**
22825
+ * Route a tool call to the appropriate MCP child via the daemon-hosted
22826
+ * bundler. `name` is the prefixed (`mcp__<server>__<tool>`) name; args is
22827
+ * the raw JSON object the LLM produced.
22828
+ */
22829
+ /**
22830
+ * List every server entry in the alfe bundler store. Lets agents inspect
22831
+ * what they've already registered before adding a new one.
22832
+ */
22833
+ function handleMcpListServers(manager = mcpManagerRef) {
22834
+ if (!manager) return {
22835
+ ok: false,
22836
+ error: {
22837
+ code: "MCP_MANAGER_UNAVAILABLE",
22838
+ message: "MCP manager not initialized"
22839
+ }
22840
+ };
22841
+ return {
22842
+ ok: true,
22843
+ payload: { servers: manager.listServers() }
22844
+ };
22845
+ }
22846
+ /**
22847
+ * Register a new MCP server in the alfe bundler store on behalf of the
22848
+ * agent. Owned as `'manual'` so the agent can later remove it without
22849
+ * an expectedOwner conflict — matches what `alfe mcp add` does from the
22850
+ * CLI. The daemon's bundler will pick the change up via the manager's
22851
+ * store watcher and spawn the child on the next tool-factory call.
22852
+ */
22853
+ async function handleMcpAddServer(params, manager = mcpManagerRef) {
22854
+ if (!manager) return {
22855
+ ok: false,
22856
+ error: {
22857
+ code: "MCP_MANAGER_UNAVAILABLE",
22858
+ message: "MCP manager not initialized"
22859
+ }
22860
+ };
22861
+ const p = params;
22862
+ if (typeof p.id !== "string" || p.id.length === 0) return {
22863
+ ok: false,
22864
+ error: {
22865
+ code: "INVALID_PARAMS",
22866
+ message: "id is required (string)"
22867
+ }
22868
+ };
22869
+ const config = buildServerConfig(p);
22870
+ if (!config) return {
22871
+ ok: false,
22872
+ error: {
22873
+ code: "INVALID_PARAMS",
22874
+ message: "expected either { command, args?, env?, cwd? } for stdio or { url, transport, headers? } for remote"
22875
+ }
22876
+ };
22877
+ try {
22878
+ await manager.addServer(config, {
22879
+ id: p.id,
22880
+ owner: "manual"
22881
+ });
22882
+ return {
22883
+ ok: true,
22884
+ payload: { id: p.id }
22885
+ };
22886
+ } catch (err) {
22887
+ const message = err instanceof Error ? err.message : String(err);
22888
+ logger$1.warn({
22889
+ id: p.id,
22890
+ err: message
22891
+ }, "mcp.add_server failed");
22892
+ return {
22893
+ ok: false,
22894
+ error: {
22895
+ code: "MCP_ADD_FAILED",
22896
+ message
22897
+ }
22898
+ };
22899
+ }
22900
+ }
22901
+ function buildServerConfig(p) {
22902
+ if (typeof p.command === "string" && p.command.length > 0) {
22903
+ const cfg = { command: p.command };
22904
+ if (Array.isArray(p.args) && p.args.every((a) => typeof a === "string")) cfg.args = p.args;
22905
+ if (p.env && typeof p.env === "object" && !Array.isArray(p.env)) {
22906
+ const env = {};
22907
+ for (const [k, v] of Object.entries(p.env)) if (typeof v === "string") env[k] = v;
22908
+ cfg.env = env;
22909
+ }
22910
+ if (typeof p.cwd === "string") cfg.cwd = p.cwd;
22911
+ return cfg;
22912
+ }
22913
+ if (typeof p.url === "string" && p.url.length > 0) {
22914
+ const transport = p.transport === "streamable-http" ? "streamable-http" : "sse";
22915
+ const cfg = {
22916
+ url: p.url,
22917
+ transport
22918
+ };
22919
+ if (p.headers && typeof p.headers === "object" && !Array.isArray(p.headers)) {
22920
+ const headers = {};
22921
+ for (const [k, v] of Object.entries(p.headers)) if (typeof v === "string") headers[k] = v;
22922
+ cfg.headers = headers;
22923
+ }
22924
+ return cfg;
22925
+ }
22926
+ return null;
22927
+ }
22928
+ /**
22929
+ * Drop a server entry the agent previously registered. Restricted to
22930
+ * `manual`-owned entries so the agent can't accidentally clobber
22931
+ * integration-installed or CLI-installed servers (the daemon owns
22932
+ * those; the agent can ask the user to uninstall an integration via
22933
+ * the dashboard).
22934
+ */
22935
+ async function handleMcpRemoveServer(params, manager = mcpManagerRef) {
22936
+ if (!manager) return {
22937
+ ok: false,
22938
+ error: {
22939
+ code: "MCP_MANAGER_UNAVAILABLE",
22940
+ message: "MCP manager not initialized"
22941
+ }
22942
+ };
22943
+ const { id } = params;
22944
+ if (typeof id !== "string" || id.length === 0) return {
22945
+ ok: false,
22946
+ error: {
22947
+ code: "INVALID_PARAMS",
22948
+ message: "id is required (string)"
22949
+ }
22950
+ };
22951
+ try {
22952
+ return {
22953
+ ok: true,
22954
+ payload: { removed: await manager.removeServer(id, { expectedOwner: "manual" }) }
22955
+ };
22956
+ } catch (err) {
22957
+ const message = err instanceof Error ? err.message : String(err);
22958
+ if (message.includes("owned by")) return {
22959
+ ok: false,
22960
+ error: {
22961
+ code: "MCP_OWNER_MISMATCH",
22962
+ message
22963
+ }
22964
+ };
22965
+ logger$1.warn({
22966
+ id,
22967
+ err: message
22968
+ }, "mcp.remove_server failed");
22969
+ return {
22970
+ ok: false,
22971
+ error: {
22972
+ code: "MCP_REMOVE_FAILED",
22973
+ message
22974
+ }
22975
+ };
22976
+ }
22977
+ }
22978
+ async function handleMcpCallTool(bundler, params) {
22979
+ if (!bundler) return {
22980
+ ok: false,
22981
+ error: {
22982
+ code: "MCP_BUNDLER_UNAVAILABLE",
22983
+ message: "MCP bundler not initialized"
22984
+ }
22985
+ };
22986
+ const { name, args } = params;
22987
+ if (typeof name !== "string" || name.length === 0) return {
22988
+ ok: false,
22989
+ error: {
22990
+ code: "INVALID_PARAMS",
22991
+ message: "name is required (string)"
22992
+ }
22993
+ };
22994
+ try {
22995
+ return {
22996
+ ok: true,
22997
+ payload: await bundler.callTool(name, args)
22998
+ };
22999
+ } catch (err) {
23000
+ const message = err instanceof Error ? err.message : String(err);
23001
+ logger$1.warn({
23002
+ tool: name,
23003
+ err: message
23004
+ }, "mcp.call_tool failed");
23005
+ return {
23006
+ ok: false,
23007
+ error: {
23008
+ code: "MCP_CALL_FAILED",
23009
+ message
23010
+ }
23011
+ };
23012
+ }
23013
+ }
22489
23014
  function handleIntegrationReport(params, pluginId) {
22490
23015
  const { name, status, detail } = params;
22491
23016
  if (!name || !status) return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.0.49",
3
+ "version": "0.1.1",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,10 +22,12 @@
22
22
  "pino-roll": "^1.2.0",
23
23
  "smol-toml": ">=1.6.1",
24
24
  "ws": "^8.18.0",
25
- "@alfe.ai/ai-proxy-local": "^0.0.9",
26
- "@alfe.ai/config": "^0.0.8",
27
- "@alfe.ai/integration-manifest": "^0.0.11",
28
- "@alfe.ai/integrations": "^0.0.33"
25
+ "@alfe.ai/agent-api-client": "^0.1.4",
26
+ "@alfe.ai/ai-proxy-local": "^0.0.10",
27
+ "@alfe.ai/config": "^0.0.9",
28
+ "@alfe.ai/integration-manifest": "^0.1.0",
29
+ "@alfe.ai/integrations": "^0.1.1",
30
+ "@alfe.ai/mcp-bundler": "^0.1.1"
29
31
  },
30
32
  "license": "UNLICENSED",
31
33
  "scripts": {