629 Commits

Author SHA1 Message Date
Yi Deng
331c949b95
fix(gateway): require admin for global skills management endpoints (#3855)
* fix(gateway): require admin for global skills management endpoints

The skills router exposed its management endpoints with no authorization
check (only Depends(get_config)), while the MCP router guards the
equivalent global extensions_config mutations with require_admin_user
(added in the #3425 security hardening). Skills storage is global/shared
across users (no per-user path), lives in the same extensions_config.json
as MCP servers, and custom skill SKILL.md content is injected into every
user's agent system prompt. Any authenticated non-admin user could mutate
or read global skills that affect all tenants:

- POST /api/skills/install: install from an arbitrary thread_id, writing
  into the global custom skills tree
- PUT /api/skills/custom/{skill_name}: rewrite a global skill, injecting
  instructions into all users' agent prompts (cross-tenant prompt injection)
- DELETE /api/skills/custom/{skill_name} and rollback: tamper with global skills
- GET /api/skills/custom, GET .../{skill_name}, GET .../history: read raw
  global custom skill bodies/history
- PUT /api/skills/{skill_name} (enable toggle): writes the shared
  extensions_config.json and refreshes the system prompt for every tenant,
  so a non-admin could enable/disable any skill globally. There is no
  per-user skill state, so this is a global mutation, not a preference.

Add require_admin_user to every endpoint above, mirroring the MCP router.
The shared read path used internally by update/rollback was extracted into
a non-auth helper (_read_custom_skill_response) so internal reuse does not
double-check auth.

Only the read-only GET /api/skills and GET /api/skills/{skill_name} stay
open to normal users: they return just name/description/enabled and back
the user-facing settings UI.

Tests:
- New tests/test_skills_router_authz.py: a non-admin user gets 403 on every
  guarded endpoint (including the enable toggle); basic listing stays open;
  admins can still toggle.
- Update tests/test_skills_custom_router.py to authenticate as admin.
- pytest tests/ -k skill -> 350 passed, 1 skipped; ruff clean.

Signed-off-by: DengY11 <151997860+DengY11@users.noreply.github.com>

* fix(frontend): gate skill enable/install UI behind admin + handle 403

Follow-up to the backend change that made the global skills mutations
admin-only. Without a matching UI change, a non-admin user would hit a
silent 403 when toggling a skill in Settings -> Skills or clicking
"Install skill" on a .skill artifact.

- core/skills/api.ts: add SkillRequestError (status + isAdminRequired),
  throw it from loadSkills/enableSkill on non-ok responses and from
  installSkill on 403 (other install errors keep the soft-failure
  contract).
- core/skills/hooks.ts: useSkills no longer retries on SkillRequestError.
- skill-settings-page.tsx: show an "admin required" message on 403, and
  disable the enable toggle for non-admins (mirrors the MCP tools page).
- artifact-file-detail.tsx / artifact-file-list.tsx: only render the
  "Install skill" action for admins, and surface an admin-required toast
  if a 403 still occurs.
- i18n: add settings.skills.adminRequired / installAdminRequired (en + zh).

Auth/no-auth and static-website modes synthesize an admin user, so these
gates do not affect single-user/local deployments.

Verified locally: pnpm check (eslint + tsc) passes with no new errors,
pnpm build succeeds, and the dev server renders / and /login (200) with
no compile/runtime errors.

Signed-off-by: DengY11 <151997860+DengY11@users.noreply.github.com>

* style(frontend): format skills hook for prettier

Keep the admin-guard skills hook aligned with Prettier output so the frontend format check passes in CI.

---------

Signed-off-by: DengY11 <151997860+DengY11@users.noreply.github.com>
2026-07-02 11:17:23 +08:00
Ryker_Feng
ddb097a72f
feat(community): add Brave image search community tool (#3866)
* Add Brave image search community tool

* fix(community): length-cap Brave web_search queries

Apply _clean_query in web_search_tool so over-long queries are trimmed
to Brave's 400-char limit before the API call, matching image_search_tool
and avoiding HTTP 422 from the Brave Search API.

* fix(community): harden Brave image search SSRF guard and dimension mapping

Address PR review findings:
- Catch ValueError from urlparse so a malformed bracketed-IPv6 URL skips
  one item instead of crashing the whole image_search call
- Reject IPv6 literals embedding a non-global IPv4 (IPv4-mapped, 6to4,
  NAT64, IPv4-compatible), closing the loopback/private SSRF bypass
- Report width/height from the dict of the URL actually returned, so a
  surviving thumbnail no longer reports the dropped original's dimensions

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-02 11:00:49 +08:00
Huixin615
3748344303
fix(frontend): validate attachment limits before upload (#3900)
* fix(frontend): validate attachment limits before upload

* fix(frontend): avoid duplicate upload limit toasts
2026-07-02 10:32:53 +08:00
Nan Gao
4fcb4bc366
feat(subagents): persist and display subagent step history (#3779) (#3845)
* feat(subagents): persist and display subagent step history (#3779)

Capture both assistant turns and tool outputs during subagent execution,
stream them in task_running events, and persist them as subagent.* run
events so the subtask card's step timeline survives a reload.

Backend:
- step_events.py: pure layer (capture_step_message, build_subagent_step,
  subagent_run_event) shared by streaming and persistence
- executor.py: capture ToolMessage outputs, not just AIMessage turns
- worker.py: persist task_* custom events to RunEventStore (category
  "subagent" keeps them out of the thread feed; list_events backfills)

Frontend:
- core/tasks/steps.ts + api.ts: SubtaskStep model, messageToStep,
  eventsToSteps, mergeSteps, fetchSubtaskSteps
- subtask card accumulates live steps and backfills on expand
- carry run_id onto history content messages for the events endpoint

* fix(subagents): show AI turns in subtask card + paginate step backfill (#3779)

Two follow-ups to the subagent step-history feature:

Problem 1 — reload backfill could silently truncate the step timeline because
list_events capped at 500 events (seq-ASC) across the whole run. Add task_id
filtering + an after_seq forward cursor to list_events (all three stores +
abstract base + the /events route), and make fetchSubtaskSteps page through one
task's subagent.step events until a short page. No schema migration: the DB
filter rides the existing run-scoped index via event_metadata["task_id"].

Problem 2 — the card only rendered tool steps, so persisted AI turns were never
shown. Replace toolStepsForDisplay with stepsForDisplay: interleave AI reasoning
turns (with text) and tool steps by message_index, drop blank-text AI turns, and
drop the trailing final-answer AI turn when completed (already shown as result).
Card renders AI steps as muted clamped markdown with a sparkles icon.

Tests: store task_id/after_seq filtering + pagination across memory/db/jsonl,
the /events route forwarding, stepsForDisplay rules, and fetchSubtaskSteps
pagination. Docs updated in both AGENTS.md.

* make format

* fix(subagents): capture full multi-tool step tail, batch step persistence, cap tool-call args (#3779)

Address PR review findings on the subagent step-history feature:

1. executor.py streamed on stream_mode="values" and captured only
   messages[-1] per chunk, so a multi-tool-call turn (ToolNode appends
   one ToolMessage per call in a single super-step) lost all but the last
   tool output in both the live task_running stream and the persisted
   history. Replace with capture_new_step_messages, which walks the
   newly-appended tail (and still re-checks the trailing message on
   no-growth chunks so id-less in-place replacements survive).

2. worker.py persisted each step with the store's low-frequency put()
   (a per-thread advisory lock per call); a deep subagent (max_turns=150)
   emits hundreds of steps on the hot stream loop. Replace with
   _SubagentEventBuffer, which batches via put_batch (flush on terminal
   subagent.end, at FLUSH_THRESHOLD, and in the worker finally).

3. build_subagent_step capped only text; tool_calls[].args were copied
   verbatim, so a large write_file/bash payload produced an unbounded
   subagent.step row. Cap each call's serialized args at
   SUBAGENT_STEP_MAX_CHARS, flagged args_truncated.

Tests updated/added for all three; AGENTS.md refreshed.

* fix(subagents): merge backfill into latest subtask state; reuse message_content_to_text (#3779)

Address the remaining two PR review findings:

4. subtask-card's fetchSubtaskSteps().then(updateSubtask) closed over a
   stale tasks snapshot: a late-resolving backfill wrote setTasks({...stale}),
   clobbering SSE steps/status and sibling subtasks that arrived during the
   fetch. useUpdateSubtask now reads/writes through a tasksRef mirroring the
   latest state (ref-to-latest), and the pure per-subtask transition is
   extracted to core/tasks/subtask-update.ts::computeNextSubtask (unit-tested).

5. step_events._content_to_text duplicated deerflow.utils.messages.
   message_content_to_text; call the shared helper instead (guarding None
   content with 'or ""' so a tool-call-only turn still renders as "").

Tests added for computeNextSubtask and the None-content case; AGENTS.md docs updated.
2026-07-02 07:43:09 +08:00
xiawiie
2e15e3fe0d
fix: generate fallback title for interrupted first-turn runs (#3874)
* fix: generate title for interrupted first turn

* test(title): cover partial-exchange + dict-form messages

Harden the interrupted-run fallback path added in 19fc34fd:

- TitleMiddleware._should_generate_title now accepts a lone first-turn
  user message when allow_partial_exchange=True, so the worker can still
  derive a title if cancellation lands before any AI chunk is checkpointed.
- runtime/runs/worker._ensure_interrupted_title computes the next
  checkpoint step defensively (treat missing/non-int step as 0) and
  renames a shadowed ckpt_config local for readability.
- Add four unit tests in tests/test_title_middleware_core_logic.py:
  partial-exchange allows user-only, partial-exchange still respects an
  existing title, dict-form messages are recognized, and the sync
  fallback path derives a title from dict-form messages — matching what
  channel_values stores in the checkpoint.

Refs #3859.

* fix: persist interrupted-title via channel_versions bump

Address PR #3874 review feedback: ``_ensure_interrupted_title`` previously
called ``aput(..., new_versions={})``. LangGraph's DB-backed savers
(``PostgresSaver`` and the v4 ``SqliteSaver`` blob layout) strip inline
``channel_values`` from ``put`` and only persist blobs for channels named
in ``new_versions`` — so the fallback ``title`` channel was dropped on
read-back and ``threads_meta.display_name`` stayed ``"Untitled"`` after
refresh on those backends. The original in-memory e2e passed because
``InMemorySaver`` keeps the inline snapshot verbatim.

Fix mirrors ``_rollback_to_pre_run_checkpoint`` in the same file: bump
``channel_versions["title"]`` (via ``checkpointer.get_next_version`` when
available, else int/string fallbacks), persist the new version on the
checkpoint, and declare it in ``new_versions`` so the DB savers actually
write the blob.

Regression coverage in ``tests/test_run_worker_rollback.py``:

- ``test_ensure_interrupted_title_bumps_channel_version_and_declares_it_in_new_versions``
  — exact ``aput`` invariants: ``new_versions == {"title": 1}``, the
  written checkpoint's ``channel_versions["title"]`` is bumped, and the
  pre-existing ``messages`` version is preserved.
- ``test_ensure_interrupted_title_bumps_existing_string_version`` —
  string-shaped prior version (some savers use UUID-style versions);
  bumped value must differ from the prior, no overwrite-in-place.
- ``test_ensure_interrupted_title_skips_when_title_already_set`` — title
  short-circuit; no extra ``aput``.
- ``test_ensure_interrupted_title_returns_none_when_no_checkpoint`` —
  no checkpoint yet; returns ``None`` without writing.
- ``test_ensure_interrupted_title_round_trip_with_real_sqlite_checkpointer``
  — full round-trip against a real ``AsyncSqliteSaver`` on a disk-backed
  DB, then closes and re-opens the saver to simulate a fresh
  connection. The fallback title must still be present on the second
  ``aget_tuple``. This is the exact scenario the review flagged.

Validated locally with the full backend suite: 5195 passed, 18 skipped.

Refs #3859. Addresses review on #3874.

* test(worker, title): harden interrupted-title fallback for every saver

Defensive coverage on top of the channel_versions fix (commit 05253957),
addressing edge cases surfaced during a second-pass review of #3874.

Worker:
- Extract version bump into ``_bump_channel_version(checkpointer, current)``
  with explicit fallbacks for int / float / numeric-string / UUID-shaped
  string / None / bool, AND a wrap-around defense when the saver's
  ``get_next_version`` raises or returns an unchanged value. The
  invariant is: returned version MUST differ from the prior. Without
  this, a saver bug (or a custom backend) could leave
  ``new_versions={"title": v}`` no-op on DB savers — the very class of
  bug the original review pointed out.

Title middleware:
- Coerce ``state.get("messages")`` from ``None`` to ``[]`` on both
  ``_should_generate_title`` and ``_build_title_prompt``. A
  partially-initialized checkpoint can carry ``messages=None`` on the
  channel_values channel (the worker reads raw channel_values, not
  BaseMessages), and the default kwarg only protects against a missing
  key. Repro: ``TypeError: 'NoneType' object is not iterable`` from
  the next() generator — confirmed by reverting the fix and watching
  ``test_*_handles_none_messages_channel`` go red.

Tests (TDD-verified red→green for the new asserts):
- ``test_run_worker_rollback.py``:
  * ``_bump_channel_version`` — 8 tests covering every version type
    (int, float, numeric string, UUID-style string, None, bool) and
    every saver-side fault mode (no ``get_next_version`` / raising /
    stuck on identity).
  * ``test_ensure_interrupted_title_*`` — 5 additional helper
    boundary tests: title.enabled=false short-circuit; empty
    messages list; messages=None; aput-error propagation (helper
    contract: caller swallows, not the helper); idempotency on a
    real InMemorySaver across two invocations.
  * ``test_ensure_interrupted_title_preserves_non_title_channel_versions``
    — pins that ``new_versions`` only contains ``"title"`` and that
    other channels' versions are untouched (regression anchor for a
    sloppier draft that bumped every channel).
  * ``test_worker_finally_block_swallows_helper_exceptions`` — pins
    the integration contract: even if the helper raises, the worker's
    threads_meta status sync still runs and ``publish_end`` is still
    awaited so the SSE stream closes cleanly.

- ``test_title_middleware_core_logic.py``:
  * 4 additional tests: ``messages=None`` on both
    ``_should_generate_title`` and ``_build_title_prompt``; the
    ``role: user`` / ``role: assistant`` (OpenAI-style) dict
    normalization; partial-exchange path with a dict-form message.

Verification:
- ``PYTHONPATH=. uv run pytest tests/ -x --ignore=tests/blocking_io -q``
  → 5215 passed, 18 skipped.
- ``ruff check`` + ``ruff format --check`` clean on every touched file.
- Red/green TDD verification: temporarily reverted the
  ``new_versions={}`` fix → 4 new tests went red as expected; restored
  and the suite is green again. Same red/green dance for the
  ``messages=None`` coercion.

Refs #3859. Addresses second-pass review on #3874.

* fix(title): ignore dict context reminders in fallback

* fix(worker): link interrupted-title checkpoint to its parent

The title-bump checkpoint written by ``_ensure_interrupted_title`` was
landing without a ``parent_checkpoint_id`` — a real orphan in the
LangGraph history graph. Reproduction (disk-backed AsyncSqliteSaver):

  [seed] checkpoint_id = 1f173dbc...
  [helper] wrote title = "Why is the sky blue?"
  [issue 1] new checkpoint = 1f173dbc..., parent = None
  [issue 1] is new checkpoint orphaned? True

Root cause: ``_ensure_interrupted_title`` built ``write_config`` as
``{"thread_id": ..., "checkpoint_ns": ...}`` only. ``BaseCheckpointSaver``
implementations read ``configurable.checkpoint_id`` from that config as
the *parent* id when inserting (see ``langgraph/checkpoint/sqlite/aio.py``
``aput``: ``config["configurable"].get("checkpoint_id")`` becomes the
``parent_checkpoint_id`` column). With no value, the saver writes NULL —
the new checkpoint is a tree root.

Consequences:
- Any future LangGraph ``runs.resume_from`` / time-travel feature has no
  backward edge to walk past the title-bump.
- History-visualization UIs built on ``alist()`` render the title-bump as
  a sibling of the prior checkpoint, not its descendant.

Fix: read ``checkpoint_id`` off the tuple's own config and thread it into
``write_config["configurable"]["checkpoint_id"]`` before calling ``aput``,
the same pattern every middleware-driven write uses.

Three new regression tests against real ``AsyncSqliteSaver`` (disk-backed,
fresh connections so we exercise the on-disk read path):

- ``test_ensure_interrupted_title_links_new_checkpoint_to_its_parent`` —
  asserts ``latest.parent_config["configurable"]["checkpoint_id"]`` equals
  the seeded checkpoint id. TDD red-green verified: reverting the fix
  flips this test red with ``AssertionError: title-bump checkpoint must
  have a parent_config``.
- ``test_ensure_interrupted_title_appears_in_history_with_audit_marker``
  — pins the audit contract: the title-bump entry in ``alist()`` carries
  ``metadata.source == "update"`` and ``metadata.writes`` contains
  ``runtime_interrupt_title``. This is a deliberate design choice — we
  do NOT hide the entry from history (audit trail belongs in the saver),
  but its source and writes marker MUST be unambiguous so UIs/tools can
  identify it.
- ``test_ensure_interrupted_title_survives_immediate_next_turn`` —
  cancel → immediate user follow-up scenario. Simulates the agent's next
  turn appending a (user, ai) pair without touching the title channel,
  then opens a fresh saver and verifies the title is still present after
  the next-turn checkpoint write. Pins the channel-version-blob
  invariant established by commit 05253957 — without the
  ``new_versions={"title": v}`` declaration there, the title blob would
  vanish from the DB and this test would read back ``None``.

Verification:
- ``PYTHONPATH=. uv run pytest tests/ -x --ignore=tests/blocking_io -q``
  → 5222 passed, 15 skipped.
- ``ruff check`` + ``ruff format --check`` clean on every touched file.
- Reproduction script confirms ``parent_checkpoint_id`` is now non-null
  and the next-turn read-back preserves the fallback title.

Refs #3859.

* Revert "fix(worker): link interrupted-title checkpoint to its parent"

This reverts commit c763ed9334781db1acdce0f5f33d663d8d5f80ff.

* test: trim over-engineered test coverage

Reduce review surface area on PR #3874 by dropping defensive tests that
don't pin a real invariant. After self-review:

- ``_bump_channel_version``: 8 tests → 2 (happy path + saver-error
  fallback). Dropped float / bool / numeric-string / UUID-string /
  missing-get-next-version / stuck-get-next-version branches — those
  are speculative scaffolding for savers we don't ship.
- ``_ensure_interrupted_title``: dropped ``returns_none_when_title_disabled``,
  ``returns_none_with_no_user_message``, ``returns_none_when_no_checkpoint``
  — boundary guards already exercised by the e2e test and the
  ``handles_none_messages_channel`` regression anchor.

Net: -107 lines of test code. Remaining coverage still pins every
red-green-verified invariant (channel_versions bump, string-version
bump, idempotency, sqlite round-trip, non-title channel preservation,
aput-error contract, worker finally swallowing, partial-exchange).

Verification: 5209 passed, 15 skipped.

* fix: harden interrupted title finalization

* fix: serialize interrupted title finalization

* fix: preserve interrupt semantics during title finalization

* fix: preserve delayed interrupted title recovery

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-02 00:13:01 +08:00
Ryker_Feng
a8f950feb6
feat(community): add Browserless web_capture screenshot tool (#3881)
* feat(community): add Browserless web_capture screenshot tool

Add a web_capture tool that renders a page via Browserless /screenshot and
presents it through the artifact system, alongside the existing Browserless
web_fetch provider.

Hardening:
- SSRF guard: reject URLs resolving to private/loopback/link-local (incl. the
  169.254.169.254 cloud-metadata endpoint)/reserved/multicast/unspecified
  addresses; opt out via allow_private_addresses for internal targets.
- Surface a warning when Browserless renders a target page that itself
  responded with a non-2xx/3xx status (X-Response-Code), so an error/anti-bot
  page is not mistaken for valid visual evidence.
- Dedupe colliding output filenames instead of silently overwriting prior
  captures.

Docs: comment out token: $BROWSERLESS_TOKEN in tool examples (an unset $VAR
fails AppConfig startup) and document allow_private_addresses.

* fix(community): format web_capture guard + document local Browserless startup

Address PR #3881 review: fix the lint-backend failure (ruff format on
browserless/tools.py) and add local Browserless startup instructions to
CONFIGURATION.md so reviewers can run the service to try web_fetch/web_capture.
2026-07-01 23:41:58 +08:00
Xinmin Zeng
b431053ef0
fix(frontend): stop rendering reasoning twice for reasoning+answer messages (#3870)
A reasoning+content AI message with no tool calls was grouped into both an
`assistant:processing` group (the ChainOfThought panel above the bubble) and an
`assistant` group (the bubble's own Reasoning collapsible), so its reasoning
rendered twice. Such a message now becomes only the assistant bubble; the
ChainOfThought panel keeps handling intermediate reasoning and tool steps.

Closes #3868
2026-07-01 23:35:48 +08:00
AochenShen99
442248dd06
feat: preserve durable context across summarization (#3887)
* feat: preserve durable context across summarization

* fix: harden durable context review gaps

* style: format delegation ledger live test

* chore: remove stale delegation ledger prefix

* fix: address durable context review feedback
2026-07-01 22:49:17 +08:00
chengjoey
e5424cbab9
feat(sandbox): add E2B sandbox provider (#3883)
Adds ``deerflow.community.e2b_sandbox.E2BSandboxProvider`` with parity
to AioSandboxProvider: metadata-keyed per-thread persistence, server-
side idle timeout, warm-pool reclaim with liveness checks, /mnt/user-data
bootstrap symlinks, dead-sandbox auto-rebuild, and release-time mirror
of agent outputs back to the host artifact directory.

Signed-off-by: joey <zchengjoey@gmail.com>
2026-07-01 11:15:27 +08:00
qin-chenghan
68c968f6f3
fix(frontend): keep orphan tool messages visible (#3880)
* fix(frontend): keep orphan tool messages visible

LangGraph `messages-tuple` stream mode can emit tool-result events
out of order or replay them from subagent state (e.g. the bash subagent
under LocalSandboxProvider with allow_host_bash: true). When that
happens, the tool message arrives after a terminal assistant/human
group, so getMessageGroups' lastOpenGroup() returns null.

The previous behaviour was console.error + drop, which silently hid
the tool result from the UI - the user could not see the tool output
or tool-call records even though the agent executed the action
correctly (backend + Langfuse trace were both fine).

Fallback now attaches the orphan tool message to the most recent group
so the UI shows it. Adds a unit test that covers the orphan-tool path
and a duplicate-stream regression test.

* test(frontend): make orphan-tool replay test actually reach the fallback

The previous fixture was `human → ai(tool_calls) → tool → tool(replay)`
with no terminal group in between, so both tool messages hit the
unchanged happy path (`open.messages.push(...)`). Neither message was
an orphan, the new `else if (groups.length > 0)` fallback was never
exercised, and the `>= 1` length assertion was satisfied trivially by
t-1a alone.

Interleave a terminal assistant message between the original tool
result and the replayed one, so t-1b arrives when lastOpenGroup()
returns null. Now the strict assertion that t-1b is reachable can only
pass via the new fallback branch.

Review feedback from @willem-bd on PR #3880.

* fix(frontend): satisfy noUncheckedIndexedAccess in orphan-tool fallback

The fallback branch pushed into `groups[groups.length - 1].messages`
directly, which trips `noUncheckedIndexedAccess` under the project's
strict TS config and breaks lint-frontend, e2e-tests, and the
full-stack render CI layer.

Take `groups[groups.length - 1]` into a local `lastGroup` and check
for `undefined` explicitly. The `else if (groups.length > 0)` guard
becomes redundant once `lastGroup` is checked, so the branches are
folded into the outer `else` to keep the structure flat. Behavior is
unchanged: orphan tools still attach to the most recent group, and
the empty-groups diagnostic still logs at ERROR level.

Review feedback from @willem-bd on PR #3880.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-01 11:12:20 +08:00
xiawiie
2453718acd
fix(title): avoid default LLM call before stream end (#3885)
* fix(title): avoid default LLM call before stream end

* fix(title): keep default fallback local

* fix(title): harden fallback and replay e2e

* docs(title): align fallback title behavior
2026-06-30 18:58:02 +08:00
Huixin615
b3c312b795
fix(frontend): retain presented artifacts in header dropdown (#3854)
* fix(frontend): retain presented artifacts in dropdown

* refactor(agents): trim file editing prompt context

* Revert "refactor(agents): trim file editing prompt context"

This reverts commit cad105265024335222e374692dde37424d11024b.
2026-06-28 23:30:40 +08:00
DanielWalnut
9654ba2c13
fix auth setup redirects (#3844) 2026-06-28 16:10:44 +08:00
Xinmin Zeng
22290c1616
fix(frontend): preserve messages across context summarization (#3825) (#3826)
After SummarizationMiddleware runs, the merged conversation view could drop
already-displayed messages (previous assistant output, current user input),
leaving a nearly-empty thread.

Root cause: the display merge combines `visibleHistory` (archived history, a
React `useState` in useThreadHistory) with `persistedMessages` (live thread,
the LangGraph SDK external store via useSyncExternalStore). On summarization
the backend removes every live message and onUpdateEvent re-archives them via
an async `appendMessages` setState. Those two state systems are scheduled
independently, so a render can observe the post-summary (shrunk) thread before
the archive setState commits — the rescued messages are then absent from BOTH
merge inputs and get dropped.

Fix: bridge the async gap with a synchronous `pendingArchivedMessagesRef`
buffer written the moment onUpdateEvent computes the moved messages and read by
the merge on every render, so correctness no longer depends on how the two
channels interleave. The buffer drains once history confirms absorption and
only injects messages missing from history (live copies stay authoritative,
order preserved). It is tagged with the thread it was captured from and the
merge overlays it only when that matches the viewed `threadId` (the same prop
visibleHistory is gated on), so it can never leak into another thread or the
new-chat screen — a read-only check, no render-phase ref mutation.

Extracts the moved-message derivation and the merge overlay into pure,
unit-tested helpers (computeSummarizationMovedMessages, resolvePreservedHistory,
pruneConfirmedArchivedMessages) with regression coverage for the full rescue
pipeline.
2026-06-28 11:48:56 +08:00
Huixin615
3e2f1bbe49
fix(frontend): show filename for presented artifacts missing from thread.values.artifacts (#3198)
Closes #3192

Root cause
----------
The artifact preview header is driven by a Radix Select whose <SelectValue>
renders the label of the <SelectItem> matching the current value. The option
list was built solely from `artifacts` in ArtifactsContext, which is only
synced from `thread.values.artifacts` (chat-box.tsx). Artifacts surfaced via
the message-layer `present_files` tool call are never written back to
`thread.values.artifacts`, so when such a file is selected its filepath has
no matching <SelectItem>. Radix then renders an empty trigger and the header
filename appears blank, even though the preview body loads correctly.

Fix
---
Compute `artifactOptions` as a defensive union: if the currently selected
filepath is missing from `artifacts`, prepend it so a matching <SelectItem>
always exists. This keeps the header label in sync with the active file
without changing context semantics or coupling the UI to a future
auto-discovery mechanism (see existing TODO in chat-box.tsx).

Tests
-----
Add an e2e case that mocks a thread whose only artifact is delivered via
`present_files` (thread.values.artifacts = []) and asserts both the header
title and preview content render. All 21 e2e tests pass.
2026-06-27 23:07:12 +08:00
Huixin615
7c8a17c650
fix(frontend): preserve artifacts during streaming updates (#3791) 2026-06-27 23:06:26 +08:00
Huixin615
b5cac5e721
fix: add heading anchors to markdown artifact previews (#3805)
* fix: add markdown heading anchors for artifacts

* fix: scope markdown anchors to artifact previews

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-27 22:59:47 +08:00
zgenu
69cf4f4d86
fix frontend notification permission refresh (#3768) 2026-06-25 23:32:29 +08:00
Huixin615
b7d2dcc67c
fix: block unresolved suggestion template placeholders (#3764)
* fix: block unresolved suggestion placeholders

* fix: satisfy suggestions config hook deps
2026-06-25 23:15:58 +08:00
DanielWalnut
ff7ecdbd37
docs: adopt AGENTS.md as source of truth (CLAUDE.md imports via @AGENTS.md) + refresh module guides (#3770)
* docs: add root-level CLAUDE.md to orient the monorepo

Adds a thin top-level CLAUDE.md that maps the monorepo and delegates depth
to backend/CLAUDE.md and frontend/CLAUDE.md, per issue #3761.

Includes the project overview + service topology (Nginx 2026, Gateway 8001,
Frontend 3000, optional Provisioner 8002), a top-level repository map, root
`make` vs. per-module command sections, "where to go next" links to the module
guides and primary root docs, and the repo-wide cross-cutting conventions
(documentation-update policy, TDD expectation, format before pushing).

No code or behavior changes; root points down, modules own depth.

Closes #3761

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: make AGENTS.md the source of truth, CLAUDE.md a thin @AGENTS.md importer

Adopt the AGENTS.md convention so the same agent guidance serves Claude Code,
Codex, and other tools. At each level (root, backend, frontend) the content
lives in AGENTS.md and CLAUDE.md just imports it via `@AGENTS.md`.

- root: move the monorepo orientation layer to AGENTS.md; CLAUDE.md -> @AGENTS.md.
  Fix an incorrect "TUI" reference (not present on main) and repoint the module
  links to the AGENTS.md files.
- backend: move the guide to AGENTS.md (was an AGENTS.md -> @CLAUDE.md pointer;
  direction is now flipped). Refresh stale content: rebuild the full middleware
  chain (~26 ordered steps incl. InputSanitization, ToolOutputBudget,
  DynamicContext, TokenBudget, SafetyFinishReason) from the actual build
  functions; drop the brittle "11 middleware components" count; expand the
  community-tools list to the real set.
- frontend: merge the practical Next.js guide with the existing AGENTS.md's
  unique sections (LangGraph diagram, tech-stack versions, interaction
  ownership, resources) into one AGENTS.md (CLAUDE.md -> @AGENTS.md). Fix the
  stale src/ layout (remove the no-longer-present server/ better-auth entry;
  add the now-active auth/agents/blog/... modules and routes) and drop a bogus
  interaction-ownership bullet referencing files that don't exist.

Docs only; no code or behavior changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:15:07 +08:00
Boooobby
4a8f94eb28
fix(frontend): improve chat math rendering (#3557)
* fix(frontend): improve chat math rendering

* fix(frontend): refine math rendering stability

* fix(frontend): address review feedback on math preprocessing

- Fix escaped-backslash blind spot: consume \\\\ as a unit so \\( and
  \\[ are not mis-interpreted as math delimiters when the backslash
  itself is escaped.
- Thread inInlineCode state across lines so multi-line backtick code
  spans are protected from delimiter conversion.
- Remove double math normalization: preprocessStreamdownMarkdown now
  only handles Mermaid; math normalization lives solely in
  ClipboardSafeStreamdown, preventing non-idempotent double passes.
- Wire parseIncompleteMarkdown to isLoading in MarkdownContent so
  Streamdown's streaming-incomplete-math handling is actually active
  during streaming.

* fix(frontend): address streamdown math review issues

* refactor(frontend): move streamdown preprocessing out of ai elements
2026-06-24 17:17:36 +08:00
zgenu
11415875c4
fix(frontend): make recent chat rows fully clickable (#3733)
* fix(frontend): make recent chat rows fully clickable

* Address recent chat row review comments
2026-06-24 09:48:59 +08:00
zgenu
14d9bb87a4
feat: add prompt history recall (#3718) 2026-06-23 08:18:14 +08:00
Huixin615
0ee35ca38f
fix: preserve locale in documentation links (#3717) 2026-06-23 07:53:53 +08:00
kapil971390
d050fba07f
fix(token-budget): acquire _lock in _apply, before_agent, and _drain_pending_warnings (#3714)
_lock was defined in __init__ and used only in reset(), leaving _apply(),
before_agent(), _clear_run_state(), and _drain_pending_warnings() unprotected.
Concurrent after_model callbacks for the same run_id could race on
usage_accum.input += diff_input, causing lost-update on token counters.

Also fix stray bare 'a' character in frontend/src/content/zh/introduction/index.mdx
that rendered as visible text on the documentation page.

Co-authored-by: kapilvus <kapilvus@gmail.com>
2026-06-22 20:44:07 +08:00
Jiahan Chen
6fb22bb311
test(frontend): migrate unit tests to rstest (#3703)
* test(frontend): migrate unit tests to rstest

* docs: updates AGENTS.md

* test(frontend): fix rstest lint formatting
2026-06-22 16:12:49 +08:00
Shatlyk
e418d72915
feat: add introduction documentation pages in English and Chinese and update application path references (#3690) 2026-06-22 08:24:44 +08:00
Zhipeng Zheng
c0ce759763
fix(security): inject system context as SystemMessage for role isolation (#3630) (#3661)
P0: DynamicContextMiddleware role isolation — framework data as SystemMessage,
memory as HumanMessage (OWASP LLM01), user input unchanged.

Includes E2E hash stabilization: frontend hide_from_ui filter fix in
suggest_agent, backend _canonical_messages() hide_from_ui skip, fixture
hash updates.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-21 20:56:42 +08:00
Zheng Feng
5654a082f5
feat(frontend): internationalize login page (en-US, zh-CN) (#3677)
The login page used hardcoded English strings. Add a `login` section to
the i18n Translations interface and both locale files, then wire the
login page to `useI18n()` so all titles, labels, placeholders, buttons,
SSO hints, and error messages resolve from the active locale.
2026-06-21 19:08:58 +08:00
jp0xz
a6dd2876f0
feat(community): add GroundRoute web search + fetch engine (#3675)
* feat(groundroute): add GroundRoute community web_search + web_fetch tools

GroundRoute is a meta search layer over six engines (Serper, Brave, Exa,
Tavily, Firecrawl, Perplexity) with price-based routing and failover. This
adds a self-contained community engine module (httpx only, no new required
deps) mirroring community/brave + community/tavily:
- web_search: POST /v1/search, normalize to {title,url,snippet,source_engine}.
- web_fetch: fetch a URL via mode=page.
- unit tests covering normalization, auth, clamping, and graceful errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(groundroute): register GroundRoute search + fetch in wizard and config

Add GroundRoute to the setup wizard provider lists (SEARCH_PROVIDERS +
WEB_FETCH_PROVIDERS) and as commented web_search + web_fetch examples in
config.example.yaml, mirroring tavily/serper/brave so SEARCH_API can select it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(groundroute): apply repo ruff format (line-length 240)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(groundroute): add GroundRoute to tools docs and config reference

Adds GroundRoute as a web_search and web_fetch option in the en + zh
tools.mdx pages (new tab alongside Tavily/Brave/Exa/etc.) and documents
GROUNDROUTE_API_KEY in CONFIGURATION.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(groundroute): define empty groundroute extra for clean install

The docs install line 'uv add deerflow-harness[groundroute]' (mirroring the
tavily/exa/firecrawl pattern) referenced an undefined extra, which uv accepts
but warns about. GroundRoute needs no extra packages (httpx is a core dep), so
declare an empty 'groundroute' extra in deerflow-harness optional-dependencies
so the documented command resolves without a warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(groundroute): per-tool api key + honor caller max_results (review)

Address maintainer review on PR #3675:
- _get_api_key(tool_name): web_fetch now reads the web_fetch config block's key
  instead of always web_search, so a flow that pairs GroundRoute fetch with a
  different search engine authenticates correctly. Mirrors serper/exa/firecrawl.
- web_search honors a caller-supplied max_results (sentinel default None),
  falling back to the configured value only when omitted, so the documented
  parameter is no longer silently discarded.
- warn-once is now keyed per tool. Tests cover both fixes (web_fetch key,
  agent max_results honored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-21 15:55:10 +08:00
Zheng Feng
ee8ad1bc67
feat(auth): add OIDC SSO support (#3506)
Add provider-agnostic OIDC authentication with Keycloak-compatible configuration, frontend SSO UI support
2026-06-21 15:47:53 +08:00
AnoobFeng
5ddf698895
fix(frontend): reset new chat on client-side navigation (#3673)
* fix(frontend): reset new chat on client-side navigation

Drive the chat reset effect with Next.js's reactive pathname instead of the render-time window.location-derived flag.

During App Router transitions, window.location may still point to the previous thread until commit, leaving chat state stale until another UI interaction triggers a render. Preserve the stale 'new' param guard so created thread UUIDs are not overwritten.

* test(frontend): add e2e to cover history-only new chat reset

* docs(frontend): clarify new chat pathname synchronization
2026-06-21 11:44:36 +08:00
Chetan Sharma
4572217038
feat: persist AI turn duration in backend and UI (#3663)
* feat: persist AI turn duration in backend and UI

* chore: restore uv.lock to match main

* refactor(frontend): use Math.floor instead of Math.ceil for reasoning timer
2026-06-21 09:35:45 +08:00
Huixin615
ca9428d0cd
feat: support regenerating latest answer (#3637)
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-20 18:59:42 +08:00
vantanco
90328d5651
Avoid blank previews for unsupported artifacts (#3644) 2026-06-20 00:15:11 +08:00
Chetan Sharma
df5d90fbb1
feat(frontend): implement (thought for x second) thinking duration indicator (#3627) 2026-06-19 23:44:10 +08:00
Willem Jiang
deb736b819
Update the project version for the next developement (#3659) 2026-06-19 23:12:00 +08:00
Willem Jiang
98127f5845
Prepare 2.0.0 release (#3603)
* bump the version of deer-flow to 2.0.0

* Added CHANGELOG to the release branch

* Update the changelogs files with the latest changes
2026-06-19 22:48:30 +08:00
Nguyen DN
654f5e1c66
fix(frontend): render full content for multi-part AI messages (#3649)
* fix(frontend): render full content for multi-part AI messages

    Gemini streams the first content block as a {type:text} object carrying
    the thinking signature, then emits continuation deltas as bare strings.
    The content extractors only kept {type:text} objects and dropped the
    string parts, truncating each AI message to its first block.

    Handle string elements in extractContentFromMessage, extractTextFromMessage,
    and textOfMessage so the full message renders.

    Fixes #1000

* test(frontend): cover multi-part AI message content extraction

Add a regression test for the #1000 truncation: an AI message whose
content is [{type:text, signature}, "bare string"] (Gemini's shape after
LangChain merge_content). Fails on main, passes with the extractor fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(frontend): cover textOfMessage multi-part content extraction

Add tests for the bare-string continuation case and the null-when-empty
contract, and document why textOfMessage joins flat ("") while the body
extractors join with "\n". Addresses review feedback on #3649.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 21:46:59 +08:00
Chetan Sharma
3e055d8f43
feat(suggest_agent): stop frontend from fetching when suggestions disabled (#3599)
* feat(suggest_agent): stop frontend from fetching when suggestions disabled

* wait for suggestions config to load before fetching

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-19 18:17:21 +08:00
AnoobFeng
a692576993
fix(frontend): resolve stale subagent running state after stop (#3639)
* fix(frontend): resolve stale subagent running state after stop

Subtask cards were recreated from task tool calls as in_progress whenever
the thread was loading. If a user stopped a run before the task tool returned
a ToolMessage, the card could remain running after reload, and could flip back
to running when a later user turn started streaming.

Derive pending subtask state from the current assistant turn instead of the
global thread loading flag. A task now stays in progress only while its own
turn is loading or while a matching ToolMessage exists for result parsing;
otherwise it is marked failed.

Add unit coverage for task ToolMessage matching and for the later-turn loading
regression.

* fix(frontend): refresh subagent card on terminal status transition

- notify React via a ref + post-render effect when a subtask flips to
  completed/failed, so SubtaskCard updates without relying on an
  unrelated MessageList re-render
- tighten the current-turn heuristic to `groupIndex === lastGroupIndex`
  (precomputed once) — fixes the rare case where an earlier subagent
  group in the same turn was kept in_progress, and drops the per-group
  slice+some scan to O(1)
- rename `getPendingSubtaskStatus` → `derivePendingSubtaskStatus`
- narrow `toolCall.id` at MessageList call sites; skip task tool calls
  without an id instead of `id!`
- add Playwright e2e covering reload-after-stop showing `Subtask failed`
2026-06-19 16:25:17 +08:00
Huixin615
86a4744941
fix(frontend): improve mobile workspace layout (#3646) 2026-06-19 11:14:49 +08:00
Nan Gao
68ba4198b8
fix(channels): make channel connect flow deterministic (#3582)
* fix(channels): make channel connect flow deterministic

* make format

* fix(channels): apply connect-code before allowed_users on telegram and wechat

The bind-bootstrap reorder shipped for slack/dingtalk only. Telegram and
WeChat still gate _check_user/allowed_users before connect-code dispatch, so
a newly allowlisted-but-unbound user is silently rejected when binding via the
browser deep-link / connect-code flow — the same deadlock the PR fixes.

- telegram: consume the /start deep-link token before the allowed_users gate.
- wechat: handle the /connect code before the allowed_users gate, and defer
  inbound file extraction + context-token tracking past the gate so blocked
  senders no longer trigger CDN downloads or token bookkeeping.

Adds regression tests for both adapters mirroring the slack/dingtalk coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(channels): enforce single-active-owner invariant at the DB layer

_revoke_other_active_owners did a SELECT-then-UPDATE in app code with no row
lock or constraint covering active rows. Under READ COMMITTED, two concurrent
connect-code consumes for the same (provider, external_account_id, workspace_id)
from different owners could each observe "no other active owner" and both commit
a connected row, leaving find_connection_by_external_identity nondeterministic.

- Add a partial unique index on (provider, external_account_id, workspace_id)
  WHERE status != 'revoked' (portable to SQLite >= 3.8.0 and PostgreSQL) so the
  database guarantees at most one non-revoked row per external identity.
- Reorder upsert_connection to revoke other owners' active rows before the new
  connected row is flushed (so the index is satisfied at commit), wrapped in a
  bounded rollback-and-retry loop. A losing concurrent writer now retries
  against the now-visible state instead of committing a duplicate.

Adds DB-constraint, revoked-slot-reuse, and concurrent-upsert regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(channels): harden connect-status polling primitive

pollChannelConnectionUntilResolved was a free-floating recursive setTimeout
started from onSuccess with no cancellation, no per-provider dedup, a redundant
second endpoint per tick, and an unbounded loop on a non-finite expires_in.

- Extract a framework-agnostic, cancellable poller (connect-poll.ts) that polls
  only listChannelConnections() and invalidates the providers query once when the
  bind resolves, instead of fetching both endpoints every tick.
- Guard expires_in with a finite check + default window so undefined/NaN can no
  longer produce a poll loop that runs until the page closes.
- Track one active poll handle per provider in useConnectChannelProvider via a
  ref Map: a new connect cancels the prior poll for that provider, and a useEffect
  cleanup cancels all polls on unmount.

Adds unit tests for resolve-and-stop, cancellation, and non-finite-expiry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(channels): stop leaking blocked-sender content in DingTalk INFO log; document bind semantics

Moving the allowed_users gate past _extract_text meant the parsed-message INFO
log (text=%r, first 100 chars) fired for senders that allowed_users would have
rejected, defeating the filter's noise/privacy role. Move that log to after the
allowed_users gate so blocked senders' message text never reaches INFO logs.

Also document the two operator-relevant semantic changes in backend/CLAUDE.md:
connect-code dispatch runs before allowed_users (so allowed_users is no longer a
bind-time defense; the model relies on code confidentiality + 600s TTL + one-time
consumption), and the single-active-owner-per-external-identity transfer semantics
now backed by the partial unique index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(channels): note connect-code-vs-allowlist and ownership transfer in operator guide

Mirror the backend/CLAUDE.md notes in the operator-facing IM_CHANNEL_CONNECTIONS.md:
connect codes are consumed before allowed_users (so a not-yet-allowlisted user can
still complete a first bind, and allowed_users is not a bind-time defense), and an
external identity has at most one active owner with last-bind-wins transfer enforced
at the DB layer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(channels): lift connect-code dispatch into Channel base class

Each adapter duplicated the ordering-sensitive boilerplate of extracting a
/connect code and guarding on the connection repo before its allowed_users gate.
The duplication is what let telegram/wechat drift and keep the gate ahead of the
bind. Centralize it:

- Move `_connection_repo` onto Channel.__init__ (removing 7 duplicate assignments).
- Add Channel._pending_connect_code(text), which guards on the repo and extracts
  the code, documenting that adapters MUST consult it before authorization so a
  browser-initiated bind can bootstrap a not-yet-authorized identity.
- Route slack, discord, feishu, dingtalk, wechat, and wecom through the helper.
  This also fixes a latent inconsistency where slack dispatched a bind even when
  no connection repo was configured.

Pure refactor — the full channel suite stays green; adds a direct unit test for
the base helper's contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* make format

* fix(channels): redact DingTalk parsed-message INFO log content

Log text_len instead of the first 100 chars of message text, so message
content never reaches INFO logs (the after-gate move already keeps blocked
senders out entirely). This takes over the redaction from #3584 so only this
PR touches dingtalk.py, letting the two PRs merge in any order conflict-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:15:31 +08:00
Ryker_Feng
0bbbbc06f4
feat(community): add Serper Google Images provider for image_search (#3575)
* feat(community): add Serper Google Images provider for image_search

Add a Serper-backed `image_search` tool alongside the existing Serper
`web_search` provider, so users with a SERPER_API_KEY can pull Google
Images results as reference images for downstream image generation.

- Share request/response handling between web_search and image_search
  via `_serper_post` / `_response_items`, with bounded `max_results`
  (capped at 10) and query normalization.
- Add a best-effort SSRF guard (`_safe_public_url`) that rejects
  non-http(s), localhost and private/non-global IP image URLs; filtered
  entries are dropped and never consume the result limit.
- doctor: flag literal `api_key` values in config as a warning and steer
  users toward `.env` + `$SERPER_API_KEY`.
- Docs/config: document the Serper image_search provider and SERPER_API_KEY,
  and discourage committing literal keys to config.yaml.
- Tests: cover the provider end-to-end (100% line coverage on tools.py)
  and the doctor literal-key warning path.

* fix(community): block obfuscated IPv4 literals in Serper image SSRF guard

The image_search SSRF guard only rejected dotted-decimal IP literals; encoded
forms such as decimal (http://2130706433/), hex (0x7f000001) and octal
(0177.0.0.1) raised ValueError in ip_address() and were allowed through, even
though many HTTP clients resolve them to private addresses like 127.0.0.1.

Add _decode_ipv4() to permissively decode these inet_aton-style encodings and
apply the same is_global check; hostnames that do not decode to an IP (e.g.
cafe.com) are still treated as hosts and left to fetch-time re-validation.

Addresses PR review feedback. Tests cover decimal/hex/octal loopback and
private encodings plus non-IP edge cases; tools.py stays at 100% line coverage.

* test(community): cover IPv4-mapped IPv6 URL filtering

* fix(community): address Serper image search review feedback

- Block trailing-dot hostname SSRF bypass (localhost./127.0.0.1.) in
  _safe_public_url by stripping the FQDN root label before checks.
- Keep a filtered image/thumbnail URL empty instead of collapsing onto
  its counterpart, preserving the high-res/preview contract.
- Evaluate the SSRF guard once per field rather than twice.
- Treat a null-typed organic/images field as "no results" rather than a
  malformed payload.
- doctor.py: when a config $VAR is unset, fall through to the default env
  var before reporting it as not set.
2026-06-18 07:36:35 +08:00
Nan Gao
926406e0d6
fix(channels): make runtime provider state authoritative (#3580)
* fix(channels): make runtime provider state authoritative

* make format

* fix(channels): close runtime provider config races and status gaps

Address review findings on the runtime-provider-state change:

- configure/disconnect now re-read the live app.state.channels_config
  after the worker await and mutate only the affected provider key in
  place, so a concurrent mutation for a different provider is no longer
  clobbered by a stale pre-await snapshot.
- disconnect revokes DB connection rows before committing the store and
  cache, so a repo failure cannot leave the store/cache "disconnected"
  while the DB keeps "connected" rows a later re-configure would
  silently reactivate.
- _provider_response preserves non-connected statuses (e.g. revoked)
  when the provider is unavailable, only masking a stale "connected"
  row as not_connected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:45:46 +08:00
Eilen Shin
25fbd25b05
fix(frontend): cap deeply nested list indentation to prevent render crash (#3393) (#3570)
* fix(frontend): cap deeply nested list indentation to prevent render crash

Deeply nested lists make marked's recursive list tokenizer overflow the
call stack during Streamdown's lexing useMemo, throwing an uncaught
"RangeError: Maximum call stack size exceeded" that replaces the chat
route with an error page (issue #3393); on larger stacks the same input
exhausts the heap, which the render error boundary cannot catch.

Mirror the existing capBlockquoteNesting guard with capListNesting, which
clamps leading whitespace to 200 columns (~100 nesting levels) only when
pathologically deep indentation is present, leaving normal content and
fenced code untouched. Wire both through capMarkdownNesting.

* fix(frontend): satisfy prettier format check in preprocess test

* fix(frontend): exempt indented code from list-indent cap (PR #3570 review)

* fix(frontend): keep capping all deep indentation outside fenced code

Revert the indented-code exemption from the PR #3570 review nit. Taken
literally the suggested guard (insideFence || INDENTED_CODE_RE.test(line))
no-ops capListNesting, because INDENTED_CODE_RE matches every line with
4+ leading spaces — i.e. exactly the deep-indent lines the cap targets.
A context-aware exemption (only treat 4+-space lines as code after a
blank line) instead reopens the crash: blank-separated deeply nested list
items get exempted and still blow up marked (verified: OOM at depth ~1.5k).

Unlike blockquotes (markers take <=3 leading spaces, so deep-quote lines
never look like indented code), list vs. indented-code indentation is
ambiguous line-by-line, so any exemption is exploitable. Keep capping all
deep indentation outside fenced code; the only cost is mild corruption of
a >200-column indented-code line, which never occurs in real content and
is strictly preferable to a render crash. Add a regression test locking
the blank-line case.
2026-06-14 22:19:54 +08:00
zgenu
34e126ee4b
fix(frontend): reset active chat after deletion (#3519) 2026-06-14 22:06:19 +08:00
Ryker_Feng
6e839342a7
feat(community): add Brave Search web search tool (#3528)
* feat(community): add Brave Search web search tool

Add a community web_search provider backed by the official Brave Search
API (https://api.search.brave.com/res/v1/web/search). API key is read
from the tool config (inline api_key) or the BRAVE_SEARCH_API_KEY env
var. Output schema (title/url/content) matches existing search tools.
No new dependencies (uses the existing httpx). Also wires up the setup
wizard, doctor health check, config example, and EN/ZH docs.

* refactor(community): drop redundant [:count] slice in Brave search

The Brave API already caps results via the `count` request param, so
client-side slicing was redundant. Tests now simulate the API honoring
`count` instead of relying on the slice. Addresses PR review nit.

* style(tests): apply ruff format to test_doctor.py

Collapse multiline write_text calls onto single lines to satisfy the
CI ruff formatter (lint-backend was failing on format --check).
2026-06-13 22:47:35 +08:00
Huixin615
a17d2ff8f8
fix(mcp): surface admin-required state on settings tools page (#3527) (#3533)
GET /api/mcp/config returns 403 for non-admin users, but the previous
client returned the error body as MCPConfig, causing MCPServerList to
crash with 'Cannot convert undefined or null to object' on
Object.entries(config.mcp_servers).

- api.ts: introduce MCPConfigRequestError; loadMCPConfig and
  updateMCPConfig now throw it (carrying status + isAdminRequired)
  instead of letting non-2xx bodies leak through as parsed config
- tool-settings-page.tsx: render a friendly 'admin privileges required'
  empty state when the React Query error is an admin-required
  MCPConfigRequestError; keep MCPServerList resilient with
  Object.entries(servers ?? {}) and an empty-state for no servers
- i18n: add settings.tools.adminRequired and settings.tools.empty in
  en-US, zh-CN and the Translations type
- tests: cover 403 / 5xx / instanceof / detail-fallback for both
  loadMCPConfig and updateMCPConfig in tests/unit/core/mcp/api.test.ts

Refs: #3527
2026-06-13 07:36:57 +08:00
Xinmin Zeng
503eeac788
fix(frontend): render user messages as plain text and cap blockquote nesting (#3502)
* fix(frontend): render user messages as plain text and cap blockquote nesting

User messages are typed or pasted plain text, not authored Markdown, but
they were rendered through the full Streamdown pipeline. Pasted source
files got fragmented (indented chunks become code blocks, paragraphs
collapse and lose indentation), "$...$" spans were KaTeX-ified, and a
message with thousands of nested ">" markers overflowed the call stack
in marked's recursive blockquote lexer, permanently crashing the thread
on every load.

Render human message content verbatim with pre-wrap instead, and cap
blockquote nesting at 100 levels at the Streamdown chokepoint so model
output cannot trigger the same recursion either.

Closes #3500

* fix(frontend): absorb marked lexer crashes with a render fallback boundary

Review found two gaps in the nesting cap: marked's list and blockquote
tokenizers are mutually recursive, so a list marker in front of the
quote chain ("- > > > ...") bypassed the blockquote-only regex and
still overflowed the stack; and the line-based rewrite was fence-blind,
silently truncating literal ">" runs inside code blocks.

Add an error boundary around Streamdown that renders the raw content as
plain pre-wrap text when rendering throws (retrying on the next content
change), keep the cap as a fast path for the dominant pure-">" case,
and make it skip fenced and indented code lines.
2026-06-12 16:15:40 +08:00