Files
multica/packages/views/settings/components/integrations-tab.tsx
Bohan Jiang 73b0015475 feat(vcs): make self-hosted Git providers self-host-only (MUL-3772, MUL-5138) (#5888)
* feat(vcs): gate self-hosted Git providers to self-host deployments only (MUL-3772)

The Forgejo/Gitea/GitLab integration is intended for self-hosted Multica, where
Multica can reach a Git instance on the operator's own network. On the managed
multi-tenant cloud it adds an SSRF surface (connect validates a user-supplied
instance URL from the server) and would store third-party Git tokens for all
tenants under one key, while only serving the small subset of users whose
instance is publicly reachable. Product decision: offer it on self-host only.

- Add an explicit deployment switch MULTICA_VCS_INTEGRATION_ENABLED (default
  off). Connect, rotate, and webhook now require BOTH the switch on AND a valid
  MULTICA_VCS_SECRET_KEY — the switch is the product boundary, not key presence
  alone. When off, connect/rotate return 404 and the webhook returns a bare 404
  (no config leak), independent of the frontend.
- /api/config exposes vcs_integration_available (mirrors the switch, omitted
  when false) so the Settings UI hides the whole "Git providers" section on
  cloud instead of surfacing an operator-only "missing key" hint.
- docker-compose.selfhost.yml defaults the switch on; .env.example documents it.
- Docs (en/zh) lead with a callout: available on self-hosted Multica only, not
  Multica Cloud, and clarify "self-hosted" means Multica itself, not just Git.

#5006 / #5883 stay in place — the schema and backend capability are retained;
this only gates availability. No cloud VCS connection can exist (connect always
required the key, which the cloud never set), so nothing needs migrating.

Verified: go build/vet + VCS/config handler tests on a fresh migrated DB
(incl. a new disabled-deployment 404 test); pnpm typecheck (core + views) and
the integrations-tab + core schema/config vitest suites pass.

Co-authored-by: multica-agent <github@multica.ai>

* fix(vcs): complete self-host integration gating (MUL-5138)

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-24 16:39:22 +08:00

62 lines
2.6 KiB
TypeScript

"use client";
import { useQuery } from "@tanstack/react-query";
import { LarkTab } from "./lark-tab";
import { ComposioTab } from "./composio-tab";
import { SlackTab } from "./slack-tab";
import { VCSTab } from "./vcs-tab";
import { ApiError } from "@multica/core/api";
import { composioToolkitsOptions } from "@multica/core/composio";
import { useConfigStore, useFeatureEnabled } from "@multica/core/config";
import { COMPOSIO_MCP_APPS_FLAG } from "@multica/core/feature-flags";
import { useT } from "../../i18n";
import { SettingsSection, SettingsTab } from "./settings-layout";
// Integrations is the umbrella tab for third-party platform connections.
// GitHub has its own top-level tab (see github-tab.tsx); everything else
// — currently Lark, Composio, Slack, and the self-hosted Git providers (Forgejo /
// Gitea / GitLab), with Linear etc. to follow — lives in here under its own
// section heading so additional integrations slot in without changing the IA.
// IntegrationsTab is just the host; each integration owns its own description
// and install flow.
export function IntegrationsTab() {
const { t } = useT("settings");
const composioEnabled = useFeatureEnabled(COMPOSIO_MCP_APPS_FLAG, false);
// Composio is hidden entirely until the feature is enabled and a key is
// configured server-side. A 503 from the toolkits endpoint means the server
// withheld the integration despite the frontend flag being on.
const composioToolkits = useQuery({
...composioToolkitsOptions(),
enabled: composioEnabled,
});
const composioUnconfigured =
composioToolkits.error instanceof ApiError && composioToolkits.error.status === 503;
// Self-host-only integration: the managed cloud reports this false (field
// omitted from /api/config), so the whole section — header included — is
// hidden there rather than showing an operator-only "missing key" message.
const vcsAvailable = useConfigStore((s) => s.vcsIntegrationAvailable);
return (
<SettingsTab title={t(($) => $.page.tabs.integrations)}>
<SettingsSection title={t(($) => $.lark.section_title)}>
<LarkTab />
</SettingsSection>
{composioEnabled && !composioUnconfigured && (
<SettingsSection title={t(($) => $.composio.section_title)}>
<ComposioTab />
</SettingsSection>
)}
<SettingsSection title={t(($) => $.slack.section_title)}>
<SlackTab />
</SettingsSection>
{vcsAvailable && (
<SettingsSection title={t(($) => $.vcs.section_title)}>
<VCSTab />
</SettingsSection>
)}
</SettingsTab>
);
}