* fix(skills): stop reporting an env path, shebang or flag as an env dump
* fix(skills): report shell env dumps only at a command position
`shell-env-dump` matched the word `env` anywhere it was not preceded by a path, dot, dash or word character, so ordinary argument text (`echo env`) and comment text (`# env`, `# export -p`) were still reported as environment dumps. Anchor the match to a shell command position -- the start of a line or right after a `;`, `&`, `|`, `(`, `)` or `{` separator, optionally behind `NAME=value` assignment prefixes -- and add regression cases for argument and comment text.
* fix(skills): ignore an env dump inside a comment or heredoc body
`shell-env-dump` matches `env`, `printenv` and `export -p` at a command
position, but the text it sees is still raw shell. A `;` inside a comment
(`# documentation; env is only an example`) and a bare `env` line inside
heredoc data both sit at a command position on paper while the shell runs
neither, so both were reported as environment dumps.
Reduce the text to shell code before matching: a quote-aware pass strips
comments (only a `#` at a word start opens one) and blanks heredoc bodies,
covering `<<-` tab stripping and quoted delimiters, while keeping one output
line per input line so reported line numbers stay correct. Real commands
after a separator (`true; env`) and on the line that opens a heredoc
(`env <<EOF`) still report.
* fix(skills): report env dumps after a keyword and stop masking ${#...}
The command-position anchor for `shell-env-dump` accepted only a line start or a
`;`, `&`, `|`, `(`, `)` or `{` separator, so real dumps were dropped: after a
reserved word that introduces a command (`if true; then env; fi`,
`while true; do printenv; done`), after `exec` (`exec env`) and inside a backtick
substitution (``V=`env` ``). Accept those positions too, with a guard so a path
component (`./then env`) does not arm the anchor, and add the backtick to the
separator set.
A `{` directly before a `#` is not a word start -- bash reads `${#HOME}` as the
length operator and `}#` as part of a word, while a brace group needs the space
of `{ # ...`. The comment heuristic no longer treats `{` and `}` as comment
openers, so `n=${#HOME}; env` keeps its `; env` and is reported again, while
`${env}` (a variable read, not a command) stays silent.
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
* fix(agents): keep malformed todo statuses recoverable
* docs(agents): fit todo guidance within inherited size limit
* docs(agents): clarify todo attribution guidance
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(browser): drain live cleanup before releasing session
* test(browser): cover cancellation precedence during cleanup
* style(browser): separate cancellation regressions
* fix(gateway): scope per-run message and event reads to the run's data identity
start_run stamps run-event rows with the raw trusted owner id, but
GET /runs/{rid}/messages and /runs/{rid}/events called the event store with
user_id=AUTO, which resolves to the internal caller's make_safe_user_id form.
On the database event store, owners whose id contains characters outside
[A-Za-z0-9_-] (IM channel ids such as "feishu:owner-777") therefore got an
empty list. The regenerate source-run scan had the same mismatch and fell
back or returned 409.
Pass _run_scope_user_id() explicitly, as the thread message routes already
do (#5448), and add user_id to RunEventStore.list_messages_by_run() so every
backend accepts it like list_messages()/list_events(). Browser and API
sessions keep their per-user filter.
* fix(gateway): scope workspace-changes and archive reads to the run's data identity
The same raw-owner mismatch reached two more run-scoped routes. On the SQL
stores, GET /runs/{rid}/workspace-changes reported no changes because its
event lookup used user_id=AUTO, and GET /runs/{rid}/artifacts/archive
answered 404 because both its run-row lookup and its run.delivery event
lookup did.
Pass _run_scope_user_id() to both, and let get_workspace_changes_response()
forward an explicit user_id to the event store.
* docs(changelog): reference #6282 in the run-scope read fix entries
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(runtime): keep renewing the lease while a staged terminal run finalizes
With an event store, run_agent stages the terminal status in memory and
commits it to the run store only after the journal flush, delivery receipt,
workspace scan and duration checkpoint. _renew_leases selected runs by local
status, so it stopped renewing as soon as the status was staged. A
finalization longer than lease_seconds + grace_seconds let a peer, or this
worker's own reconciler, claim the still-active row and record the
successful run as an orphan error.
The worker now marks RunRecord.terminal_commit_pending until the deferred
commit is attempted, and lease renewal treats it like the existing
scheduled-goal barrier: keep renewing, release on a confirmed own commit,
and fence when a peer claims the row. A fenced run stops renewing.
* docs(changelog): reference #6263 for the staged terminal lease renewal fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skills): report secret assignments whose key is quoted
The line-oriented secret sweep required the credential name to be followed
directly by ':' or '=', so a JSON config ('{"api_key": "..."}') never matched
while the same binding in YAML, .env, .ini or shell did. Allow one optional
quote between the key and the separator.
* fix(skills): match secret assignments whose key has a separator prefix
The line-oriented sweep anchored the credential word with `\b`, which cannot
land after `_` because `_` is a word character. So `access_token`,
`client_secret` and `MY_API_KEY` — the common spellings in OAuth and
service-account config — matched nothing, and `_SECRET_TOKEN_PATTERNS` does
not backstop JWT- or `GOCSPX-`-style values, so such a package produced no
finding at all.
Guard the word with "not preceded by an alphanumeric" instead: a separator
(`_`, `.`, `-`) may now introduce it, while a run of letters (`tokenizer`,
`secretive`, `credentials_file`) still stays quiet.
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
* fix: include completed child usage in lead budget checks
* test: require completed usage middleware in lead budget regressions
* docs: fit middleware guidance within the size budget
Condense the subagent, loop, and token budget guard descriptions while retaining their limits, lifecycle rules, and completed child usage ordering.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
is_eval_fixture_path returned on the first 'evals' segment, so a path whose
'evals/fixtures/' came after a plain 'evals' segment read as a non-fixture.
Move the 'fixtures' test into the loop condition so every segment is checked.
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
* fix(channels): follow Buzz threads mapped in the connection repository
With channel_connections enabled, ChannelManager stores a bound author's
thread mapping only in the connection repository, but Buzz's thread-follow
gate read only the JSON ChannelStore. engaged_thread was therefore always
False for authors bound with /connect, and every unmentioned reply in a
thread the bot was already answering was silently dropped.
Buzz now resolves the connection identity before the mention gate and looks
the thread up through lookup_thread_id, a helper extracted from
ChannelManager._lookup_thread_id that both sides now share, so the gate and
the manager can no longer disagree about which store owns a mapping.
* fix(channels): drop the manager's own JSON-store read in the skill whitelist
_resolve_available_skill_names fell back to reading the JSON ChannelStore
when the caller's lookup_thread_id found nothing. For a bound message that
lookup is repo-only, so a legacy unbound mapping for the same chat decided
which custom agent's skill whitelist applied. The parameter is now required
and the caller passes "" when nothing is mapped.
Also pin the Buzz thread-follow fix against the real SQL connection
repository (bound through a real /connect) instead of a fake.
* docs(changelog): reference #6232 for the Buzz thread-follow fix
* fix(channels): log Buzz drops of unmentioned replies with no engaged thread
The thread-follow gate was the one drop in _handle_chat_event without a
log line. Log it at DEBUG with the thread root and whether the author was
bound, matching the allowlist drop, so a report that the bot stopped
following replies can be told apart from a lost bind or missing mapping.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Path(client).name == "docker" misses the Windows CLI (docker.EXE), so
every real-compose test routed through the standalone-Compose wrapper,
whose shift strips the compose subcommand; the root CLI then rejected
the script's --env-file probe with "unknown flag: --env-file". Compare
the stem instead so the CLI is recognized on Windows too.
Root cause identified by @xihongshichaojidan8 in #6259.
Fixes#6261
Co-authored-by: xiaodu55 <xiaodu55@users.noreply.github.com>
* feat(extensions): add full Custom Agent team collaboration plugin
* fix(extensions): register agent teams in the capability catalog
* fix(agent-teams): recover incomplete teams and preserve request capacity
* fix(agent-teams): preserve clarification waits and active handoff lineage
* fix(agent-teams): align mention and input text limits
* fix(gateway): read uvicorn's WEB_CONCURRENCY in the remaining worker-count readers
* fix(gateway): name the worker spelling an operator can act on
The browser refusal and the dedupe warning both took the count but printed
GATEWAY_WORKERS, so on the launches that pass uvicorn no --workers the advice
silences the gate while the extra processes keep starting. Resolve which
variable set the count and print that, and let an unparsable GATEWAY_WORKERS
fall through to WEB_CONCURRENCY instead of reporting a single worker.
Also stop the comments from claiming the readers are equivalent: they share
the two spellings and the blank-means-unset rule, but channel_connections.py
turns an unparsable value into 0 and refuses while these stay inert.
* docs: state the single-worker requirement under both spellings
The browser tool group and the inbound dedupe warning now refuse or warn on
WEB_CONCURRENCY as well, so guidance that names only GATEWAY_WORKERS=1 is
incomplete for the shipped Dockerfile and serve.sh launches, where uvicorn
takes the process count from WEB_CONCURRENCY.
* test(features): isolate the browser capability surface from the shell's worker count
browser_capability() asks for the Gateway worker count, which this change makes
readable from WEB_CONCURRENCY, so an exported count in the invoking shell turned
test_features_reports_browser_control_enabled_when_configured_and_runtime_available
into a failure that has nothing to do with the router.
* fix(gateway): read uvicorn's WEB_CONCURRENCY in the multi-worker safety gates
The SQLite/scheduler gates in deps.py derived the worker count from
GATEWAY_WORKERS alone, but backend/Dockerfile and scripts/serve.sh launch
uvicorn without --workers, and uvicorn then takes the count from
WEB_CONCURRENCY. A Gateway scaled that way starts N processes with every
gate inert, which is exactly what the gates exist to prevent.
Read both variables, preferring GATEWAY_WORKERS, treat a blank value as
unset the way the compose command's ${GATEWAY_WORKERS:-1} does, and name
the variable that actually set the count in the refusal text. The channel
connection gate already reads both (channel_connections.py).
* fix(gateway): fall through to the fallback worker count
A non-numeric GATEWAY_WORKERS made the shared reader return one worker
immediately, so on the launches that pass no --workers (backend/Dockerfile,
scripts/serve.sh, backend/Makefile gateway) an operator's
GATEWAY_WORKERS=abc plus WEB_CONCURRENCY=4 started four processes while every
gate stayed inert. Keep looking at the remaining spellings and report a single
worker only after all of them have been tried.
The gate file also needs to be independent of the invoking shell: with
WEB_CONCURRENCY exported, the pre-existing
test_gate_treats_invalid_env_as_single_worker already failed on this branch,
because the reader now consults that variable too.
* feat(runtime): add P0 per-LLM-call telemetry and summarization no-op counter
Measurement only; no runtime behavior changes.
- RunJournal enriches llm.ai.response / llm.error metadata with
langchain_run_id, caller_category, provider, model, stop_reason, status,
token counts and rendered request size (chars). Unavailable values are None.
- Summarization middleware emits a middleware:summarize event per summarizer
call and counts outputs identical to the prior summary_text. The call is
never skipped.
- All telemetry fails soft.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(runtime): register summarize middleware tag, document telemetry contract
- Add MIDDLEWARE_SUMMARIZE_TAG to the middleware event catalog and the
contract's known_tags; emit the middleware:summarize event with it.
- Document the optional per-call telemetry metadata on llm.ai.response and
llm.error in contracts/run_event_stream_contract.json.
- Measure request size by walking string leaves instead of json.dumps on the
event-loop thread.
- Initialise summarization call/no-op counters in __init__.
- Move RunJournal telemetry docs to the runtime guide.
- Add tests: llm.error without a start, replayed on_llm_end, and a DB-store
metadata round trip.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(runtime): refresh token counters on usage replay
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
#6229 gave the scheduler its own integer knobs, but they accept YAML
booleans: recursion_limit: true loads as 1, so every scheduler-launched
run dies at its first LangGraph super-step - the same hole the
app-config guard already closes for Gateway runs (#6171), now on the
same-named field. max_concurrent_runs: true serializes the scheduler
and poll_interval_seconds: true polls five times a second.
Mirror the app-config boolean guard: a before-validator rejects booleans
with a field-specific error across all six integer fields, keeping
integers and numeric strings as-is. lease_seconds and
queue_timeout_seconds already reject true via their higher ge floors,
but with a misleading "should be greater than or equal to" message -
the guard gives every field the same boolean-specific error.
Signed-off-by: yetuge <2219677952@qq.com>
YAML booleans coerce into the ge=1 integer backstops that bound every
subagent and ACP invocation: timeout_seconds: true became 1 (the executor's
future.result(timeout=...) then killed every subagent run after one second)
and max_turns: true became 1 (the LangGraph recursion budget collapsed to a
single super-step). The ACP agent timeout mirrors subagents.timeout_seconds
and shared the hole. false was already rejected by ge=1, so only true
slipped through - silently. Mirror the app-config boolean guard: reject
bools before coercion in SubagentOverrideConfig, CustomSubagentConfig and
SubagentsAppConfig (timeout_seconds/max_turns/max_total_per_run) and
ACPAgentConfig (timeout_seconds), keeping None, integers, and numeric
strings as-is.
Signed-off-by: yetuge <2219677952@qq.com>
* fix(memory): treat facts deleted during unlocked DeerMem scans as removed
load(), reload(), and the full rebuild_index() scan list agent fact
files without the storage locks. A delete committed between the listing
and opening a file raised MemoryStorageCorruption for intact data, which
surfaced as HTTP 500 from the memory API and an empty memory block in
the prompt. A listed fact that no longer exists is now skipped; an entry
that is still present but unreadable is still corruption.
* docs(changelog): reference #6255 for the DeerMem vanished-fact fix
* docs(memory): note lock coverage and heuristic limits of the vanished-fact skip
* fix(skillscan): report secrets bound by unpacking and container literals
The AST walk behind secret-env-assignment claims "precision, never less
coverage", but it only read a single assignment target and a bare string
value, so a credential moved into `host, api_key = endpoint, "..."` or
into `api_key = ["..."]` stopped being reported even though the pre-#5648
line sweep still reported the same bytes.
Pair unpacking targets with values positionally (fixed head and tail only,
so a runtime-sized slice names nothing) and assert on each literal a
container or conditional holds, in source order.
* fix(skillscan): distinguish mapping labels from credentials
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
The channel runtime config store's atomic 0o600-temp + replace + chmod
cannot land a POSIX mode on Windows, where chmod is a no-op
approximation. Skip the two POSIX-bound tests and guard the trailing
mode assertion of the chmod-failure test, keeping its platform-
independent log-behavior coverage everywhere.
Fixes#6256
Co-authored-by: xiaodu55 <xiaodu55@users.noreply.github.com>
* refactor(config): converge the boolean guards into one shared helper
Willem-bd's #6171 review follow-up: a tiny shared helper lets the next numeric field get the guard in one line and unifies the messages. Consolidates 14 validator copies across 9 config files into deerflow/config/_boolean_guards.reject_boolean(value, info, kind=...); the bare per-file messages gain their field name prefix, command_timeout keeps its distinct wording.
Signed-off-by: yetuge <2219677952@qq.com>
* refactor(config): converge stream_bridge_config and pin the guard wording
Review follow-ups on #6226: convert the last surviving per-file copy (stream_bridge_config) so deerflow/config is fully converged, make kind required keyword-only so a float field cannot silently inherit integer wording, and pin the unified field-prefixed messages with direct tests.
Signed-off-by: yetuge <2219677952@qq.com>
* test(config): cast the boolean-guard test probe to ValidationInfo
The _info probe was annotated -> object, so a type checker flags every call
site that passes its result to reject_boolean's info: ValidationInfo
parameter (runtime was fine since the helper only reads .field_name).
Return cast(ValidationInfo, _Probe(field_name)) to keep the probe honest
for both.
Signed-off-by: yetuge <2219677952@qq.com>
---------
Signed-off-by: yetuge <2219677952@qq.com>
* fix(memory): sign DeerMem reload before reading the document
reload() read the document before computing its cache signature, so a
write committed in between cached the old document under the new
signature and later load() calls served stale memory until the next
write. Compute the signature first, matching load().
* test(memory): cover markdown storage and agent scope in reload race test
* docs(changelog): reference #6238 for the DeerMem reload fix
* fix(telegram): deny all users when allowed_users has no valid ID
TelegramChannel parsed channels.telegram.allowed_users with int() and
dropped every entry that failed without logging, and _check_user treated
an empty result as "no allowlist". A list of @usernames therefore opened
the bot to everyone. A single string ID was iterated character by
character ("123456" allowed users 1-6 and blocked 123456), which is what
an allowed_users: $ENV reference resolves to, and null or a bare integer
crashed the channel at startup.
The allowlist is now parsed into frozenset[int] | None: None (unset,
null, empty list, blank string) still allows everyone, a single ID is a
one-entry list, entries that are not positive numeric IDs (@usernames,
floats, booleans, comma-joined strings) are dropped with a warning, and
a configured list with no valid ID denies every user and logs an error.
* docs(changelog): reference #6230 for the telegram allowed_users fix
* fix(telegram): say "positive" in the allowed_users drop warning
0 and negative integers are numeric but still dropped, so "expected one
numeric Telegram user ID" misdescribed them. The warning now asks for a
positive numeric ID, matching _parse_telegram_user_id, and the drop test
covers 0 and -5 alongside @alice.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(extensions): add host-bound Agent run control
* fix(extensions): preserve internal launches and scope run control
* docs(extensions): use a plugin-local run operation key
* fix(channels): run Discord connection-repository work on the Gateway loop
discord.py delivers messages on a private loop in the client thread, and
the Discord adapter awaited the channel-connection repository there even
though its SQLAlchemy engine and pool belong to the Gateway loop. On
PostgreSQL the first Discord message after the Gateway used the pool
failed with "got Future attached to a different loop" and was dropped; on
SQLite a pool-exhausting burst failed with "Queue is bound to a different
event loop" and left the pool's wait queue bound to the Discord loop, so
the Gateway's own queries then failed the same way.
Resolve the identity together with the intake commit, and run /connect
binding, on the Gateway loop through _submit_threadsafe_coroutine, as
Telegram, Feishu and DingTalk already do. Bind replies return through
_run_on_discord_loop, and start()/stop() now open and drain the
cross-thread submissions.
* docs(changelog): reference #6214 for the Discord connection-loop fix
* fix(channels): correct the unscheduled Discord bind comment and pin it
When _submit_threadsafe_coroutine refuses the bind, base.py closes the
coroutine, so consume_oauth_state never runs and the one-time code stays
valid; only the Discord message is swallowed. Say that instead of
claiming the code is consumed, and pin both halves with a test: the
attempt is handled (never a chat turn) and the code still binds later.
* fix(channels): undo Discord typing when the Gateway hand-off fails
Since identity resolution moved to the Gateway loop, both inbound paths
started typing and the ack reaction as soon as the hand-off was
scheduled. A lookup that then raised (database outage, pool-checkout
timeout) dropped the message but left an endless typing loop and a
success reaction behind.
The hand-off now returns its completion future (new
Channel._submit_threadsafe_coroutine_future, which the bool helper wraps)
and the Discord side waits on it through asyncio.shield. Typing still
registers before the hand-off settles, so a fast reply's _stop_typing
cannot run first; the ack reaction waits for the commit. On failure, or
when stop() drains the hand-off, the indicator this message started is
stopped (never one another in-flight message owns) and no ack is added.
* fix(channels): keep shared Discord typing alive for dependent messages
Two messages to the same target share one typing task: the first starts
it, a later one reuses it. If the creator's hand-off failed after a
follower had committed, or while the follower was still pending, the
creator stopped the indicator the follower relied on, and nothing
restored it while the follower's request ran.
Count the hand-offs that depend on each typing task. A pending or
committed hand-off holds the task (a commit keeps its hold until a reply
stops the indicator), a failed one releases its hold, and the indicator
is stopped only when the failed message started it and no hold remains.
The count is dropped when the task ends or the channel cancels typing.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(lark): match broker subcommand denylist through separate option values
The opt-in DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS check dropped only
`-`-prefixed tokens and compared the rest from the start, so the value in
`--profile work config show` became the leading positional and a
`config show` rule never matched. Real lark-cli 1.0.65 runs `config show`
for that shape and for `config --profile work show`.
The broker cannot know which options take a value, so a rule now matches
when its tokens appear in order among the non-flag tokens. A contiguous
match would still miss a value placed between the path tokens. Argument
values that spell a denied path in order are refused too (fail-closed).
* docs(changelog): reference #6212 for the lark broker denylist fix
* docs(lark): call denylist entries command paths, not prefixes
Entries are matched as in-order subsequences now, so the remaining
'prefix' wording in the BrokerConfig comment, the parse_deny_subcommands
and run_lark_cli docstrings, and the broker README undersold the rule.
Also rewrap the comment and docstring lines this PR had pushed past ~80
columns.
* fix(sandbox): scope ignore patterns to the search root in glob/grep
Rebuilt on top of current main so the changelog entry lands in the
Unreleased (2.2.0) Fixed section rather than the now-released 2.1.0 block.
* test(sandbox): cover the include_dirs glob branch at an ignored search root
The file.list_path branch only ran against a non-ignored tree, so a
regression there would go unnoticed. Two cases now exercise it with an
ignored root: the requested root keeps its contents, and ignored
descendants stay hidden.
* fix(config): reject boolean integer settings in app_config
Follows the loop-detection and circuit-breaker guards: YAML booleans next
to integer knobs load cleanly, and Pydantic coerces booleans to 1/0.
- recursion_limit coerced from `true` to 1, so every Gateway run that does
not supply its own limit hit the LangGraph recursion ceiling at the
first super-step (max_recursion_limit likewise clamped the ceiling to 1).
- The five llm_call integers coerced similarly: retry_max_attempts as
`true` disabled retries, max_concurrent_calls as `true` serialized the
pipeline.
All seven integer fields now fail configuration loading with a
field-specific error, while valid integers and numeric strings keep their
existing behavior. Mirrors the loop-detection helper (#6017) and the
in-file circuit-breaker validator.
Signed-off-by: yetuge <2219677952@qq.com>
* docs(changelog): record the app-config boolean rejection
Changelog entries for #6171, mirroring the #6017 entry format in both
languages.
Signed-off-by: yetuge <2219677952@qq.com>
* test(config): pin the False-side coercion regression in the boolean guards
Per review: the tests only exercised `True`. On the `ge=1` fields `false`
already failed the constraint pre-fix, but on the `ge=0` fields
(`max_concurrent_calls`, the two delay fields) `false` coerced silently to
the harmless-looking `0`, disabling the concurrency cap with no error.
Parametrize both boolean tests over `[True, False]` to lock that regression
in as well.
Signed-off-by: yetuge <2219677952@qq.com>
---------
Signed-off-by: yetuge <2219677952@qq.com>