Skip to content

chore: release 1.0.2b6 - #1612

Merged
jubaoliang merged 59 commits into
mainfrom
release/1.0.2b6
Oct 4, 2026
Merged

jubaoliang merged 59 commits into
mainfrom
release/1.0.2b6

Conversation

@jubaoliang

@jubaoliang jubaoliang commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

[1.0.2b6] - 2026-10-04

新增

  • Agent Mail 连接器(授权、邮件工具、新邮件任务)
  • LDAP 目录登录
  • 专家默认对话模式;输入栏「+」菜单;未开审批时隐藏批准入口
  • 飞牛 ARM 安装包与多架构镜像

变更

  • octop-harness 升到 1.0.1;飞牛安装改为建账号向导

修复

  • 误报流式失败、提问卡不弹出、TLS 下内部 MCP、过长工具名
  • 远程存储卡住堵住启动;S3 / Postgres 浏览;桌面 beta 覆盖安装;飞牛 8089 残留
  • Windows 全盘存储根、邮箱非 ASCII 头、缺失界面文案
  • PWA 诊断页在手机上可滚动

Release checklist

  • CI green
  • Merge this PR into main
  • After merge, GitHub Action auto-pushes v1.0.2b6 tag on main tip

github-actions Bot and others added 30 commits September 27, 2026 02:21
….0.2b3

chore: sync main into develop after 1.0.2b3
Align EN/CN README with shipped features and current install/channel
docs; fix GitHub heading anchors and drop obsolete extras examples.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Dark-theme surfaces that use var(--fn-bg-container, #fff) — most
visibly the knowledge-base Markdown preview body, toolbar and outline
panel — resolved to a white background while text stayed light, leaving
preview content invisible. Define the variable as #ffffff (light) and
#141414 (dark), and point the Admin/Users badge tints that borrowed the
name for a translucent fill at --fn-bg-tertiary so their look is
unchanged.

Fixes #1215
Store group order, item placement, and hidden entries on the user so navigation can be rearranged without showing items the account cannot access.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…tter (#1222)

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…#1220)

Collapse the dual user_role_id / role model so users.role and invites.role
hold the template public id, block deleting roles still in use, and keep
the chat composer visible under mobile browser visualViewport.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Link labels now match the product names while the repository URLs stay unchanged.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Marketplace plugins written before the octop_harness rename fail to install
and enable with ModuleNotFoundError. Alias that import onto the installed package.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
#1255)

POST /workspace/archive writes archive entries straight into the
workspace; _safe_zip_name only blocks traversal, so a zip could plant
files under _builtin_skills/ and .octop/_builtin_skills/. That prefix is
owned by Octop: DELETE and POST /workspace/move refuse it via
_assert_workspace_mutable and sync_octop_builtin_skills only prunes its
own retired names, so planted skills both load as kind=builtin and stay
irremovable through the API. This restores the #1105 guard (filter plus
warnings reporting) together with its regression test, which a later
"restore prior behaviors" commit had removed along with the filter.

Fixes #1254

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
About 30 --fn-* custom properties were referenced across dashboard
styles but never defined in theme-vars.css. References without a
fallback resolved to invalid-at-computed-value-time (transparent
surfaces, e.g. the Connectors page sunken wells), and fallbacks were
often light-only literals that broke the dark theme. Define the
missing tokens for both themes, aligned with their nearest existing
design-system values, and fix the misspelled --text-secondary /
--border-color references that could never resolve.

Fixes #1239
* feat(ollama): allow configuring local model download directory

Users who move Ollama models off the default path could list custom models
but Octop treated them as not downloaded. Persist an OLLAMA_MODELS directory,
scan manifests for downloaded names, and pass the path when starting serve.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ollama): keep service running when saving models dir

Saving the download directory no longer sends enabled=false, so a running
Ollama daemon is not stopped. Disk-scanned models report size 0 so the UI
does not show the manifest file size as the model size.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…1311)

Give the bundled assistant a product overview plus links to octop.cloud and the docs, and keep CLI setup for changes on the running instance.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ollama): list local models even when Octop service is off

Windows users often run Ollama themselves. Octop only queried the daemon
when its managed-service toggle was on, so already-pulled models showed
as not downloaded. List from a reachable daemon without starting serve,
and mark IDs from fetch-models as downloaded.

* docs: note Ollama downloaded-model detection when the service toggle is off
`_stream_frames` (the mobile JPEG fallback) retried forever: on `TimeoutError`
it logged and `continue`d, and on `captured is None` it just `continue`d. A
phone unplugged mid-stream, or a dead adb bridge, makes every capture fail at
once, so the loop spun at the frame interval indefinitely — a warning per
iteration, a log flood, and a canvas frozen on the last frame with nothing ever
sent to explain it.

The desktop sibling `desktop/stream.py::_stream_loop` already counts consecutive
misses, warns once, and after 30 sends an error frame and breaks. This mirrors
that: timeouts and `None` both count as misses, the first one is logged once,
and at `_CAPTURE_MISS_LIMIT` (30) the stream sends the
`{"type": "error", "message": ...}` frame the rest of this router already uses
and returns. A successful frame resets the streak, so an occasional dropped
frame still does not tear the stream down.

Folding the timeout branch into the shared counter also removes the
per-iteration warning flood.
…rs (#1307)

`octop.cli.support.state` keeps a private reader/writer for
`~/.octop/cli_state.json` and both halves are looser than the rest of the tree.

`load()` parses with a bare `json.loads(path.read_text(...))`, so an
unparseable file raises a `json.JSONDecodeError` traceback. This file is read by
nearly every CLI command (`config`, `agent`, `db`, `acting`, `ctx`, the REPL),
so one damaged file aborts all of them at once.

`save()` writes with `path.write_text(...)` — truncate, then write. A crash or a
full disk mid-write leaves exactly the truncated file that `load()` then
refuses, with no warning and no `ensure_root()` in between.

`infra/utils/json_file.py` exists for this; its docstring is explicit that a
torn write "creates the same precondition" as a hand-edit slip and that callers
"must not fall back to an empty dict". Four call sites already route through it
(`cli/commands/run.py`, `agents/plugins/manager.py`, `agents/plugins/seed.py`,
`setup/service.py`).

`load()` now reads via `read_json_object` and translates `JsonFileCorruptError`
into `corrupt_config_error(...)` (`CONFIG_FILE_CORRUPT`) — the same policy as
every other `OCTOP_HOME` JSON reader. `save()` writes via `write_json_atomic`
(mkstemp + `os.replace`) so a reader never observes a partial file. A non-object
payload such as `[1, 2]` also raises the typed error now, instead of a stray
`dict.update` `TypeError`.
OCTOP_LANGFUSE_ENABLED is set by .env.example and forwarded by docker-compose.yml, but nothing reads it: Octop keeps the switch in the settings table (observability_langfuse_enabled) and the langfuse client honours LANGFUSE_TRACING_ENABLED. Add a test that fails while a shipped Langfuse variable has no reader.
media/preview streams user-controlled bytes (chat attachments, tool
outputs) inline on the dashboard origin. image/svg+xml is previewable,
so a navigated preview URL executed as a live document on the app
origin and could run attacker script — the endpoint also accepts the
access_token query parameter for media loads. Serve previews with
Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff;
image/video previews keep working.

Fixes #1242

Co-authored-by: jubaoliang <jubaoliang@gmail.com>
* feat: add Octop↔Octop Bridge for remote agent access

Enable admin-managed peer connections with WS tunnel, path allowlist,
hello_ack handshake, SSRF guards, and Chat shadow-agent hydrate so
operators can probe and use remote agents from the local dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: serialize bridge turn chunks and flag remote experts

LangChain HumanMessage objects in peer turn.chunk frames crashed
json.dumps; add a bridge JSON default. Show a 远端/Remote badge on
shadow experts in the picker and sidebar lists.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: bridge avatars, named remote badges, and auto-reconnect

Keep bundled / CDN icon URLs for shadow experts and only proxy uploaded
avatars. Show the bridge display name on remote badges. Add a default-on
auto-reconnect switch that backs off on disconnect and disables itself
after repeated failures, recording the reason in last_error.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add card/table views for bridge connections

Match the storage page card layout and add a Segmented toggle so
admins can switch between a card grid and a table of remote links.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: render bridge expert icons correctly on the chat page

Encode bridge agent ids in avatar URLs the same way as chat WS paths,
fall back to Lucide when the image fails, size portraits to fill the
sidebar avatar, and refresh AgentContext when icon_url changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: tunnel peer models and knowledge bases for bridge chat

Allow read-only GET of peer providers/resolved, active-model, and
knowledge-bases (plus capability) through the Bridge tunnel, expose
local bridge connection routes, and point the chat composer at those
sources when talking to a shadow expert. Hide local connectors for
remote sessions. Also allow agent subagents paths on the tunnel.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: scope bridge chat expert picker to peer agents

When the active expert is a Bridge shadow, the composer expert picker
and @-mentions only list peers on that connection (not local experts).
Encode bridge agent ids for skills and subagents list/install paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: hint remote chat pickers to manage settings on the peer

In Bridge sessions, replace local “manage …” footers for models,
skills, knowledge bases, experts, and subagents with a muted
“edit on remote” tip and a toast instead of navigating to local admin.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: polish bridge remote UX and tunnel agent status

Allowlist GET …/status so hubs can read real peer harness state (peers need
this build). Keep history-migration hub-local, skip noisy bridge polls in the
UI, move Bridge under Settings, and mark remote experts with cable + name.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: tunnel remote expert catalogs and polish bridge connection UX

Hub can edit peer subagents, memory, channels, tools, plugins, and MBTI
through the tunnel; surfaces that cannot hop show an in-drawer hint. Peer
must run this build so the new allowlist takes effect.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: group cloud-collab experts and unify header-tunneled composer APIs

Keep the selected remote expert on disconnect, gate peer-only surfaces in the UI, and stop forking composer/task requests through dedicated bridge endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add search to the Experts toolbar

Filter my experts, teams, and the library from the existing button row so long catalogs stay scannable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: keep inbound cloud-collab cards peer-owned

Inbound links can only edit name/icon/notes; disconnect, delete, and auto-reconnect stay disabled. Peer offline shows as 离线, peer delete removes the card, and the default inbound name is the short connection id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: polish inbound cloud-collab UX and unify 对端 copy

Peer-initiated links can only edit display info and cannot redial;
offline cards may be deleted. Local-only pages hint when a peer
expert is selected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: map peer team speakers onto Bridge shadows

Peer team chats stamped local member ids, so the hub fell back to the host
avatar. Rewrite roster and stream speakers onto bridge:{cid}:{id} shadows,
and keep the local team picker from mixing in peer experts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: stop rewriting team thread ids in the Bridge tunnel

Member job threads are {room}~{agent}. A blanket JSON replace of the peer
agent id turned those into hub shadows, so history 404ed on the peer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: rewrite peer media URLs and keep bridge turn composer fields

Live send_file_to_user frames still pointed at peer-local /api/agents/{id}.
Map every teammate id in tunneled JSON, and copy the full dashboard turn
body so knowledge bases and HITL policy are not dropped on the peer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: rename bridge probe tests to avoid pytest module clash

tests/unit/bridge/test_probe.py collided with tests/unit/mobile/test_probe.py
during collection (same basename).

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: collapse extra experts on Bridge cards and fill avatars

Show at most four experts on a connection card, with the rest behind
expand. Portraits fill the 40px ring without a tinted background.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: lulinzhi37-alt <lulinzhi37@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
….0.2b5

chore: sync main into develop after 1.0.2b5
* fix(update): load prerelease changelog from versioned PyPI JSON

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(fnos): retry and cache fnpack downloads

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
skill_packages gains copy_policy (snapshot default / lock / deny), set by the
creator or admin via PATCH. deny rejects the workspace copy endpoint with
SKILL_PACKAGE_COPY_DENIED and disables the dashboard button (can_copy). lock
stamps each copied SKILL.md with origin/locked frontmatter; the existing
package-only write guard now also rejects writes to stamped copies for
everyone but the origin package creator (admins keep an escape hatch for
orphaned copies). Copy attempts are audited. Agent-tool-level write
protection for locked copies needs harness support, tracked separately.

Co-authored-by: vicfei <4058491+vicfei@users.noreply.github.com>
httpx 0.28 treats 192.168.0.0/16 as the exact IP 192.168.0.0, so LAN
hosts hit HTTP_PROXY. Also split Windows semicolon lists, parse IPv6
CIDR, and bypass loopback under macOS/Windows system proxies.

Fixes #1347

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: jubaoliang <jubaoliang@gmail.com>
技术报告 TECH-REPORT-qq-mail-header-bug.md 的复现与修复:

- 根因:email.message_from_bytes 默认 compat32 策略下,邮件头含裸
  非 ASCII 字节时 get() 返回 email.header.Header 对象,直接传入
  json.dumps 抛 TypeError: Object of type Header is not JSON
  serializable,真实邮箱场景 100% 必现
- 修复(报告方案 B):新增 _parse_message() 统一以
  email.policy.default 解析,_safe_header() 强制 str();所有面向
  JSON 的头字段(From/Subject/Date)走统一封装
- 附带收益:规范 MIME 编码头(=?utf-8?q?...?=)现在解码为可读
  中文,而非原始编码串
- _extract_body:声明字符集无已装 codec(如 unknown-8bit/伪造名)
  时回退 UTF-8(errors=replace 只挡字节错误,挡不住 LookupError)
- 新增 13 项回归测试:裸 UTF-8 头、MIME 编码头、破损编码字、
  unknown charset 回退、multipart、纯 ASCII 不回归;已在未修复
  代码上验证测试会失败(TypeError)

Co-authored-by: jubaoliang <jubaoliang@gmail.com>
extra stays empty on darwin-arm64, linux-* and windows-amd64 because
write_green_overrides only emits an override file for windows-arm64 and
darwin-amd64. "${extra[@]}" under set -u aborts on the macOS default
/bin/bash before uv ever runs, so `make -f desktop/portable/Makefile
wheels` fails on the host platform. Use the guarded expansion form the
repo already applies in scripts/install.sh.
The detail drawer and the copy modal both fetch the same endpoint and both
wrote their response unconditionally, so a slow click on package A could
overwrite a later click on package B: the drawer showed the wrong skills and
the copy action posted the wrong package id. Reuse the existing
createDetailRequestGate helper so only the newest request per surface lands.
The cache file is shared by every official account but the record only held
the token and its expiry, so the reuse check looked at time alone. After
switching WECHAT_APP_ID the publisher kept sending the previous account's
token for up to two hours: draft/add succeeded against the wrong account, the
command exited 0, and it still printed a successful publish. Tag each record
with the AppID it was issued for and refuse to reuse a token that was not
fetched for the account currently configured.
c020627 and others added 22 commits September 30, 2026 15:13
`streamMobileSse` reports completion through `onDone`, but it was the only
stream in `api/modules` that could end without calling it - and the only one
that could call it twice.

* The read loop breaks on EOF and the `.then` body just returns. When the body
  closes without a terminal frame the callback never fires; `desktop.ts` and
  `browser.ts` both settle after their loop. Mobile can really truncate:
  `infra/mobile/setup.py` spawns `bash` with no guard, so a host without a
  shell raises before the router can write its `{"done": ...}` frame.
* A terminal frame did not stop the loop, so any later frame fired `onDone`
  again. `desktop.ts`/`browser.ts` `return` straight after settling.

`pages/Control/RemoteAndroid` only leaves its "installing" phase inside
`onDone`, so the first case leaves the panel spinning with the install button
disabled and no toast, and the second runs the completion path twice.

Both paths now funnel through a one-shot `settle()`.
…lient (#1308)

`_fetch_ranking_json` was the only fetch in this module that hand-rolled
`urllib.request.urlopen` instead of calling `_http_request`.

`response.read()` therefore buffered the whole body with no size limit, where
`_http_request` checks `Content-Length` and then reads in `HTTP_READ_CHUNK`
slices up to `MAX_HTTP_BYTES` — the search path already passes a cap.

Its `except` ladder also stopped at `HTTPError`/`URLError`/`TimeoutError`, so a
mid-stream `ConnectionResetError` or an `http.client.HTTPException` escaped as a
raw exception. The endpoint that serves rankings (`routers/skills.py::
_hub_rankings`, used by `GET /skills/hub/rankings` and
`GET /agents/{id}/skills/hub/rankings`) only maps `SkillHubMarketError` to 502
and `SkillHubMarketTimeout` to 504, so an upstream hiccup became an opaque 500.

Rankings now go through `_http_request` like `_fetch_search_json` and
`_download_skillhub_package` already do. The 4 MiB JSON cap is named
`_MAX_JSON_BYTES` and used by both JSON fetches instead of repeating the literal.

`_Response` in `tests/unit/agents/test_skillhub_market.py` is the stub that
`test_fetch_ranking_json_uses_showcase_endpoint` hands back from `urlopen`, so it
has to accept the `size` argument `_http_request` passes. It now wraps the
payload in `io.BytesIO` and reads exactly like `_BytesResponse` next door, which
also makes the read loop stop on the first empty chunk instead of spinning.
* fix(dashboard): add the missing locale entries for t() keys

About 200 t() keys used across the dashboard were absent from
locales/en.json (and 113 from locales/zh.json). i18next fell back to
the call-site defaults, so the English UI rendered large chunks of
hardcoded Chinese (memory panel, connectors onboarding, skill
recording guide, proactive-care config, ...) and a few spots showed
raw key paths; the en/zh parity checks could not catch it because
both bundles missed the same keys. Add the missing entries to both
bundles and switch the two skill-record call sites whose defaults
interpolated runtime values to real i18next interpolation.

Fixes #1238

* fix(dashboard): align zh memory strings with current in-code defaults

Per review on #1241: once these keys land in the bundle, i18next stops
using defaultValue, so zh must match the current UI defaults.

* fix(dashboard): mention the English stop keyword in the skill-recording guide

skillRecordGuide.step4Desc only told English users to type "结束" to stop
recording, but END_KEYWORDS also accepts "end" and "stop recording" — users
following their own language had no way to discover the keyword from the guide.
Mirror the existing bilingual keyword style (e.g. skills.skillNamePlaceholder)
and list the English variant next to the Chinese one.

Re-applied on top of the rebuilt branch: upstream develop was rewritten, and
this change had been part of the previous (now superseded) rebase.
Co-authored-by: jubaoliang <jubaoliang@gmail.com>
…ows (#1429)

The picker derived a single browse root from `Path.home().anchor`, so a
user whose profile lives on `C:` could not reach `D:` at all. That was
structural, not a policy filter: `host_fs_tree_root()` returns one path,
`/api/filesystem/defaults` hands it over as `tree_root`, and
`rootDirTree.sanitizeTree` keeps exactly one tree and returns
`[cleaned]` - every sibling of that root was dropped on each update, so
even a hand-injected `D:/` node could not survive. `/filesystem/dirs`
only ever listed children of `C:/`, and the router exposed no endpoint
that enumerates drives.

Enumerate ready drive roots instead:

* `host_dirs.host_browse_roots()` returns `/` on POSIX and every ready
  drive on Windows, falling back to the home anchor when none respond so
  the picker never renders an empty tree. Probing uses `os.path.isdir`
  rather than `GetLogicalDrives` so mapped network drives are covered
  too, with no ctypes call.
* `GET /api/filesystem/roots` exposes that list, and
  `/api/filesystem/defaults` now also returns `browse_roots`. A
  `workspace_root_dir` policy jail still collapses both to that one path,
  which is the point of the policy.
* `RootDirSelect` takes `treeRoots` and renders a forest.
  `sanitizeTree` keeps one tree per root while still deduping globally -
  that set is what fixes antd#37228 duplicate rows, and drive roots are
  distinct paths, so global uniqueness is still the right rule.
  `ancestorDirPaths` now takes the longest containing root, so `D:/x/y`
  expands under `D:/` even though `C:/` is listed first. Single-string
  roots keep working, so the existing tests stayed valid.

Second, the hint text promised a sandbox that does not exist off Linux:
`ensure_bubblewrap` returns `not_linux` with "bubblewrap jail is
Linux-only; execute uses plain local_shell", so a non-root `root_dir`
only bounds the agent's *tool* paths while the agent process keeps
running with the server account's own filesystem access. That is a weaker
guarantee than the copy claimed, and worth correcting on its own.
`/api/filesystem/defaults` now reports `jail_enforced`, and
`backendRootDirJailHint` splits into a real-sandbox variant and
`backendRootDirPathLimitHint` for hosts without one. `backendRootDirDesc`
no longer says `/` unconditionally, which was only true on POSIX.

Drive enumeration and `shutil.which` sit behind `_ready_drive_roots()` /
`_bwrap_on_path()` seams: patching `os.path.isdir` or `shutil.which`
globally breaks pytest's own path handling mid-test.
Let users sign in with their directory (Active Directory / OpenLDAP)
credentials through the existing login form. Reuses sso_providers
(kind='ldap') and user_sso_identities, so no schema change is needed.

Authentication is search-then-bind: a service account resolves the user and
only the returned DN is bound with the submitted password, so user input is
never used as a bind identity. Roles come from a role template at first
provisioning only; changing a directory group later never re-templates an
existing account. Directory outages and wrong passwords stay distinguishable
(502 LDAP_UNAVAILABLE vs 401 AUTH_FAILED).

Hardening from review:

- An account holding its own password stops at the local check, so a local
  secret is never forwarded to the directory nor counted against that
  directory's own lockout policy.
- A lookup without a unique exact identifier match is refused instead of
  being bound against the first hit.
- A failure during the user bind surfaces as LDAP_UNAVAILABLE rather than a
  401 credential verdict.
- Binds are throttled before they reach the directory, keyed per identifier
  and client address, with the address resolved from a trusted peer so a
  spoofed X-Forwarded-For cannot reset the budget.
- Enabling a plain ldap:// URL without StartTLS is rejected; a disabled
  certificate check is surfaced as a warning.
- auto_provision defaults to off, with an optional allowed_groups allow-list
  and a group-search mode (member / uniqueMember / memberUid) for directories
  that do not expose memberOf. Nested groups are not resolved.
- The identity key is configurable (entryUUID / objectGUID) and blank by
  default; a connectivity probe reports which attribute the directory
  exposes.
- Config changes are audited against the acting admin, and referrals are no
  longer followed during binds.

Tests cover the above without a live directory (ldap3.Connection is the only
thing replaced); a live test exercises a real directory when configured.

Co-authored-by: jubaoliang <jubaoliang@gmail.com>
* feat(dashboard): tuck composer extras into a plus-menu flyout

Keep HITL, shortcuts, and attachments on the toolbar. Mode, model, connectors, knowledge, skills, experts, and subagents open from a plus menu with a right-side panel. Chat user avatars now match the account photo or icon.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: keep the composer PR changelog scoped to chat UX

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the Docker/native wizards on an account-only flow, fail native start
when 8089 never comes up, and vendor harness storage_errors so current
Octop can boot against PyPI harness 1.0.0.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The no-model notice sat as a full-width strip above the composer. Show a centered empty state on new chats and keep the in-thread banner aligned with the input column.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…st (#1545)

Turn exhausted model-call retries into a recovery prompt with the real
cause, unwrap that wrapper in stream/UI classification, and keep
background inbox jobs marked failed when the installed harness can see
the [model_call_failed] mark.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Move JWT helpers and peer-turn runners out of infra→api imports, align
dashboard en/zh leaf keys, and let octop init proceed when only sidecar
files exist. Changelog records LDAP group mapping and skill lock scope.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…#1572)

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: adapt S3/Postgres backends and restore admin storage browse

Published octop-harness 1.0.1 plus Octop-side spec mapping and protocol
wrap stop expert start and Admin tree listing from failing on the older
deepagents_backends surface. Browse now caches the backend session and
can preview or download files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: keep Windows paths valid in storage browse fixtures

f-string JSON turns backslashes into illegal escapes, so row_to_backend_spec
sees an incomplete filesystem config on Windows CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: write browse fixture bytes without Windows newline translation

Path.write_text() converts LF to CRLF on Windows, so download assertions
saw b'# hello\r\n'.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The numeric comparator treated 1.0.2b4 and 1.0.2b5 as equal, so
replacing the desktop executable kept the persistent older runtime.

Port the existing Python parse_version key instead of adding a
third-party PEP 440 library. Overlay installs can replace b4 with b5;
backup, replacement rollback, and no-downgrade stay unchanged.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…1573)

* feat(connectors): add isolated Agent Mail CLI tools

* feat(connectors): add Agent Mail device authorization

* feat(dashboard): add Agent Mail connector setup

* docs(connectors): document Agent Mail and CLI verification

* fix(connectors): keep Agent Mail checks portable and focused

* feat(connectors): trigger cron tasks from Agent Mail watch

* feat(dashboard): configure Agent Mail new-mail tasks

* docs(api): clarify Agent Mail trigger examples

* style(dashboard): refine Agent Mail auth and task hints

* test(connectors): preserve Windows watch process cleanup

* fix(connectors): gate Agent Mail writes with HITL and cron read-only

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…1588)

Keep other pages expert-only, group remotes as 云端·专家/团队, and
only reveal 更多 when the chip row cannot fit the current selection.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: keep hung remote backends from blocking boot

S3/Postgres still use the old harness protocol and a per-call event loop, so one stuck List/Get can prevent HTTP from coming up. Run that I/O on a dedicated worker with timeouts, fail the one expert, and remap leftover unwritable workspace paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: avoid MagicMock workspace_dir in boot timeout test

Windows rejects the mock's stringified path when the fake harness mkdirs workspace_dir. Register the agent with an empty config instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…1594)

Composer, IM/CLI /help, and slash catalogs showed /approve even when neither HITL nor command-guard require_approval was on. CLI also went silent on a pause, and typed /approve hit a stub instead of resuming the turn.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
#1595)

Harness projection dropped chunk["interrupts"], so the dashboard left
the tool running and never showed the question card.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…mes (#1599)

Ordinary answers that mention 429 or timeouts no longer become error
bubbles; internal MCP uses https when TLS is on without breaking startup
logs; MCP tool names are clamped to 64 characters.

Fixes #1074, #1499, #1527.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the generic mail fallback with the official smiley mark so the catalog and picker match other built-in connectors.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
assert reachable.json()["ok"] is True
# ``detail`` embeds the probed address, so an unrelated success message
# cannot pass this check.
assert "ldap://directory.example.org" in reachable.json()["detail"]
rule.get("sameAs") == "wizard_admin_password" for rule in confirm.get("rules", [])
)
if rel.startswith("fnos/docker/"):
assert "ghcr.io" in json.dumps(data[1], ensure_ascii=False)
jubaoliang-tencent and others added 4 commits October 4, 2026 15:46
The layout content wrapper clips overflow, and the debug page had no scroll container of its own, so the lower checks and action buttons were unreachable on phones.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
LangGraph evaluates interrupt `when` without the HITL thread ContextVar,
so persisted allow_all/allow_tools never skipped execute. Resolve thread
id from the runnable config and auto-resume leftover dashboard cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jubaoliang
jubaoliang merged commit eb28011 into main Oct 4, 2026
11 of 12 checks passed
@jubaoliang
jubaoliang deleted the release/1.0.2b6 branch October 4, 2026 23:47
jubaoliang added a commit that referenced this pull request Oct 5, 2026
* chore: release 1.0.2b6 (#1612)

* docs: refresh README roadmap, anchors, and product sync (#1211)

Align EN/CN README with shipped features and current install/channel
docs; fix GitHub heading anchors and drop obsolete extras examples.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dashboard): define missing --fn-bg-container theme variable (#1216)

Dark-theme surfaces that use var(--fn-bg-container, #fff) — most
visibly the knowledge-base Markdown preview body, toolbar and outline
panel — resolved to a white background while text stayed light, leaving
preview content invisible. Define the variable as #ffffff (light) and
#141414 (dark), and point the Admin/Users badge tints that borrowed the
name for a translucent fill at --fn-bg-tertiary so their look is
unchanged.

Fixes #1215

* feat(dashboard): let each account customize the sidebar (#1218)

Store group order, item placement, and hidden entries on the user so navigation can be rearranged without showing items the account cannot access.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* style(dashboard): apply Prettier to files that drifted from the formatter (#1222)

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: store role-template id on users.role and harden mobile composer (#1220)

Collapse the dual user_role_id / role model so users.role and invites.role
hold the template public id, block deleting roles still in use, and keep
the chat composer visible under mobile browser visualViewport.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: use product names for sibling project links in the README (#1225)

Link labels now match the product names while the repository URLs stay unchanged.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(publish): bump version badges in all localized READMEs (#1229)

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: load plugins that still import harness_agent (#1260)

Marketplace plugins written before the octop_harness rename fail to install
and enable with ModuleNotFoundError. Alias that import onto the installed package.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(backup): skip the Octop-owned _builtin_skills prefix on zip import (#1255)

POST /workspace/archive writes archive entries straight into the
workspace; _safe_zip_name only blocks traversal, so a zip could plant
files under _builtin_skills/ and .octop/_builtin_skills/. That prefix is
owned by Octop: DELETE and POST /workspace/move refuse it via
_assert_workspace_mutable and sync_octop_builtin_skills only prunes its
own retired names, so planted skills both load as kind=builtin and stay
irremovable through the API. This restores the #1105 guard (filter plus
warnings reporting) together with its regression test, which a later
"restore prior behaviors" commit had removed along with the filter.

Fixes #1254

Co-authored-by: jubaoliang <jubaoliang@tencent.com>

* fix(dashboard): define the missing --fn-* design tokens (#1240)

About 30 --fn-* custom properties were referenced across dashboard
styles but never defined in theme-vars.css. References without a
fallback resolved to invalid-at-computed-value-time (transparent
surfaces, e.g. the Connectors page sunken wells), and fallbacks were
often light-only literals that broke the dark theme. Define the
missing tokens for both themes, aligned with their nearest existing
design-system values, and fix the misspelled --text-secondary /
--border-color references that could never resolve.

Fixes #1239

* feat(ollama): allow configuring local model download directory (#1279)

* feat(ollama): allow configuring local model download directory

Users who move Ollama models off the default path could list custom models
but Octop treated them as not downloaded. Persist an OLLAMA_MODELS directory,
scan manifests for downloaded names, and pass the path when starting serve.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ollama): keep service running when saving models dir

Saving the download directory no longer sends enabled=false, so a running
Ollama daemon is not stopped. Disk-scanned models report size 0 so the UI
does not show the manifest file size as the model size.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(skills): let octop-assistant answer product and help questions (#1311)

Give the bundled assistant a product overview plus links to octop.cloud and the docs, and keep CLI setup for changes on the running instance.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ollama): list local models even when Octop service is off (#1316)

* fix(ollama): list local models even when Octop service is off

Windows users often run Ollama themselves. Octop only queried the daemon
when its managed-service toggle was on, so already-pulled models showed
as not downloaded. List from a reachable daemon without starting serve,
and mark IDs from fetch-models as downloaded.

* docs: note Ollama downloaded-model detection when the service toggle is off

* fix(mobile): stop the JPEG stream after a run of failed captures (#1309)

`_stream_frames` (the mobile JPEG fallback) retried forever: on `TimeoutError`
it logged and `continue`d, and on `captured is None` it just `continue`d. A
phone unplugged mid-stream, or a dead adb bridge, makes every capture fail at
once, so the loop spun at the frame interval indefinitely — a warning per
iteration, a log flood, and a canvas frozen on the last frame with nothing ever
sent to explain it.

The desktop sibling `desktop/stream.py::_stream_loop` already counts consecutive
misses, warns once, and after 30 sends an error frame and breaks. This mirrors
that: timeouts and `None` both count as misses, the first one is logged once,
and at `_CAPTURE_MISS_LIMIT` (30) the stream sends the
`{"type": "error", "message": ...}` frame the rest of this router already uses
and returns. A successful frame resets the streak, so an occasional dropped
frame still does not tear the stream down.

Folding the timeout branch into the shared counter also removes the
per-iteration warning flood.

* fix(cli): read and write cli_state.json through the shared JSON helpers (#1307)

`octop.cli.support.state` keeps a private reader/writer for
`~/.octop/cli_state.json` and both halves are looser than the rest of the tree.

`load()` parses with a bare `json.loads(path.read_text(...))`, so an
unparseable file raises a `json.JSONDecodeError` traceback. This file is read by
nearly every CLI command (`config`, `agent`, `db`, `acting`, `ctx`, the REPL),
so one damaged file aborts all of them at once.

`save()` writes with `path.write_text(...)` — truncate, then write. A crash or a
full disk mid-write leaves exactly the truncated file that `load()` then
refuses, with no warning and no `ensure_root()` in between.

`infra/utils/json_file.py` exists for this; its docstring is explicit that a
torn write "creates the same precondition" as a hand-edit slip and that callers
"must not fall back to an empty dict". Four call sites already route through it
(`cli/commands/run.py`, `agents/plugins/manager.py`, `agents/plugins/seed.py`,
`setup/service.py`).

`load()` now reads via `read_json_object` and translates `JsonFileCorruptError`
into `corrupt_config_error(...)` (`CONFIG_FILE_CORRUPT`) — the same policy as
every other `OCTOP_HOME` JSON reader. `save()` writes via `write_json_atomic`
(mkstemp + `os.replace`) so a reader never observes a partial file. A non-object
payload such as `[1, 2]` also raises the typed error now, instead of a stray
`dict.update` `TypeError`.

* fix(gateway): preserve IM attachment metadata in history (#1276)

* fix(docker): ship the langfuse env var the client actually reads (#1258)

OCTOP_LANGFUSE_ENABLED is set by .env.example and forwarded by docker-compose.yml, but nothing reads it: Octop keeps the switch in the settings table (observability_langfuse_enabled) and the langfuse client honours LANGFUSE_TRACING_ENABLED. Add a test that fails while a shipped Langfuse variable has no reader.

* fix(api): sandbox media preview responses (#1243)

media/preview streams user-controlled bytes (chat attachments, tool
outputs) inline on the dashboard origin. image/svg+xml is previewable,
so a navigated preview URL executed as a live document on the app
origin and could run attacker script — the endpoint also accepts the
access_token query parameter for media loads. Serve previews with
Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff;
image/video previews keep working.

Fixes #1242

Co-authored-by: jubaoliang <jubaoliang@gmail.com>

* feat: add Octop↔Octop cloud collab for remote experts (#1281)

* feat: add Octop↔Octop Bridge for remote agent access

Enable admin-managed peer connections with WS tunnel, path allowlist,
hello_ack handshake, SSRF guards, and Chat shadow-agent hydrate so
operators can probe and use remote agents from the local dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: serialize bridge turn chunks and flag remote experts

LangChain HumanMessage objects in peer turn.chunk frames crashed
json.dumps; add a bridge JSON default. Show a 远端/Remote badge on
shadow experts in the picker and sidebar lists.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: bridge avatars, named remote badges, and auto-reconnect

Keep bundled / CDN icon URLs for shadow experts and only proxy uploaded
avatars. Show the bridge display name on remote badges. Add a default-on
auto-reconnect switch that backs off on disconnect and disables itself
after repeated failures, recording the reason in last_error.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add card/table views for bridge connections

Match the storage page card layout and add a Segmented toggle so
admins can switch between a card grid and a table of remote links.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: render bridge expert icons correctly on the chat page

Encode bridge agent ids in avatar URLs the same way as chat WS paths,
fall back to Lucide when the image fails, size portraits to fill the
sidebar avatar, and refresh AgentContext when icon_url changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: tunnel peer models and knowledge bases for bridge chat

Allow read-only GET of peer providers/resolved, active-model, and
knowledge-bases (plus capability) through the Bridge tunnel, expose
local bridge connection routes, and point the chat composer at those
sources when talking to a shadow expert. Hide local connectors for
remote sessions. Also allow agent subagents paths on the tunnel.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: scope bridge chat expert picker to peer agents

When the active expert is a Bridge shadow, the composer expert picker
and @-mentions only list peers on that connection (not local experts).
Encode bridge agent ids for skills and subagents list/install paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: hint remote chat pickers to manage settings on the peer

In Bridge sessions, replace local “manage …” footers for models,
skills, knowledge bases, experts, and subagents with a muted
“edit on remote” tip and a toast instead of navigating to local admin.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: polish bridge remote UX and tunnel agent status

Allowlist GET …/status so hubs can read real peer harness state (peers need
this build). Keep history-migration hub-local, skip noisy bridge polls in the
UI, move Bridge under Settings, and mark remote experts with cable + name.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: tunnel remote expert catalogs and polish bridge connection UX

Hub can edit peer subagents, memory, channels, tools, plugins, and MBTI
through the tunnel; surfaces that cannot hop show an in-drawer hint. Peer
must run this build so the new allowlist takes effect.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: group cloud-collab experts and unify header-tunneled composer APIs

Keep the selected remote expert on disconnect, gate peer-only surfaces in the UI, and stop forking composer/task requests through dedicated bridge endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add search to the Experts toolbar

Filter my experts, teams, and the library from the existing button row so long catalogs stay scannable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: keep inbound cloud-collab cards peer-owned

Inbound links can only edit name/icon/notes; disconnect, delete, and auto-reconnect stay disabled. Peer offline shows as 离线, peer delete removes the card, and the default inbound name is the short connection id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: polish inbound cloud-collab UX and unify 对端 copy

Peer-initiated links can only edit display info and cannot redial;
offline cards may be deleted. Local-only pages hint when a peer
expert is selected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: map peer team speakers onto Bridge shadows

Peer team chats stamped local member ids, so the hub fell back to the host
avatar. Rewrite roster and stream speakers onto bridge:{cid}:{id} shadows,
and keep the local team picker from mixing in peer experts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: stop rewriting team thread ids in the Bridge tunnel

Member job threads are {room}~{agent}. A blanket JSON replace of the peer
agent id turned those into hub shadows, so history 404ed on the peer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: rewrite peer media URLs and keep bridge turn composer fields

Live send_file_to_user frames still pointed at peer-local /api/agents/{id}.
Map every teammate id in tunneled JSON, and copy the full dashboard turn
body so knowledge bases and HITL policy are not dropped on the peer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: rename bridge probe tests to avoid pytest module clash

tests/unit/bridge/test_probe.py collided with tests/unit/mobile/test_probe.py
during collection (same basename).

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: collapse extra experts on Bridge cards and fill avatars

Show at most four experts on a connection card, with the rest behind
expand. Portraits fill the 40px ring without a tinted background.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: lulinzhi37-alt <lulinzhi37@gmail.com>

* fix: beta upgrade notes and fnpack download resilience (#1327)

* fix(update): load prerelease changelog from versioned PyPI JSON

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(fnos): retry and cache fnpack downloads

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(experts): add default conversation mode (Ask/Plan/Craft) to expert settings (#1338)

* feat(skills): per-package copy policy for skill packages (#770) (#1336)

skill_packages gains copy_policy (snapshot default / lock / deny), set by the
creator or admin via PATCH. deny rejects the workspace copy endpoint with
SKILL_PACKAGE_COPY_DENIED and disables the dashboard button (can_copy). lock
stamps each copied SKILL.md with origin/locked frontmatter; the existing
package-only write guard now also rejects writes to stamped copies for
everyone but the origin package creator (admins keep an escape hatch for
orphaned copies). Copy attempts are audited. Agent-tool-level write
protection for locked copies needs harness support, tracked separately.

Co-authored-by: vicfei <4058491+vicfei@users.noreply.github.com>

* fix(httpx): honor CIDR entries in NO_PROXY (#1354)

httpx 0.28 treats 192.168.0.0/16 as the exact IP 192.168.0.0, so LAN
hosts hit HTTP_PROXY. Also split Windows semicolon lists, parse IPv6
CIDR, and bypass loopback under macOS/Windows system proxies.

Fixes #1347

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: jubaoliang <jubaoliang@gmail.com>

* fix(qq-mail): 修复裸非 ASCII 邮件头导致 search_emails/read_email JSON 崩溃 (#1343)

技术报告 TECH-REPORT-qq-mail-header-bug.md 的复现与修复:

- 根因:email.message_from_bytes 默认 compat32 策略下,邮件头含裸
  非 ASCII 字节时 get() 返回 email.header.Header 对象,直接传入
  json.dumps 抛 TypeError: Object of type Header is not JSON
  serializable,真实邮箱场景 100% 必现
- 修复(报告方案 B):新增 _parse_message() 统一以
  email.policy.default 解析,_safe_header() 强制 str();所有面向
  JSON 的头字段(From/Subject/Date)走统一封装
- 附带收益:规范 MIME 编码头(=?utf-8?q?...?=)现在解码为可读
  中文,而非原始编码串
- _extract_body:声明字符集无已装 codec(如 unknown-8bit/伪造名)
  时回退 UTF-8(errors=replace 只挡字节错误,挡不住 LookupError)
- 新增 13 项回归测试:裸 UTF-8 头、MIME 编码头、破损编码字、
  unknown charset 回退、multipart、纯 ASCII 不回归;已在未修复
  代码上验证测试会失败(TypeError)

Co-authored-by: jubaoliang <jubaoliang@gmail.com>

* fix(desktop): expand the optional wheel overrides under bash 3.2 (#1363)

extra stays empty on darwin-arm64, linux-* and windows-amd64 because
write_green_overrides only emits an override file for windows-arm64 and
darwin-amd64. "${extra[@]}" under set -u aborts on the macOS default
/bin/bash before uv ever runs, so `make -f desktop/portable/Makefile
wheels` fails on the host platform. Use the guarded expansion form the
repo already applies in scripts/install.sh.

* fix(dashboard): gate SkillPackagesTab detail and copy requests (#1333)

The detail drawer and the copy modal both fetch the same endpoint and both
wrote their response unconditionally, so a slow click on package A could
overwrite a later click on package B: the drawer showed the wrong skills and
the copy action posted the wrong package id. Reuse the existing
createDetailRequestGate helper so only the newest request per surface lands.

* fix(wechat-ops): scope the publisher token cache to its AppID (#1332)

The cache file is shared by every official account but the record only held
the token and its expiry, so the reuse check looked at time alone. After
switching WECHAT_APP_ID the publisher kept sending the previous account's
token for up to two hours: draft/add succeeded against the wrong account, the
command exited 0, and it still printed a successful publish. Tag each record
with the AppID it was issued for and refuse to reuse a token that was not
fetched for the account currently configured.

* fix(history): bound trajectory events written to the v2 archive (#1331)

ArchiveTrajectoryStore writes v2 trajectory events with
HistoryStore.put_document directly, so it skipped the clip_persisted_event
call that TrajectoryStore.append/upsert perform for the repo backend. Those
caps exist to bound SQLite row size, which applies to the shared archive
rows just as much as to trajectory_events. Apply the caps before routing so
both backends persist the same bounded event.

* fix(connectors): retry a transient streamable-HTTP MCP probe once (#1330)

* fix(connectors): retry a transient streamable-HTTP MCP probe once

The SSE probe retries once when the upstream closes the stream during the
first initialize; probe_streamable_http_mcp shares the same error
classification but returned on the first attempt, so a transient proxy or
upstream drop was reported as a broken connector.

Mirror the SSE retry: one extra attempt for connection-class failures only,
and return auth rejections (HTTP 401/403) immediately so a bad key is never
masked by the retry.

* fix(connectors): reformat probe.py to pass ruff format check under LF

The previous commit left probe_streamable_http_mcp with multi-line
logger.warning(...) calls that ruff 0.15.20 only accepts in a CRLF file.
Under the Linux CI (LF), 'ruff format --check' rewrites them to a single
line, failing 'make lint'. Reformat to the LF/single-line form ruff
expects so the lint job passes.

* fix(connectors): resolve mypy type conflict in streamable-HTTP retry

_unwrap_probe_exception_group returns dict | None, which could not be
assigned to the 'result' variable already inferred as dict[str, Any] from
the other exception branches. Use a separate 'unwrapped' local in the
BaseExceptionGroup branch so the types stay compatible.

* fix(dashboard): do not cache the timezone fallback when the fetch fails (#1326)

`useServerTimezone` stored `"UTC"` on the failure path, and the module-scope
guard `if (cachedTimezone) return cachedTimezone` then short-circuited every
later mount. One transient failure - the dashboard is commonly loaded while the
Octop service is still starting - therefore pinned UTC for the rest of the page
session, and every consumer (MessageBubble, JournalList, AuditLogPanel,
UsersListPanel, ...) rendered timestamps in the wrong zone with nothing to
suggest a reload would fix it.

The sibling settings hook `useServerUploadLimit` already models the contract:
`applyUploadLimitFetchResult` returns `cache: null` on failure so the next mount
retries, and its test is literally named "does not cache a failed fetch so the
next mount can retry".

Extracted the same seam here - `applyTimezoneFetchResult`, plus a named
`DEFAULT_SERVER_TIMEZONE` - and routed both branches through it. Successful
responses (including an empty timezone, which still falls back to UTC) keep
caching exactly as before.

* fix(mobile): settle the install stream exactly once (#1325)

`streamMobileSse` reports completion through `onDone`, but it was the only
stream in `api/modules` that could end without calling it - and the only one
that could call it twice.

* The read loop breaks on EOF and the `.then` body just returns. When the body
  closes without a terminal frame the callback never fires; `desktop.ts` and
  `browser.ts` both settle after their loop. Mobile can really truncate:
  `infra/mobile/setup.py` spawns `bash` with no guard, so a host without a
  shell raises before the router can write its `{"done": ...}` frame.
* A terminal frame did not stop the loop, so any later frame fired `onDone`
  again. `desktop.ts`/`browser.ts` `return` straight after settling.

`pages/Control/RemoteAndroid` only leaves its "installing" phase inside
`onDone`, so the first case leaves the panel spinning with the install button
disabled and no toast, and the second runs the completion path twice.

Both paths now funnel through a one-shot `settle()`.

* fix(skills): fetch SkillHub rankings through the shared capped HTTP client (#1308)

`_fetch_ranking_json` was the only fetch in this module that hand-rolled
`urllib.request.urlopen` instead of calling `_http_request`.

`response.read()` therefore buffered the whole body with no size limit, where
`_http_request` checks `Content-Length` and then reads in `HTTP_READ_CHUNK`
slices up to `MAX_HTTP_BYTES` — the search path already passes a cap.

Its `except` ladder also stopped at `HTTPError`/`URLError`/`TimeoutError`, so a
mid-stream `ConnectionResetError` or an `http.client.HTTPException` escaped as a
raw exception. The endpoint that serves rankings (`routers/skills.py::
_hub_rankings`, used by `GET /skills/hub/rankings` and
`GET /agents/{id}/skills/hub/rankings`) only maps `SkillHubMarketError` to 502
and `SkillHubMarketTimeout` to 504, so an upstream hiccup became an opaque 500.

Rankings now go through `_http_request` like `_fetch_search_json` and
`_download_skillhub_package` already do. The 4 MiB JSON cap is named
`_MAX_JSON_BYTES` and used by both JSON fetches instead of repeating the literal.

`_Response` in `tests/unit/agents/test_skillhub_market.py` is the stub that
`test_fetch_ranking_json_uses_showcase_endpoint` hands back from `urlopen`, so it
has to accept the `size` argument `_http_request` passes. It now wraps the
payload in `io.BytesIO` and reads exactly like `_BytesResponse` next door, which
also makes the read loop stop on the first empty chunk instead of spinning.

* fix(dashboard): add the missing locale entries for t() keys (#1241)

* fix(dashboard): add the missing locale entries for t() keys

About 200 t() keys used across the dashboard were absent from
locales/en.json (and 113 from locales/zh.json). i18next fell back to
the call-site defaults, so the English UI rendered large chunks of
hardcoded Chinese (memory panel, connectors onboarding, skill
recording guide, proactive-care config, ...) and a few spots showed
raw key paths; the en/zh parity checks could not catch it because
both bundles missed the same keys. Add the missing entries to both
bundles and switch the two skill-record call sites whose defaults
interpolated runtime values to real i18next interpolation.

Fixes #1238

* fix(dashboard): align zh memory strings with current in-code defaults

Per review on #1241: once these keys land in the bundle, i18next stops
using defaultValue, so zh must match the current UI defaults.

* fix(dashboard): mention the English stop keyword in the skill-recording guide

skillRecordGuide.step4Desc only told English users to type "结束" to stop
recording, but END_KEYWORDS also accepts "end" and "stop recording" — users
following their own language had no way to discover the keyword from the guide.
Mirror the existing bilingual keyword style (e.g. skills.skillNamePlaceholder)
and list the English variant next to the Chinese one.

Re-applied on top of the rebuilt branch: upstream develop was rewritten, and
this change had been part of the previous (now superseded) rebase.

* fix(agents): propagate exhausted model retries as failures (#1012)

Co-authored-by: jubaoliang <jubaoliang@gmail.com>

* fix(dashboard): browse every drive in the storage-root picker on Windows (#1429)

The picker derived a single browse root from `Path.home().anchor`, so a
user whose profile lives on `C:` could not reach `D:` at all. That was
structural, not a policy filter: `host_fs_tree_root()` returns one path,
`/api/filesystem/defaults` hands it over as `tree_root`, and
`rootDirTree.sanitizeTree` keeps exactly one tree and returns
`[cleaned]` - every sibling of that root was dropped on each update, so
even a hand-injected `D:/` node could not survive. `/filesystem/dirs`
only ever listed children of `C:/`, and the router exposed no endpoint
that enumerates drives.

Enumerate ready drive roots instead:

* `host_dirs.host_browse_roots()` returns `/` on POSIX and every ready
  drive on Windows, falling back to the home anchor when none respond so
  the picker never renders an empty tree. Probing uses `os.path.isdir`
  rather than `GetLogicalDrives` so mapped network drives are covered
  too, with no ctypes call.
* `GET /api/filesystem/roots` exposes that list, and
  `/api/filesystem/defaults` now also returns `browse_roots`. A
  `workspace_root_dir` policy jail still collapses both to that one path,
  which is the point of the policy.
* `RootDirSelect` takes `treeRoots` and renders a forest.
  `sanitizeTree` keeps one tree per root while still deduping globally -
  that set is what fixes antd#37228 duplicate rows, and drive roots are
  distinct paths, so global uniqueness is still the right rule.
  `ancestorDirPaths` now takes the longest containing root, so `D:/x/y`
  expands under `D:/` even though `C:/` is listed first. Single-string
  roots keep working, so the existing tests stayed valid.

Second, the hint text promised a sandbox that does not exist off Linux:
`ensure_bubblewrap` returns `not_linux` with "bubblewrap jail is
Linux-only; execute uses plain local_shell", so a non-root `root_dir`
only bounds the agent's *tool* paths while the agent process keeps
running with the server account's own filesystem access. That is a weaker
guarantee than the copy claimed, and worth correcting on its own.
`/api/filesystem/defaults` now reports `jail_enforced`, and
`backendRootDirJailHint` splits into a real-sandbox variant and
`backendRootDirPathLimitHint` for hosts without one. `backendRootDirDesc`
no longer says `/` unconditionally, which was only true on POSIX.

Drive enumeration and `shutil.which` sit behind `_ready_drive_roots()` /
`_bwrap_on_path()` seams: patching `os.path.isdir` or `shutil.which`
globally breaks pytest's own path handling mid-test.

* feat(auth): add LDAP directory login (#1123)

Let users sign in with their directory (Active Directory / OpenLDAP)
credentials through the existing login form. Reuses sso_providers
(kind='ldap') and user_sso_identities, so no schema change is needed.

Authentication is search-then-bind: a service account resolves the user and
only the returned DN is bound with the submitted password, so user input is
never used as a bind identity. Roles come from a role template at first
provisioning only; changing a directory group later never re-templates an
existing account. Directory outages and wrong passwords stay distinguishable
(502 LDAP_UNAVAILABLE vs 401 AUTH_FAILED).

Hardening from review:

- An account holding its own password stops at the local check, so a local
  secret is never forwarded to the directory nor counted against that
  directory's own lockout policy.
- A lookup without a unique exact identifier match is refused instead of
  being bound against the first hit.
- A failure during the user bind surfaces as LDAP_UNAVAILABLE rather than a
  401 credential verdict.
- Binds are throttled before they reach the directory, keyed per identifier
  and client address, with the address resolved from a trusted peer so a
  spoofed X-Forwarded-For cannot reset the budget.
- Enabling a plain ldap:// URL without StartTLS is rejected; a disabled
  certificate check is surfaced as a warning.
- auto_provision defaults to off, with an optional allowed_groups allow-list
  and a group-search mode (member / uniqueMember / memberUid) for directories
  that do not expose memberOf. Nested groups are not resolved.
- The identity key is configurable (entryUUID / objectGUID) and blank by
  default; a connectivity probe reports which attribute the directory
  exposes.
- Config changes are audited against the acting admin, and referrals are no
  longer followed during binds.

Tests cover the above without a live directory (ldap3.Connection is the only
thing replaced); a live test exercises a real directory when configured.

Co-authored-by: jubaoliang <jubaoliang@gmail.com>

* feat(dashboard): tuck composer extras into a plus-menu flyout (#1513)

* feat(dashboard): tuck composer extras into a plus-menu flyout

Keep HITL, shortcuts, and attachments on the toolbar. Mode, model, connectors, knowledge, skills, experts, and subagents open from a plus menu with a right-side panel. Chat user avatars now match the account photo or icon.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: keep the composer PR changelog scoped to chat UX

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(fnos): harden App Center install and native startup (#1539)

Keep the Docker/native wizards on an account-only flow, fail native start
when 8089 never comes up, and vendor harness storage_errors so current
Octop can boot against PyPI harness 1.0.0.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dashboard): center the empty-chat model prompt (#1542)

The no-model notice sat as a full-width strip above the composer. Show a centered empty state on new chats and keep the in-thread banner aligned with the input column.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: give the model a concrete retry failure instead of a generic toast (#1545)

Turn exhausted model-call retries into a recovery prompt with the real
cause, unwrap that wrapper in stream/UI classification, and keep
background inbox jobs marked failed when the installed harness can see
the [model_call_failed] mark.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: keep infra off api and tidy post-b3 leftovers (#1550)

Move JWT helpers and peer-turn runners out of infra→api imports, align
dashboard en/zh leaf keys, and let octop init proceed when only sidecar
files exist. Changelog records LDAP group mapping and skill lock scope.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(fnos): stop native leftover on 8089 so App Center can start again (#1572)

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: adapt S3/Postgres backends and restore admin storage browse (#1565)

* fix: adapt S3/Postgres backends and restore admin storage browse

Published octop-harness 1.0.1 plus Octop-side spec mapping and protocol
wrap stop expert start and Admin tree listing from failing on the older
deepagents_backends surface. Browse now caches the backend session and
can preview or download files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: keep Windows paths valid in storage browse fixtures

f-string JSON turns backslashes into illegal escapes, so row_to_backend_spec
sees an incomplete filesystem config on Windows CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: write browse fixture bytes without Windows newline translation

Path.write_text() converts LF to CRLF on Windows, so download assertions
saw b'# hello\r\n'.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(desktop): compare portable versions with parse_version rules (#1561)

The numeric comparator treated 1.0.2b4 and 1.0.2b5 as equal, so
replacing the desktop executable kept the persistent older runtime.

Port the existing Python parse_version key instead of adding a
third-party PEP 440 library. Overlay installs can replace b4 with b5;
backup, replacement rollback, and no-downgrade stay unchanged.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(connectors): support Agent Mail auth, tools and new-mail tasks (#1573)

* feat(connectors): add isolated Agent Mail CLI tools

* feat(connectors): add Agent Mail device authorization

* feat(dashboard): add Agent Mail connector setup

* docs(connectors): document Agent Mail and CLI verification

* fix(connectors): keep Agent Mail checks portable and focused

* feat(connectors): trigger cron tasks from Agent Mail watch

* feat(dashboard): configure Agent Mail new-mail tasks

* docs(api): clarify Agent Mail trigger examples

* style(dashboard): refine Agent Mail auth and task hints

* test(connectors): preserve Windows watch process cleanup

* fix(connectors): gate Agent Mail writes with HITL and cron read-only

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(dashboard): show teams in the memory and channels agent picker (#1588)

Keep other pages expert-only, group remotes as 云端·专家/团队, and
only reveal 更多 when the chip row cannot fit the current selection.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: keep hung remote backends from blocking boot (#1586)

* fix: keep hung remote backends from blocking boot

S3/Postgres still use the old harness protocol and a per-call event loop, so one stuck List/Get can prevent HTTP from coming up. Run that I/O on a dedicated worker with timeouts, fail the one expert, and remap leftover unwritable workspace paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: avoid MagicMock workspace_dir in boot timeout test

Windows rejects the mock's stringified path when the fake harness mkdirs workspace_dir. Register the agent with an empty config instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(hitl): hide approval surfaces unless tools can actually pause (#1594)

Composer, IM/CLI /help, and slash catalogs showed /approve even when neither HITL nor command-guard require_approval was on. CLI also went silent on a pause, and typed /approve hit a stub instead of resuming the turn.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: restore ask_user_question cards after LangGraph v2 interrupt move (#1595)

Harness projection dropped chunk["interrupts"], so the dashboard left
the tool running and never showed the question card.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: stop false stream errors, TLS internal MCP, and overlong tool names (#1599)

Ordinary answers that mention 429 or timeouts no longer become error
bubbles; internal MCP uses https when TLS is on without breaking startup
logs; MCP tool names are clamped to 64 characters.

Fixes #1074, #1499, #1527.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(connectors): add official Agent Mail logo (#1608)

Replace the generic mail fallback with the official smiley mark so the catalog and picker match other built-in connectors.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: release 1.0.2b6

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dashboard): let the PWA debug page scroll on mobile

The layout content wrapper clips overflow, and the debug page had no scroll container of its own, so the lower checks and action buttons were unreachable on phones.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: note PWA debug scroll in 1.0.2b6 changelog

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: bump uv.lock package version to 1.0.2b6

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hitl): honor allow-all session bypass for later tool approvals

LangGraph evaluates interrupt `when` without the HITL thread ContextVar,
so persisted allow_all/allow_tools never skipped execute. Resolve thread
id from the runnable config and auto-resume leftover dashboard cards.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Dang Zitou <dengzitao888@163.com>
Co-authored-by: Frank Zhang <77115469+Master-Frank@users.noreply.github.com>
Co-authored-by: XiaoChen <1326713348@qq.com>
Co-authored-by: locip123 <116343912+locip123@users.noreply.github.com>
Co-authored-by: lihongyuan99 <64824864+lihongyuan99@users.noreply.github.com>
Co-authored-by: lulinzhi37-alt <lulinzhi37@gmail.com>
Co-authored-by: Fei.Chen <cnfeichen@163.com>
Co-authored-by: vicfei <4058491+vicfei@users.noreply.github.com>
Co-authored-by: 猫猫摸大鱼 <58991169+miaowmint@users.noreply.github.com>
Co-authored-by: 智浪淘沙 <zhilangtaosha@qq.com>
Co-authored-by: Suroy <77138019+zsuroy@users.noreply.github.com>
Co-authored-by: MakerFly <223868679+MakerFlyFly@users.noreply.github.com>

* chore: sync main into develop after 1.0.2b6

---------

Co-authored-by: jubaoliang <jubaoliang@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Dang Zitou <dengzitao888@163.com>
Co-authored-by: Frank Zhang <77115469+Master-Frank@users.noreply.github.com>
Co-authored-by: XiaoChen <1326713348@qq.com>
Co-authored-by: locip123 <116343912+locip123@users.noreply.github.com>
Co-authored-by: lihongyuan99 <64824864+lihongyuan99@users.noreply.github.com>
Co-authored-by: lulinzhi37-alt <lulinzhi37@gmail.com>
Co-authored-by: Fei.Chen <cnfeichen@163.com>
Co-authored-by: vicfei <4058491+vicfei@users.noreply.github.com>
Co-authored-by: 猫猫摸大鱼 <58991169+miaowmint@users.noreply.github.com>
Co-authored-by: 智浪淘沙 <zhilangtaosha@qq.com>
Co-authored-by: Suroy <77138019+zsuroy@users.noreply.github.com>
Co-authored-by: MakerFly <223868679+MakerFlyFly@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.