* fix(adapters): stop SSRF and arbitrary local file reads in media handling
The platform adapters fetch URLs and open paths that come from inbound
messages, so both are attacker-influenced.
New guard module pkg/utils/outbound_url.py:
- assert_safe_outbound_url: checks the scheme, rejects literal private /
loopback / link-local / reserved / multicast addresses, and resolves the
hostname so a public-looking name pointing at an internal address is refused
too. Optional allowed_domains confines a fetch to a known prefix.
- assert_safe_local_media_path: resolves a caller-supplied path and requires it
to stay inside the media roots (data dir + temp dir), so symlinks and .. cannot
escape. Directories are refused.
Wired in:
- DingTalk send_message: session_webhook is taken from the inbound event and was
POSTed verbatim, so a forged event could redirect reply content (including LLM
output) to any host. Now restricted to dingtalk.com.
- DingTalk send_attachment: component.url was fetched with no destination check;
now goes through the SSRF guard, which also covers cloud metadata.
- DingTalk upload_image_media: caller-supplied file_path was opened directly; now
confined to the media roots.
- Slack attachment_bytes: component.path was opened directly and uploaded to the
channel (arbitrary file exfiltration); now confined to the media roots.
- Slack download_media: redirects were followed with no destination check. Every
hop is now validated, the hop count is bounded, and a redirect off the Slack
domain drops the bearer token.
Tests: 25 new cases for the guard (internal address forms, scheme rejection,
suffix impostor, traversal, symlink escape, directory). The Slack credential test
now stubs DNS because the guard resolves before fetching, and asserts
allow_redirects is False for every case since redirects are handled manually.
Co-authored-by: multica-agent <[email protected]>
* fix(web): hide passkey UI unless the core reports it is supported
The core refuses every passkey route on a cloud deployment (403
edition_unsupported) and reports passkey_login_enabled/passkey_supported as
false. The WebUI still rendered the controls:
- Login page read 'passkey_login_enabled !== false || passkey_supported', so an
absent flag counted as enabled. It now requires an explicit true.
- The account settings panel always rendered the passkey section and called
GET /passkeys on open, which 403s on cloud. It now reads the capabilities
first, skips the list call when unsupported, and does not render the section.
Also drop the client-supplied origin from both passkey options requests. The
server resolves the WebAuthn RP ID and expected origin from api.webui_url /
api.webhook_prefix, so sending window.location.origin was dead weight that would
invite a future handler to trust it again.
Verified: tsc --noEmit clean, eslint clean (only two pre-existing hook-dep
warnings), pnpm build succeeds.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
The platform adapters fetch URLs and open paths that come from inbound
messages, so both are attacker-influenced.
New guard module pkg/utils/outbound_url.py:
- assert_safe_outbound_url: checks the scheme, rejects literal private /
loopback / link-local / reserved / multicast addresses, and resolves the
hostname so a public-looking name pointing at an internal address is refused
too. Optional allowed_domains confines a fetch to a known prefix.
- assert_safe_local_media_path: resolves a caller-supplied path and requires it
to stay inside the media roots (data dir + temp dir), so symlinks and .. cannot
escape. Directories are refused.
Wired in:
- DingTalk send_message: session_webhook is taken from the inbound event and was
POSTed verbatim, so a forged event could redirect reply content (including LLM
output) to any host. Now restricted to dingtalk.com.
- DingTalk send_attachment: component.url was fetched with no destination check;
now goes through the SSRF guard, which also covers cloud metadata.
- DingTalk upload_image_media: caller-supplied file_path was opened directly; now
confined to the media roots.
- Slack attachment_bytes: component.path was opened directly and uploaded to the
channel (arbitrary file exfiltration); now confined to the media roots.
- Slack download_media: redirects were followed with no destination check. Every
hop is now validated, the hop count is bounded, and a redirect off the Slack
domain drops the bearer token.
Tests: 25 new cases for the guard (internal address forms, scheme rejection,
suffix impostor, traversal, symlink escape, directory). The Slack credential test
now stubs DNS because the guard resolves before fetching, and asserts
allow_redirects is False for every case since redirects are handled manually.
Co-authored-by: multica-agent <[email protected]>
* LANG-145: bind WebAuthn to server config and make passkeys OSS-only
Two problems in the passkey implementation:
1. The RP ID and expected origin came from the request: a JSON 'origin' field,
then the Origin/Referer headers, then the request URL. WebAuthn's
anti-phishing guarantee depends on values the server chose; trusting the
request lets a sibling host under the same registrable domain obtain a valid
assertion for a victim's credential. Resolution now follows the same rule the
OAuth and invitation links already use: api.webui_url, then
api.webhook_prefix, then the configured port on loopback.
2. Passkeys are now an OSS-only feature. Cloud deployments authenticate through
their own account service, so every passkey route (register options/verify,
auth options/verify, list, rename, delete) refuses with
edition_unsupported, and /account-info advertises neither passkey_login_enabled
nor passkey_supported. Both cloud flavours are covered: system.edition ==
'cloud' and deployment.mode == 'cloud'.
Tests:
- New unit suite for the edition matrix and for origin resolution (including
that client-supplied origin/Origin/Referer are ignored).
- The three 'reverse proxy scenario' tests asserted the old, unsafe behaviour
(server trusting the request). They now assert the server-config contract.
- test_smoke cloud capabilities expectation updated to passkey_* = false.
Co-authored-by: multica-agent <[email protected]>
* style(api): drop the typing import left unused by the WebAuthn origin change
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
Boot continues for minutes after the first directory snapshot is committed:
platform ports reconnect, knowledge bases and pipelines reload, and a cold
shared Runtime restore may still be running. The refresh loop only started at
the end of that stage, so any boot slower than the projection staleness window
failed closed while restoring knowledge bases, and Core then exited during
startup - every retry hit the same window.
Start the single refresh task as soon as the first snapshot is committed and
keep the later start as a guarded fallback so the loop is never created twice.
Observed on the production Cloud instance: a boot that took ~120s to reach
knowledge base restore raised 'Cloud directory projection is stale' and the
process exited, while the same image had booted earlier with a faster start.
Co-authored-by: multica-agent <[email protected]>
Cloud deployments need a way to tell readers what is happening to the
instance they are using (maintenance windows, service notices) without
shipping a release per notice.
Add a cloud.banner config block (switch, tone, dismissal, link targets and
per-language copy) and expose the resolved banner on the unauthenticated
/api/v1/system/info payload so the WebUI can render it before login. The
banner resolves to null on OSS deployments, so a self-hosted copy of
config.yaml never shows managed-service copy. Copy is plain text; only
absolute http(s) link targets are rendered.
The WebUI renders it once from the root layout, offsets the fixed app shell
and the wizard through --cloud-banner-height, and keeps a dismissal bound to
the notice it dismissed so new copy reappears.
Co-authored-by: multica-agent <[email protected]>
- creditsLabel: 每月赠送积分 → 模型积分 (zh), Monthly credits → Model credits (en), 毎月のクレジット → モデルクレジット (ja)
- creditsHelp: append note that own model providers remain usable after credits run out, all locales
- WorkspaceSwitcher trigger: cap workspace name to 6rem, remove justify-start
- HomeSidebar: add ArrowUp upgrade icon button next to workspace switcher (cloud_projection only), opens CloudPlansDialog lazy-loaded
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Set no-referrer on message thumbnails and previews to prevent
QQ image requests from returning 400 and triggering ORB.
Add browser coverage for image loading and request headers.
Apply group response rules before busy-session steering. Initialize usable private-message and group-mention routes for new bots, including Wizard-created pipelines, and preserve member-configured rules across saves and retries.
Fix empty pipeline configuration, refresh the processor sidebar after automatic creation, and show creation feedback. Align routing regressions and use Node 24 for the browser CI job.
Correct the task's erroneous agent attribution while preserving every code tree and the original human contribution.
Co-authored-by: multica-agent <[email protected]>
- Add covering indexes through migration 0040
- Preserve PostgreSQL FORCE RLS state when marking historical summaries
- Add query-plan and migration regression tests to CI
Count and paginate lightweight records, then load payloads only for the
selected page to avoid materializing large historical messages.
Add regression coverage for oversized history, ingress deduplication,
stable pagination, and PostgreSQL tenant isolation.
- Maintain counts and display snapshots in source transactions with replay-safe deduplication.
- Read session lists exclusively from summaries and remove execution-only filters.
- Automatically initialize historical summaries in bounded, resumable batches during OSS startup.
- Add behavioral contracts covering accounting, retention, query cost and PostgreSQL concurrency.
Verified with the required session_summary suite and real container upgrade startup.
* fix(tools): require python3 for Box native file scripts
Unversioned `python` is still Python 2 on some host-rootfs nsjail
mounts. Falling back to it made read/write/edit scripts fail with
SyntaxError on f-strings (#2603).
Resolve only `python3`, and return a clear JSON error when it is
missing instead of invoking Python 2.
Fixes#2603
* style(tools): format Box interpreter error command
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Li-john1021 <[email protected]>
Co-authored-by: Chin Rock <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Set client_info_tag='langbot' on the Valkey Search vector store's glide
client, so CLIENT SETINFO LIB-NAME reports GlidePy(langbot) and operators
can attribute Valkey usage to LangBot. client_name is unchanged.
client_info_tag was added in valkey-glide 2.5.2, so raise the floor from
>=2.4.1 to >=2.5.2 (pyproject, install hint, docs) and regenerate uv.lock.