3836 Commits

Author SHA1 Message Date
Storm
fbf34e9cfc
fix(helm): plumb lark-cli runtime images into the provisioner (#5835)
* fix(helm): plumb lark-cli runtime images into the provisioner

The provisioner, docker-compose, and the root README all support
`LARK_CLI_INIT_IMAGE` (Pattern A) and `LARK_CLI_BROKER_IMAGE` (Pattern B),
but the chart never exposed them, so a Helm operator following the README
had no value to set and silently got a sandbox without a `lark-cli`
runtime (`lark-cli: command not found` at chat time).

- values.yaml: `provisioner.larkCliInitImage` / `larkCliBrokerImage`, both
  empty by default, so the rendered Pod spec is unchanged for existing
  releases.
- provisioner-deployment.yaml: render each env var only when non-empty.
  The provisioner keys off `bool(env)`, so omitting the variable keeps
  "feature off" identical to the pre-change chart.
- chart README: new "Sandbox lark-cli runtime (optional)" section; root
  README points Helm users at the two keys.
- backend/tests/test_helm_provisioner_lark_cli_images.py: render
  regression test (red before the template change).

* docs(helm): drop duplicated lark-cli callout in chart README

Review on #5835 caught the "An image here does not authenticate anyone
by itself" blockquote pasted twice verbatim in the new chart README
section. Remove the second copy and keep the first as the section's
closing note. Docs-only; the rendered chart is unchanged.
2026-09-30 09:16:00 +08:00
RongJie G
d642dfdd2f
fix(mcp): reserve notification launches atomically (#5965)
* fix(mcp): reserve notification launches atomically

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>

* fix(mcp): classify busy notification conflicts by type

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 09:12:11 +08:00
illusion
f9f6a36243
docs(zh): add the missing External Chat Message Roles section to README_zh (#6059)
* docs(zh): add the missing External Chat Message Roles section to README_zh

Adds the 'External Chat Message Roles' section to the Chinese README,
mirroring the section present in the English README under the Security
Notice. Same position (between Gateway Admin Is Equivalent to Code
Execution and Deployment Defaults), same content semantics, translated
rather than paraphrased.

Covers the HTTP 400 rejection of client-supplied system/developer
messages, the no-rewrite-of-existing-checkpoints note, and the full
local-verification PoC instructions (poc_external_system_message_injection.py,
--expect vulnerable / --expect blocked). The referenced script exists at
backend/tests/poc_external_system_message_injection.py.

The English TOC does not list this subsection either, so the zh TOC is
left unchanged to stay consistent (same approach as #5409).

* Fix minor language inconsistencies in README_zh.md

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-30 08:36:31 +08:00
huyengiang101086-spec
f119cacbc3
fix(frontend): bind Windows dev server to loopback by default (#6034)
Co-authored-by: huyengiang101086-spec <270684046+huyengiang101086-spec@users.noreply.github.com>
2026-09-30 08:12:31 +08:00
Hyeonsang Cho
ed09efdb7f
fix(config): upgrade the config.yaml the Gateway loads (#5991)
* fix(config): upgrade the config.yaml the Gateway loads

scripts/config-upgrade.sh resolved DEER_FLOW_CONFIG_PATH, then
backend/config.yaml, then <checkout>/config.yaml. make dev exports
DEER_FLOW_PROJECT_ROOT=<checkout>, so the Gateway reads the checkout copy
first: with both files present the script upgraded a file nothing loads
and left the live config outdated while reporting success. It also
ignored DEER_FLOW_PROJECT_ROOT and a DEER_FLOW_CONFIG_PATH set in .env,
and fell back to another file when DEER_FLOW_CONFIG_PATH named a
missing one.

Ask the harness resolver (AppConfig.resolve_config_path) instead of
copying its order, defaulting DEER_FLOW_PROJECT_ROOT to the checkout as
serve.sh does. A missing DEER_FLOW_CONFIG_PATH or invalid project root
now fails with the Gateway's error; no config anywhere still creates
<checkout>/config.yaml from the example.

* docs(changelog): reference #5991

* test(config): pin config-upgrade seeding a missing config from the example

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 22:10:29 +08:00
Bob
9b95f64e45
fix(sandbox): make glob patterns root-relative with recursive ** (#6046)
path_matches backs the glob tool and grep's glob= filter for every sandbox provider. It relied on PurePosixPath.match, which matches from the right and treats ** as a single-segment * on Python 3.12+, so only a leading **/ was recursive: src/**/*.py missed src/top.py and anything two or more levels deep, and docs/*.md also matched vendor/docs/b.md.

Match patterns that contain a / segment by segment from the search root, with ** spanning zero or more directories. Patterns without / keep matching the basename at any depth.
2026-09-29 21:50:59 +08:00
Zhengxu (Joshua) Jin
f64cc16385
docs(helm): fix the external PostgreSQL example in the chart README (#6038)
* docs(helm): fix the external PostgreSQL example in the chart README

The example set postgresql.external.host/port/database/username/password,
but the chart has never read those keys; it only reads
postgresql.external.databaseUrl or existingSecret. With the documented
values the chart creates no postgres Secret and the gateway gets no
DATABASE_URL, while config.yaml still references $DATABASE_URL, so the
config fails to load. Show databaseUrl, with existingSecret as the
alternative.

* docs(helm): clarify external database URL encoding

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 21:32:42 +08:00
xiaodu55
b30be67e00
fix(tests): write file_signature fixtures as bytes so CRLF never leaks in (#6042)
On Windows, Path.write_text and the text-mode append translate the
fixtures' newlines to CRLF on disk, so read_config_with_signature
correctly returns b'...info\r\n' while the tests assert the byte-exact
b'...info\n' - three tests fail on Windows (CI's Linux never sees the
translation). The product behavior is correct; the fixtures must write
the exact bytes they assert.

write_bytes / binary append for the three byte-sensitive tests: the
file now passes 10/10 on Windows.
2026-09-29 21:01:18 +08:00
yetuge
5312271f2b
fix(community): close the per-call Firecrawl client's async HTTP pool (#6013)
* fix(community): close the per-call Firecrawl client's async HTTP pool

The firecrawl tools build a fresh AsyncFirecrawlApp for every web_search
and web_fetch call. The migration to the async client (#5786) kept the
per-call construction, but AsyncFirecrawlApp eagerly creates an
httpx.AsyncClient-backed pool in its constructor and exposes no public
teardown, so each tool call abandons one unclosed pool. The sibling
providers either scope their httpx clients with 'async with' or close
them in a finally (tavily); firecrawl now does the same via a
best-effort teardown that tolerates SDK shapes without the v2 client.

Signed-off-by: yetuge <2219677952@qq.com>

* fix(firecrawl): make per-call pool teardown resilient

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

* test(firecrawl): skip the real-SDK teardown test when the private layout differs

The real-SDK test hard-asserted the locked firecrawl-py 4.23.0 private
layout (_v2_client.async_http_client._client.is_closed), so an
environment resolving another version in the declared >=1.15.0 range
died with AttributeError instead of skipping — while the helper (and
test_teardown_survives_a_client_without_v2_attributes) deliberately
treat a missing or reshaped layout as a no-op. Guard on the same
attributes and skip when they are absent.

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 21:00:36 +08:00
Stella-xixi
65ec5a475d
fix(uploads): preserve human message metadata (#5972) 2026-09-29 17:59:25 +08:00
Shxiao
f87a04e581
fix(sandbox): unwrap Overwrite-wrapped sandbox state in tools and middlewares (#6015)
* fix(sandbox): unwrap Overwrite-wrapped sandbox state in tools and middlewares

* fix(changelog): restore database entry and add #4380 reference link, simplify task_tool unwrap

* fix(subagents): preserve Overwrite wrapper when task_tool forwards sandbox state to executor

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 17:24:29 +08:00
Storm
1b691689da
fix(integrations): keep Lark runtime status consistent (#5994)
* fix(integrations): keep Lark runtime status consistent

Probe sandbox runtime readiness for every Lark mutation response outside the credential lock. Expose explicit sandbox_runtime_probed state so the frontend can distinguish conservative fallbacks without inferring semantics from runtime detail text, while cancelling stale reads before updating the cache.

* style(frontend): format Lark cache test

* fix(integrations): snapshot Lark credential status under the lock

Why this change is needed:
PR #5994 moved the post-mutation status read outside the per-user
credential lock so the new sandbox runtime probe would not extend the
critical section. As flagged in review, that lets a concurrent app
switch or auth flow land between the committed mutation and the status
read; in remote-provisioner mode the runtime probe can spend seconds
before probe_lark_auth runs, so the raced switch can clear the new
tokens and make a completed login report success=false, or mix an old
app_id with a newer auth state.

What changed:
- add _LarkCredentialSnapshot (app config + auth probe) and capture it
  in the same _lark_credential_lock section that commits the mutation
- thread the snapshot through _get_lark_mutation_status into
  get_lark_integration_status(credential_snapshot=...); only the
  credential-independent runtime probe still runs after the lock
- apply to complete_lark_auth, complete_lark_config, and
  set_lark_app_credentials; install keeps the one-shot status build

Problem solved:
Mutation responses stay tied to the just-committed credential
generation: a raced app switch can no longer flip a completed
device-code login to success=false or blend a stale app_id with newer
auth state, while the runtime probe stays off the lock.

* fix(integrations): address Lark status review follow-ups

- install_lark_integration: snapshot credential-derived status fields
  under _lark_credential_lock so the install response cannot mix newer
  credential state read across the unlocked runtime probe
- useInstallLarkIntegration: route the cache write through
  cacheLarkMutationStatus so install cancels in-flight reads and keeps
  probed runtime fields under frontend/backend version skew
- README: soften the UI claim - the status API exposes
  sandbox_runtime_probed and the mutation cache honors it

* test(integrations): tighten Lark status fixtures and guidance docs

- cache.test: use a distinct mutation runtime mode in the unprobed-merge
  case so the assertions prove the cached runtime fields win
- e2e mock: include broker in the sandbox_runtime_mode union cast so the
  fixture cannot drift from LarkSandboxRuntimeMode
- AGENTS.md (gateway + skills): mention sandbox_runtime_probed alongside
  the existing runtime readiness fields, keeping the gateway guidance
  file under its byte budget

* fix(integrations): harden Lark mutation cache against skew and races

- cacheLarkMutationStatus: await cancelQueries before writing so an
  in-flight GET whose fetch already resolved cannot clobber the fresher
  mutation status, matching the existing threads/archive pattern
- self-heal version skew: an unprobed (pre-field backend) mutation
  response now triggers a status refetch instead of risking the
  conservative runtime fallback lingering until the next focus/remount
- api: normalize a missing sandbox_runtime_probed to false at the
  boundary so the declared-non-optional type matches skewed runtimes
- tests: pin the skew contract (missing-field cases for cache and api)
  and add a useInstallLarkIntegration regression test proving the
  install cache write goes through cacheLarkMutationStatus
- README: describe the probed flag as cross-version semantics instead of
  claiming the same-version API surfaces both states
- lark_cli: note that install's credential snapshot runs the auth probe
  subprocess while holding the credential lock

* refactor(integrations): derive sandbox_runtime_probed inside the resolver

_resolve_sandbox_runtime_readiness now returns a fourth probed element
next to the branch that produces the unevaluated remote fallback, so the
'was readiness actually evaluated' invariant can no longer drift from the
fallback logic it mirrors. Callers and resolver-level tests updated to
pin probed per branch.
2026-09-29 17:23:36 +08:00
NanPan
907910957b
fix(gateway): drain managed subagent mutations on cancellation (#6023)
* fix(gateway): drain managed subagent mutations on cancellation

* test(gateway): pin managed subagent mutation drain

* fix(gateway): strengthen managed subagent mutation drain typing

* test(gateway): cover managed subagent cancellation drain
2026-09-29 17:11:07 +08:00
Hyeonsang Cho
1cd42cf42e
fix(doctor): check the config file the Gateway actually loads (#5987)
* fix(doctor): check the config file the Gateway actually loads

make doctor always inspected <checkout>/config.yaml and ignored
DEER_FLOW_CONFIG_PATH and DEER_FLOW_PROJECT_ROOT. A missing override
that stops the Gateway from starting still reported "config.yaml found"
and "config.yaml loadable", and a valid override pointing elsewhere got
the wrong file checked.

Doctor now resolves the path through the harness's own resolver,
AppConfig.resolve_config_path, after defaulting DEER_FLOW_PROJECT_ROOT
to the checkout as scripts/serve.sh does for make dev. An override the
Gateway would reject fails "config.yaml found" with the Gateway's error
and the config checks skip; an unimportable harness is reported rather
than raised.

* docs(changelog): reference #5987

* fix(doctor): hand the config-location variables over like serve.sh

Review follow-up. serve.sh sources the checkout .env over the shell and
then replaces an unset or empty DEER_FLOW_PROJECT_ROOT with the
checkout. Doctor loaded .env shell-first and only defaulted an unset
root, so a shell DEER_FLOW_CONFIG_PATH / DEER_FLOW_PROJECT_ROOT that
differed from .env, or an empty root, made it check a different file
than the Gateway loads.

For those two variables the .env value now overrides the shell (other
keys keep doctor's shell-first loading), and an empty root becomes the
checkout, before the harness resolver runs.

* fix(doctor): report any harness import failure and expand ~ like source

Review follow-up.

- The harness-import guard caught only ImportError, so a harness that is
  present but broken differently (TypeError/SyntaxError while the module
  body runs) still crashed doctor. Catch Exception, as
  check_config_loadable does, and name the exception type in the report.
- serve.sh's `source .env` expands an unquoted leading `~` in
  DEER_FLOW_CONFIG_PATH / DEER_FLOW_PROJECT_ROOT and keeps a quoted one
  literal; dotenv_values expands neither. Expand the unquoted form so a
  `~/...` path in .env no longer fails a setup that starts, and keep the
  quoted form literal so doctor does not pass one the Gateway rejects.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 16:55:21 +08:00
NanHan Qing
f8b69bd84b
fix(mcp): close thread-scoped MCP sessions on thread deletion (#5956)
* fix(mcp): close thread-scoped MCP sessions on thread deletion

Deleting a thread removed its filesystem data, checkpoints, runs, events,
feedback, metadata and browser session, but never closed its persistent MCP
sessions. MCPSessionPool.close_scope() had no production caller at all, so a
recreated thread id under the same user could reuse stale stateful MCP server
state, and the leaked owner task plus subprocess kept holding memory and file
descriptors.

Add MCPSessionPool.close_thread_scope(user_id, thread_id) and call it from the
reserved deletion path under the same best-effort policy as the neighbouring
cleanups. It matches every incarnation of the thread -- the legacy user:thread
scope and any v2:[user, thread, incarnation] scope -- because a deletion cannot
reliably read the current incarnation: the record may predate incarnation
tracking, and a new incarnation can be minted concurrently. close_scope() is
unchanged for callers that already hold one exact key.

Add mcp_scope_belongs_to_thread() to own the matching rule, and hoist the v2:
prefix into a shared constant so the encoder and matcher cannot drift.

Refs #5188

* test(mcp): bind owner identity in thread cleanup checks

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-29 16:53:18 +08:00
xiaodu55
8a3a309d1c
fix(tests): make the mcp task service suite deterministic on Windows (#5995)
Follow-up to #5940/#5941: with the hang gone, 12 tests further down the
file fail on native Windows. One disease, several signatures: the tests
patch _CANCELLATION_DRAIN_TIMEOUT_SECONDS to 0.01, but on Windows +
CPython 3.12 a 10ms wait_for over an already-created task times out
immediately (Windows timer granularity, ~15.6ms default tick), so
instant claims are abandoned to the background handoff and every
claim-completes-in-the-foreground premise breaks - abandoned fresh
claims (single_flight), cancellation landing after run_once already
returned (repeated_outer), releases that never happen in the
foreground (poll_retry / notification_failure).

- raise the patched drain 0.01 -> 0.05 in the five affected tests:
  clears the granularity tick with headroom, keeps every existing 0.2s
  bound valid, and the gate-blocked-claim abandonment the single-flight
  tests exercise still drains-timeout as designed
- loosen test_run_once_releases_claim_when_driver_is_missing_or_fails'
  strict next_poll_at > now + 5s to >=: Windows datetime.now() can
  return the same tick for the whole run_once, making the strict
  comparison a clock artifact; the backoff floor is the invariant

On Windows the file now passes 126/126 in ~20s (was 12 failed). Linux
semantics unchanged: claims complete in microseconds inside a 50ms
drain there.

Fixes #5993
2026-09-28 22:47:29 +08:00
Shxiao
b87ebd9402
fix(database): validate pool settings and command_timeout against booleans and non-finite values (#6009) 2026-09-28 22:46:39 +08:00
kuse
857eac45cd
fix(agents): treat non-finite read_file line bounds as unusable in loop detection (#5990)
_coerce_line_number() caught only (TypeError, ValueError), but a
model-emitted JSON number such as 1e999 parses to float('inf') and
int(inf) raises OverflowError, which escaped _track_and_check() through
after_model() and crashed the run. Treat the bound as unusable (None),
matching the OverflowError guards already merged for the search-tool
coercers.
2026-09-28 22:37:30 +08:00
yetuge
160fd50a28
fix(sandbox): reject non-finite health_check_skip_seconds in config (#5997)
`health_check_skip_seconds` was the only float field in SandboxConfig
without `allow_inf_nan=False`, and `ge=0` alone does not stop inf
(`inf >= 0` is true). A config.yaml value of `1e999` loads as the string
"1e999" and pydantic's float coercion turns it into inf, so the field
accepted it and the BoxLite warm-reuse site then skipped the health
check permanently: `skip_seconds > 0 and (time.time() - released_at) <
skip_seconds` holds forever for inf, so every box this instance
recently released is promoted without validation instead of failing
fast at config load.

Add `allow_inf_nan=False`, matching the sibling sandbox float fields
(`bash_command_timeout`, `renewal_interval_seconds`, `ttl_multiplier`)
and the ownership lease-TTL model validator.

Red/green: `test_sandbox_config_rejects_non_finite_health_check_skip_seconds`
parametrizes inf and "1e999"; both were accepted before the fix (RED)
and are rejected after. Full boxlite suite: 54 passed / 15 skipped.

Regression: the sandbox/config/ownership subset fails 17 tests on this
Windows host both with and without the change, with a byte-identical
failure-name set (environment-type: process-spawn and file-stat
behavior), so no regressions are introduced.

Signed-off-by: yetuge <2219677952@qq.com>
2026-09-28 22:36:19 +08:00
Daoyuan Li
5850b0da9f
fix(tenki): parse sticky environment values as booleans (#5849)
* fix(tenki): parse sticky environment values as booleans

* docs(tenki): keep agent guidance within chain limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 22:22:43 +08:00
lau0708
8712d495e2
fix(channels): grant sandbox read bits to Feishu/DingTalk inbound uploads (#5998)
The channel manager persists inbound attachments with
`write_upload_file_no_symlink` (root-owned, mode 0o600) and then relaxes them
to group/other readable via `_make_inbound_file_sandbox_readable`. In
AIO/Docker sandbox mode the sandbox runs as a non-root user on the bind-mounted
thread directory, so without those bits the file is unreadable and the agent
sees an empty attachment. The HTTP upload route applies the same permit.

`FeishuChannel._receive_single_file` and `DingTalkChannel._receive_single_file`
persist their own bytes and hand back a virtual path instead of routing content
through the manager, so neither reached that permit:

- Feishu leaves `msg.files` populated with `file_key` descriptors, but the
  manager's `_read_http_inbound_file` requires a `url` key, so its ingest pass
  reads no data and skips the upload.
- DingTalk clears `msg.files`, so the ingest pass returns immediately.

Telegram is unaffected because it hands raw bytes to the manager through
`INBOUND_FILE_CONTENT_KEY`, which is why it was covered when the permit was
added. Apply `apply_upload_sandbox_permits(..., S_IRGRP | S_IROTH)` right after
each channel's own write, off the event loop, mirroring the manager and HTTP
paths.

Tests: `test_channel_file_attachments.py` gains a mounted-provider regression
per channel asserting the persisted upload is group/other readable.
2026-09-28 22:19:32 +08:00
yc2bgr8
712a11c0cd
feat(community): migrate firecrawl tools to AsyncFirecrawlApp (#5786)
Replace the synchronous FirecrawlApp with AsyncFirecrawlApp in the
firecrawl community tools, continuing the TODO.md planned feature
('Replace sync requests with httpx.AsyncClient in community tools').

web_search_tool and web_fetch_tool become async functions and await
client.search()/client.scrape(). Return values (SearchData.web,
Document.markdown/metadata) are identical between the sync and async
clients, so callers see no change; error handling and the api_key /
api_url construction path are unchanged.

Tests now invoke the async tools via ainvoke under @pytest.mark.anyio,
and the client search/scrape methods are mocked with AsyncMock.
2026-09-28 18:08:46 +08:00
jiangjiang248
50d2d68556
fix(llm): avoid quadratic think-block stripping on unclosed tags (#5854)
* fix(llm): avoid quadratic think-block stripping on unclosed tags

* test(llm): pin think tag parsing edge cases

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 16:38:12 +08:00
xiaodu55
f67b1f574e
fix(tui): report headless --print/--json failures instead of leaking tracebacks (#5963)
* fix(tui): report headless --print/--json failures instead of leaking tracebacks

The headless --print and --json runners dispatched straight into the
embedded client with no error boundary, so session-creation, thread-
resolution, chat, and stream failures escaped main() and surfaced as
unhandled Python tracebacks from the console script - for example the
documented CheckpointModeMismatchError case, which the checkpoint
operations/troubleshooting docs recorded as a raw traceback.

Both runners now catch Exception (KeyboardInterrupt/SystemExit still
propagate), mirroring the TUI's stream_actions() boundary:

- --print writes one concise 'Error: ...' line to stderr and returns 1
- --json appends a terminal {"type": "error"} record to the NDJSON
  stream (diagnostics stay on stderr) and returns 1; every stdout line
  remains independently valid JSON, including failures mid-stream

The checkpoint operations/troubleshooting docs (en + zh) no longer
describe the headless runners as printing an uncaught traceback.

Fixes #5962

* fix(tui): close the broken-pipe edge and document the --json error contract

Review follow-ups on #5963 (both non-blocking):

- print(answer) sat outside the headless boundary, so a consumer closing
  the pipe early (deerflow --print ... | head) still got a
  BrokenPipeError traceback plus the interpreter's shutdown-flush noise.
  The final --print write and the --json terminal error-record write now
  catch BrokenPipeError, point the stdout descriptor at devnull (the
  documented SIGPIPE-adjacent recipe) and exit 1 without a traceback.
- The terminal {"type": "error"} record is a real contract for --json
  scripters but was only documented in the checkpoint pages; the
  backend/docs/TUI.md command table and the README cheat line now state
  the error record and the exit-1 semantics where CLI consumers look.
2026-09-28 16:16:40 +08:00
xbzz1018
f56d276122
fix(models): reject blank Codex CLI tokens (#5959)
* fix(models): reject blank Codex CLI tokens

Signed-off-by: xbzz1018 <xbzz1018@users.noreply.github.com>

* test(models): cover Codex token normalization path

Signed-off-by: xbzz1018 <xbzz1018@users.noreply.github.com>

---------

Signed-off-by: xbzz1018 <xbzz1018@users.noreply.github.com>
Co-authored-by: xbzz1018 <xbzz1018@users.noreply.github.com>
2026-09-28 16:16:15 +08:00
Wenchao An
52a3e2564f
fix(mcp): isolate personal connections from deployment configuration (#5966)
* fix(mcp): isolate personal connections from deployment configuration

* fix(mcp): preserve personal task ownership and repair plugin E2E fixtures

* fix(mcp): handle corrupt personal config and cache owned calls

* fix(mcp): pin personal endpoint addresses and handle invalid listings

* fix(mcp): recheck current authority for privileged personal connections
2026-09-28 16:02:34 +08:00
RongJie G
a3d75e843f
fix(gateway): distinguish bridge failures from disconnects (#5623)
* fix(gateway): distinguish bridge failures from disconnects

* fix(gateway): harden SSE terminal disconnect fencing

---------

Co-authored-by: CorgiBoyG <CorgiBoyG@users.noreply.github.com>
2026-09-28 16:00:52 +08:00
Grapette.L
e747dff159
fix(community): coerce InfoQuest crawl timeouts before comparing them (#5883)
config.example.yaml documents web_fetch timeout/fetch_time/navigation_timeout and
the two search_time_range options as seconds, and a $VAR reference resolves
verbatim from the environment, so any of them can reach InfoQuestClient as a
string. The client compares each with 0, which raises TypeError for a string,
None or a mapping and takes the whole web_fetch/web_search/image_search call
down. Reuse the provider-local coercion the sibling tools already have: accept
an int, integral float or integer-form string, otherwise warn and keep the
documented -1 (no filter).
2026-09-28 15:50:42 +08:00
Hyeonsang Cho
dfad2dcc74
fix(frontend): keep additional_kwargs on the optimistic bubble through an upload (#5982)
* fix(frontend): keep additional_kwargs on the optimistic bubble through an upload

When a send carried files, the upload-complete update replaced the
optimistic human message's additional_kwargs with only the uploaded
files. Quote and conversation reference chips vanished until the server
echoed the message, and project attachments staged with the send were
missing from the bubble both while uploading and after. The submitted
message was always intact.

The optimistic copy (before and after the upload) and the submit now
build additional_kwargs through one helper,
buildHumanMessageAdditionalKwargs, and share a single uploaded-files
value, so the bubble always matches what was sent.

* docs(changelog): reference #5982

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 15:49:38 +08:00
theater
85acf643c0
fix(subagents): terminalize batches after exhausted leases expire (#5897)
* fix(subagents): terminalize batches after exhausted leases expire

* docs(subagents): stay within agent guidance budget
2026-09-28 15:29:18 +08:00
haochen
1333c440ef
fix(config): accept UTF-8 BOM in extensions configuration (#5983)
* fix(config): accept UTF-8 BOM in extensions configuration

* fix(support): accept BOM in extensions config summary

---------

Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 15:25:06 +08:00
destire-mio
711035d2d1
fix(agents): key generic tool calls on all non-narration args in loop detection (#5872)
* fix(agents): key generic tool calls on all non-narration args in loop detection

Layer 1 keyed every tool without a dedicated rule on its first salient
field (path/url/query/command/...) and dropped all other arguments, so
paging a URL or a search (start_index, page, cursor) and rewriting one
skill file collapsed onto one key and hard-stopped on the fifth distinct
call. Key on every argument except the narration field (description),
which keeps the #1905 bypass closed.

Fixes #5871

* fix(agents): drop description only for sandbox UI narration in loop keys

update_agent/setup_agent persist description and MCP tools such as
create_issue send it, so description-only updates must stay distinct.
Drop it only for bash/ls/glob/grep, where it is the UI explanation of
the call and was already outside the key before this PR.

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
2026-09-28 15:23:51 +08:00
kuse
c0c28a3f13
fix(agents): preserve response_metadata in dynamic_context user copy (#5977)
* fix(agents): preserve response_metadata in dynamic_context user copy

The re-id'd user message was rebuilt field-by-field, dropping
response_metadata (and any other caller-attached field) from what the
run actually sees. model_copy keeps the documented {id}__user swap
while preserving the rest, matching the pattern already used by the
pii_redaction, thread_data and input_sanitization middlewares.

Fixes #5976

* fix(agents): preserve message metadata during upload injection

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 15:21:01 +08:00
xbzz1018
00c35fcce6
fix(uploads): reject boolean upload limits (#5920)
Signed-off-by: xbzz1018 <xbzz1018@users.noreply.github.com>
Co-authored-by: xbzz1018 <xbzz1018@users.noreply.github.com>
2026-09-28 15:17:41 +08:00
Stellar鱼
c917c2cc8f
fix(sandbox): reject non-finite OpenSandbox timeouts (#5944) 2026-09-28 15:16:16 +08:00
ZJPex
1129ee4e8a
feat: add bounded JSON syntax acceptance criteria (#5947)
* feat: add bounded JSON syntax acceptance criteria

* fix(subagents): authorize bounded local JSON acceptance reads

* docs: keep JSON acceptance guidance within chain limit

* docs: use English for JSON acceptance comments and guidance

* fix(subagents): keep unauthorized JSON probes unverified
2026-09-28 15:13:34 +08:00
lekt8
f889ce7517
feat(community): add Unbrowse web fetch provider (#5981)
* feat(community): add Unbrowse web fetch provider

Add a community web_fetch provider backed by Unbrowse
(https://unbrowse.ai), a hosted service that returns a page as markdown,
over plain HTTP when that is enough and through a cloud browser when the
page needs JavaScript. The tool sends one JSON-RPC tools/call POST
(unbrowse.scrape) to Unbrowse's MCP endpoint, so it needs no extra Python
package and no MCP client (uses httpx, already a dependency).

Unbrowse is not a general web search engine, so only web_fetch is added.

Changes:
- backend/packages/harness/deerflow/community/unbrowse/{__init__,tools}.py
  web_fetch_tool with the same output shape as the Sofya, Exa and
  Firecrawl fetch tools (# <title> plus markdown, 4KB cap). API key is read
  from the config.yaml api_key field or the UNBROWSE_API_KEY env var.
  Optional render: auto|never|always; unknown values log a warning and fall
  back to auto. JSON-RPC errors, isError tool results, HTTP errors and
  malformed payloads come back as "Error: ..." strings.
- backend/tests/test_unbrowse_tools.py: key resolution, render handling,
  request shape, structuredContent vs text payloads, error paths.
- scripts/doctor.py, scripts/wizard/providers.py: register the key and the
  web_fetch provider; backend/tests/test_doctor.py covers both doctor paths.
- config.example.yaml, .env.example, backend/docs/CONFIGURATION.md,
  tools/AGENTS.md, EN/ZH harness/tools.mdx: document the provider.

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

* docs(changelog): add Unbrowse web_fetch provider entry

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 15:08:49 +08:00
Totoro
e6a3f83f2f
fix(goal): preserve active goals when artifact delivery fails (#5980)
* fix(goal): preserve active goals when artifact delivery fails

* chore(goal): explain completion guards and log skipped clears

* docs(goal): clarify required artifact delivery

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-09-28 14:44:45 +08:00
yetuge
5092a45943
fix(tools): make the search coercion guards catch OverflowError (#5957)
* fix(tools): make the search coercion guards catch OverflowError

The community search tools coerce max_results (and Sofya's content
limit) from YAML config or tool parameters through guards that catch
only TypeError and ValueError. `int(float('inf'))` raises OverflowError
instead, so a YAML `.inf` (or a JSON `Infinity`) value still crashes
the coercion - SearXNG's search even fails wholesale with
'cannot convert float infinity to integer'.

Catch OverflowError alongside them so an infinite value falls back to
the default like every other invalid input. NaN is already covered by
ValueError.

Signed-off-by: yetuge <2219677952@qq.com>

* test(tools): move the GroundRoute inf test to module scope

The regression test was appended inside TestWebSearchTool, ending the
class early and turning its last two methods into uncollected nested
functions. Move it after TestWebFetchTool so pytest collects all 20
tests again.

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
2026-09-28 14:35:42 +08:00
Hyeonsang Cho
1e26f00664
fix(scheduler): stop interval-task metadata edits from failing with 500 on the reused next_run_at (#5964)
* fix(scheduler): coerce serialized timestamps in ScheduledTaskRepository.update()

Editing an interval task's title or prompt returned 500. The edit dialog
always sends schedule_spec beside the changed field, and when the cadence
is unchanged the PATCH route keeps existing["next_run_at"] — a value the
repository had handed back serialized as an ISO string. update() assigned
it to the DateTime column untouched, so SQLite raised StatementError
("only accepts Python datetime") and Postgres a DataError; only a cadence
change worked, because that path computes a fresh datetime.

Coerce every serialized timestamp update() accepts through the existing
_coerce_datetime, sharing one _TIMESTAMP_KEYS definition with the row
serializer, as update_after_launch() already does (#4785). The router test
now drives the real SQLite repository for the same-cadence rename and the
cadence change; the in-memory stand-in the other router tests use never
saw the type mismatch.

* docs(changelog): reference #5964 in the scheduled-task PATCH entries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 14:29:20 +08:00
yetuge
34a105c084
fix(middleware): preserve message fields when PII redaction rebuilds a user message (#5968)
* fix(middleware): preserve message fields when PII redaction rebuilds a user message

PiiRedactionMiddleware._process_request rebuilt the redacted HumanMessage
with a hand-constructed constructor call copying only content, id, name and
additional_kwargs, silently dropping response_metadata and every other field
of the original. That is the third and last site of the family already fixed
for ThreadDataMiddleware (#5823) and InputSanitizationMiddleware (#5875);
the review on #5823 scoped the follow-up to the two input-sanitization sites
and left this one out.

The middleware was also inconsistent with itself: _redact_tool_message in the
same file already rebuilds with model_copy and carries the comment explaining
that a hand-built constructor call silently drops response_metadata.

The user message now rebuilds with msg.model_copy(update={...}). The content
is deep-copied because _redact_content appends untouched content blocks by
reference: without the copy, mutating a non-text block on the redacted
message would write through to the message retained in thread state.

Red/green: test_redacted_user_message_preserves_other_fields asserts
response_metadata/id/name/additional_kwargs survive the rewrite — red on main
(response_metadata == {}), green here. A second test guards the aliasing
behaviour above and goes red if the deep copy is dropped. Both suites 258
passed; ruff 0.15.12 check + format clean repo-wide.

Fixes #5967

Signed-off-by: yetuge <2219677952@qq.com>

* fix(middleware): keep the PII rewrite on copy failure and detach metadata

Addresses the two copy-boundary findings on the previous commit.

1. A content block may carry a value whose copy raises. deepcopy(content) then
   raised out of _process_request, _try_process fell back to the original
   request, and the handler received raw PII unredacted. Copying now falls back
   to a shallow per-container copy, so a copy failure still produces a redacted
   message and never bypasses redaction.

2. msg.model_copy(update=...) is shallow, so the rebuilt message and the
   original shared one response_metadata dict: mutating metadata on the
   model-facing message wrote through to the request message retained in thread
   state. response_metadata is now copied independently.

Red/green: test_preserved_metadata_is_not_shared_with_the_original and
test_redaction_survives_a_content_block_that_cannot_be_copied both fail on the
reviewed commit (the second with the raw address reaching the handler) and pass
here. Suites: pii 77 passed, and pii + input_sanitization + uploads 327 passed.
ruff check + format --check clean repo-wide (1694 files).

Also drops the "third and last site" wording from the previous commit message's
claim: UploadsMiddleware is a further site of the same pattern (#5972).

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
2026-09-28 14:21:21 +08:00
Willem Jiang
c89ba45f7f
chore(doc):fix the changelog section error (#5868) 2026-09-28 11:49:20 +08:00
Hyeonsang Cho
b72753c226
fix(gateway): load skills off the event loop in the listing and detail routes (#5945)
* fix(gateway): load skills off the event loop in the listing and detail routes

GET /api/skills, GET /api/skills/custom and GET /api/skills/{name} called
load_skills() inline. Resolving the caller's storage builds absolute
paths, and load_skills walks every public and custom skill directory and
parses each SKILL.md, so the work grows with the number of installed
skills. #5747 moved the same call off the loop for get_custom_skill and
documented why; these three routes were missed, and the strict
Blockbuster gate raises BlockingError on each of them.

Offload the load through one shared _load_user_skills helper (also used
by update_skill's reload), and add a gate module covering the three
routes including get_skill's 404 branch.

* docs(changelog): reference #5945 in the skills listing blocking I/O entries
2026-09-28 09:18:29 +08:00
NanHan Qing
2377a49217
fix(gateway): close pooled MCP sessions on shutdown (#5961)
* fix(gateway): close pooled MCP sessions on shutdown

The Gateway lifespan closes the browser session manager on the way out but
never closed MCPSessionPool, so every pooled MCP session was abandoned when
the worker exited: the owner task's transport __aexit__ never ran and a stdio
MCP server or SSE/HTTP connection had no orderly teardown.

MCPSessionPool.close_all() already existed for exactly this and had no
production caller -- only the config-retirement path used close_all_sync().
The async form is the right one here: close_all_sync()'s own docstring directs
callers running inside a live event loop to await close_all() instead of
signalling and returning.

Wired into lifespan shutdown with the same bounded, best-effort shape as the
neighbouring browser-session hook.

* fix(gateway): close MCP sessions after run drain

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 09:14:27 +08:00
lihongyuan99
bc0167636f
fix(persistence): keep a literal plus in a postgres DSN query (#5978)
dsn_with_search_path rebuilt the query with parse_qsl + urlencode. parse_qsl
decodes '+' as a space, an HTML-form rule libpq does not apply, so any existing
parameter carrying a literal plus came back as %20 and the server received a
different value: application_name=web+app became 'web app', and
options=-c%20timezone%3DUTC+8 became '-c timezone=UTC 8'. The function's own
docstring already states the %XX-only rule and that existing parameters are
preserved.

Rebuild the query from the raw pairs instead, passing every non-options
parameter through untouched and decoding options with unquote.
2026-09-28 09:04:26 +08:00
Inference_
17f6b93bcb
fix(subagents): preserve native document blocks in context snapshots (#5948)
* fix(subagents): preserve native document blocks in context snapshots

Signed-off-by: Inference1 <3617237115@qq.com>

* fix(subagents): neutralize native document snapshot text

Signed-off-by: Inference1 <3617237115@qq.com>

* docs: fit subagent guidance within inherited limit

---------

Signed-off-by: Inference1 <3617237115@qq.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-28 08:58:55 +08:00
NanPan
827acf51dc
fix(sandbox): drain policy response mutation on cancellation (#5949)
* fix(sandbox): drain policy response mutation on cancellation

* test(sandbox): cover policy response cancellation drain

* fix: sort sandbox middleware lease imports

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-27 21:05:16 +08:00
NanPan
67ffbfdbb5
fix(runtime): drain subagent event flush on cancellation (#5950)
* fix(runtime): drain subagent event flush on cancellation

* test(runtime): pin subagent event flush cancellation ownership

* docs(subagents): pin event flush cancellation ownership

* docs: trim subagent guidance to fit inherited budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-27 21:04:39 +08:00
yetuge
052ad76f04
fix(scheduler): re-read the run row under the writer before guarding update_status (#5860)
update_status guarded expected_lease_owner (and the protect_terminal
backfill) on a plain session.get. On SQLite a concurrent requeue plus
re-claim can commit between that read and this session's write, so the
stale guard passed and the late write clobbered the fresh claim —
clearing the new owner's lease fencing and overwriting its status. The
guard now re-reads under the parent's writer lock (the same task -> run
lock order as fail_launching_run / expire_queued_runs), with
populate_existing so the identity map does not serve the pre-lock row
back untouched. This is the run-row counterpart of the staleness #5777
fixed for the task-level lease release.

Red/green: tests/test_scheduled_task_dispatch_race.py gains a parking
session proxy that commits a requeue + re-claim by another scheduler
between update_status's read and its write — on main the late write
returned True and clobbered the claim; with the change it is fenced
(False) and the re-claim survives.

Signed-off-by: yetuge <2219677952@qq.com>
2026-09-27 21:00:09 +08:00
Hyeonsang Cho
931d2eeb81
fix(models): deep-merge when_thinking_enabled on the legacy path (#5894)
* fix(models): deep-merge when_thinking_enabled on the legacy path

The legacy thinking path (profiles without a `reasoning:` block, i.e. every
existing config.yaml) applied `when_thinking_enabled` with a shallow
`dict.update`, so a template such as `extra_body.thinking.type=enabled`
replaced the profile's whole `extra_body` and silently dropped sibling keys
like GLM's `extra_body.tool_stream` — but only while thinking was on. The
disable branch of the same function already deep-merged into `extra_body`,
and the contract path added in #5780 deep-merges through `_merge_settings`
for exactly this reason (its docstring names `tool_stream`). The on/off
asymmetry cannot be intended: the same profile sent `tool_stream` with
thinking off and lost it with thinking on.

Route the enable template through `_merge_settings` as well. Precedence is
unchanged (template values win on conflicts); the only observable
difference is that keys present only in the profile now survive a
thinking-on request. Everything else on the legacy path — the synthesized
`reasoning_effort=minimal`, the disable payloads, the supports_thinking
guard — is untouched.

Tests pin both directions for a base extra_body beside a thinking template,
recursive merging of nested chat_template_kwargs, and template precedence.

* docs(changelog): reference #5894 in the when_thinking_enabled entries

* fix(models): keep the template's vLLM switch spelling authoritative when merging

Review follow-up. Deep-merging the thinking template into a profile keeps
both spellings of vLLM's toggle when they differ — base
`chat_template_kwargs.enable_thinking: false` plus a template using the
legacy `thinking: true` alias — and VllmChatModel's normalizer maps the
alias onto `enable_thinking` only when the latter is absent, so the
profile's `false` won and thinking stayed off despite thinking_enabled=True.
The previous shallow replacement never hit this because it discarded the
base switch. The contract path had the same trap since it already
deep-merged, and both disable branches had the mirror image (a base
`enable_thinking: true` outlived a template `thinking: false`).

Route every thinking on/off merge through _merge_thinking_payload: it drops
the profile's *other* spelling of the switch before the deep merge, so the
payload's spelling decides in both directions on both paths, and unrelated
chat_template_kwargs still survive.

Tests cover both spelling combinations, both directions, and both paths,
asserting the switch as the provider will send it after normalization.

* fix(models): deep-merge the legacy disable template and mirror the vLLM switch instead of pruning

Review of the previous head found two regressions. The legacy
when_thinking_disabled branch was still dict.update-ed, so a profile that
declares both templates (the shape of most extra_body examples) kept
tool_stream while thinking was on and lost it while off. And the merge
pruned the profile's other vLLM switch spelling for every model class,
although only VllmChatModel maps `thinking` onto `enable_thinking`: on a
plain ChatOpenAI profile the prune removed the only key vLLM reads, so
thinking-off could no longer switch reasoning off.

Route when_thinking_disabled through _merge_thinking_payload too, and
replace the prune with mirroring: after the deep merge, the value the
payload declares for its switch spelling is copied onto the profile's
other spelling when the profile carries it. VllmChatModel collapses the
pair, plain OpenAI-compatible classes send both, and either way the
server reads the template's intent. No spelling is invented, a payload
that spells both is left alone, and non-mapping template values are
forwarded unchanged instead of raising. _VLLM_THINKING_SWITCHES is now
the single definition used by the disable, toggle and dialect helpers.

Docs state the merge semantics (keys are never removed; a template can
only add or override) and drop the "byte-for-byte" wording; tests cover
the disable template on/off, nested inheritance, non-mapping values, the
raw payload for a non-normalizing class, and the no-invention rule.

* fix(models): copy thinking templates on merge and document the never-remove rule

Review follow-up. Templates merged into the constructor settings were
stored by reference wherever the profile lacked the key, so the kwargs
handed to the model class aliased the cached ModelConfig; the mirroring
step now writes into that mapping, so make _merge_settings and
_deep_merge_dicts deep-copy every template mapping they store. Pinned at
every depth by a test that also checks the profile is unchanged.

Deep-merging the legacy when_thinking_disabled template means it can no
longer clear a key the profile's base extra_body sets: a base
extra_body.thinking {type: enabled, budget_tokens} reaches the provider
as {type: disabled, budget_tokens}, which Anthropic-style APIs reject.
The synthesized disable payloads and the contract path already behaved
this way. State the rule and the migration (enable-only keys belong in
when_thinking_enabled) in the changelog, models/AGENTS.md and
configuration.mdx, and pin it with a test for both shapes.

Route the legacy synthesized OpenAI disable through
_merge_thinking_payload too (identical output), narrow the docstring and
guidance wording about the native-Anthropic disable, and make the two
vLLM switch suites resolve the real VllmChatModel / ChatOpenAI classes
so a class-gated merge would fail the case it is named after.
2026-09-27 20:52:13 +08:00