Commit Graph
34 Commits
Author SHA1 Message Date
2213734215 MUL-8253 feat(web): opt-in Google Analytics via GOOGLE_ANALYTICS_ID (#9544)
* feat(web): opt-in Google Analytics via GOOGLE_ANALYTICS_ID (MUL-8253)

Co-authored-by: multica-agent <[email protected]>

* fix(i18n): add missing zh-Hant keys for model override and scheduled wakeup

Co-authored-by: multica-agent <[email protected]>

* fix(web): keep credential-bearing URLs out of Google Analytics

Co-authored-by: multica-agent <[email protected]>

* feat(selfhost): pass GOOGLE_ANALYTICS_ID through compose and helm

Co-authored-by: multica-agent <[email protected]>

* fix(web): stop GA for the document after sensitive navigation

Co-authored-by: multica-agent <[email protected]>

* test(views): await subscription cleanup and dialog focus

Co-authored-by: multica-agent <[email protected]>

* fix(web): stop GA before credential-bearing link clicks and form submits

GA4 enhanced measurement reports outbound/download link_url and
form_destination without a URL change, so signed attachment downloads
leaked their signature. Stop the tag for the document in a window
capture listener before gtag's own listeners observe the interaction.
Object URLs are exempt. Adds jsdom tests and a live gtag.js regression
covering the ~5s batch delay.

Co-authored-by: multica-agent <[email protected]>

* test(web): slim GA tests; move views test fixes to #9555

Drop the opt-in live-tag e2e spec (never runs in CI) and condense the GA
unit tests to the cases that guard against credential leaks. The two
unrelated views test-timing fixes now live in #9555.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Bohan-J <[email protected]>
2026-10-26 18:04:40 +08:00
LinYushen cf1b841a88 Revert "MUL-8243: feat(selfhost): wire feature flags through Compose and Helm…" (#9533)
This reverts commit 8981630a87.
2026-10-26 12:41:05 +08:00
wrj97 8981630a87 MUL-8243: feat(selfhost): wire feature flags through Compose and Helm (#9523)
* feat(selfhost): wire feature flags through Compose and Helm

Feature flags were documented in .env.example but neither self-host
deployment path forwarded them: the Compose backend environment is an
explicit allowlist and the Helm ConfigMap had no flag entries. Workflows
shipped GA in v0.7.0 but stayed hidden on a default self-host install.

- forward MULTICA_FEATURE_FLAGS_FILE and FF_PROJECT_WORKFLOWS_V1 (default
  true) from the Compose backend service
- render backend.config.featureFlags as FF_<FLAG_KEY> entries in the Helm
  ConfigMap, keep Workflows on for --reuse-values upgrades, and add
  featureFlagsFile for operators who mount a YAML rule file
- document flag precedence, self-host defaults, and per-path overrides in
  SELF_HOSTING_ADVANCED.md and .env.example
- extend selfhost-config.test.sh and helm-config.test.sh so the wiring and
  the documented defaults cannot drift silently again

* docs(feature-flags): add the self-host feature flag reference

Add a Feature flags page under Self-hosting in the documentation site,
translated into all five locales, and link it from the self-hosting
section of every navigation tree. The page covers flag precedence, the
self-host defaults, Compose and Helm overrides, the YAML rule file, and
how to verify a change through /api/config.
2026-10-26 12:40:29 +08:00
7ebaf00e7c MUL-8136: feat(server): restrict workspace creation to listed emails (self-host) (#9411)
* feat(server): allow workspace creation only for listed emails (self-host)

Add WORKSPACE_CREATION_ALLOWED_EMAILS. When set, POST /api/workspaces
returns 403 for callers whose email is not listed. Empty keeps the
current behavior; DISABLE_WORKSPACE_CREATION=true still blocks everyone.
No instance-admin role, no domain allowlist, no UI hiding.

Refs #9400, MUL-8136

Co-authored-by: multica-agent <[email protected]>

* docs(self-host): clarify which signup vars /api/config exposes

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-22 08:49:07 +08:00
1e01173fb4 MUL-8060 fix(daemon): trust the backend's SSL_CERT_DIR CAs when syncing repos (#9312)
* fix(daemon): report the checkout's own repo sync error

ensureRepoReady read the sync failure from a per-workspace slot that any
concurrent background sync overwrote, so a later successful sync of another
repo erased the error and checkout reported only "repo is configured but
not synced". syncWorkspaceReposContext now returns its error to the caller
and the shared slot is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(server): share SSL_CERT_DIR CAs with daemons for git

Daemons clone with the system git, which never sees the backend's
SSL_CERT_DIR, so a repository on a host with an internal CA could not
sync even after the operator made the backend trust that CA.

The new gitca package mirrors crypto/x509's SSL_CERT_DIR loading, skips
the system directories, and keeps only certificates crypto/x509 trusts.
The register and workspace repos responses carry them as ca_certs. The
daemon writes its system CA bundle plus those certificates to a bundle
file, adds http.sslCAInfo to its git environment, and records it in the
cache and isolated checkout config so agents' fetch and push use it too.
An empty value removes the bundle; Windows daemons skip it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: document private CA trust for daemons

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(daemon): keep operator CA trust and existing checkouts working

Review of the private CA support found two regressions for repositories
that already worked:

- Removing the last backend CA deleted the bundle while existing linked
  worktrees and isolated checkouts still pointed at it, so their HTTPS
  fetch and push failed. The bundle is now rewritten to the machine's own
  trust instead, and each repository drops the reference the next time
  the cache configures it.
- The bundle started from the system bundle and was injected through the
  environment, overriding an operator's general http.sslCAInfo and any
  repository-local value. It now starts from the file a general
  http.sslCAInfo names (system bundle otherwise), the environment no
  longer carries it, clone passes it with -c, and repository config is
  only set where unset and only unset where it is ours. URL-scoped
  settings, repository-local values, and GIT_SSL_CAINFO keep precedence.

Isolated checkouts record the setting before their first checkout, which
can lazily fetch from origin, and clones before the refspec migration
fetch.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Iliya Anastasov <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-21 14:25:27 +08:00
Bohan Jiangandmultica-agent 4f5fbc9216 fix(vcs): explain untrusted TLS certificates and trust private CAs via Helm (MUL-7639) (#8761)
* fix(vcs): report untrusted provider TLS certificates on connect (MUL-7639)

ConnectVCS reported every non-token failure as "could not reach the
provider instance" and logged nothing, so a Gitea behind a private CA
looked like a network problem. Log the underlying validation error and
tell certificate failures (untrusted CA, host name mismatch, other
verification failures such as expiry) apart from unreachable instances.
Status codes are unchanged.

Co-authored-by: multica-agent <[email protected]>

* feat(helm): trust extra CA certificates in the backend (MUL-7639)

backend.extraCACerts.configMap mounts an existing ConfigMap of PEM
certificates read-only and points SSL_CERT_DIR at the system directory
plus the mount, so the backend trusts an internal CA without dropping
public CAs or disabling TLS verification. Unset renders unchanged.

Co-authored-by: multica-agent <[email protected]>

* docs(self-host): document trusting a private CA (MUL-7639)

Co-authored-by: multica-agent <[email protected]>

* fix(helm): render when values predate extraCACerts (MUL-7639)

helm upgrade --reuse-values from an older chart carries no
backend.extraCACerts key, and reading .configMap on it failed the whole
render with a nil pointer even when no CA was wanted. Fall back to an
empty dict, and cover the missing-key, default and enabled renders in
the chart test.

Co-authored-by: multica-agent <[email protected]>

* docs(self-host): restart Compose backend after replacing a CA (MUL-7639)

up -d keeps the running container when only a mounted CA file changed,
so the backend kept its old trust store. Use restart instead.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-09-24 13:04:01 +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 <[email protected]>

* fix(maintenance): ship bounded container operations driver

Co-authored-by: multica-agent <[email protected]>

* test(cursor): synchronize ownership before finalization

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-15 17:47:54 +08:00
dee1ed6bf9 MUL-7236: add anonymous self-host telemetry (#8252)
* MUL-7236 add anonymous self-host telemetry

Co-authored-by: multica-agent <[email protected]>

* fix(telemetry): apply review feedback

Co-authored-by: multica-agent <[email protected]>

* fix(telemetry): address review nits

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-15 15:21:11 +08:00
d3f1e64ea8 MUL-7055: refactor issue search to candidate-first pipeline (#8041)
* perf(search): use candidate-first issue search

Co-authored-by: multica-agent <[email protected]>

* fix(search): address candidate-first review feedback

Co-authored-by: multica-agent <[email protected]>

* fix(search): address review nits

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-07 13:11:03 +08:00
7c1d826a98 chore(metrics): remove database-sampled metrics (#7819)
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 17:06:17 +08:00
3e55ce4db0 MUL-6882: remove unused active database samplers (#7817)
* perf(metrics): remove unused active samplers

Co-authored-by: multica-agent <[email protected]>

* chore(metrics): document retired active samplers

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 16:25:58 +08:00
64ec7f5416 MUL-6737: fix(timeouts): close the two gaps the 2h inactivity budget left open (#7699)
* fix(timeouts): close the two gaps the 2h inactivity budget left open (MUL-6737)

Two independent places where a timeout still answered the wrong question.

Codex runs a semantic-inactivity timer inside the app-server protocol, and
unlike the daemon's watchdog it cannot see that a tool is in flight: a
commandExecution that prints nothing for the window trips it anyway. At a
fixed 10m ceiling that timer killed a quiet test suite or an
output-buffering `docker build` long before the 2h daemon budget meant to
protect it, which made "we raised the budget to 2h" false for Codex users.
It now derives from the daemon budget, taking the larger of the idle and
tool windows because it stands in for both. A tool budget of 0 means "never
force-stop during a tool", which this timer cannot express, so it falls back
to the idle budget rather than running unbounded; with the watchdog suite
disabled entirely Codex keeps its own built-in default, as it always has.
The env override and flag are unchanged.

Queued tasks expired on a pure wall clock: queued longer than a TTL meant
failed. That conflated "nobody is coming for this" with "the queue ahead is
long", and only the first is a failure. MUL-6558 was the second — a
self-hosted runtime with low concurrency held its own queue past 2h and
healthy work died as queued_expired. The TTL knob added then only moved the
cliff. Expiry now keys on whether the owning runtime still proves liveness,
the same signal FailTasksForOfflineRuntimes already uses for
dispatched/running rows, so a daemon going down retires its queued and its
in-flight work on one clock instead of two. A busy runtime keeps its
backlog for as long as it takes. MULTICA_TASK_QUEUED_TTL is removed with
the wall clock it configured.

Heartbeat age is read directly instead of gating on runtime.status='online'
so a row stuck at 'online' with a dead heartbeat still releases its queue,
and rows with no usable runtime binding — runtime_id IS NULL from before
migration 251, whose CHECK landed NOT VALID, or a runtime row that is gone —
are expired on sight rather than stranded forever by a JOIN that matches
nothing.

Tests: the queued sweep test now runs both phases against the same rows,
asserting the 5h-old task survives while its runtime heartbeats (the
MUL-6558 regression) and only expires once that runtime goes silent.
Assertions are scoped to the test's own task ids, because a runtime-keyed
sweep legitimately also collects queued rows other tests left on the shared
fixture runtime.

Co-authored-by: multica-agent <[email protected]>

* fix(sweeper): give a queued task its own reconnect grace before expiring it

Review catch: keying expiry on runtime liveness alone was too aggressive in
one direction. Enqueue binds a task to agent.runtime_id without checking
that the runtime is up (task.go CreateAgentTask call sites), so for a
runtime that has already been dark for longer than the grace, the liveness
clause is satisfied the moment a new task is created — assigning an issue
to a laptop closed overnight would fail it inside one 30s sweep tick
instead of waiting for the machine to come back. That is the opposite of
what "reconnect grace" promises.

Require the row's own age as well: a queued task now waits a full grace,
counted from when it started waiting, before it can be given up on. A
heartbeating runtime still never expires its backlog, so the MUL-6558 fix
is unchanged. The age bound also stops the sweep re-evaluating the runtime
subquery against every queued row on every tick.

Also remove the deploy surfaces the previous commit missed — the knob was
deleted from the server but helm (values + configmap), the self-host
compose file, and the CI-run helm render assertions still advertised it, so
operators would have kept setting a variable nothing reads. Same for five
orphaned comment lines in .env.example describing the removed variable.

Correct the fail-closed comment on the unbound-runtime arms: they are not
reachable for "rows predating migration 251" — runtime_id was NOT NULL from
migration 004 until 251 replaced it with a CHECK that every insert and
update is verified against, and the migration-004 FK is ON DELETE RESTRICT.
The arms stay as defence against a future schema change, which is what the
comment now says.

Tests: a third phase covers the reported case — a task enqueued against an
already-dead runtime survives, then expires once it has waited a full grace
of its own. Phase 2's expectation is corrected accordingly: the row created
at now() no longer expires when its runtime dies, because it has not waited
its own grace yet.

Co-authored-by: multica-agent <[email protected]>

* docs(timeouts): state both queued-expiry conditions and correct the FK claim

Review follow-up on b567511.

The user docs still described queued expiry as a single condition — "fails
once the runtime has been silent past the reconnect grace" — which is the
pre-b567511 behaviour and skips exactly the case that commit fixed. A
reader assigning work to a machine that went offline last night would
conclude the task already qualifies to fail. All four locales of tasks,
daemon-runtimes and troubleshooting now state both conditions and why the
second one exists.

The environment-variables pages contradicted themselves: the env table had
been updated to say the Codex semantic budget is derived, while the
`multica config` supported-keys table further down still advertised a 10m
default. That table now describes the resolution rule instead of a value.

The schema comment added in b567511 named the wrong constraint.
agent_task_queue_runtime_id_fkey (migration 004) is ON DELETE CASCADE; the
ON DELETE RESTRICT one is agent_runtime_id_fkey on agent.runtime_id. The
conclusion is unchanged — a queued row cannot reference a deleted runtime —
but the mechanism is a cascade plus migration 251's explicit unbind of
history rows, not a restrict.

Also log the resolved codex_semantic_inactivity at daemon startup, for the
same reason tool_watchdog is logged: it is now a derived value, so without
it the effective Codex budget cannot be read anywhere until a timeout
fires. And correct a test comment that still claimed both non-exempt rows
fail — the fresh row now stays queued, which is the assertion right below it.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-28 18:00:35 +08:00
74a7de9218 MUL-6741: retire heartbeat index and runtime sampler metrics (#7622)
* perf(metrics): remove runtime sampler series

Co-authored-by: multica-agent <[email protected]>

* perf(db): retire runtime heartbeat index

Based on the root-cause analysis and implementation exploration by @importcpp in multica-ai/multica#7599.

Co-authored-by: multica-agent <[email protected]>

* fix(metrics): address runtime sampler review

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-27 15:10:21 +08:00
LinYushen 3b1018e9c9 Revert "MUL-6016: feat(task): retry exact Codex capacity failures (#6747)" (#7586)
This reverts commit 8fda4f22d5.
2026-08-26 18:05:17 +08:00
LinYushen 55fe792483 refactor(cloud): use URL-only internal connection (#7566)
* refactor(cloud): use URL-only internal connection

* refactor(entitlement): remove Cloud rollout action

* refactor(cloud): remove unused client tuning
2026-08-26 14:40:19 +08:00
8fda4f22d5 MUL-6016: feat(task): retry exact Codex capacity failures (#6747)
* docs: record Codex capacity retry design

* docs: add Codex capacity retry implementation plan

* fix(taskfailure): recognize exact Codex capacity error

* feat(task): retry exact Codex capacity failures

* feat(server): configure Codex capacity retries

* docs: expose Codex capacity retry setting

* fix(task): align capacity retry comments

---------

Co-authored-by: cuijs <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-08-26 12:50:20 +08:00
LinYushen baf1bbf340 fix(billing): make Cloud seat enforcement automatic (#7542)
Replace the separate Fleet/capacity URLs and seat-capacity switches with the single managed MULTICA_CLOUD_URL connection. Strict seat admission turns on whenever Multica is Cloud-connected and fails closed when the capacity machine token is absent or invalid; the recovery worker starts only for a valid executor so live intents keep their retry budget during credential outages.

Split invitation throttling into a non-consuming precheck and post-reservation consumption. Persistent capacity_full and capacity_overcommitted rejections charge only the actor budget; transient failures charge none. Preserve every Cloud/proxy HTTP 429 as retryable with both Retry-After forms, defer outbox rows without spending attempts, and use Cloud's rate-limit scope so a workspace 429 delays only that tenant while global or unscoped 429s stop the current batch.

BREAKING: delete/unset MULTICA_FLEET_URL, MULTICA_CLOUD_FLEET_URL, MULTICA_CLOUD_FLEET_TIMEOUT, MULTICA_SUBSCRIPTION_CAPACITY_ENABLED, MULTICA_SUBSCRIPTION_CAPACITY_URL and MULTICA_SUBSCRIPTION_CAPACITY_WORKER_ENABLED before rollout. The startup guard rejects any non-empty value, so MULTICA_SUBSCRIPTION_CAPACITY_ENABLED=false still blocks boot. Configure MULTICA_CLOUD_URL, MULTICA_CLOUD_TIMEOUT and MULTICA_SUBSCRIPTION_CAPACITY_SERVICE_TOKEN instead; entitlements keep their independent MULTICA_ENTITLEMENT_POLICY_* configuration.

Companion: multica-ai/multica-cloud#57
2026-08-26 12:42:42 +08:00
LinYushen 60a55ec57b feat(billing): purchase a seat from the invite flow (#7538)
* feat(billing): purchase seats from full invite flow

* fix(billing): harden full-capacity invite flow

* fix(billing): explain overcommit with cloud seat occupancy

Cloud refuses a reserve on used + reserved, but the overcommit toast filled
members_over_capacity_description with the product member count. The most
common trigger is a first ledger snapshot whose members alone still fit
inside the purchased seats, so the message read "3 members but only 3
purchased seats" and did not explain the rejection.

Add occupancy_over_capacity_description, which names occupied seats,
purchased seats, members and pending invitations, and feed it from the
Cloud summary. members_over_capacity_description stays as-is: the billing
tab banner is genuinely member-count driven.
2026-08-25 17:19:46 +08:00
da7131ab59 MUL-6625: enforce prepaid member seat capacity (#7491)
* MUL-6625: enforce prepaid member seat capacity

Co-authored-by: multica-agent <[email protected]>

* fix(seat-capacity): address review findings

Co-authored-by: multica-agent <[email protected]>

* fix(seat-capacity): close retry and worker races

Co-authored-by: multica-agent <[email protected]>

* fix(seat-capacity): bound lock waits during rollback

Co-authored-by: multica-agent <[email protected]>

* fix(seat-capacity): pin the cross-repo confirm settlement budget

Multica Cloud must wait longer than this worker's whole retry budget before it
treats a member row as one its ledger never recorded; adopting a member whose
confirm is still retrying counts one person as two seats, and an over-estimate
is what asks a customer to buy seats they do not owe.

That budget was only implicit here — 10 attempts of exponential backoff plus a
tick and a lock wait each — and Cloud's horizon had been set from a single
retry. Extract the backoff into retryBackoff, expose SettlementBudget, and
assert the worst case (29m15s) stays inside Cloud's 45m horizon, so raising
maxAttempts or the backoff cap fails review here instead of widening the window
in which Cloud cannot quantify a ledger.

Co-authored-by: multica-agent <[email protected]>

* Revert "fix(seat-capacity): pin the cross-repo confirm settlement budget"

This reverts commit d0ec36f647.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-25 12:51:44 +08:00
Jiayuan Zhangandmultica-agent a4e1ea7140 MUL-6581: expose globally versioned Plugin Public API (#7446)
* feat(plugins): expose globally versioned public API

Co-authored-by: multica-agent <[email protected]>

* fix(plugins): proxy same-origin public API

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-08-23 17:15:46 +08:00
Martin MontesandMartin Montes 5a80f802d3 MUL-6558 feat(server): make task queue expiry configurable via MULTICA_TASK_QUEUED_TTL (#7418)
Lets self-hosted deployments raise the queued-task expiry window above the built-in 2h default, so low-concurrency runtimes stop losing legitimately-pending work to queued_expired. Default behavior is unchanged. Covers .env.example, Compose, Helm values/ConfigMap, and the behavior docs in all four locales.

Co-authored-by: Martin Montes <[email protected]>
2026-08-23 12:56:06 +08:00
6431076012 MUL-6552 docs(self-hosting): correct the PostgreSQL extension requirements (#7421)
* docs(self-hosting): correct the PostgreSQL extension requirements

The docs claimed Multica requires pgvector and told operators to run
`CREATE EXTENSION IF NOT EXISTS vector`. Nothing in the codebase uses it:
across all 429 migrations there is no `vector` column and no migration
creating the extension, and no Go package imports a pgvector driver. On a
stock PostgreSQL 17 that `CREATE EXTENSION` fails outright, so the
instruction actively blocked the "use your own PostgreSQL" path it appeared
in.

There are exactly four `CREATE EXTENSION` statements in the migration set.
`pgcrypto` (001) and `pg_trgm` (137) are bare statements that fail the
migration when missing; both ship with every standard PostgreSQL 17 build.
`pg_bigm` (032) and `pg_cron` (076) are wrapped in `DO ... EXCEPTION` and
skip gracefully. Document all four with their real status, and note that a
`schema_migrations` row does not prove a conditionally skipped object
exists.

`pg_bigm` is described as what it actually is — a CJK search-quality
improvement, not a correctness requirement. Migrations 138-142 install
portable `pg_trgm` fallback indexes covering every column the search
handlers touch, so a deployment without `pg_bigm` still searches on an
index.

References to the bundled image name are left alone: those describe which
image the compose file pulls, which is still accurate. The Helm chart
description and values comment are updated because they present pgvector as
part of the stack rather than as an image name.

Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs(self-hosting): document the local disk attachment storage variables

The File Storage section only covered S3 and CloudFront, but local disk is
what the backend uses whenever `S3_BUCKET` is unset — every default
self-host install. `LOCAL_UPLOAD_DIR` and `LOCAL_UPLOAD_BASE_URL` were
absent from the self-hosting docs.

The default `./data/uploads` resolves against the backend's working
directory. Inside the bundled image that is `/app`, where the compose file
mounts the `backend_uploads` volume, so it happens to be correct. Running
the binary manually resolves it against whatever directory the process was
launched from, which silently scatters attachments as that directory
changes and leaves `attachment` rows pointing at files the server no longer
looks for.

Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs(search): fix the migration numbers in the statement-timeout comment

The comment on `searchStatementTimeout` cited migration 134 as the one that
installed the extensions the search fast path needs. 134 is
`134_runtime_profile_add_qoder`, which widens a `protocol_family` CHECK
constraint and has nothing to do with search. The migration that runs
`CREATE EXTENSION IF NOT EXISTS pg_trgm` is 137.

The referenced index list is also extended to 137-142: 137 only installs
the extension, while the trigram indexes themselves are created by 138-142.

Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-22 11:18:59 +08:00
53e15b35c6 MUL-6502: fix startup recovery during PostgreSQL outages (#7372)
* fix(startup): retry transient database outages

Co-authored-by: multica-agent <[email protected]>

* fix(startup): address database retry review

Co-authored-by: multica-agent <[email protected]>

* test(startup): address retry review nits

Co-authored-by: multica-agent <[email protected]>

* fix(startup): preserve native pgx connect timeout

Co-authored-by: multica-agent <[email protected]>

* fix(startup): apply timeout fallback to services

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-21 14:33:04 +08:00
3460f9aeaf MUL-6342: enforce entitlement-backed autopilot quotas (#7194)
* feat(autopilot): enforce entitlement run quotas

Co-authored-by: multica-agent <[email protected]>

* chore(deploy): expose entitlement policy config (#7195)

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(autopilot): address quota review findings

Co-authored-by: multica-agent <[email protected]>

* fix(autopilot): finish quota review nits

Co-authored-by: multica-agent <[email protected]>

* refactor(autopilot): simplify quota schema

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-19 15:51:26 +08:00
73b0015475 feat(vcs): make self-hosted Git providers self-host-only (MUL-3772, MUL-5138) (#5888)
* feat(vcs): gate self-hosted Git providers to self-host deployments only (MUL-3772)

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

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

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

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

Co-authored-by: multica-agent <[email protected]>

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

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-24 16:39:22 +08:00
a90aa92d0c fix(web): resolve API and docs upstreams at runtime (#4840)
* fix(web): resolve upstream URLs at runtime

* fix(web): keep unconfigured upstreams same-origin

* fix(web): restore dev-only localhost fallbacks for API and docs upstreams

`next dev` on a developer machine now falls back to the conventional
http://localhost:8080 backend (honoring BACKEND_PORT) and
http://localhost:4000 docs origin when nothing is configured, so a bare
`pnpm dev:web` keeps proxying out of the box. Builds and the runtime
proxy keep the strict resolvers, so prebuilt images still leave unset
upstreams unproxied.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Naiyuan Qing <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-24 11:02:46 +08:00
Christian Rodriguez 7b89c5104c feat(helm): add per-component affinity and tolerations (#5490)
Allow scheduling backend, frontend, and postgres pods onto tainted or
labeled nodes independently via values.yaml.
2026-07-16 11:59:21 +08:00
feifeigoodandClaude Sonnet 4.6 30e1e546cf MUL-3355: feat(helm): support external PostgreSQL via postgres.external.enabled
Closes #4237

Add a `postgres.external.enabled` toggle (default: false) so operators
can skip the built-in Deployment/PVC/Service and point Multica at any
externally managed PostgreSQL (CNPG, RDS, Cloud SQL, Neon, etc.).

When enabled:
- postgres Deployment, PVC, and ClusterIP Service are not rendered
- The hard-coded DATABASE_URL env var is omitted from the backend pod;
  DATABASE_URL must be supplied in existingSecret and reaches the
  container through the existing envFrom.secretRef mechanism
- POSTGRES_DB / POSTGRES_USER are omitted from the ConfigMap (unused
  when DATABASE_URL is provided directly)

Also update the existingSecret creation example in values.yaml to show
both the built-in and external-postgres variants so users know to add
DATABASE_URL instead of POSTGRES_PASSWORD when switching modes.

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-07-03 13:56:10 +08:00
945d684afd MUL-2965 fix(metrics): harden business sampler quality (#3738)
* fix(metrics): harden business sampler quality (MUL-2965)

Co-authored-by: multica-agent <[email protected]>

* fix(metrics): alert on sampler acquire failures

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-06-04 11:21:51 +08:00
LinYushenandmultica-agent 24ea169d89 fix(migrate): serialize startup migrations with pg advisory lock (#3658)
cmd/migrate previously ran a check-then-apply loop on a *pgxpool.Pool
with no locking, so two backend pods starting at the same time (multi-
replica Deployment, scale-up, or a manual run overlapping with pod
startup) could both pass the EXISTS check on a pending migration and
race on the DDL or the schema_migrations INSERT, crashing the loser.

Take a single connection from the pool, hold a session-level
pg_advisory_lock for the entire migration loop, and release it on the
way out. We use the blocking variant so a late arriver queues behind
the current runner and then no-ops on the EXISTS checks instead of
crash-looping. The loop deliberately stays outside a transaction so
existing CREATE INDEX CONCURRENTLY migrations keep working.

Also refresh the values.yaml / backend.yaml comments next to
backend.replicas: the chart still ships replicas: 1 by default, but
that is now a recommendation (Recreate strategy, no leader split), not
a correctness requirement.

Refs https://github.com/multica-ai/multica/issues/3647

Co-authored-by: multica-agent <[email protected]>
2026-06-03 15:51:03 +08:00
LinYushenandmultica-agent a6247ad714 helm: gate uploads PVC behind backend.uploads.persistence.enabled (#3655)
Adds a value (default true for backward compatibility) that gates the
uploads PersistentVolumeClaim, the backend container's volumeMount, and
the pod-spec volume. Operators who serve uploads from S3 (S3_BUCKET set)
can now set backend.uploads.persistence.enabled=false to drop the PVC
entirely, removing the ReadWriteOnce Multi-Attach barrier on the storage
side for replicas > 1.

Also makes the PVC accessModes configurable (default [ReadWriteOnce]) so
operators with a ReadWriteMany-capable StorageClass can share the
uploads volume across replicas without object storage.

Documentation: values.yaml comments and the SELF_HOSTING.md resource
list are updated to describe the new toggle.

Refs: https://github.com/multica-ai/multica/issues/3646

Co-authored-by: multica-agent <[email protected]>
2026-06-02 17:21:26 +08:00
90ddfb04e2 feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777) (#3441)
* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-05-28 16:42:08 +08:00
202200bc62 feat: publish helm chart to ghcr (#3415)
Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-05-28 15:04:05 +08:00
3df26ddd28 feat(self-host): add Helm chart for Kubernetes deployment (#2377)
* Include k8s deployment instructions

* Use helm for deployment

* docs(self-host): add Helm / Kubernetes deployment to quickstart (en + zh)

* fix(helm): gate backend ExternalName alias behind a value

The unprefixed Service/backend in the chart is load-bearing, but as
written it limits the chart to one release per namespace and fails
helm install whenever a Service/backend already exists in the
namespace (without --take-ownership).

Gate the alias behind frontend.compatibility.backendAlias (default
true, so existing installs are unchanged). Operators running a web
image with a patched REMOTE_API_URL can set it to false to drop the
Service entirely. Document the one-release-per-namespace constraint
and the opt-out in values.yaml and the SELF_HOSTING.md Kubernetes
section.

Addresses review item #1 on PR #2377.

* fix(helm): add backend startupProbe so cold installs survive migrations

The entrypoint runs `./migrate up` before serving traffic. On a cold
cluster (Postgres still coming up) this can take minutes, during which
the livenessProbe (initialDelaySeconds 30 / periodSeconds 30) trips and
restarts the pod 1-2 times.

Add a startupProbe on /healthz (failureThreshold 30, periodSeconds 10,
~5 min budget). Kubernetes disables liveness/readiness until it passes,
so migrations finish without the pod being killed, and the aggressive
livenessProbe is untouched for steady-state. Update the SELF_HOSTING.md
install step, which no longer expects 1-2 restarts.

Addresses review item #2 on PR #2377.

* fix(helm): roll backend pods on config/secret change via checksum annotations

envFrom does not watch the referenced ConfigMap/Secret, and helm
upgrade alone does not change the pod template hash, so editing
values.yaml + `helm upgrade` left the old backend pods running stale
config.

Add checksum/config (hash of the rendered configmap.yaml) and
checksum/secret (hash of the live existingSecret via lookup, since it
is created out-of-band and has no chart template) to the backend pod
template. Config edits now actually re-roll the backend on upgrade,
and Secret rotations do too. lookup is empty under
`helm template`/`--dry-run`; that placeholder is harmless and
documented inline.

Addresses review item #3 on PR #2377.

* docs(self-host): sync quickstart with new startupProbe behavior

SELF_HOSTING.md was updated to reflect that the backend now stays
Running but not Ready while Postgres comes up (startupProbe absorbs
it, so no restart), but the EN/ZH quickstart docs still described the
pre-startupProbe behavior of "may restart 1-2 times". Bring them in
line.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan Jiang <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-05-25 12:53:01 +08:00