2599 Commits

Author SHA1 Message Date
Xubin Ren
835bab5f5a fix(exec): stabilize Windows shell tests 2026-05-21 16:10:09 +08:00
Xubin Ren
ccbc0bb6e3
Merge PR #3923: feat(tools): optimize coding workflows
feat(tools): optimize coding workflows
2026-05-21 15:55:13 +08:00
Xubin Ren
722b760eae feat(webui): stream apply patch edit progress 2026-05-21 15:44:01 +08:00
Xubin Ren
23d5148a57 fix(provider): dedupe repeated tool ids in history 2026-05-21 15:33:49 +08:00
Xubin Ren
d29fcaf5d1 refactor(agent): internalize tool contract prompt 2026-05-21 15:21:39 +08:00
Xubin Ren
581faa34f7 Merge remote-tracking branch 'origin/main' into codex/coding-tooling-optimization 2026-05-21 14:44:56 +08:00
Xubin Ren
7e3af8c38b docs(tools): add general tool workflow contract 2026-05-21 14:44:34 +08:00
Haisam Abbas
e645fbcb34 fix shell guard url path detection 2026-05-21 14:42:11 +08:00
Xubin Ren
4f895e6307 refactor(providers): centralize gateway reasoning control 2026-05-21 14:41:50 +08:00
olgagaga
0cd2f626c0 fix(providers): inject OpenRouter reasoning.effort for thinking models
Follow-up to #3851: that PR added `extra_body.thinking={type: disabled}`
for MiMo via OpenRouter, but OR doesn't forward provider-specific
thinking shapes to upstream — it strips unknown extra_body fields and
uses its own unified `reasoning` parameter. So MiMo via OR kept
thinking despite the injection (reproduced by @ClearPlume on #3851
with identical kwargs but provider switched from openrouter → xiaomi_mimo).

For known thinking-capable models (Kimi, MiMo) routed via the
openrouter spec, also inject `extra_body.reasoning = {effort: <effort>}`
in OR's documented enum ("none"|"minimal"|"low"|"medium"|"high"|"xhigh").
OR translates this to the upstream model's native shape.

Existing tests updated to expect both fields on the OR path. The direct
xiaomi_mimo and moonshot paths are unchanged (the new branch is gated
on spec.name == "openrouter"). Flash and non-MiMo models on OR continue
to receive no injection.
2026-05-21 14:41:50 +08:00
Xubin Ren
44ef697aac docs(tools): clarify coding tool guidance 2026-05-21 14:28:39 +08:00
chengyongru
e2b51fa5dc fix(weixin): prevent silent message drops from poll exceptions and expired tokens
- Remove suppress(Exception) from poll loop and message processing; add
  logger.exception so inbound errors are visible.
- Check both ret and errcode on send to avoid silent drops when iLink
  returns ret != 0 with errcode == 0.
- Proactively refresh context_token via getconfig before sending if the
  cached token is older than 60s. This prevents message loss on long
  agent turns and cron pushes without relying on complex retry logic.

Refs: openclaw/openclaw#61174, NousResearch/hermes-agent#21011
2026-05-21 13:41:05 +08:00
Xubin Ren
7e122d6e49 chore(tools): merge main and resolve conflicts 2026-05-21 12:53:42 +08:00
hanyuanling
de0a8f5e41 fix(webui): keep new chat during session refresh 2026-05-21 12:42:56 +08:00
Xubin Ren
3d3ebf1110 test(provider): cover duplicate streaming tool call ids 2026-05-21 12:28:24 +08:00
chengyongru
77ec55bf8e fix(provider): deduplicate streaming tool_call_ids for parallel calls 2026-05-21 12:28:24 +08:00
Xubin Ren
8141df0d3f fix(tools): stabilize session output test 2026-05-21 01:32:27 +08:00
Xubin Ren
5f0ba05de5 feat(tools): tighten patch and session workflows 2026-05-21 01:25:20 +08:00
chengyongru
886e7e43d5 fix(signal): bypass base is_allowed for policy-approved messages
Override _handle_message to publish directly to the bus for messages
that have already passed _check_inbound_policy. The denied DM pairing
path calls super()._handle_message() to issue pairing codes via the
base class. This avoids cross-policy leakage where e.g. group open
policy would cause is_allowed to incorrectly allow denied DM senders.

Also includes:
- SSE: strip one optional leading space after 'data:' per spec
- Convert 20+ f-string log calls to loguru lazy formatting
- Add end-to-end tests for DM/group routing through the full chain
- Add cross-policy test (dm allowlist + group open) for pairing
- Add Signal channel documentation to docs/chat-apps.md
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
b3d0d24a52 fix(signal): consult pairing store in is_allowed
BaseChannel.is_allowed ORs is_approved (the pairing store) into the
allow decision; the signal override dropped that step and only looked
at config.allow_from. With the new DM-pairing flow in place, an
approved-via-pairing sender's next message would have failed the
allow check and triggered another pairing code in a loop.

OR in a normalized check against the pairing store: walk each part of
the pipe-joined sender_id through _normalize_signal_id and call
is_approved for each variant, so an approval stored under one form
(phone with/without "+", UUID/ACI) still matches when the next inbound
uses a different form. Mirrors how slack.py:643 handles it.

Also tightens the empty-allowlist warning to only fire when nothing
else granted access, since pairing-store hits are now a valid path.

Not part of the original review, but Comments 2 and 3 turn this latent
gap into a broken round-trip — included so the pairing UX actually
works.
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
82dfe8c1f7 fix(signal): join multi-line SSE data with newline per spec
Per the SSE spec, multiple data: lines within a single event must be
joined with \n before parsing. signal-cli emits single-line JSON so
this was latent, but the joining was wrong.

Addresses review comment on PR #3852.
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
dc33247671 fix(signal): route denied DMs through _handle_message for pairing code
Previously _check_inbound_policy returned (False, chat_id) for DMs
that failed the allowlist and the caller dropped them — so unapproved
DM senders never saw a pairing code. Mirror Slack: when the policy
gate denies a DM but dm.enabled is true, still call
_handle_message(content="", is_dm=True) so BaseChannel can issue the
pairing reply. Group denials stay a hard drop.

Combined with the previous is_dm forwarding, unapproved DM senders
now receive a pairing code through the standard flow.

Addresses review comment on PR #3852.
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
d376ec129d fix(signal): pass is_dm to _handle_message so DM pairing flow runs
BaseChannel._handle_message uses is_dm to decide whether to issue a
pairing code when is_allowed rejects the sender. Without it the base
class treats every denied message as a group message and silently
drops it. Forward is_dm=not is_group_message so unapproved DM users
get a pairing code through the standard flow.

This change only takes effect once denied DMs actually reach
_handle_message (next commit); on its own it is a no-op since the
policy gate still short-circuits before this call.

Addresses review comment on PR #3852.
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
d653f23aba fix(signal): raise on signal-cli error response so send is retriable
_send_http_request collapses every exception path into a {"error": ...}
dict, so the if "error" in response branch inside send() is the only
place where send failures surface. Logging-only there meant the
ChannelManager retry mechanism never fired. Raise RuntimeError so the
base-class retry path is exercised; the outer try/except already
re-raises into the caller.

Addresses review comment on PR #3852.
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
96767ca179 Cleanup 2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
b300ea495f fix(signal): normalize composite sender_ids in is_allowed too
The base BaseChannel.is_allowed() does a literal ``sender_id in allow_from``
check, but Signal's sender_id is a pipe-joined composite of phone/UUID
parts. After splitting an allowlist entry like ``+phone|uuid`` into two
separate entries, the per-DM gate accepted it but the base gate still
denied because the composite sender string wasn't literally in the list.

Override is_allowed on SignalChannel to delegate to
_sender_matches_allowlist, which already splits both sides on ``|`` and
normalizes each part. _sender_matches_allowlist itself now also splits
allowlist entries on ``|`` so legacy composite entries keep working too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
632f41e418 test(signal): cover markdown adjacency, nesting, and malformed input
The existing markdown suite was strong on UTF-16 offsets and chunk
redistribution but had no coverage for nested or adjacent styles, no test
that an unmatched opener round-trips as plain text, and no test for the
blockquote/inline-code interaction. Add six cases including the
documented contiguous-BOLD output for `# **wrap** me`, which Signal
renders as one visual span.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
9c486b90d5 test(signal): consolidate channel-capture setup into one factory
Two test classes (TestHandleDataMessageDM, TestHandleDataMessageGroup)
plus three TestCommandHandling tests each repeated the same handful of
lines: build a channel, mock _handle_message to record kwargs, replace
_start_typing with a no-op, paper over the assignment with type: ignore.

Hoist the pattern into _make_channel_with_capture and call it from all
five sites. Drops 30+ lines of duplication and 7 type: ignore comments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
590ac99c8a test(signal): cover SSE receive loop and the empty-phone start guard
Previously the SSE loop and the empty-phone-number short-circuit in start()
had zero coverage. Both now have tests: a fake httpx stream feeds canned
SSE lines, exercising the valid-frame, invalid-JSON, non-200, and
no-http-client paths; start() with an empty phone number is asserted to
return without entering the HTTP loop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
7733a7840e refactor(signal): split _handle_data_message into policy and assembly helpers
The receive-path handler was ~165 lines deep into nested DM/group policy
checks, buffer mutations, mention stripping, attachment downloads, and
final bus forwarding. Pull the policy gate out into _check_inbound_policy
(returns (allow, chat_id), still appends to the group buffer once allowed)
and the text+media construction into _assemble_inbound_content. The
top-level method now reads as orchestration only.

Add TestCheckInboundPolicy that exercises the helper directly across the
DM/group policy permutations, including the buffer side effect, so the
new seam is locked in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
83aed43682 feat(signal): make signal-cli attachments directory configurable
The inbound attachment loop hardcoded ~/.local/share/signal-cli/attachments
as the source path. That is the daemon's default on Linux but not on macOS
or Windows, and breaks if the daemon was launched with XDG_DATA_HOME set.

Add SignalConfig.attachments_dir as an optional override. When unset the
behavior is unchanged; when set the value is run through Path.expanduser()
so ~ is honored.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
ad7c1ac381 refactor(signal): wrap top-level receive handler with _safe_handle
Replace the inline try/except at the end of _handle_receive_notification
with a small async context manager that swallows the exception, logs
self.logger.error with the offending payload's repr (bounded to 200 chars),
and attaches the traceback via logger.opt(exception=True).

The previous log line only carried `e`, so diagnosing a bad envelope from
production logs required correlating timestamps. The wrapper is generic so
future receive/dispatch sites can adopt it; for now only this site uses it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
882d4139d7 fix(signal): normalize identifiers when matching DM allowlist
The DM allowlist check split sender_id on '|' and looked for raw membership
in the allow_from list. Senders carry their phone number with a leading
'+' but admins routinely write allowlist entries without it (or vice
versa), and UUID/ACI matches were case-sensitive. Both forms now flow
through _normalize_signal_id, so an entry like 19995550001 matches a
sender +19995550001 and a UUID matches case-insensitively.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
ca72f6b6c9 refactor(signal): hygiene cleanups around constants, typing, and config
- Hoist the cell-strip patterns to module level so they match the rest of
  the module's regex style and aren't reparsed on every call.
- Type the markdown transform callback and the mention id walker so the
  inline Callable signature is no longer an untyped Any.
- Add _HTTP_TIMEOUT_SECONDS alongside the other class-level tunables.
- Reject group_message_buffer_size <= 0 in a Pydantic field_validator
  rather than silently disabling the buffer at write time.
- Mark SignalConfig.allow_from as a computed_field so it shows up in
  model_dump() instead of being invisible to serialization.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
96eb3b7194 fix(signal): redistribute textStyle ranges across split message chunks
split_message can break a long Signal payload into multiple JSON-RPC sends,
but the previous code attached the full textStyle list only to chunk 0.
Style ranges in later chunks were dropped, and ranges whose offsets pointed
past chunk 0's end were sent as invalid metadata against chunk 0.

Add _partition_styles, which rebases each range against the chunk it lives
in (in UTF-16 code units, matching the markdown converter) and splits
boundary-spanning ranges across the chunks they touch. Whitespace trimmed
by split_message's lstrip is skipped so offsets stay aligned.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
8f6b7611a2 fix(signal): emit textStyle offsets in UTF-16 code units
Signal's BodyRange (via signal-cli's textStyle) interprets start/length as
UTF-16 code units, but the Phase-3 assembly used Python's len(), which counts
code points. A single non-BMP character (e.g. an emoji) earlier in a message
shifted every subsequent styled span left by one unit, dropping the last
letter of bold/italic words.

Track a running UTF-16 offset in the assembly loop and add regression tests
covering emojis, supplementary CJK, ZWJ sequences, and a multi-section
message that mirrors the reported failure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
1a6fe093e7 fix(signal): drop duplicate self in unconfigured-account log call
Addresses review feedback on HKUDS/nanobot#3852: self.self.logger.error
would crash if the phone_number guard ever fired.
2026-05-21 01:00:36 +08:00
Kaloyan Tenchov
8ec1025193 feat(signal): add Signal channel support
Integrates signal-cli daemon via HTTP JSON-RPC as a nanobot channel.
Supports DMs and group chats with open/allowlist access policies,
markdown→Signal text style conversion, typing indicators, attachment
handling, group message context buffering, and automatic reconnect
with exponential backoff.

Includes unit tests for channel lifecycle, message routing, mention
detection, markdown conversion, and message splitting.

Originally based on https://github.com/HKUDS/nanobot/pull/601.
2026-05-21 01:00:36 +08:00
Xubin Ren
480ca28a2d feat(tools): improve coding workflow recovery 2026-05-21 00:58:05 +08:00
Xubin Ren
3e154bb5cf fix(tools): align exec platform test doubles 2026-05-20 23:42:55 +08:00
Xubin Ren
6851fa57a6 feat(tools): optimize coding workflows 2026-05-20 23:08:21 +08:00
chengyongru
09a692be6f docs(readme): add multi-language doc site links
Link nanobot.wiki documentation in 10 languages from README header:
English, 简体中文, 繁體中文, Español, Français, Bahasa Indonesia,
日本語, 한국어, Русский, Tiếng Việt.
2026-05-20 22:37:11 +08:00
Xubin Ren
eae51333ad fix(providers): point Skywork at APIFree agent endpoint 2026-05-20 12:33:03 +08:00
moran
6194a9b919 docs(configuration): fix APIFree formatting — merge wrapped description into single line 2026-05-20 12:33:03 +08:00
moran
61ae869610 feat(providers): add APIFree support
Add APIFree as a built-in OpenAI-compatible provider. APIFree offers
agent-optimised models such as skywork-ai/skyclaw-v1 through an
OpenAI-compatible API at https://api.apifree.ai/agent/v1.

Changes:
- Register apifree provider in the provider registry
- Add config schema field
- Add documentation with configuration example
- Add provider tests, websocket channel tests, and webui tests
- Add provider icon in settings UI
2026-05-20 12:33:03 +08:00
Xubin Ren
3eebe08dba fix(exec): detach stdin for shell commands 2026-05-20 12:07:17 +08:00
Xubin Ren
38a5f09f02 refactor: preserve cold-start lazy boundaries 2026-05-20 12:02:23 +08:00
chengyongru
af9f8d54b8 perf: optimize gateway cold start from ~6.9s to ~460ms (#3918)
Channel lazy load: discover_enabled() only imports enabled channel
modules instead of all 18 modules with heavy SDKs (telegram, discord,
slack, etc). discover_all() now delegates to discover_enabled().

Lazy OpenAI client: defer AsyncOpenAI() + httpx construction to
_ensure_client() with asyncio.Lock double-checked locking. openai
and httpx imports moved from module-level into _ensure_client().

Minor: lazy Nanobot/RunResult and CronService exports via __getattr__.

Benchmark: 6910ms → 460ms (-93.3%)
2026-05-20 12:02:23 +08:00
Xubin Ren
1391aa3d57 fix(tests): make settings workspace path portable 2026-05-20 02:20:44 +08:00
Xubin Ren
e00220bdb6 feat(providers): add Skywork provider support 2026-05-20 02:20:44 +08:00