11 Commits
Author SHA1 Message Date
Bohan Jiangandmultica-agent 53578958bc fix(selfhost): pass MULTICA_DINGTALK_SECRET_KEY through and guard integration keys (#8617)
The backend service environment in docker-compose.selfhost.yml is an
explicit allowlist, and MULTICA_DINGTALK_SECRET_KEY was never on it:
Docker self-hosters who set the key in .env still got a disabled DingTalk
integration. Telegram had the same gap until #8611.

- Map MULTICA_DINGTALK_SECRET_KEY into the backend service.
- Document MULTICA_SLACK_SECRET_KEY and MULTICA_TELEGRAM_SECRET_KEY in
  .env.example next to the other channel keys.
- Extend selfhost-config.test.sh: every key the server loads through
  secretbox.LoadKey must be mapped in docker-compose.selfhost.yml and
  listed in .env.example. The list comes from the server, not the docs,
  because Telegram's key was missing from both.
- Run script-checks when server/cmd/server/router.go changes, where the
  integration gates live, so a new channel key trips the guard in the PR
  that adds it.

Co-authored-by: multica-agent <github@multica.ai>
2026-09-21 12:07:23 +08:00
02a8ff3e95 feat(maintenance): add resumable internal backfill API (MUL-7365) (#8436)
* feat(maintenance): add resumable internal API backfills (MUL-7365)

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

* fix(maintenance): ship bounded container operations driver

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

* test(cursor): synchronize ownership before finalization

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

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-15 17:47:54 +08:00
6a65922ef7 MUL-7016: Add read replica infrastructure (#7979)
* feat(server): add read replica infrastructure (MUL-7016)

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

* fix(server): make replica fallback passive (MUL-7016)

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

* fix(server): address replica routing nits (MUL-7016)

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

* refactor(server): tighten replica routing API (MUL-7016)

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

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-03 18:54:53 +08:00
53e15b35c6 MUL-6502: fix startup recovery during PostgreSQL outages (#7372)
* fix(startup): retry transient database outages

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

* fix(startup): address database retry review

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

* test(startup): address retry review nits

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

* fix(startup): preserve native pgx connect timeout

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

* fix(startup): apply timeout fallback to services

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

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-21 14:33:04 +08:00
658b0b7d9f MUL-6406: fix(auth): fail fast in production on insecure JWT secret defaults (#7212)
* fix(auth): fail fast in production on insecure JWT secret defaults

* chore(deploy): require a strong JWT_SECRET in compose and docs

* fix(ci): make selfhost config test pass with required JWT_SECRET

The compose file's JWT_SECRET:? error message contained a colon-space
sequence that YAML parsed as a mapping indicator, breaking docker compose
config. Quote the openssl hint instead.

.env.example now ships an empty JWT_SECRET, so the test's docker compose
config calls fail interpolation; supply a throwaway secret from the
environment, which Compose lets outrank the env file.

* fix(ci): seed JWT_SECRET into the recipe .env in selfhost config test

The Makefile includes .env and bare-exports every variable to the recipe
environment, so the make-driven recipe cases clobbered the test's
JWT_SECRET export with the empty value .env.example ships. The stub's
`docker compose config` then failed interpolation with stderr discarded,
produced empty JSON, and crashed the node parser. Seed the throwaway
secret into the recipe .env, which both make and Compose load.

* docs(env): use a CSPRNG for the Windows JWT_SECRET generation hint

Get-Random is not cryptographically secure, so the Windows hint could
leave self-hosters with a predictable signing key. Point at
RandomNumberGenerator instead, per review.

* docs(selfhost): fix stale JWT secret comments

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

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 12:54:19 +08:00
24d8c521a6 MUL-6364: make the LLM client retry budget explicit, wired, and validated (#7201)
* feat(llm): make the LLM client retry budget explicit, wired, and validated

Config.MaxRetries was a bare int that nothing outside tests ever set. The
production wiring in handler.New never passed it and no MULTICA_LLM_* env var
fed it, so the field was configurable in name only and every deployment ran on
the SDK default.

Its semantics were also inverted. Because the zero value and an explicit 0 are
the same int, "0" fell into the unset branch and silently produced 2 retries,
while a negative — clamped to 0 to dodge the panic in option.WithMaxRetries —
was the only value that actually disabled retries.

- MaxRetries becomes *int so unset, 0 (disabled) and N (exact) stay distinct.
  New always passes the budget to the SDK explicitly, including the default, so
  the enforced policy and the reported one cannot drift apart.
- Add MULTICA_LLM_MAX_RETRIES as the single configuration source. Non-numeric,
  negative and above-ceiling values fail the boot instead of being corrected
  into something that looks configured. The ceiling of 5 is a latency budget:
  backoff reaches ~21s at 6 retries while the internal callers of this layer
  time out after 8s and 20s.
- Report the effective policy at startup via Client.RetryBudget, whose fields
  are counts and a fixed enum so the line cannot carry a key or gateway URL.
- Document the four states in .env.example and the environment-variable docs,
  and record what the budget does not cover: the GenerateJSON parameter
  negotiation and a stream that breaks after ChatStream returns.

Tests pin every state by upstream request count, so the unset-versus-zero
collapse cannot come back unnoticed.

Refs MUL-6364, closes #7154

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

* fix(llm): reject invalid retry budgets at the client boundary and wire self-host

Review found the retry budget still reachable in two invalid shapes, and never
reachable at all on the main self-hosted deployment path.

- docker-compose.selfhost.yml declares the backend environment as an explicit
  allowlist and mapped none of the MULTICA_LLM_* variables, so every value
  .env.example and the docs told a self-hoster to set was dropped before the
  container: the LLM layer read as unconfigured no matter what they wrote. Map
  all four, and assert propagation in scripts/selfhost-config.test.sh, plus a
  drift guard so a future MULTICA_LLM_* documented without a mapping fails the
  script instead of silently going nowhere.
- Config.MaxRetries took a *int, so a negative still reached llm.New, which
  warned and used the default instead. Warning is not silence, but substituting
  a different budget is still the implicit correction #7154 asked to remove.
  It now takes *RetryOverride, whose field is unexported and whose only
  constructor rejects negatives — the invalid state is unrepresentable, so New
  has no correction branch left to take.
- The budget is a ceiling, not a quota: only retryable failures consume it, and
  a success or the caller's deadline can end a call sooner. Say "at most N"
  rather than "exactly N" in the field docs, .env.example and all four
  environment-variable docs.

The negative-value test now asserts rejection instead of pinning the fallback
as contract.

Refs MUL-6364, closes #7154

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-08-19 15:00:56 +08:00
mxbao063-baoandmichelle 02b73d5874 fix(selfhost): forward SMTP_FROM_EMAIL to backend (#6653)
Co-authored-by: michelle <michelle@michelledeMacBook-Pro.local>
2026-08-11 15:54:51 +08:00
cac453fe01 MUL-5505: fix(selfhost): one source of truth for the backend host port (#6168)
* fix(selfhost): read the published host port back from Compose (MUL-5505)

`make selfhost` polled the wrong port when the backend host port was passed
through the environment. Make and Compose disagree on variable precedence: an
assignment in an included makefile (the Makefile `include`s .env) overrides a
real environment variable, while for Compose the environment overrides .env.
Because .env.example ships BACKEND_PORT uncommented, .env always pinned the
recipe's view of the port, so `BACKEND_PORT=9000 make selfhost` published the
stack on 9000 while the health check hammered 8080 for 60s and then reported
"Services are still starting" on a perfectly healthy install. The success
message printed the same wrong backend URL. FRONTEND_PORT had the same
inversion, affecting the printed frontend URL.

Stop re-deriving the host port in the recipe and ask Compose, which is the only
authority on what it published. The wait-and-report block (three port
references per target, duplicated across selfhost and selfhost-build) moves into
scripts/selfhost-wait.sh, which resolves both host ports via `docker compose
port` and falls back to the compose default chain when the stack is not up.

Also fix the input contract: `PORT` was the only port variable documented in
SELF_HOSTING_ADVANCED.md, yet compose read only BACKEND_PORT, so setting the
documented variable was silently ignored. The backend host port mapping now
falls back to `${PORT:-8080}`, matching Makefile and scripts/local-env.sh, and
the docs describe both variables as host ports.

Container-internal ports (8080/3000), the global PORT derivation used by local
dev, and the 127.0.0.1 bindings are unchanged; the default self-host experience
is byte-identical.

Fixes #6145

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

* fix(selfhost): make PORT the real backend port and test the whole chain

Addresses review feedback on #6168.

The first round's root-cause claim was wrong. It said `BACKEND_PORT=9100 make
selfhost` published on 9100 while the health check probed 8080. It does not:
when make drives Compose, Compose inherits make's exported values, so both
sides agree on 8080 and the override is silently dropped instead. Re-measured
through the real recipe, the genuine disagreements were:

  - `.env` setting PORT with no BACKEND_PORT: probe followed PORT, Compose
    published ${BACKEND_PORT:-8080} — 9100 vs 8080.
  - `make selfhost PORT=8080` over a .env with BACKEND_PORT=9100: probe 8080,
    Compose published 9100.

Both are the "wrong port variable" defect the GitHub issue names, and reading
the port back from Compose fixes both. The comments and PR text that blamed
make/Compose precedence are corrected.

The PORT fallback added to docker-compose.selfhost.yml was also unreachable:
.env.example shipped BACKEND_PORT=8080 uncommented, so the fallback never
engaged and `SELF_HOSTING_AI.md`'s "edit PORT and FRONTEND_PORT" instruction did
nothing. Pick one contract and apply it everywhere: PORT is the backend port to
edit, BACKEND_PORT/API_PORT/SERVER_PORT are optional aliases that override it.
That is already what Makefile, scripts/local-env.sh, scripts/install.sh and
install.ps1 do; compose and the web dev fallback were the outliers.

  - .env.example ships BACKEND_PORT commented out, like its sibling aliases, so
    editing PORT works out of the box.
  - apps/web/config/runtime-urls.ts walks the same alias chain instead of
    reading BACKEND_PORT alone, so `pnpm dev` follows an edited PORT.
  - SELF_HOSTING_AI.md and SELF_HOSTING_ADVANCED.md state the contract.

Configuration that cannot take effect is now reported instead of ignored.
scripts/selfhost-preflight.sh warns when an alias in the env file overrides an
edited PORT, and when a port from the shell environment is overridden by the env
file. The Makefile captures the pristine origins before its include, because
afterwards there is no way to tell that `PORT=9000 make selfhost` was asked for.

Tests now cross environment -> make -> compose -> report for real. The docker
stub is no longer told what to publish: on `up` it asks the real
`docker compose config` to interpolate the compose file with the environment the
recipe actually handed it, records that, and answers `port` from the recording.
Verified failing on the old code — with the old recipe and compose file the
suite reports published 8080 against probe 9100, the exact defect.

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

* ci: gate the self-host test on scripts/selfhost-preflight.sh too

The new preflight script was missing from the frontend path filter, so a
change to it alone would skip the test that covers it.

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

* fix(selfhost): honour the full backend port alias chain in Compose

Addresses the second review round on #6168.

The contract this PR documents is

  BACKEND_PORT -> API_PORT -> SERVER_PORT -> PORT -> 8080

and Makefile, scripts/local-env.sh, scripts/install.sh, scripts/install.ps1 and
apps/web/config/runtime-urls.ts all implement it. docker-compose.selfhost.yml
implemented only BACKEND_PORT -> PORT -> 8080, so `API_PORT=9100` or
`SERVER_PORT=9200` in .env still published 8080.

That is not only a direct-Compose concern. scripts/install.sh:407 derives its
health-check port from the full chain and then runs this compose file, so an
alias it honours but Compose ignores puts the probe on 9100 while the stack
listens on 8080 — the original #6145 defect, reached through the installer.

  - docker-compose.selfhost.yml publishes the full nested chain, verified to
    keep the same precedence and the same 8080 default.
  - scripts/selfhost-wait.sh's fallback matches, so the degraded path agrees
    with what Compose would have published.
  - The Makefile captures the pristine origins of API_PORT and SERVER_PORT too,
    and the preflight reports them, so no alias can be silently overridden by
    the env file while the others are reported.

Tests pin the chain on the boundaries a recipe-level test cannot see, because
Makefile normalises all four aliases into PORT before any recipe runs: a matrix
over the direct Compose path asserts each alias moves the published port with
the right precedence, and every case additionally asserts that
scripts/install.sh's resolver returns the same value Compose published. That
invariant is the one that actually protects the installer.

Verified failing without the fix: with the two-level chain restored the suite
reports `expected 9200 / compose published 8080 / installer resolved 9200`, and
with the alias warnings narrowed it reports the missing API_PORT notice.

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

* fix(selfhost): resolve one winner before reporting ignored port config

Addresses the third review round on #6168.

The preflight walked each alias independently, so it made a separate "wins"
claim per alias. With

  PORT=9000
  BACKEND_PORT=8000
  API_PORT=7000
  SERVER_PORT=6000

in .env it announced that BACKEND_PORT, API_PORT and SERVER_PORT each won, when
only BACKEND_PORT=8000 can. Two of the three notices were simply false.

It also compared only same-named shell and file variables, so shadowing across
aliases stayed silent: .env with PORT=8000 and BACKEND_PORT=8000 plus a shell
API_PORT=7000 produced no output at all, even though API_PORT was discarded.

Resolve the winner along the documented chain first — within a variable the env
file beats the environment, then BACKEND_PORT beats API_PORT beats SERVER_PORT
beats PORT — and report only the inputs that winner actually shadows. Output now
names one winning input and lists the rest, whichever variable or source they
came from. An input carrying the winning value is not reported: it is redundant
but the user still gets the port they asked for, so there is nothing to fix.

Tests cover both cases the old logic got wrong: every alias set at once must
yield exactly one winner claim and list the others as unused, and an env-file
alias beating a lower-priority shell alias must be reported. Also asserted that
a redundant same-value alias and a default configuration both stay silent.

Measured against the previous implementation, phrasing aside: with all four
values set it made 3 winner claims where there is now 1, and on the cross-alias
case it printed nothing where API_PORT is now reported.

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

* fix(selfhost): track env-file existence so empty port assignments resolve right

Addresses the fourth review round on #6168.

The Makefile passed only values to the preflight, so the script inferred "the
env file does not set this" from an empty string. An explicit empty assignment is
a different input from an absent one: make lets `BACKEND_PORT=` in the env file
override `BACKEND_PORT=9000` from the environment, and the variable then drops out
of the alias chain instead of setting a port.

With .env holding PORT=8080 and BACKEND_PORT=, invoked as
`BACKEND_PORT=9000 make selfhost`, the stack published 8080 and the health check
probed 8080 — correct — while the preflight announced

  the backend host port resolves to 9000 from BACKEND_PORT (environment)
  Set but unused: PORT=8080 (.env)

naming the ignored value as the winner and the winning value as unused. A report
that contradicts the startup is worse than no report. FRONTEND_PORT= had the
matching hole: it shadowed the environment, Compose fell back to 3000, and the
preflight said nothing.

The Makefile now captures ENV_FILE_<VAR>_IS_SET from $(origin) alongside the
value, for all five port variables via one $(foreach) instead of hand-written
lines. The preflight treats a variable the file defines as taking the file's
value even when empty, so it shadows the environment, and an empty value never
wins — resolution continues down the chain and falls back to 8080. Inputs the
winner shadows are still listed, including ones shadowed by an empty assignment.
The frontend check mirrors this and now names the port that actually resolved.

Recipe-level tests cover an empty alias over a shell value, every link of the
chain emptied at once, and the frontend equivalent — each asserting the reported
winner, the Compose published port and the probed port all agree. Against the
previous implementation the first case fails with `reported: 9000 / actual: 8080`.

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

* fix(selfhost): installers read the published port; drop the second parser

Addresses all four blockers from the consolidated review on #6168.

1. Both installers probed and printed the wrong port. scripts/install.sh:402 and
   scripts/install.ps1 start Compose inheriting the current environment, and
   Compose lets that environment outrank .env — but the installers then derived
   the probe port and the summary URL from .env alone. Measured on this branch
   before the fix, every port variable diverged:

     ambient PORT=9100        -> compose 9100, installer 8080
     ambient BACKEND_PORT=9200 -> compose 9200, installer 8080
     ambient API_PORT=9300     -> compose 9300, installer 8080
     ambient SERVER_PORT=9400  -> compose 9400, installer 8080
     ambient FRONTEND_PORT=3100 -> compose 3100, installer 3000

   Both now read the ports back once with `docker compose port` after `up -d`
   and reuse that single result for the health check and the summary. If the
   query fails they fail loudly instead of claiming success. The .env-derived
   helpers are deleted rather than left beside the new path.

2. The Make preflight is removed entirely, as the review recommended. It
   modelled `origin=environment` and `origin=file` but not `command line`, so
   `make selfhost BACKEND_PORT=9000` over a .env with API_PORT=7000 announced
   7000 while Compose published 9000. That was its fourth wrong report in four
   rounds, because it was a second parser of precedence rules — the very thing
   this PR removes elsewhere. `docker compose port` is the runtime truth, so the
   script, the Makefile captures and the macro are gone.

3. Tests now cover the installer paths that were unguarded. scripts/install.test.sh
   gains a `--with-server` matrix (defaults, .env PORT, all four backend aliases,
   FRONTEND_PORT, ambient overriding .env for all five, and explicit-empty
   fallback) plus a case proving an unresolvable port fails loudly. A new
   scripts/install.ps1.test.ps1 drives the same matrix through Start-LocalInstall.
   Every case asserts Compose's published port == the probed URL == the printed
   URL. Ambient variables are explicitly cleared per case so a runner's own PORT
   cannot leak. The Makefile matrix gains the command-line origin cases. CI runs
   the PowerShell suite on windows-latest in the always-on installer job, and the
   frontend filter now includes both installers.

4. Docs state the real contract: the alias order is shared, but which *source*
   wins is per entry point — Compose prefers the environment, make prefers the
   included env file, and a make command-line assignment outranks both. The
   removed preflight's promise is gone from SELF_HOSTING_AI.md, and the compose
   header no longer claims the installer derives its own health-check port.

Verified failing without the fixes: the Bash matrix reports
`[ambient PORT beats .env] compose published 9500 / probed 9100 / printed 9100`
against the old installer, and the PowerShell matrix reports
`printed backend=8080` against an injected summary divergence.

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

* fix(selfhost): keep web dev proxy off frontend port

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-31 14:46:13 +08:00
YYClaw ee7ba83f53 fix(self-host): apply setup config to daemon (MUL-5269) (#5880)
Fixes two connected self-host setup failures in local worktree development.

Generated worktree environments now expose the backend HTTP origin through
MULTICA_PUBLIC_URL, and existing generated worktrees derive the missing value
at startup through both scripts/local-env.sh and the Makefile. Explicit
values, including an intentionally empty same-origin setting, are preserved.

setup and setup self-host now reconcile an existing same-profile daemon after
authentication so it loads the newly written server URL and token. An idle
daemon is restarted behind the existing restart preflight; when active tasks
exist, setup leaves the daemon running to avoid cancelling work and prints an
actionable profile-aware restart command instead.

Closes #5879
2026-07-30 15:10:40 +08:00
4df6c1468d fix: validate selfhost compose env defaults (#4138)
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-15 15:43:10 +08:00
YOMXXX bfb7c85491 fix(selfhost): derive local port URLs from env (MUL-2506) (#2939)
* fix(selfhost): derive local port URLs from env

* fix(selfhost): derive local script URLs
2026-05-24 13:05:53 +08:00