415 Commits

Author SHA1 Message Date
chengyongru
96eb965aae
feat(webui): show the actual fallback model (#5017) 2026-07-23 15:57:13 +08:00
chengyongru
c7393c785e
feat(providers): add xAI Grok OAuth with capability-gated X Search (#5035) 2026-07-23 11:55:16 +08:00
chengyongru
c22efb5f7a
feat(agent): make model presets session-scoped (#4866) 2026-07-23 00:38:49 +08:00
chengyongru
66690fdb0c
fix(webui): deliver late subagent results as new turns (#4992) 2026-07-22 23:04:36 +08:00
chengyongru
3748f664b2
feat(config): watch runtime configuration changes (#5026) 2026-07-22 13:08:39 +08:00
Kris Lu
89d8c055a8 fix(providers): sanitize UTF-16 surrogates at provider request boundary
Symptom
-------
LLM requests intermittently fail with:
  'utf-8' codec can't encode characters in position N-N+1: surrogates not allowed
when messages contain emoji-heavy content (e.g. HTML with mixed emoji + JSON round-trips).
This blocks the affected session until the session file is quarantined.

Root cause
----------
Surrogate sanitization was only applied at the CLI entry point
(nanobot/cli/commands.py: _sanitize_surrogates). Requests entering
the LLM provider layer through other channels (Feishu, cron, webui,
tool results, memory injection) had no defensive cleaning, so any
message that happened to carry unpaired UTF-16 surrogates (from an
upstream JSON round-trip with ensure_ascii=True on ill-formed input,
memory rehydration, or third-party content) would blow up at
json.dumps -> HTTP encode time inside the provider client.

Fix
---
1. Extract sanitize_surrogates() and sanitize_surrogates_deep() into
   nanobot/utils/helpers.py as the single source of truth. Both use
   utf-16-le round-tripping with errors='surrogatepass' / 'replace',
   so paired surrogates reconstruct back into their real code point
   and lone surrogates collapse to U+FFFD.
2. Make nanobot/cli/commands.py:_sanitize_surrogates a thin wrapper
   that re-exports the shared helper (backward compatible).
3. Add defense-in-depth at the LLM provider boundary in
   nanobot/providers/base.py:_sanitize_empty_content by running
   sanitize_surrogates_deep over each message and its content blocks
   right before requests are serialized to JSON.

Non-goals
---------
- truncate_text() is intentionally left untouched. Python str slicing
  cannot split a single code point into surrogate halves, so it is
  not the source of lone surrogates.
- session/manager storage layer is untouched. Archived sessions
  reproduced the failure only through the request path, not through
  storage.

Verification
------------
- New regression suite tests/providers/test_sanitize_surrogates.py
  covers: paired surrogate reconstruction, lone surrogate replacement,
  identity return on clean input (zero allocation), deep recursion on
  dict/list/tuple, provider _sanitize_empty_content integration, and
  full utf-8 encodability of the sanitized request body.
- 14/14 new tests pass; full existing test module also green.
- Replayed 58 archived real session messages plus adversarial
  lone-surrogate injection through the provider path with no encode
  errors after the fix.

Impact
------
- No behaviour change for clean inputs (sanitize_surrogates_deep is
  an identity return when no surrogate is present).
- Fails-safe: unpaired surrogates degrade to U+FFFD instead of
  aborting the entire request.
2026-07-21 19:17:58 +08:00
chengyongru
462a0dfb0f
refactor(channels): make built-in channels self-contained (#4908)
* refactor(channels): own setup and instance contracts

* refactor(channels): isolate management contracts

* refactor(channels): normalize activation contracts

* fix(channels): enforce management contracts

* refactor(channels): finish setup ownership migration

* fix(channels): harden management contracts

* fix(channels): enforce lazy loading and runtime ownership

* fix(feishu): make multi-instance startup idempotent

* fix(webui): render channel setup contracts cleanly

* fix(feishu): stop websocket clients cleanly

* fix(channels): enforce persistence and activation gates

* fix(channels): preserve global feature action scope

* fix(channels): apply defaults for single plugins

* fix(channels): enforce management contract boundaries

* refactor(feishu): remove identity helper indirection

* fix(channels): preserve management setup contracts

* refactor(channels): generalize instance settings UI

* refactor(channels): package channel plugins with web UI metadata

* refactor(channels): make built-ins self-contained packages

* test(channels): colocate tests with channel packages

* fix(dingtalk): use official brand icon

* feat(channels): colocate webui translations

* docs(channels): clarify plugin ownership

* test(exec): remove output wait race

* refactor(channels): unify plugin descriptors

* fix(channels): enforce descriptor-owned contracts

* refactor(channels): finish package-owned plugin setup

* refactor(channels): use repository-owned packages only

* fix(channels): self-describe dependencies and runtime state

* fix(channels): warn about legacy entry points
2026-07-19 23:30:49 +08:00
chengyongru
7aaac37bca fix(triggers): require channel enablement predicate 2026-07-19 22:33:29 +08:00
Pei Futong
91514ad0b1 fix(triggers): reject deliveries to disabled channels 2026-07-19 22:33:29 +08:00
chengyongru
d4e0294734 fix(gateway): stop channels before draining tasks 2026-07-15 22:25:08 +08:00
chengyongru
681edfa6f3 fix(providers): honor Codex proxy config consistently 2026-07-15 20:01:48 +08:00
chengyongru
aa70aa48f9
fix(cli): point onboarding to the WebUI launcher (#4938) 2026-07-15 10:34:46 +08:00
chengyongru
63a6d5d07d fix(heartbeat): retain history after empty runs 2026-07-15 01:19:48 +08:00
Arthur K.
dcb37259fa feat(heartbeat): custom evaluator prompt 2026-07-15 01:19:48 +08:00
Olu B
06f47fa540 fix: standardize --config help text across CLI commands 2026-07-14 23:51:39 +08:00
chengyongru
234e895e5a
fix(codex): align OAuth defaults and setup docs (#4910)
* fix(codex): align OAuth defaults and setup docs

* docs(codex): clarify provider settings key

* docs(codex): simplify OAuth setup guidance
2026-07-13 17:04:33 +08:00
Xubin Ren
a65a6f334c test(gateway): cover slow health clients 2026-07-13 15:16:51 +08:00
Xubin Ren
3824206ab2 fix(gateway): harden health endpoint exposure 2026-07-13 15:16:51 +08:00
Xubin Ren
2c78976728
fix(cli): restore enter before webui prompts 2026-07-13 13:32:29 +08:00
Xubin Ren
fe0717b385
feat(webui): add guided setup flows
* feat(channels): add guided setup flows

* test(channels): preserve setup config values

* fix(channels): reflect saved setup state

* refactor(channels): simplify setup state metadata

* fix(channels): harden setup lifecycle

* refactor(channels): centralize setup contracts

* fix(channels): route setup actions through webui shim

* fix(channels): adapt settings for compact screens

* fix(models): preserve default preset display

* feat(models): add curated Codex catalog

* fix(webui): stop attached gateway on interrupt

* fix(webui): simplify apps catalog

* docs(webui): clarify apps and runtime features

* feat(settings): add guided capability setup

* fix(webui): harden setup and managed services

* test: keep managed runtime checks portable

* test: scope POSIX runtime coverage

* fix(webui): simplify file settings

* feat(files): bundle document reading

* fix(webui): harden setup request boundaries

* fix(webui): prevent channel setup status squeeze

* fix(settings): group provider compatibility aliases

* refactor(settings): remove redundant setup surfaces

* fix(webui): harden guided setup lifecycle

* fix(webui): preserve channel setup compatibility
2026-07-13 13:11:46 +08:00
Arthur K.
053722a2a2 fix(heartbeat): rewrite prompt to execute tasks instead of reporting 2026-07-13 12:02:37 +08:00
Xubin Ren
c111aaa7ee fix(dream): gate periodic commits on content changes 2026-07-11 11:16:59 +08:00
Aleksander W. Oleszkiewicz (Alek)
0a1c98c142 Dream jobs don't create git commits if there were no changes
fixes #4872
2026-07-11 11:16:59 +08:00
Eric Yang
a1fbfd9f7b fix(shell): drop gateway-wide waitpid(-1) zombie reaper
A global reaper can race asyncio's child watcher for just-exited
create_subprocess_exec children, causing Process returncode 255 instead
of the real command status. Keep only owned-PID reaps after wait/communicate
/kill paths in shell and exec_session.

Addresses review feedback on #4840.
2026-07-10 20:19:44 +08:00
Eric Yang
c9e014fdea fix(shell): reap zombie processes on all subprocess exit paths
The previous fix (dbcc7cb5) only added os.waitpid() to _kill_process(),
covering the timeout/cancel path of one-shot exec. Zombies continued to
accumulate because several other exit paths never reaped children:

- _ExecSession.kill(): sent SIGKILL + process.wait(5s) but had no
  os.waitpid() fallback if the wait timed out
- ExecTool.execute() generic exception handler: leaked the subprocess
  if communicate() raised an unexpected error
- Normal completion paths: relied entirely on asyncio's child-watcher,
  which can miss exits inside Docker containers (pidfd/SIGCHLD gaps)

Changes:
- Extract _reap_pid() helper for consistent, safe os.waitpid(WNOHANG)
- Add _reap_pid() fallback to _ExecSession.kill() via try/finally
- Add _reap_pid() safety-net after normal process exit in both
  ExecTool.execute() and _ExecSession.poll()
- Kill + reap subprocess in the generic except Exception handler
- Add periodic zombie reaper background task (every 30s) in the
  gateway as a last line of defense
2026-07-10 20:19:44 +08:00
chengyongru
45a6466c2c style: trim CSI-u shortcut comments
Maintainer edit: simplify the explanatory comments for the CSI-u shortcut while preserving the existing behavior and regression coverage.
2026-07-10 19:32:47 +08:00
wangjunwei
99e04c2da2 fix(cli): handle CSI-u Shift+Enter instead of dumping raw escapes
Terminals speaking the CSI-u (kitty / fixterms) keyboard protocol -- kitty,
Ghostty, WezTerm, and terminal panes that default to it -- encode Shift+Enter
as the escape sequence "\x1b[13;2u". prompt_toolkit 3.0 has no support for
that protocol and no default mapping for the sequence, so its Vt100Parser
fails to recognise it and dumps the raw bytes ("^[[13;2u") straight into the
prompt buffer. The multiline-input work kept only Alt+Enter, so on these
terminals Shift+Enter now leaks visible escape garbage into the input.

Register "\x1b[13;2u" (absent from prompt_toolkit's default ANSI_SEQUENCES,
so setdefault() overrides nothing) and bind it to insert a newline, matching
the Alt+Enter behaviour. This is best-effort: prompt_toolkit cannot negotiate
the protocol, so we only react to a CSI-u sequence a terminal already emits;
terminals that collapse Shift+Enter into plain Enter fall back to Alt+Enter,
which stays the primary shortcut.

Add a real-PromptSession regression test asserting the sequence inserts a
newline rather than leaking raw escape bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 19:32:47 +08:00
chengyongru
7768672c5b feat: add file edit diff progress view
Capture file edit snapshots through runner tool lifecycle hooks and render unified diffs in the WebUI with folding and truncation controls.
2026-07-09 10:42:43 +08:00
chengyongru
444f488563 fix(webui): repair bootstrap secret launch paths 2026-07-08 21:01:48 +08:00
chengyongru
88143a8bf0 fix(webui): gate bootstrap API token issuance 2026-07-08 21:01:48 +08:00
Aleksander W. Oleszkiewicz (Alek)
be8ac1e484 removed duplicated code 2026-07-08 20:56:06 +08:00
Aleksander W. Oleszkiewicz (Alek)
f074aa7d80 feat: add --refresh flag to onboard command for non-interactive config updates 2026-07-08 20:56:06 +08:00
chengyongru
883776358e fix: keep local api serve unauthenticated
maintainer edit: Align OpenAI-compatible API auth with the WebSocket channel boundary: loopback serve remains usable without a key, while wildcard binds still fail before agent initialization unless api.api_key is configured.
2026-07-08 12:16:12 +08:00
chengyongru
460c62c0b0 fix: validate api key before serve setup
maintainer edit: Check api.api_key before template sync and AgentLoop construction so missing-key startup errors are not hidden by provider or workspace initialization failures.
2026-07-08 12:16:12 +08:00
hamb1y
6c59332a8a fix: require api key for serve 2026-07-08 12:16:12 +08:00
chengyongru
29e99d3742 docs: document Alt+Enter multiline input
maintainer edit: document the supported interactive CLI multiline shortcut and trim comments left after removing Shift+Enter support.
2026-07-07 15:41:08 +08:00
chengyongru
01a0f5aaf3 fix: remove unreliable Shift+Enter shortcut
maintainer edit: keep Alt+Enter as the supported multiline input path and remove the terminal-dependent Shift+Enter ANSI sequence patch.
2026-07-07 15:41:08 +08:00
wangjunwei
77a6003255 fix(cli): make Alt+Enter insert a newline on LF-as-Enter terminals
On terminals that send a bare LF for plain Enter (WSL is the case
prompt_toolkit itself calls out), Alt+Enter arrives as ESC + LF
("\x1b\x0a" = Escape + ControlJ), not the ESC + CR the existing
"escape","enter" binding matches. The Escape was swallowed and the bare
LF hit prompt_toolkit's default submit, so the documented "universally
supported" Alt+Enter newline fallback failed on exactly the terminal path
plain Enter is preserved for.

Bind ESC + ControlJ to insert a newline too, and add a real-PromptSession
regression test covering the WSL Alt+Enter path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:41:08 +08:00
wangjunwei
25a477050d fix(cli): make the xterm modifyOtherKeys Shift+Enter encoding insert a newline
"\x1b[27;2;13~" (the older xterm modifyOtherKeys / rxvt encoding of
Shift+Enter) is already registered by prompt_toolkit by default -- as
Keys.ControlM, i.e. plain Enter/submit. The previous `setdefault()` call was
therefore a silent no-op against it: this Shift+Enter variant kept behaving
like a submit instead of inserting a newline, even after the ControlJ/WSL
fix, since setdefault only sets missing keys.

Assign directly to override that default for both known Shift+Enter
sequences, since inserting a newline is the whole point of the binding.
Add a regression test driving a real PromptSession/parser with this exact
sequence, since a mocked-buffer test can't observe prompt_toolkit's default
ANSI_SEQUENCES entries taking priority.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:41:08 +08:00
wangjunwei
ea0516e655 fix(cli): stop hijacking ControlJ for Shift+Enter, it breaks Enter on WSL
Keys.ControlJ is also the literal LF byte ("\x0a") that some terminals send
for a plain Enter keypress -- prompt_toolkit's own default bindings handle
this by re-feeding it as ControlM/submit, and calls out WSL by name as the
case that needs it. Binding our Shift+Enter handler to ControlJ shadowed
that default, so on any terminal sending LF for Enter, pressing Enter only
ever inserted a newline and the prompt could never be submitted.

Register the CSI-u Shift+Enter sequences against Keys.ControlF3 instead: an
enum member prompt_toolkit declares but never wires to a default ANSI
sequence or key binding, so it's only reachable through our own mapping.

Also add a regression test that drives a real PromptSession/Vt100Parser
with a raw LF byte -- the existing key-binding test invoked handlers
directly against a mocked buffer, which exercises the handler logic but not
prompt_toolkit's key-resolution precedence, so it couldn't have caught this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:41:08 +08:00
wangjunwei
18a230de75 feat(cli): support multiline input via Shift+Enter / Alt+Enter
The interactive prompt used a single-line buffer (multiline=False), so
there was no way to compose a multi-line message before submitting.

Switch to a multiline-capable buffer with custom key bindings: Enter still
submits (unchanged feel), Alt+Enter always inserts a newline, and
Shift+Enter inserts a newline on terminals that emit a distinguishable
CSI-u sequence for it (kitty, iTerm2 with the fixterms protocol) by mapping
those sequences onto the otherwise-unused ControlJ key.
2026-07-07 15:41:08 +08:00
Xubin Ren
d04ad1a5b4 fix(gateway): resolve runtime config path for state refresh 2026-07-06 15:55:15 +08:00
dajiaohuang
67b56cba74 @
fix(gateway): self-heal state file PID on server startup

After /restart on Windows, the gateway process gets a new PID (os.execv
creates a new process on Windows), but the state file at
run/gateway.<suffix>.json still contains the old PID from the initial
background spawn.  Nothing rewrites it, leaving state inconsistent.

Add GatewayRuntime.refresh_state_pid() — a classmethod that reads the
existing state file, updates the PID and identity to the current
process, and writes atomically.  Call it early in _run_gateway() so the
state file is always correct regardless of how the process was started
(initial spawn, os.execv, or subprocess.Popen).

On POSIX os.execv preserves the PID, so this is a no-op in normal
operation there, but still beneficial after any unusual restart path.

Fixes #4511
@
2026-07-06 15:29:11 +08:00
Kenneth Zhao
105230cc34 fix(cli): print response text when streaming fails in interactive mode 2026-07-06 15:28:10 +08:00
hamb1y
9eade9be5f test: update quick start provider expectations 2026-07-06 12:13:00 +08:00
Kenneth Zhao
f0c989ba2d
fix(dream): ground memory audit records in the real git diff (#4673)
* fix(dream): ground commit messages and cursor advance in the real git diff

Dream consolidation could emit a /dream-log audit record that did not match
the actual file changes: build_dream_commit_message appended the LLM's
unverified resp.content, dream_run_completed only checked the stop reason, and
file contents were deliberately omitted from the prompt. The combination let a
single-turn self-report become the durable audit record.

- gitstore: add summarize_working_tree() — a structured, machine-derived
  summary (per-file +N/-M, totals, capped unified diff) of working-tree
  changes vs HEAD. Pure filesystem/git ground truth, never LLM narrative.
- memory: build_dream_commit_message now takes the diff body instead of resp;
  dream_content_diff() exposes the real delta over SOUL/USER/MEMORY.md only
  (excludes .dream_cursor so cursor writes aren't mistaken for edits);
  build_dream_prompt embeds current file contents so the model edits reality,
  not a stale mental model.
- builtin/cli: both Dream paths now compute the diff, gate cursor advance on
  a non-empty delta (no-op runs no longer swallow history), and commit with
  the diff-grounded message. Non-git workspaces fall back to the completion
  check.
- dream.md: document that contents are embedded, and add a chain-of-
  verification guardrail so the model's summary cannot claim unmade edits.

A regression test proves a lying resp.content never reaches the audit log
while the real diff does.

* fix(dream): mark non-UTF-8 memory files as binary in diff summary

Address review feedback (Q1 on PR #4673): summarize_working_tree read
working-tree files with errors="replace", which would emit U+FFFD
replacement chars into the audit record if a memory file ever held
invalid UTF-8 — misrepresenting the diff it is meant to make truthful.

Switch to errors="strict" and catch UnicodeDecodeError: a non-UTF-8
(or binary/corrupt) file is now recorded as "{path}: binary or
non-UTF-8 file changed" and omitted from the unified diff, so the
audit record stays honest. An empty diff block is also suppressed when
all changes are binary.

Adds a defensive regression test asserting no replacement char leaks.
2026-07-06 12:12:55 +08:00
axelray-dev
6a95877196 fix(providers): standardize oauth_cli_kit error messages across CLI and WebUI 2026-07-06 12:11:03 +08:00
Xubin Ren
067e0c4a40
feat(cli): add safe WebUI first-run launcher (#4688) 2026-07-03 18:58:04 +08:00
chengyongru
5283ceae85
Add optional Nanobot plugin controls (#4396)
* feat: add optional nanobot features

* test: update azure install hint expectation

* fix: validate optional feature extras

maintainer edit: verify requested dependency extras before treating optional features as installed, propagate restart state from feature enablement, and align docs with the new plugins enable command.

* fix: bound optional feature installs

maintainer edit: make optional feature installs time out as a normal install failure instead of leaving the WebUI or CLI action waiting indefinitely.

* feat: slim optional channel dependencies

* fix: log optional install commands

* fix(webui): gate remote feature installs

* docs: clarify webhook plugin example

* fix(webui): harden optional feature installs

* fix: install optional deps without package fallback

* fix(cli): refine plugin feature controls

* fix(webui): count enabled nanobot features

* fix(webui): allow slow feature install routes

* fix(webui): allow disabling websocket channel

* fix(plugins): simplify optional feature controls

* fix(webui): polish apps catalog states

* fix(webui): confirm nanobot support installs

* fix(webui): polish nanobot install dialog

* fix(webui): suppress empty websocket handshakes

* fix(webui): clarify apps plugin summary

* fix(webui): localize workspace access copy

* fix(plugins): polish optional feature controls (#4691)

---------

Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
2026-07-03 18:17:52 +08:00
chengyongru
acb0e853ff refactor(trigger): share automation turn delivery 2026-07-02 13:32:46 +08:00