Gemini CLI runtime was removed in MUL-3617 (#4503), but the canonical
"12 built-in providers" list still advertised [Gemini](/providers#gemini)
(a now-dangling anchor) and omitted CodeBuddy, which the daemon actually
auto-detects (config.go probes `codebuddy`). Swap Gemini -> CodeBuddy
across all 16 occurrences (index / how-multica-works / cloud-quickstart /
daemon-runtimes x en/zh/ja/ko); the count stays at twelve.
MUL-3861
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Match the code fix (#4703): the "link your account" link is built from the web
app URL (MULTICA_APP_URL ?? FRONTEND_ORIGIN), which a normal deployment already
sets — not MULTICA_PUBLIC_URL (the backend/API URL). Updates en + zh + ja + ko.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(slack): declutter the Slack connect UI
Trim the Slack bring-your-own-app UI to match the leaner Lark card and
stop burying the setup behind prose nobody reads:
- Drop the "Required bot scopes: …" block from the connect dialog.
- Shorten the Slack integration card description to mirror the Lark
card; the token/admin details stay in the setup docs.
- Remove the dialog intro paragraph and the per-field token hints;
replace the small "Read the setup guide" link with a larger,
more prominent step-by-step guide link.
Removes the now-unused i18n keys (byo_dialog_intro, byo_bot_token_hint,
byo_app_token_hint, byo_scopes_hint) across en/zh-Hans/ja/ko.
* docs(slack): drop the users:read warning callout
The bot manifest already lists users:read as a required scope (with the
bots.info rationale in the scopes table), so the standalone warning
callout was redundant. Removed across en/zh/ja/ko.
* docs: channel integrations overview + Slack bot page + Slack app setup guide (MUL-3666)
- channels.mdx: channel-engine overview with an architecture diagram (Mermaid),
the inbound pipeline, the session/context model, and the authorization gates
(account binding + workspace membership) — all shared by Lark and Slack.
- slack-bot-integration.mdx: the Slack channel page (mirrors lark-bot-integration)
— BYO connect flow, usage (@ in channel / DM / /issue), one-bot-per-agent,
permissions, and self-host (MULTICA_SLACK_SECRET_KEY).
- create-slack-app.mdx: standalone step-by-step — create a Slack app from a
copy-paste manifest, install it, and grab the bot + app-level tokens.
- meta.json: list the three pages under Integrations.
English (canonical) only this pass; zh/ja/ko localization to follow.
Co-authored-by: multica-agent <github@multica.ai>
* docs(slack): inline full manifest + step-by-step setup into the Slack page (MUL-3666)
The Slack page only linked out for setup, which read as too thin. Fold the
complete, code-verified app manifest and the full walkthrough (create from
manifest → install + bot token → app-level token with connections:write →
connect in Multica) directly into slack-bot-integration.mdx, plus a table
explaining what each scope/event is for.
Remove the now-redundant standalone create-slack-app.mdx (its content lives on
the Slack page) and update meta.json + the channels.mdx links accordingly, so
there's one comprehensive Slack page and no duplicated manifest to drift.
Co-authored-by: multica-agent <github@multica.ai>
* docs(i18n): translate channel + Slack pages to zh/ja/ko and add to nav (MUL-3666)
Adds Simplified Chinese, Japanese, and Korean versions of channels.mdx and
slack-bot-integration.mdx, and lists both pages under Integrations in
meta.{zh,ja,ko}.json. The copy-paste manifest YAML and dotenv blocks are kept
byte-identical to the English source across all languages; in-page anchors in
the channel page point at the slug of each translated heading.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(docs): add reusable VideoEmbed and embed intro video on zh homepage
Add a provider-agnostic, click-to-load <VideoEmbed> component (Bilibili now,
YouTube reserved) and embed the Chinese intro video (BV1cv7Y6gEg7) at the top
of the Chinese docs homepage. The facade renders on first paint; the
third-party player iframe only mounts on user click, so first paint pays
nothing for an external player or its trackers. Registered in both docs MDX
component maps so it is reusable on any docs page.
Scope is zh docs only — no usecase, no other locales, no analytics, no video
hosting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* docs(zh): drop duration from intro video title
Use the duration-free title "Multica 中文介绍视频" for the homepage VideoEmbed
instead of a minute-count phrasing. Copy accuracy only; no component, layout,
or provider changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* docs: document MULTICA_<PROVIDER>_ARGS default agent argument env vars
The backend default-args env-var layer (MULTICA_CLAUDE_ARGS / MULTICA_CODEX_ARGS /
MULTICA_CODEBUDDY_ARGS) shipped in #1807 but was never added to the docs site
environment-variables page. Document the variable, its precedence relative to
per-agent custom_args, POSIX shell-word parsing, and the shared blocked-flags
filter. Closes the docs follow-up requested in #1467.
* docs: refine MULTICA_<PROVIDER>_ARGS wording and sync zh/ja/ko translations
Reword the English section so daemon-wide default args read as a default
baseline rather than a hard ceiling (per-agent custom_args are appended
afterward and can override), and drop the uncertain --max-budget-usd example.
Sync the new env var row and section into the zh/ja/ko docs pages.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs: tidy agent runtime provider pages, add per-runtime FAQ (MUL-3617)
- Remove the Gemini CLI provider from install-agent-runtime and providers
across all four languages (Google folded the standalone CLI into
Antigravity). Update tool counts 12 -> 11 and the dependent
session-resumption, MCP, and skill-path sections.
- Add the Hermes profile custom_args workaround as a per-runtime FAQ note
under providers#hermes (supersedes #4497, which placed it in agents-create).
- Fix stale Japanese install copy that claimed only Claude Code reads
mcp_config and linked to a non-existent anchor.
Co-authored-by: multica-agent <github@multica.ai>
* docs: add Qoder and CodeBuddy runtimes to provider pages (MUL-3617)
Document the two newly added runtimes on install-agent-runtime and
providers across all four languages:
- Qoder (Alibaba): ACP-over-stdio CLI `qodercli`, shares the transport
with Hermes/Kimi/Kiro; session/resume, ACP mcpServers, dynamic model
discovery, native skills at .qoder/skills/.
- CodeBuddy (Tencent): Claude Code-compatible CLI `codebuddy`, driven via
stream-json; --resume, --mcp-config, dynamic models, .claude/skills/.
Update tool counts 11 -> 13 and the MCP section (now ten of thirteen
consume mcp_config; the other three still ignore it).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(daemon): discover local skills from ~/.agents/skills (MUL-3333)
Upgrade local skill discovery and import from a single provider root to an
ordered multi-root scan: the runtime's own skill directory (e.g.
~/.claude/skills) first, then the cross-tool universal root ~/.agents/skills.
- Rename localSkillRootForProvider -> localSkillRootsForProvider, returning
ordered roots [provider, universal] with a kind classifier.
- listRuntimeLocalSkills iterates the roots, gives each root its OWN visited
set (so a cross-root symlink alias is not collapsed), dedupes strictly by
Key with the provider root winning, and sorts once after the merge.
- loadRuntimeLocalSkillBundle walks the same priority order and only falls
through to the next root on os.IsNotExist; any other stat error is returned
so import never silently resolves a different same-key skill.
- Add a Root ("provider" | "universal") field to the local skill summary
(daemon + handler structs and the TS RuntimeLocalSkillSummary type) so the
UI can label a skill's origin without a future schema break.
Backward compatible: every skill visible today keeps its Key, SourcePath and
FileCount; the universal root only surfaces additional, non-conflicting skills.
Out of scope (follow-up issues): execution-time injection of ~/.agents/skills
into runtimes (e.g. Codex seedUserCodexSkills) and workspace-relative
.agents/skills discovery.
Tests cover universal-root discovery + import, provider-wins conflict
priority, both-roots merge, missing/both-missing roots, nested layouts,
IsNotExist fallback, the no-fallthrough-on-read-error guarantee, and the
per-root visited cross-root symlink alias. Docs updated in en/zh/ja/ko.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): fall through to next root when a same-key dir has no SKILL.md
loadRuntimeLocalSkillBundle previously only fell through to the next root on
os.IsNotExist for the skill DIRECTORY. A provider-root directory that shares a
skill's key but contains no SKILL.md (so listRuntimeLocalSkills descends past
it and surfaces the universal-root skill instead) made load stop on the
invalid provider dir and error — list and load disagreed, and the import the
user picked from the list could not be fetched.
Make the validity predicate match list: a root "has" the skill at a key only
when it is a directory containing a SKILL.md. A missing entry, a non-directory,
or a directory without a SKILL.md all mean "this root doesn't have it" and we
continue to the next root. Only a genuine non-IsNotExist stat error or an
unreadable existing SKILL.md (permission/IO) is returned, so we still never
silently substitute a different-content same-key skill from a lower-priority
root (Eve review #1, preserved by the existing read-error guard test).
Adds regression tests for the provider-dir-without-SKILL.md and provider-non-dir
fall-through cases.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(skills): structured conflict + overwrite path for local skill re-import
Local-skill re-import previously failed (or silently skipped) on a same-name
collision and, on delete+reimport, changed the skill UUID and dropped agent
bindings. This adds a structured conflict result and a creator-only overwrite
write path so a re-import can update the existing skill in place.
- New terminal import status `conflict` carrying { existing_skill_id,
existing_created_by, can_overwrite }; can_overwrite = requester is the
skill creator (canOverwriteSkillByLocalImport — intentionally narrower than
canManageSkill: admins edit in-app, not via re-import).
- Conflict is detected at daemon-report time (the effective name is only known
once the bundle arrives) via GetSkillByWorkspaceAndName, with the unique
constraint as a race backstop.
- Import requests carry action=overwrite + target_skill_id, persisted through
both the in-memory and Redis LocalSkillImportStore (the heartbeat → daemon
payload is unchanged; overwrite is resolved server-side).
- overwriteSkillWithFiles updates by target_skill_id in one tx: re-checks
existence (workspace-scoped) and creator permission, then replaces
description/content/config and fully replaces files (pruning files absent
from the new bundle). Preserves id, created_by, created_at, name, and
agent_skill bindings. Publishes skill:updated (not skill:created).
- Boundaries: target deleted or permission lost → failed (no fallback to
create-by-name); any mid-write error rolls back the tx, leaving the original
skill untouched. Retrying a terminal request is a no-op.
Tests cover: creator/non-creator conflict (can_overwrite), overwrite preserves
UUID + agent binding + prunes removed files, non-creator overwrite fails,
deleted target fails without create fallback, retry idempotency, and Redis
round-trip of the new fields.
Backend half of MUL-2701. Contract change: same-name local imports now return
status `conflict` instead of `failed` — the Desktop/core client must be updated
to consume it (sibling task).
MUL-2800
Co-authored-by: multica-agent <github@multica.ai>
* fix(skills): gate structured conflict behind client opt-in; guard overwrite target name
Addresses review feedback on PR #3498 (MUL-2800).
Backward compatibility: a same-name local import now returns the new `conflict`
status only when the initiating client opts in via `supports_conflict` (an
overwrite request implies it). Older clients — already-installed Desktop builds
whose poll loop only understands `failed`/`timeout` — keep the legacy `failed`
+ "a skill with this name already exists" behavior, so upgrading the backend
ahead of the client no longer regresses the import UX. This is the installed-app
API-compat boundary the repo's CLAUDE.md calls out.
Also: the overwrite write path now verifies the incoming effective name matches
the target skill's current name (errSkillOverwriteNameMismatch -> failed),
preventing a stale/wrong target_skill_id from writing one skill's content onto
another. Creator-only + workspace scoping already prevent privilege escalation;
this narrows the API so it can't be misused.
Refactored LocalSkillImportStore.Create to a LocalSkillImportRequestInput params
struct (the signature had grown to 8 positional args; the opt-in flag pushed it
over). supports_conflict is persisted in both the in-memory and Redis stores.
Tests: conflict tests now opt in; added a legacy-client test (no flag ->
failed + legacy message) and an overwrite name-mismatch test.
MUL-2800
Co-authored-by: multica-agent <github@multica.ai>
* feat(skills): resolve local import conflicts in desktop
Co-authored-by: multica-agent <github@multica.ai>
* fix(skills): preserve bulk flow after conflict resolution
Co-authored-by: multica-agent <github@multica.ai>
* feat(cli): add skill import conflict strategies
Co-authored-by: multica-agent <github@multica.ai>
* fix(i18n): sync skill import locale keys
Co-authored-by: multica-agent <github@multica.ai>
* docs: explain skill import conflict handling
Co-authored-by: multica-agent <github@multica.ai>
* docs: refresh skill import source map anchors
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): honor MULTICA_SERVER_URL in setup self-host
`multica setup self-host` resolved the backend URL only from the
--server-url flag, falling back to http://localhost:8080 when the flag
was absent. It never consulted MULTICA_SERVER_URL, even though that env
var is documented on the root --server-url flag and in `multica --help`,
and is honored by every other command via resolveServerURL. A self-host
user who set the env var instead of the flag still hit localhost and got
"Server at http://localhost:8080 is not reachable".
Route server-url and app-url through cli.FlagOrEnv so the documented env
vars (MULTICA_SERVER_URL / MULTICA_APP_URL) are honored when the matching
flag is not set, with the flag still taking precedence. userProvided now
reflects flag-or-env, so an env-sourced remote URL still triggers the
explicit app_url prompt. Not platform-specific despite the report.
Fixes GitHub #3912.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): normalize MULTICA_SERVER_URL in setup self-host
MULTICA_SERVER_URL is documented as a ws:// daemon address
(ws://localhost:8080/ws) and every other command normalizes it via
NormalizeServerBaseURL before use. setup self-host consumed the resolved
value raw and probed <url>/health, so a self-hoster who set the
documented ws:// form would still fail the reachability check.
Run the flag/env value through normalizeAPIBaseURL (ws->http, wss->https,
strip /ws) so the documented form works and the stored server_url stays a
clean http(s) base. Add a normalization test case and a focused test for
the MULTICA_APP_URL env path (review nit).
Co-authored-by: multica-agent <github@multica.ai>
* docs(self-host): note setup self-host honors MULTICA_SERVER_URL / MULTICA_APP_URL
Document that `setup self-host` reads the env vars when the matching flag
is omitted (flag wins), and that MULTICA_SERVER_URL accepts the ws://…/ws
daemon form. Added to en/zh/ja/ko quickstart for parity.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(daemon): wire agy --model and model discovery for Antigravity
agy 1.0.6 added a --model flag and an `agy models` catalog command, which
were the #1 blocker in the earlier agy-backend review (MUL-3125). The
antigravity backend already shipped but deliberately dropped opts.Model
because agy 1.0.1 had no way to select a model.
- buildAntigravityArgs now passes --model <display name> when opts.Model is
set; the value is the exact `agy models` display string (spaces + parens),
passed as a single exec arg so no shell quoting is needed.
- Block --model in custom_args so it can't override the managed value.
- ListModels("antigravity") enumerates via `agy models` (no static fallback:
agy silently no-ops on unrecognised models, so a stale guess would turn a
typo into a successful empty run).
- ModelSelectionSupported now returns true for every built-in provider; the
hook stays for any future model-less runtime.
- Daemon probe reads MULTICA_ANTIGRAVITY_MODEL for the daemon-wide default.
Co-authored-by: multica-agent <github@multica.ai>
* docs(providers): mark Antigravity model selection as supported
Antigravity gained --model in agy 1.0.6 (MUL-3125). Update the provider
matrix + prose (en/zh/ja/ko) from "managed internally / no --model" to
dynamic discovery via `agy models`, and refresh the now-stale picker
comments. Flag the display-string (not slug) shape and agy's silent no-op
on unrecognised values.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): reject unknown Antigravity model at spawn (MUL-3125)
agy exits 0 with empty output on an unrecognised --model, so a stale/typo'd
value would surface as a 'completed' but empty task. Validate opts.Model
against the `agy models` catalog in Execute before spawning: a non-empty
model the CLI does not advertise fails fast with an actionable error listing
the real choices. opts.Model is the single funnel for agent.model and the
MULTICA_ANTIGRAVITY_MODEL default, so this one check covers every source
(UI free-text, API, persisted value, env) — addressing Elon's review that a
UI-only guard is bypassable.
Validation is fail-OPEN: if the catalog can't be discovered we pass the
value through and let agy resolve it, so a discovery hiccup never blocks a
run. Pure antigravityModelError() is unit-tested (valid / unknown / near-miss
/ empty-model / empty-catalog); verified live against real agy 1.0.6.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(lark): serve Feishu and Lark from one deployment, per installation
The Lark integration was locked to a single open-platform host chosen
deployment-wide (MULTICA_LARK_HTTP_BASE_URL / _CALLBACK_BASE_URL,
defaulting to open.feishu.cn), so one deployment could talk to only the
mainland Feishu cloud OR Lark international — never both. Teams on the
other tenant could not use the integration at all.
Make the host per-installation. The device-flow installer already
auto-detects the tenant (Lark emits tenant_brand="lark" mid-poll); we now
persist that as lark_installation.region, carry it on
InstallationCredentials.Region, and resolve the open-platform host per
call (REST + WS bootstrap) from the region. An explicit cfg.BaseURL
(env / httptest) still overrides every region, so existing tests and
staging/proxy setups keep working.
- migration 116: lark_installation.region TEXT NOT NULL DEFAULT 'feishu'
CHECK (region IN ('feishu','lark')) — existing rows are all mainland.
- lark.Region enum + OpenPlatformBaseURL/RegionOrDefault helpers.
- registration: thread the detected region into finishSuccess so the
install-time GetBotInfo hits the right cloud AND the row records it.
- every credential-build site (patcher, replier, WS provider, union_id
backfill) copies region off the installation row.
- region is part of the WS supervisor fingerprint so a re-install that
switches cloud restarts the connection.
- API: surface region on the installation listing DTO.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* feat(lark): surface installation region in settings UI
Read the per-installation region off the listings response: build the
"Manage in Lark" dev-console host from it (open.feishu.cn vs
open.larksuite.com instead of a hardcoded mainland host) and render a
Feishu / Lark badge on each connected bot. The field is optional and
defaults to Feishu when an older server omits it (API-compat). Adds the
region_feishu / region_lark labels to all four locales.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* docs(lark): document simultaneous Feishu + Lark support
The cloud each bot belongs to is now auto-detected at install and stored
per installation, so one deployment serves both. Replace the old
"point MULTICA_LARK_HTTP_BASE_URL at larksuite for international tenants"
guidance (now just an optional override) in all four locales.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* fix(lark): repair legacy Lark-international installs on upgrade
Review follow-up (MUL-3083). Migration 116 backfilled every existing
lark_installation to region='feishu', assuming all historical rows were
mainland. But self-host deployments could already run Lark international
via the deployment-wide MULTICA_LARK_HTTP_BASE_URL override, so those
rows are really Lark — clearing the override after upgrade (which the new
docs invite) would route them to open.feishu.cn and break them.
Add a one-shot startup repair, BackfillRegionFromLegacyOverride, fired
off the hot path like BackfillBotUnionIDs: when the deployment's global
base-URL override targets open.larksuite.com, relabel the still-default
'feishu' rows to 'lark'. Gating on the deployment-wide override is what
makes it safe — every pre-existing install on such a deployment was Lark.
Idempotent; no-op on mainland / fresh deployments. Verified end-to-end
against a scratch DB (flip then 0-row idempotent re-run).
Also document that a Lark/飞书 app_id is globally unique across both
clouds, which is what makes the app_id-keyed token cache and the
UNIQUE(app_id) constraint safe across regions (review nit).
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* docs(lark): fix ops guidance to match auto per-installation region
Review follow-up (MUL-3083). .env.example and docker-compose.selfhost.yml
still told operators that international Lark requires pointing both base
URLs at open.larksuite.com — now wrong, and it would push a fresh
deployment back into a single-cloud override. Rewrite them: the base
URLs are optional deployment-wide overrides; normal dual-cloud operation
keeps them empty. Document the first-boot auto-relabel for deployments
migrating off the old single-cloud override, across the integration docs
(en/zh/ja/ko).
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(github): populate connected account name on install [MUL-3078]
The Settings → GitHub connection card was rendering 'Connected to
unknown' because:
1. fetchInstallationAccount in the setup callback hit GitHub's
/app/installations/{id} endpoint unauthenticated. That endpoint
requires App JWT auth; the call returned 401, and the function
fell through to the 'unknown' placeholder which was persisted as
account_login.
2. The installation webhook handler did upsert the row with the real
login when GitHub later delivered installation.created, but it
never published a github_installation:created event. The frontend
query stayed stale, so the UI kept showing 'unknown' even after
the row had been refreshed.
Fix:
- Add optional GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY env vars. When
set, signGitHubAppJWT mints a short-lived RS256 JWT (back-dated 60s
for clock skew, capped at 9m to stay inside GitHub's 10m max) and
fetchInstallationAccount uses it as a Bearer token. The setup
callback now writes the real org/user name on install.
- When the new env vars are not configured, the call still falls
through to 'unknown' as before — but the webhook handler now
publishes EventGitHubInstallationCreated after the upsert, so the
realtime listener invalidates the installations query and the UI
converges to the real value within seconds, no manual refresh.
Tests cover JWT signing (claims, signature, malformed PEM, partial
config), fetchInstallationAccount with a JWT-gated httptest mock,
and the webhook refresh + broadcast on a seeded 'unknown' row.
Docs updated for .env.example and github-integration /
environment-variables in en, zh, ja, ko.
Co-authored-by: multica-agent <github@multica.ai>
* test(github): defuse JWT clock-bomb by injecting parser time [MUL-3078]
PR review caught that TestSignGitHubAppJWT_ClaimsAndSignature signed the
token with a fixed 'now' (2026-06-05T12:00:00Z) but parsed it with a
default jwt.Parse, which uses real time.Now() for exp validation. Once
real wall clock crossed the token's exp (now + 9m = 12:09:00Z), the
test would have flipped to a deterministic failure on every CI run.
Inject the same fixed 'now' into the parser via jwt.WithTimeFunc so
both signing and validation share one clock. Verified independently
that without the fix the parser rejects the token as 'expired', and
with the fix it accepts.
Also clarified the fetchInstallationAccount comment to be unambiguous
about what 'do not block' actually means: the HTTP call IS synchronous
(no independent timeout, pre-existing wart), but a failure here just
falls back to the unknown placeholder rather than aborting the
callback.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Active long-running sessions are no longer killed by a fixed wall-clock deadline. Liveness is delegated to the idle watchdog (MULTICA_AGENT_IDLE_WATCHDOG, default 30m) with a larger in-flight-tool budget (MULTICA_AGENT_TOOL_WATCHDOG, default 2h). MULTICA_AGENT_TIMEOUT is an opt-in absolute cap (default 0 = no cap). The server-side 2.5h sweeper is unchanged as a coarse backstop.
Fixes#3745.
Rewrite the 'Usage Dashboard Rollup (Required)' section in SELF_HOSTING.md
and apps/docs/content/docs/getting-started/self-hosting.zh.mdx so that:
- The DB-backed in-process scheduler (sys_cron_executions, MUL-2957) is
documented as the default for fresh self-host installs. The bundled
pgvector/pgvector:pg17 image works as-is and no operator step is required.
- pg_cron, external cron, systemd timer, and Kubernetes CronJob are demoted
to a 'Compatibility paths (existing deployments only)' subsection. They
remain supported (advisory lock 4246 prevents double-writes) but are no
longer the recommended setup.
- A retirement sequence is added for production environments that already
have a pg_cron job: confirm in-process SUCCESS rows in sys_cron_executions,
then cron.unschedule the redundant entry; leave the pg_cron extension
installed unless other workloads stop depending on it.
- The two upgrade callouts that pointed to the removed
'Usage Dashboard Rollup -> Option C' anchor are repointed to
SELF_HOSTING_ADVANCED.md#usage-dashboard-rollup, which already documents
the auto-hook backfill and the recovery flow.
Refs MUL-3077.
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): add Integrations tab with Lark Bot bind entry
The agent detail page now has an Integrations tab alongside the inspector's
Integrations section. It reuses the shared LarkAgentBindButton so the
scan-to-bind / already-connected logic stays single-sourced, and adds the
not-configured / coming-soon / members-only states the sidebar has no room
for. The tab only appears once the deployment has Lark configured.
MUL-2988
Co-authored-by: multica-agent <github@multica.ai>
* docs: add Lark Bot integration guide
Covers binding a Multica agent to a Lark Bot (scan-to-install), using it
(DM / @-mention / /issue), management, permissions, and self-host setup.
Added in all four locales under the Integrations nav section.
MUL-2988
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): show bound Lark state when install_supported is false
install_supported governs only whether NEW scan-installs can complete;
already-installed bots stay manageable when the transport is unwired
(server/internal/handler/lark.go). LarkAgentBindButton checked the
install_supported gate before the existing-installation check, so a bound
agent on such a deployment showed 'coming soon' / nothing instead of
'Connected + Manage in Lark'. Reorder the guard (existing active install →
badge, before the install_supported gate) and mirror it in the new
Integrations tab. Adds regression tests for both surfaces.
MUL-2988
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The earlier SMTP_TLS / port-465 work only updated the English (and zh)
docs, leaving the ja and ko translations stale. This brings them to
parity for the SMTP relay section:
- environment-variables.{ja,ko}: correct the SMTP_PORT row (465 is
supported, not "unsupported") and add the missing SMTP_TLS row.
- self-host-quickstart.{ja,ko}: fix the stale "465 unsupported" intro
line and add the port-465 implicit-TLS example.
- auth-setup.{ja,ko}: fix the implicit-TLS row in the relay-modes table,
add the port-465 example, and align the startup-log line.
Docs-only; code blocks kept identical to English. No SMTP_EHLO_NAME
changes (already synced in #3749).
MUL-2984
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Fix attachment download for self-hosted deployments using private S3-compatible buckets without CloudFront. Closes#3721.
**Server**
- New unified `GET /api/attachments/{id}/download` endpoint that picks CloudFront / S3 presign / server proxy at request time.
- `ATTACHMENT_DOWNLOAD_MODE=auto|cloudfront|presign|proxy` and `ATTACHMENT_DOWNLOAD_URL_TTL` env knobs; `auto` routes Docker hostnames / localhost / private IPs through the proxy and public S3 endpoints through presign.
- `Storage.PresignGet` capability; S3 implementation generates presigned GET URLs.
- `attachmentToResponse` returns the unified relative endpoint instead of leaking raw unsigned S3 URLs when CloudFront is not configured. Proxy path streams via `io.Copy` with `Content-Disposition` / `Content-Length` / `Cache-Control: no-store` / `X-Content-Type-Options: nosniff`.
**Clients**
- CLI / Desktop / Mobile resolve relative `download_url` values against the configured API base. Desktop covers the Electron native download bridge and the media preview modal; Mobile covers `Linking.openURL`, the markdown image RN loader, and the composer's completed non-image file chip.
- Mobile gains a minimal Node-environment vitest lane wired into `mobile-verify.yml`.
**Docs**
- `.env.example`, `docker-compose.selfhost.yml`, `SELF_HOSTING_ADVANCED.md`, and the `environment-variables` doc set updated with the new env keys and the `ATTACHMENT_DOWNLOAD_MODE=proxy` recommendation for Docker / VPC-internal object stores.
**Tests**
- `internal/storage`, `internal/cli`, `internal/handler` (download endpoint, mode selection, proxy header, `/content` non-regression), `cmd/server` (trusted proxy parser).
- `packages/views/editor/use-download-attachment.test.tsx` and `attachment-preview-modal.test.tsx` exercise relative URL resolution + absolute pass-through.
- `apps/mobile/lib/attachment-url.test.ts` covers every helper branch plus the composer non-image chip case.
* fix(email): wire SMTP_EHLO_NAME through self-host config + docs
Follow-up to #3679, which added SMTP_EHLO_NAME in code but never exposed
it to operators.
- docker-compose.selfhost.yml: pass SMTP_EHLO_NAME through to the backend
container. The compose env block is an explicit allowlist, so without
this the override set in .env was silently dropped and never reached
the process — making the escape hatch unusable on the docker path.
- Document the var alongside its SMTP_* siblings: .env.example,
SELF_HOSTING_ADVANCED.md, environment-variables.mdx, auth-setup.mdx,
and self-host-quickstart.mdx (the last two with a strict-relay example).
- email.go: log when os.Hostname() fails instead of silently falling back
to net/smtp's lazy "localhost" — the exact greeting strict relays reject.
- Add TestNewEmailService_EHLOName covering the env override, trimming,
and the hostname fallback.
MUL-2984
Co-authored-by: multica-agent <github@multica.ai>
* fix(email): gate EHLO resolution to SMTP mode + sync docs to zh/ja/ko
Addresses review nits on this PR:
- email.go: resolve smtpEHLOName only when SMTP_HOST is set, so the
Resend / DEV-stdout paths never call os.Hostname() or emit its
failure log. The EHLO name is only ever used on the SMTP send path.
- docs: add SMTP_EHLO_NAME to the zh/ja/ko variants of
environment-variables, self-host-quickstart, and auth-setup, in sync
with the English docs updated earlier in this PR.
Note: the ja/ko self-host-quickstart and auth-setup pages were already
missing the port-465 implicit-TLS example (pre-existing i18n drift from
an earlier SMTP_TLS change, unrelated to this PR); the new EHLO block is
inserted at the correct logical anchor regardless. A full ja/ko re-sync
is left as a separate follow-up.
MUL-2984
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* i18n: add japanese locale
* fix: spacing issues
* refactor
* fix(desktop): set <html lang> before paint to avoid JA Kanji font flash
Switch the documentElement.lang sync from useEffect to useLayoutEffect so
lang is committed before the first paint. Otherwise Japanese desktop users
saw one frame of Kanji rendered with the Chinese-first fallback stack before
the html[lang|="ja"] CJK override applied. Also fix the stale selector in the
HTML_LANG comment (html[lang^="ja"] -> html[lang|="ja"]).
Addresses review nits on MUL-2893.
Co-authored-by: multica-agent <github@multica.ai>
* fix(docs): tokenize the ideographic iteration mark in JA search
Add U+3005 (々) to the Japanese search tokenizer character class. It sits just
below the kana blocks, so words like 様々 / 日々 / 個々 previously dropped the
mark and split awkwardly, hurting recall.
Addresses a review nit on MUL-2893.
Co-authored-by: multica-agent <github@multica.ai>
* fix(i18n): restore ja locale parity after merging main
Merging main brought new EN strings into agents/chat/onboarding/settings/
squads that the ja bundle (authored against an older snapshot) lacked, breaking
the locales parity test. Add the Japanese translations for the new keys
(workspace logo upload, agents runtime filter, chat session-history stop
dialog, onboarding social_github, squad archived status) and drop the two
renamed chat window keys (active_group / archived_group) that EN removed in
favour of history_group.
Fixes the failing @multica/views parity.test.ts on the FE CI for MUL-2893.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Adds OpenCode model variant discovery for thinking controls, passes saved thinking_level through opencode run --variant, and hardens verbose model parsing with fallback coverage.
* feat(email): support implicit TLS (SMTPS/465) for SMTP relay
The SMTP relay previously only did opportunistic STARTTLS: it dialed
plaintext and upgraded if the server advertised STARTTLS. Providers that
only offer implicit TLS on port 465 and do not advertise STARTTLS (e.g.
Aliyun enterprise mail) could not be used as a relay at all.
Add an SMTP_TLS env var:
- unset / starttls (default): unchanged STARTTLS-upgrade behavior.
- implicit / smtps / ssl: dial with tls.DialWithDialer (SMTPS).
Implicit TLS is auto-enabled when SMTP_PORT=465 and SMTP_TLS is unset, so
the common case works with no extra config. The startup log line now
reports the negotiated mode (starttls / implicit-tls).
Co-authored-by: multica-agent <github@multica.ai>
* feat(email): plumb SMTP_TLS through selfhost compose, warn on unknown values
The backend reads SMTP_TLS but docker-compose.selfhost.yml never forwarded
it, so SMTP_TLS=implicit on a non-standard port (or an explicit starttls
override on 465) silently did nothing inside the container. Add it to the
backend.environment block.
Also log a one-line warning when SMTP_TLS is set to an unrecognized value
(e.g. "tls"/"true"/"on"), which would otherwise fall through to STARTTLS
and fail to dial a 465 SMTPS port with no startup hint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(email): cover SMTP_TLS precedence and alias resolution
Table-driven test over NewEmailService asserting the implicit-TLS decision:
465 auto-enables implicit; explicit starttls on 465 overrides auto-detect;
implicit/smtps/ssl aliases (case-insensitive, whitespace-trimmed) force SMTPS
on any port; unknown values fall back to starttls.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* docs: document SMTPS / SMTP_TLS support, drop "465 unsupported"
Port 465 implicit TLS is now supported, so the five places that said it was
unsupported are wrong. Replace those sentences, add an SMTP_TLS row to the
environment-variables tables (EN + ZH), and add a copy-pasteable SMTPS env
block to the auth-setup pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: guofengchang <guofengchang@cumulon.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose self-host daemon setup URLs from /api/config at runtime so the Add computer dialog renders the operator's own server/app domains, while Multica Cloud defaults stay unchanged.
Fixes#3013.
Closes the runtime-side gap of #2106: previously `agent.mcp_config` was
honored only by Claude Code (via `--mcp-config <file>`); for OpenCode the
field was accepted by the API but silently ignored at execution time.
## Approach
OpenCode has no `--mcp-config` flag. Project the agent's `mcp_config`
into OpenCode via OPENCODE_CONFIG_CONTENT — OpenCode's general
inline-config injection environment variable, which accepts any subset
of OpenCode's config schema (model / agent / mode / plugin / mcp / …)
and merges at "local" scope after the project-config loop. MCP is the
only field this PR projects through that channel; if a future Multica
field needs the same channel it would assemble a combined config slice
before the env append.
The env-var route was deliberate. An earlier draft of this PR wrote
the translated MCP servers into <workdir>/opencode.json and removed
the file on cleanup; review (#3098) flagged that the task workdir is
reused across turns for the same (agent, issue), and any agent- or
user-written model / tools / permission settings in opencode.json
must survive across runs. OPENCODE_CONFIG_CONTENT avoids the workdir
entirely — nothing is written to disk, no cleanup is needed, and the
env entry dies with the spawned process.
OPENCODE_CONFIG_CONTENT was added to OpenCode in v1.4.10 (2025-09); the
official @opencode-ai/sdk uses the same env var to inject runtime
config, so the surface is stable. Verified empirically against
OpenCode 1.15.6 in our K8s runtime: `opencode debug config` returns
the injected mcp slice deep-merged with the user's global config,
and <workdir>/opencode.json is observably untouched.
## Translation surface
`agent.mcp_config` accepts two shapes for portability:
- Claude-style `{"mcpServers": {name: {url|command, ...}}}` is
translated into OpenCode's native form: `type: "local"|"remote"`,
`command` coerced to a string array, `env` renamed to `environment`.
- Native OpenCode `{"mcp": {name: ...}}` accepts the three shapes
OpenCode's schema permits and is strict-decoded against each:
- McpLocalConfig: `{type:"local", command:[…], environment?, enabled?, timeout?}`
- McpRemoteConfig: `{type:"remote", url:"…", headers?, oauth?, enabled?, timeout?}`
- bare override: `{enabled: bool}` (toggle a server inherited
from global / project config without redefining it)
Decoding uses `json.DisallowUnknownFields` so any field outside the
matching schema is rejected — matching OpenCode's
`additionalProperties: false`. Without this, a malformed payload
(e.g. `command: "node"` instead of `command: ["node"]`) would reach
OpenCode verbatim and either silently disable the server or crash
the CLI at startup.
Field-level checks the strict decoder doesn't catch:
- `timeout` must be a positive integer (rejects 0, negative, fractional)
- `oauth` must be either an object (validated against McpOAuthConfig)
or the literal `false`; primitives and `true` are rejected as ambiguous
- `oauth.callbackPort` must be in 1..65535 when set
## Precedence
Go's os/exec dedups `cmd.Env` by key keeping the LAST occurrence
(Go 1.9+). Appending OPENCODE_CONFIG_CONTENT after `buildEnv(b.cfg.Env)`
guarantees the daemon's value wins over any value the user happened
to put in `agent.custom_env` — which matches the intended semantics
(`mcp_config` is the authoritative daemon-managed field; `custom_env`
is the escape hatch). When that override happens we surface a warning
log so accidental clobbers are debuggable.
## Limitation (out of scope, accepted in review)
OpenCode also deep-merges its **global** config
(`~/.config/opencode/opencode.json`) into every session and exposes no
flag to disable that. Operators who want strict per-agent isolation
from the global layer can set:
```jsonc
// agent.custom_env on the platform
{ "XDG_CONFIG_HOME": "/tmp/opencode-isolated" }
```
…pointing at any directory without an `opencode/` subdir. OpenCode then
reads no global config and only honors what the daemon injects via
OPENCODE_CONFIG_CONTENT. Verified with `opencode debug config`.
## Changes
server/pkg/agent/opencode_mcp.go (new):
- buildOpenCodeMCPConfigContent — translates raw mcp_config into the
JSON string OpenCode accepts via OPENCODE_CONFIG_CONTENT, returns
"" when there's nothing to inject so the caller can skip the env
entry (avoids clobbering anything the user put in
agent.custom_env.OPENCODE_CONFIG_CONTENT)
- translateMCPConfigForOpenCode + helpers — Claude-style → OpenCode
native shape
- validateOpenCodeNativeMCPEntry + opencodeMCPLocal /
opencodeMCPRemote / opencodeMCPEnabledOnly / opencodeMCPOAuth
typed structs — strict-decode native-shape entries against the
schema (DisallowUnknownFields), plus targeted post-decode
assertions for timeout / oauth / callbackPort
server/pkg/agent/opencode.go:
- 12 lines of env injection in Execute(), placed AFTER buildEnv so
the daemon's value wins via os/exec dedup
- warning log when agent.custom_env duplicates the same key
- no on-disk state, no rollback closure, no post-run cleanup —
OPENCODE_CONFIG_CONTENT lives only in the spawned process env
server/pkg/agent/opencode_mcp_test.go (new):
- TestBuildOpenCodeMCPConfigContent_{Empty,Remote,Local,Native}
- TestBuildOpenCodeMCPConfigContent_NativeAcceptsAllSchemaFields —
covers each native variant round-tripping every optional field
(local with env+timeout+enabled; remote with headers+oauth-object+
timeout+enabled; remote with oauth: false; bare {enabled} override)
- TestBuildOpenCodeMCPConfigContent_RejectsMalformedNative — 31-case
table covering every constraint on Bohan-J's review: command must
be a string array, environment / headers values must be strings,
oauth must be an object or false, timeout must be a positive
integer, additionalProperties: false (per-shape allow-list checked
via DisallowUnknownFields)
- TestOpencodeBackendInjectsMCPConfigViaEnv — E2E happy path; fake
opencode binary captures $OPENCODE_CONFIG_CONTENT, asserts the
translated mcp slice is present AND <workdir>/opencode.json was
NOT written
- TestOpencodeBackendOmitsMCPEnvWhenEmpty — empty mcp_config does
NOT inject the env, preserving any value the user set in
agent.custom_env
- TestOpencodeBackendOverridesUserOpenCodeConfigContent — daemon
value wins via os/exec dedup keep-last
apps/docs/content/docs/providers.{en,zh}.mdx:
- flip OpenCode's MCP cell from ❌ to ✅
- reword the "MCP configuration: only Claude Code actually reads it"
section so OpenCode is included; describe each tool's mechanism
(Claude → `--mcp-config`, OpenCode → OPENCODE_CONFIG_CONTENT)
apps/docs/content/docs/install-agent-runtime.{en,zh}.mdx:
- update the Claude Code blurb (no longer "the only one")
- expand the OpenCode blurb to mention mcp_config support
- fix the now-broken /providers anchor
Refs #2106 (TS types and per-agent UI for mcp_config are separate
follow-ups, not in this PR).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(i18n): translate documentation corpus to Korean
Add Korean (.ko.mdx) translations for all 32 navigable docs pages plus
meta.ko.json navigation, mirroring the English source. Product terms
(Issue→이슈, Agent→에이전트, Squad→스쿼드, Runtime→런타임, Skill→스킬,
Workspace→워크스페이스, etc.) follow the in-app Korean locale at
packages/views/locales/ko/. Roles (owner/admin/member) and issue status
enums stay lowercase English per the conventions glossary.
MUL-2817
Co-authored-by: multica-agent <github@multica.ai>
* feat(docs): serve Korean docs content, remove English-fallback stopgap
Now that the *.ko.mdx corpus exists, drop the temporary docsContentLang
ko→en shim and the static-params fallback-synthesis loop so /docs/ko/*
renders real Korean content. Korean is now a first-class locale whose
params come straight from source.generateParams(). Also align the docs
home hero copy (agent→에이전트) with the app and the translated body.
MUL-2817
Co-authored-by: multica-agent <github@multica.ai>
* docs(i18n): align residual Korean UI/product terms with the app
Address review: sweep the .ko.mdx corpus for product/UI terms left in
English and match the in-app Korean locale.
- skills page title Skills → 스킬
- UI nav paths localized: Settings → 설정, Runtimes → 런타임, Agents →
에이전트, Projects → 프로젝트, Squads/New squad → 스쿼드/새 스쿼드,
Usage → 사용량, Personal Access Tokens → API 토큰, Provider → 제공자,
and the agent-create form labels (Name/Provider/Model/Instructions →
이름/제공자/모델/지침)
- see-also links Issues/Workspaces/Environment variables and
'Providers Matrix' → Korean
- kept as literals (verified): code blocks, the conventions i18n glossary
data, 'Anthropic Agent Skills' (standard name), the Squad Operating
Protocol/Roster/Instructions prompt-block names, the literal 'Project
Context' prompt section, and Xcode's Settings path
- add a docsAlternates test asserting ko hreflang is emitted when a real
*.ko.mdx page exists
MUL-2817
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777, #3433)
When self-hosters set DISABLE_WORKSPACE_CREATION=true, POST /api/workspaces
returns 403 for every caller and the UI hides every "Create workspace"
affordance (sidebar, modal, /workspaces/new page, onboarding Step 2). This
closes the gap where ALLOW_SIGNUP=false still let any signed-in user open
an isolated workspace the platform admin couldn't see.
- server: new Config.DisableWorkspaceCreation, gate in CreateWorkspace,
workspace_creation_disabled in /api/config, Go tests.
- frontend: new workspaceCreationDisabled in configStore, hide sidebar
entry, swap NewWorkspacePage / CreateWorkspaceModal / onboarding
StepWorkspace to a "creation disabled, ask for invite" state when the
flag is on, EN + zh-Hans locale strings.
- ops: .env.example, docker-compose.selfhost, helm values + configmap,
SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, environment-variables docs
(EN + zh).
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): drive create path off workspaceCreationAllowed (#3433)
PR #3441 review: when DISABLE_WORKSPACE_CREATION=true and the user already
has a workspace, StepWorkspace still walked the resume copy (`headline_resume`
/ `lede_resume` mentioning "or start another") and `creatingActive` ignored
the flag, leaving a stale clickable create CTA possible if /api/config
arrived late.
Refactor StepWorkspace to derive a single `workspaceCreationAllowed`
boolean from the config store. It now drives:
- Initial `mode` state (defaults to "existing" when disabled + reusing so
the CTA is pre-armed for the only valid action).
- `creatingActive` so the footer CTA cannot fall back into the create
branch even mid-render.
- Eyebrow / headline / lede strings — adds
`creation_disabled_{eyebrow,headline,lede}_resume` (EN + zh-Hans) for
the disabled + reusing variant.
Tests: cover the three reachable shapes — flag off + no existing, flag on
+ no existing, flag on + existing.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(agent): add Antigravity runtime backend
Adds Google's Antigravity CLI (`agy`) as the 12th supported coding-tool
runtime, alongside Claude / Codex / Cursor / Copilot / Gemini / Hermes /
Kimi / Kiro / OpenCode / OpenClaw / Pi.
The CLI emits plain assistant text on stdout (no structured event
stream), so the backend streams stdout line-by-line as `MessageText`
events and accumulates the same text as the final `Result.Output`.
Session resumption uses `--conversation <id>`; because the conversation
UUID is not echoed on stdout, the daemon routes `--log-file` to a temp
file and recovers the id from the glog-formatted log lines.
MUL-2767
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): correct Antigravity capability contract from Elon review
- ModelSelectionSupported now returns false for antigravity. `agy` has no
--model flag and antigravityBackend deliberately drops opts.Model, so
the UI must render a disabled "Managed by runtime" picker instead of
an empty dropdown plus a silently-ignored manual-entry field. Also
stop seeding AgentEntry.Model from MULTICA_ANTIGRAVITY_MODEL — the
backend would silently ignore it.
- Antigravity skills now write to {workDir}/.agents/skills/, the CLI's
native workspace path (inherits Gemini CLI's layout per
https://antigravity.google/docs/gcli-migration). Previously they went
to the .agent_context/skills/ fallback that the CLI doesn't scan.
Runtime brief moves antigravity into the native-discovery branch and
local_skills.go points the user-level skill root at
~/.gemini/antigravity-cli/skills for Runtime → local skill import.
- Doc + UI comment sync: providers matrix / install-agent-runtime /
cloud-quickstart / agents-create / tasks (session-resume support) /
skills / README all now list Antigravity in the right buckets, and
the model-picker / model-dropdown comments cite antigravity (not the
stale hermes reference) as the supported=false example.
New tests: TestAntigravityModelSelectionUnsupported,
TestInjectRuntimeConfigAntigravity (native discovery wording),
TestWriteContextFilesAntigravityNativeSkills (.agents/skills/ landing,
.agent_context/skills/ NOT written).
Co-authored-by: multica-agent <github@multica.ai>
* feat(provider-logo): swap inline placeholder for real Antigravity PNG
Replaces the hand-drawn planet+arc placeholder with the official asset
shipped from Downloads. Stored next to the component; bundlers
(Next.js / electron-vite) resolve the PNG import to a URL string at
build time. Added a small assets.d.ts so packages/views' tsc accepts
PNG / SVG module imports — there was no prior asset usage in this
package to register the declaration.
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
GH#3405 / MUL-2768. Self-host docs already point at the SMTP path, but on-prem operators ran into two gaps:
- The Option B env block in auth-setup and self-host-quickstart only showed a 587 authenticated example, with no copy-pasteable block for the most common Exchange "anonymous internal relay on port 25" pattern, and no explicit mapping between port / auth / TLS / supported-or-not.
- troubleshooting "Emails not received" only covered Resend; SMTP failures (smtp dial / starttls / auth / MAIL FROM / RCPT TO / DATA) surface as wrapped errors in the backend logs, but operators had no doc telling them which Exchange-side fix maps to each.
Adds:
- A relay-mode table (anonymous 25 / authenticated 587 / 465 still unsupported) and two copy-pasteable env blocks in both auth-setup.mdx and self-host-quickstart.mdx (EN + ZH).
- Explicit note on the EmailService startup log line so operators can confirm SMTP is the active provider after restart, without leaking credentials.
- An SMTP failure-mode table in troubleshooting.mdx (EN + ZH) keyed on the exact wrapped error string, with the Exchange-side fix for each.
No code changes; env variable surface unchanged (still SMTP_HOST / SMTP_PORT / SMTP_USERNAME / SMTP_PASSWORD / SMTP_TLS_INSECURE). Port 465 stays "not supported" pending #3340.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* MUL-2748 docs(autopilots): document webhook event filters and link from UI
Follow-up to PR #3231. The webhook event filters feature shipped
without user-facing docs and the UI section gave no hint about how
event/action are derived from inbound requests.
- Add an "Event filters" subsection to autopilots.mdx and .zh.mdx
under the existing "Trigger from a webhook" section: what the
filter does (with the `event_filtered` outcome), where the event
name and action come from (body envelope / headers / body
fallbacks), examples, a non-string-action gotcha, and a curl
recipe verifying both the allowed and filtered paths.
- Add a small ExternalLink icon next to the "Event filters" label
in WebhookEventFilterSection that opens the docs section in a new
tab. Locale-aware: zh users land on the Chinese page anchor, en
users on the English one.
Co-authored-by: multica-agent <github@multica.ai>
* docs(autopilots): expand "where event name and action come from" with curl examples
Add concrete curl request/inference pairs under each derivation step
(body envelope, headers, body fallback, default) and a "common gotcha"
explaining why filter `event=trigger, action=true` does not match
`{"trigger": true}`. Mirrors the explanation that resolved the
on-call confusion about Event filter semantics.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs(project-resources): document local_directory resource type
Add user-facing guidance for the local_directory project resource introduced
in #3283 — when to pick it over github_repo, the Desktop / CLI attach flow,
path validation rules, the daemon-scoped one-per-(project, daemon) limit,
serial task execution + waiting_local_directory status, what the daemon will
and won't touch in the user's folder, and the v1 limits to call out
(no auto branch switch / commit / PR; dirty tree carried through).
Also ship the missing Chinese counterpart of project-resources and wire it
into meta.zh.json.
MUL-2618
Co-authored-by: multica-agent <github@multica.ai>
* docs(project-resources): cover write-conflict trade-off + mixed-resource behavior
Expand the local_directory docs in response to review feedback:
- Restate "when to pick local_directory" as two distinct use cases (clone
cost; fine-grained changes needing frequent local review) instead of a
one-liner, and make the trade-off explicit: v1 ships no file-level write
lock, so the per-directory serial gate is the only protection against
cross-issue agents touching the same files.
- Add a new "Mixing resource types, and multiple local_directory resources"
section that answers: github_repo + local_directory on the same project
(local takes precedence on the bound daemon, github_repo falls back
everywhere else), and two local_directory resources (only possible
across two daemons, routed by the agent's runtime assignment, no
load-balancing).
Mirrored into the Chinese translation. typecheck + tests still pass.
Co-authored-by: multica-agent <github@multica.ai>
* docs(project-resources): tighten local_directory wording per review
- Soften "only the bound daemon can take tasks" to "only the bound
daemon uses this local directory" (zh) — aligns with the
mixed-resource fallback section where other daemons still run.
- Clarify that local_directory does not create/use a github_repo
worktree for that task (en + zh); the per-workspace repo cache may
still sync as a background behaviour.
- Match implementation for the Desktop "Add local directory" button:
it stays visible but is disabled with a hint when daemon is offline
or the per-daemon limit is reached; only the web app hides it
outright (en + zh).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603)
Only Claude Code and Codex runtimes actually enforce `skills_local` at exec
time today — Claude isolates `~/.claude/skills/` via `CLAUDE_CONFIG_DIR`,
Codex isolates `~/.codex/skills/` via per-task `CODEX_HOME`. Every other
runtime currently stores the field but treats it as a no-op, which made
the toggle in the Create Agent dialog and Skills tab misleading for those
runtimes.
Gate the toggle on `runtime.provider` so it only renders for the providers
the daemon currently isolates. Centralise the supported-provider list as
`isSkillsLocalSupportedProvider()` in `packages/core/agents` and reuse it
from the create dialog and the Skills tab. The create dialog also drops
`skills_local` from the payload when the selected runtime is unsupported,
so a runtime swap can't leave a stale `ignore` opt-in pinned where it
would never take effect.
Docs (EN + ZH) updated to say the toggle is hidden — not just "a no-op" —
for the unsupported runtimes.
Co-authored-by: multica-agent <github@multica.ai>
* docs(agents): align skills_local hint and type comment with claude+codex boundary
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The Usage / Runtime dashboards read from `task_usage_hourly`, but the
default self-host stack does not schedule `rollup_task_usage_hourly()`
anywhere — the bundled pgvector/pgvector:pg17 image ships without
pg_cron, and the backend does not run the rollup in-process. Fresh
installs see the dashboard stay at zero forever (#3244), and upgrades
from v0.3.4 → v0.3.5+ are blocked by migration 103's fail-closed guard
(#3015).
Document the three supported paths (external cron / systemd-timer /
CronJob, Postgres with pg_cron, or backfill_task_usage_hourly for
upgrades) across SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, the
quickstart pages on the docs site, and add troubleshooting entries
for both the silent-zero and the migration-guard failure modes.
Co-authored-by: multica-agent <github@multica.ai>
* feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603)
Adds an agent-scoped `skills_local` switch ("ignore" default / "merge") so
shared agents stop inheriting the operator's user-global Claude skill
directory. A single broken local skill on one operator's machine was
crashing the Claude CLI before it ever read stdin — the daemon saw a
"broken pipe" with no recoverable signal (GitHub #3052).
- DB: migration 108 adds `agent.skills_local` (NOT NULL DEFAULT 'ignore'),
with sqlc CreateAgent/UpdateAgent updates and handler validation.
- Claude runtime: when the agent is in "ignore" mode the backend points
CLAUDE_CONFIG_DIR at an empty per-task scratch dir under the task cwd
(fallback: OS temp), strips any inherited override, and cleans up after
the run. Workspace skills under `{cwd}/.claude/skills/` still load.
"merge" preserves the legacy inherit-from-machine behavior; Codex and
other isolated backends are no-ops.
- UI: new Skills toggle in the Create Agent dialog and the Agent → Skills
tab, with EN/zh-Hans copy and SkillsLocalToggle shared between the two.
- Tests: unit coverage for the new env helper, isolation dir lifecycle,
full Claude execute paths (ignore + merge), and the handler tristate
contract. Existing skills-tab test updated for the new copy.
- Docs: updated `/skills` docs (EN + ZH) and added a 0.3.7 changelog entry
in the landing-page i18n.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): preserve claude login + validate skills_local input (MUL-2603)
Address Elon's review on PR #3200:
1. Skill isolation no longer drops the operator's Claude login. The
per-task scratch dir now mirrors every entry under `~/.claude/`
as symlinks except `skills/`, so `.credentials.json`, settings,
plugins, etc. reach the CLI exactly as on the host while the
user-global skills directory stays hidden. Without this, default
`ignore` would have broken every Claude agent on a non-API-key
host the moment migration 108 landed.
2. Internal CreateAgent callers (agent_template, onboarding_shim)
now set `SkillsLocal: "ignore"`. The Go zero value was about to
trip the migration-108 CHECK constraint and 500 template /
onboarding agent creation.
3. Create / update handler validation no longer normalizes garbage
to "ignore". The strict 400 path is now reachable on bad client
input; the drift-safe `normalizeSkillsLocal` stays on the read
side only.
UI copy + docs clarified that the toggle is Claude-only; other
runtimes ignore the setting.
Verification:
- `go test ./...` green (full suite locally).
- `pnpm --filter @multica/views exec vitest run agents/components/tabs/skills-tab.test.tsx` green.
- Handler DB-backed tests still skip locally without docker (same
as Elon's run) — CI will validate the create / update paths
against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): mirror effective claude config dir with windows fallback (MUL-2603)
Address Elon's second-round review on PR #3200:
1. The per-task scratch dir now mirrors the *effective* host Claude
config dir, not unconditionally `~/.claude/`. Precedence: agent
`custom_env` CLAUDE_CONFIG_DIR > parent process env > `~/.claude/`.
Without this, an operator who pinned Claude at a managed install
(custom env CLAUDE_CONFIG_DIR) would get the wrong credentials in
the scratch dir, because `buildClaudeEnv` strips that env before
handing it to the child. We resolve the source up front and feed
it to the mirror, so the override env still points at the right
bytes.
2. Mirror entries now go through platform-aware linkers. On Windows
without Developer Mode / admin, `os.Symlink` is denied, which
previously left the scratch dir empty and broke Claude Code auth
on default `ignore`. The new helpers try symlink first, then fall
back to a directory junction (`mklink /J`) for dirs or a hardlink
(same-volume content share) / copy for files. Mirrors the
execenv/codex_home_link_windows.go pattern.
3. Tests:
- `TestResolveHostClaudeConfigDir` locks in the custom_env >
parent_env > `~/.claude` precedence.
- `TestNewIsolatedClaudeConfigDirMirrorsCustomHostDir` confirms
the scratch dir picks up `.credentials.json` from a synthetic
custom host dir, proving the source resolution actually
propagates into the mirror.
- `TestNewIsolatedClaudeConfigDirEmptyHostIsNoop` documents the
env-var-auth-only case (no host source ⇒ empty scratch dir).
- `TestMirrorHostClaudeExceptSkillsWith_FallbackWhenSymlinkFails`
exercises the Windows-no-Developer-Mode path via the new
`mirrorHostClaudeExceptSkillsWith` seam, asserting credentials
and sub-dir children still reach the scratch dir after the
symlink stand-in fails.
- `TestMirrorHostClaudeExceptSkillsWith_PropagatesFirstLinkError`
confirms callers see the per-entry error when even fallback
fails (so the warn-log fires on broken Windows installs).
- `TestCopyFileRoundTrip` covers the last-resort copy fallback
and its EXCL no-overwrite contract.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` is the
end-to-end check: an agent with custom_env CLAUDE_CONFIG_DIR
reads its credentials from the pinned dir, not `~/.claude/`.
4. Docs: `apps/docs/content/docs/skills.{mdx,zh.mdx}` updated to
describe the effective-source resolution and the Windows
fallback chain so the docs match the runtime behaviour.
Verification:
- `go test ./...` green (full server suite locally, including
`pkg/agent` 23 cases covering the new + existing isolation
paths).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean, confirming the
Windows-tagged linker file builds.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): default skills_local to merge to preserve legacy behavior (MUL-2603)
Per Bohan's product decision on PR #3200, the per-agent host-skill toggle
defaults to "merge" — the pre-MUL-2603 inherit-from-machine behavior —
so existing personal workflows that rely on locally installed Claude
Skills keep working unchanged. Agent owners explicitly opt into "ignore"
when they need to harden a shared agent against a broken local skill on
one operator's machine (GitHub #3052).
Also audited all 11 runtimes for user-global skill discovery paths and
documented the scope of the toggle. Only Claude reads a user-global
`~/.claude/skills/`; Codex isolates via `CODEX_HOME`, the ACP backends
(Hermes / Kimi / Kiro) and the JSON-stream backends (Copilot / Cursor /
Gemini / Pi / OpenCode / OpenClaw) anchor discovery to the task workdir
and never read a user-global skill directory. UI copy and docs now say
"for runtimes that support it (currently Claude Code)" everywhere so
the scope is explicit.
Changes:
- Migration 108: column default flipped to 'merge'.
- Handler CreateAgent: missing field → "merge"; explicit "ignore" /
"merge" still validated, garbage still 400.
- normalizeSkillsLocal: drift-safe coercion now lands on "merge" for
anything that isn't the exact literal "ignore".
- agent_template.go / onboarding_shim.go: internal CreateAgent callers
send "merge" instead of "ignore" to match the new default.
- Claude runtime (`claude.go`): isolate-mode gate flipped from
`SkillsLocal != "merge"` to `SkillsLocal == "ignore"`, so "" (legacy
daemons / older clients) and "merge" both walk `~/.claude/` directly.
- Create Agent dialog + Skills tab: toggle defaults to on (merge); only
duplicate of an explicit "ignore" agent carries through. The
isolation opt-in is now `skills_local: "ignore"` when the user flips
off; "merge" is omitted from the request body.
- i18n (EN + zh-Hans): copy reframed — "On (default) — merged"; "Off —
ignored. Recommended for shared agents".
- Docs (`/skills`, `/guides/agents.zh`): describe new default and
enumerate which runtimes act on the toggle.
- Landing changelog 0.3.7: retitled "Per-Agent Local-Skill Toggle"; note
the on-by-default behavior + off-to-isolate framing.
- Tests:
- `TestClaudeExecuteIsolatesHostSkillsWhenIgnoreOptedIn` replaces the
old by-default isolation case (now requires explicit "ignore").
- New `TestClaudeExecuteDefaultModeKeepsHostConfigDir` locks in that
default ExecOptions preserve the host CLAUDE_CONFIG_DIR.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` now explicitly opts
into "ignore" mode.
- Handler tests: omitted → "merge"; explicit "ignore" round-trips;
preserve-existing test seeds "ignore" and asserts "merge" flip-back.
- `TestNormalizeSkillsLocal_DriftStaysSafe`: only literal "ignore"
maps to ignore; everything else → "merge".
- `skills-tab.test.tsx`: toggle ON by default; flip OFF when agent
opted into "ignore". Intro-text matcher anchored to a more specific
phrase so it no longer collides with the toggle hint copy.
Verification:
- `go test ./...` green (full server suite locally).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean (windows-tagged linker
file still builds).
- `pnpm typecheck` green across all packages and apps.
- `pnpm --filter @multica/views test` 88 files / 771 tests green.
- `pnpm --filter @multica/core test` 43 files / 390 tests green.
- Handler DB-backed tests still skip locally without docker; CI will
validate the create / update paths against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* chore(landing): drop 0.3.7 changelog entry from this PR (MUL-2603)
The landing-page release notes belong in a separate release-prep PR, not in the feature PR.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): propagate skills_local=ignore to codex user-skill seed (MUL-2603)
Make the per-agent skills_local toggle real for Codex too, not just Claude.
Previously the toggle was only consumed by the Claude backend, while the
daemon's execenv layer always seeded Codex's per-task CODEX_HOME with the
host machine's user-installed skills from ~/.codex/skills/. A shared Codex
agent with skills_local=ignore could still inherit a broken local skill
from one operator's machine.
Now: PrepareParams/ReuseParams carry SkillsLocal; hydrateCodexSkills
skips seedUserCodexSkills when SkillsLocal == "ignore" so the per-task
CODEX_HOME exposes only workspace skills to the codex CLI. Default
("merge", or empty from older servers/clients) preserves existing
inherit-from-machine behavior. UI / docs are updated to reflect the
contract honestly: Claude Code and Codex honor the toggle; other
runtimes (Hermes / Kimi / Kiro / Copilot / Cursor / Gemini / Pi /
OpenCode / OpenClaw) leave $HOME untouched and discover user-level
skills natively, so the toggle is a no-op for them today.
New tests: TestPrepareCodexSkillsLocalIgnoreSkipsUserSeed,
TestPrepareCodexSkillsLocalMergeSeedsUserSkills, and
TestReuseCodexSkillsLocalIgnoreSkipsUserSeed cover Prepare(ignore),
Prepare(merge), and the toggle-flip-on-reuse path.
Co-authored-by: multica-agent <github@multica.ai>
* docs(skills): scope skills_local toggle copy to Claude Code + Codex (MUL-2603)
Off-state hint and Skills tab intro now explicitly call out Claude Code +
Codex as the only runtimes that honor the toggle, with "other runtimes
ignore this setting" wired into both states (en + zh-Hans), so users on
non-Claude/Codex agents don't read "Off" as runtime-wide isolation.
Docs (skills.mdx, skills.zh.mdx, guides/agents.zh.mdx) stop describing
Hermes / Kimi / Gemini / Copilot / Cursor / Pi / OpenCode / OpenClaw / Kiro
as having native user-level skill discovery; the daemon simply does not
manage user-level skill discovery for those runtimes today, and the toggle
is a no-op regardless of where it is set.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>