Skip to content

chore(release): 1.0.2b7 - #7

Merged
ra-co88 merged 150 commits into
mainfrom
release/1.0.2b7
Oct 10, 2026
Merged

ra-co88 merged 150 commits into
mainfrom
release/1.0.2b7

Conversation

@ra-co88

@ra-co88 ra-co88 commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Release 1.0.2b7, cut from develop per AGENTS.md section 10.

What this ships

The end of fork main's exposure to the AgentSelector rules-of-hooks crash, and the 401 redirect fix, in one step - as planned.

  • AgentSelector - upstream fixed the hook ordering in 95860cc; this release carries it. Fork main no longer crashes the shell when agents load.
  • FE-1 - the _redirectingToLogin latch now re-arms when a new token is stored. Previously one 401 disabled the login redirect for the rest of the page, so a user who signed back in and then expired again was stranded with failing requests and no path to login.
  • FE-14 - blocked web storage degrades to 'no token' instead of throwing and taking down every request.
  • The backend security remediation (SEC-1 through SEC-20, GATE-1) from the audit.

This is a large jump

Worth being explicit: this release takes in all of upstream/develop, which was 96 commits ahead of upstream/main at sync time, on top of the fork's own audit work. It is not an incremental delta over the previous release.

Verified before tagging

  • make all - format-all, lint, mypy --strict clean; 4624 passed, 25 skipped, 0 failed
  • npx tsc -b clean
  • npx vitest run - 1253 passed, 7 failed across 5 files, all pre-existing upstream failures confirmed by stashing our changes and re-running. Left untouched and tracked separately.
  • pre-commit hook run in full on every commit on this branch, no SKIP_PRECOMMIT

After merge

v1.0.2b7 is tagged on the main tip, then release/1.0.2b7 is deleted and main is synced back to develop.

Yourdaylight and others added 30 commits September 22, 2026 20:49
Dictate with live partial results: the composer mic streams 16 kHz PCM over a
WebSocket to Octop, which signs and proxies the audio to Tencent's realtime ASR
V2 endpoint. Credentials never leave the server; the browser only learns a
`stt_realtime` boolean from `/api/voice/active`.

- settings: add a "realtime speech recognition" switch plus an AppID field to
  the Tencent Cloud voice drawer. Both live in `voice_providers.extra_json`, so
  no schema migration is needed.
- interaction: with realtime on, the mic becomes press-and-hold (start on
  pointerdown, finish on release/leave/cancel) and stays available while a reply
  is streaming, so dictation can be queued. With the switch off the previous
  click-to-record one-shot upload path is unchanged.
- protocol: realtime ASR V2 answers with `result` (`slice_type`, `index`,
  `voice_text_str`) while the docs describe `sentences`; both are normalized in
  `infra/voice/realtime.py`.
- errors: new `VOICE_REALTIME_UNAVAILABLE` code plus localized copy for the
  Tencent realtime error codes (4002-4009, 5000-5002).

Verified end-to-end against Tencent with real speech (edge-tts -> 16 kHz PCM
over the live WebSocket) and by `make all`.
…elop-after-1.0.2b2

chore: sync main into develop after 1.0.2b2
…ntCloud#1027)

wiki_summary interpolates its `lang` argument straight into the URL host
(`https://{lang}.wikipedia.org/...`), and the tool schema the harness
builds from the signature gives the model an unconstrained `str`. A value
like "evil.com#" makes httpx resolve host=evil.com, and "localhost:8443/"
reaches a loopback port -- the response's `extract` is then echoed back
into the chat, so it is also a read-back channel.

Validate `lang` as a bare subdomain label and return the usual error card
otherwise; real codes (zh, en, zh-classical, simple, nb) are unaffected.
…1087) (TencentCloud#1088)

nan / -inf / 负数会被 float() 成功解析并原样透传给 recaptcha-v3 的判定式
`float(score) < min_score`,而该式对 NaN 恒为 False,等于静默关闭分数门槛;
inf / >1 则反向锁死所有登录。现在沿用同一解析点已有的"值不可用即回落
0.5"规则,只放行 [0, 1] 内的有限值。

Co-authored-by: sxh313 <250161920+sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…entCloud#1100) (TencentCloud#1101)

同一文件里 mkdir / delete / move / PUT /doc 都用 _assert_workspace_mutable
拦下 Octop 内置技能根 _builtin_skills,只有这两个写接口漏了:owner 可以往
_builtin_skills/<name>/SKILL.md 写内容,_resolve_skill 之后会把它当作
kind="builtin" 的技能列出来,而 delete/move 与 skills 的删除接口对 builtin
一律拒绝,重启同步也只清 RETIRED_BUILTIN_SKILLS 白名单,所以写进去就删不掉。

校验放在取 workspace 之前(与其余写接口一致),upload 校验的是最终真正使用的
target,并且先于读取请求体。

Co-authored-by: sxh313 <250161920+sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
…#1104) (TencentCloud#1105)

POST /workspace/archive 直接把归档条目写进工作区,唯一的规则
_safe_zip_name 只防目录穿越,不防保留前缀,所以一个 zip 可以在
_builtin_skills/ 和 .octop/_builtin_skills/ 下落盘。该前缀是 Octop
自有目录:delete/move 走 _assert_workspace_mutable 会拒绝它,
sync_octop_builtin_skills 也只清理 RETIRED_BUILTIN_SKILLS,于是植入的
技能既按 kind="builtin" 被加载,又无法通过 API 删除。

现在在解包处过滤该前缀,并沿用已有 warnings 字段报告跳过的条数,
imported 不再把保留路径算成成功导入。导出本来就会打包该前缀,但
无需在恢复时还原:每次 agent 启动都会从包内重新写入自有文件。

Co-authored-by: sxh313 <250161920+sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
…Cloud#1103)

UserManager.get_by_id 与 authenticate 从进程内镜像回答,而 deps.py 的
resolve_user_from_token 每个认证请求都要经过 get_by_id。octop run
--workers N,或 CLI 离线写同一 SQLite 时,另一个进程的 set_role /
disable / remove 不会更新这份镜像:被降级的管理员、被禁用甚至已删除的
用户在其它 worker 上仍按旧身份通过鉴权。

现在两个读取点回到 users 行——行不存在或 disabled 时返回 None,并顺手
清掉镜像里的过期条目;authenticate 用它本来就已经取到的行构造 User。
disable / remove 一直会弹出本地镜像,所以单进程语义不变,登录响应的
role 也不再来自这份进程私有副本。

Co-authored-by: sxh313 <250161920+sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
threads.hitl_policy 是策略的唯一事实来源:每次判定都回查该行。
octop run --workers N(或 CLI 与服务并发)共用一个库时,进程内缓存会让
另一个进程撤销的跳过继续自动放行工具调用,也会让新授予的跳过不生效;
写库失败时缓存里还会留下一个从未落库的豁免策略。

Co-authored-by: sxh313 <250161920+sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
…ned URLs (TencentCloud#1092)

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
…ncentCloud#1018)

GET /api/agents/{agent_id}/threads forwarded an unvalidated `limit` into
`LIMIT ?`. SQLite reads a negative LIMIT as "no limit", so ?limit=-1 returned the
caller's whole thread collection and ?limit=0 returned an empty page; the same
statement reaches psycopg unchanged, where a negative LIMIT is an error. Bound it
with Query(ge=1, le=HISTORY_MAX_LIMIT), the ceiling this module already uses for
message pages.

Co-authored-by: sxh <sunxianhong@ncti-gba.cn>
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
…Cloud#1108)

files.document_path() keys the stored bytes on {doc_id}{suffix}, so a rename
that drops or swaps the extension moves the row off its own file: the
original-file route 404s, delete_document() unlinks the wrong key with
missing_ok=True and leaks the bytes, and re-saving an md/txt document
writes a second file. rename_document() now restores the document suffix
the way create_text_document() already normalises it; folders and names that
already carry the right extension are untouched.

Closes TencentCloud#1107

Co-authored-by: sxh313 <250161920+sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
…centCloud#1013)

`_map_knowledge_error` matched the literal "at most 100" to classify the
document-cap error, but `KnowledgeRepo.create_document` interpolates the
per-base `max_documents`, which has been user-configurable since 90b8ca2.
Any cap other than the default 100 therefore fell through to the base-cap
branch (the repo message also contains "knowledge bases") and the user was
told they had created too many knowledge bases.

Classify both caps by the stable wording of their own messages instead of
a magic number, and drive the mapping from the real repo in a regression
test so the wording coupling cannot silently rot again.

Co-authored-by: sxh <sunxianhong@ncti-gba.cn>
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
… push hardening (TencentCloud#1114)

* feat: admin user batch policies, token UX, default FS root, and media push hardening

Add admin user batch enable/disable/delete and resource policies (token quota,
max experts), improve token inputs with K/M presets, default unrestricted
local backends to host filesystem root, and tighten gateway tool-media push
with clearer path-outside-root stream errors.

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

* fix(workspace): treat Windows drive roots as host-root sentinels

Default FS-root backends use C:/ on Windows; treating that as a scoped
jail put workspaces under the drive root and broke bootstrap/invite tests.

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

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The harness-* libraries are now published as octop-harness, octop-gateway,
octop-memory, and octop-browser.

Co-authored-by: Cursor <cursoragent@cursor.com>
Apply the remaining working-tree changes: connectors empty-state layout,
plus the knowledge, HITL, captcha, workspace, user-cache, and SSRF
adjustments with matching tests.

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

Replace orcakit-harness-agent / harness-* with octop-harness, octop-gateway, octop-memory, and octop-browser, and align docs, UI copy, and generated paths.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Persist member file refs and tool history on team walls, open docks by producer agent, and inject truncated member answers into host follow-up so wrap-up can follow their advice.

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

* QMCP-2014 feat(connectors): restore QCC OAuth with internal HTTP transport

* QMCP-2014 docs(connectors): record QCC OAuth end-to-end acceptance

---------

Co-authored-by: DuHu <duhu@greatld.com>
…encentCloud#1164)

* refactor(agents): group thin modules into domain subpackages

Move plugin helpers, memory, settings stores, persona/MBTI, and avatar
bootstrap code under plugins/, memory/, settings/, persona/, and experts/
while keeping top-level import shims so existing call sites stay stable.

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

* refactor(agents): drop compatibility shims after subpackage move

Call sites already import the new package paths; remove the top-level
re-export stubs so the agents tree only shows real modules.

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

* refactor(agents): move thread helpers into threads/

Group artifact shaping, fork, and context breakdown under
agents/threads so top-level only keeps lifecycle/config adapters.

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

* refactor(agents): fold profile/runtime/tool catalog into settings/

Move the remaining config-surface helpers under settings/ and drop the
skill_package id-list aliases in favor of dump/parse_id_list.

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

* refactor(agents): group workspace_dir and execute_env under workspace/

Keep path resolution and harness execute-env injection together as one
workspace package; leave only manager and conversation_mode at top level.

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

* refactor(infra): fix history/SkillHub ownership and drop thin providers/

Move history_projection under history/, share SkillHub host/limits and
route expert skill downloads through skills.skillhub_market, and fold
Codex OAuth helpers into agents.providers to remove the tiny providers/
package.

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

* docs: sync AGENTS.md and guides with infra/agents layout

Reflect subpackages (settings/workspace/threads/persona, history/, skills/)
and drop stale runtime.py / mbti_profiles paths after the agents reorg.

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

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…fact (TencentCloud#1032) (TencentCloud#1116)

Plugin tools returning large octop_ui payloads (e.g. a bangumi season
with 1200+ episodes, ~228KB JSON) blew up the LLM context because the
same tool-result string served both the model and the UI.

Add OctopUiOffloadMiddleware at the innermost agent middleware layer:
when a ToolMessage content is a >=4000-char string carrying a non-empty
octop_ui renderer and data (and no file:// media refs), the payload is
moved in-place to ToolMessage.artifact and the model-visible content is
replaced by a compact result that keeps title/summary. Tool id,
tool_call_id, name and status are preserved.

Frontend restores rendering from artifact: ToolCallData.artifact,
closeToolCall extraction, history replay path, and resolvePluginUiData
prefers explicit data over data_ref. serialize.py surfaces artifact on
tool_result blocks; history recorder backfills artifact as fallback.

Spec: docs/octop-ui-payload-offload.md

Co-authored-by: jubaoliang <jubaoliang@gmail.com>
Improve team streaming visibility (live speakers, generating footer, IM
dispatch/wrap-up), relax ACP tool enable under sandbox while locking
runners, strip channel think tags per show_thinking, and clarify provider
CLI usage in the assistant skill. Includes self-update/UpdateConfig WIP
from the same working tree.

Co-authored-by: Cursor <cursoragent@cursor.com>
README.md documents `octop memory list/slim` and the /memory slim
chat commands, but README_CN.md's CLI reference table predates them.
Add the three memory rows and the /memory slim paragraph in Chinese,
matching the English README.
GET /api/admin/audit-log forwarded an unvalidated `limit` into `LIMIT ?`,
the same gap TencentCloud#1018 closed for the thread list. SQLite reads a negative LIMIT
as "no limit", so ?limit=-1 returned the whole audit log in one response and
?limit=0 returned an empty page. Bound it with Query(ge=1, le=500), the
largest page the Settings -> Security audit panel offers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Cloud#1150)

* fix(config): 绑定端口越界时告警回落,--port 越界直接报错

OCTOP_PORT 只按 int() 解析、octop run --port 也只有 type=int,70000/-1
这类根本无法绑定的取值会盖掉 config.json 里本来可用的端口,进程死在
socket.bind 抛出的 OverflowError 里,报错既不含变量名也看不出端口来源;
--port 更是在启动之前就把该值写进 config.json,之后每次 octop run 都继续继承。

现按 resolve_bind 已经支持的「0 表示由系统随机分配端口」把取值收口到
0-65535:环境变量越界沿用「告警 + 不覆盖」的既有契约回落配置文件端口,
命令行 --port 越界交给 click.IntRange 在使用期错误里拦下、不落盘。

* docs: 补上绑定端口的取值范围说明

`octop run --port` 现在会渲染成 `INTEGER RANGE … [0<=x<=65535]`,环境变量表也顺手写明
`OCTOP_PORT` 接受 0–65535(0 = 由系统随机分配),与 `OCTOP_LOG_LEVEL` 那行「One of …」的写法一致。
Default stays persistent (localStorage). Unchecking keeps the JWT in
sessionStorage for this tab only; SSO popups post the token back so the
opener can store it correctly.

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

* fix(chat): default-collapse thinking in team rooms only

Keep solo chat expanded by default, and store team/solo preferences under separate localStorage keys so toggling one does not affect the other.

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

* fix(chat): keep team member replies in one bubble and clear stale live state

Continue member answer text across completed tools in team rooms, and stop
snapshots/wrap-up from leaving finished speakers marked live in the footer.

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

---------

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

Lock in that a team member stays in liveSpeakers through tool rounds even after the host done frees the composer.

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

* feat(users): add role templates, invite snapshots, and profile avatars

Roles fill defaults for create, edit, and invite without rewriting existing users. Account type stays independent of the selected role.

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

* style: format role and invite repositories for ruff

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

* fix: narrow invite role name before lookup

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

---------

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

Avoid leaking internal names like execute in the live process hint; always use the shared “正在调用工具” copy while a tool is running.

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

Match the admin reset-password inputs by prefixing current/new password with Lock and confirm with LockOpen.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Bosheng0422 and others added 27 commits October 9, 2026 15:48
Keep the thread when checkpoint cleanup fails, sweep orphan checkpoints from the CLI, and remove an owned agent's workspace when its user is deleted. Skip FnOS process-tree tests where /proc is absent.

Co-authored-by: Cursor <cursoragent@cursor.com>
pytest tmp dirs on Windows are drive paths, which workspace_plan_path leaves unchanged, so the container-root assertions never matched.

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

chore: require octop-gateway 1.0.1
…encentCloud#1874)

Desktop startup was saving a hardcoded bind address and could launch a second Octop when the preferred port was busy. Chat uploads dropped folders, shared peer experts are no longer offered for delete, and the state backend stays available only for experts that already use it.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…t-realtime-asr

feat(voice): add Tencent Cloud realtime (streaming) speech recognition
Deleting a conversation left the SQLite file the same size. Compact it immediately, or after the expert is idle.

Co-authored-by: Cursor <cursoragent@cursor.com>
pytest tmp dirs on Windows are drive paths, which workspace_plan_path leaves unchanged, so the container-root assertions never matched.

Co-authored-by: Cursor <cursoragent@cursor.com>
A hung docker info raised TimeoutExpired during collection and failed the Windows job instead of skipping the live image tests. Also drop an unused mobile overflow state that failed the dashboard typecheck.

Co-authored-by: Cursor <cursoragent@cursor.com>
…sisted-conversation-data

Fix/delete persisted conversation data
…tract-options

feat(memory): configurable distillation call limits (budget, timeouts)
docs: record audit remediation in CHANGELOG, fix stale schema claim
Sync the fork's integration branch with upstream develop (544ea75). A merge
commit, not a rebase: nothing is force-pushed and main stays an ancestor of
develop, per root AGENTS.md section 10.

Conflict resolution
-------------------

Backend (8 files) took the resolutions already validated on
feature/audit-remediation: upstream's shape plus the audit's security guards.
- src/octop/api/routers/admin.py            (upstream AUDIT_LOG_MAX_LIMIT=500)
- src/octop/api/routers/browser/harness.py
- src/octop/api/routers/users.py            (upstream role templates + guard)
- src/octop/api/routers/voice.py
- src/octop/api/routers/workspace.py        (upstream require_agent_workspace)
- src/octop/infra/agents/providers/probe.py
- tests/integration/test_providers_api.py
- tests/integration/test_users_api.py

Frontend (10 files) resolved to UPSTREAM'S VERSION, discarding our audit's
dashboard delta:
- dashboard/src/api/request.ts
- dashboard/src/components/AgentSelector.tsx
- dashboard/src/components/PwaInstallPrompt/index.tsx
- dashboard/src/layouts/Sidebar.tsx
- dashboard/src/locales/en.json
- dashboard/src/locales/zh.json
- dashboard/src/pages/Agent/Workspace/components/DocumentPreview.tsx
- dashboard/src/pages/Agent/Workspace/components/FileViewer.tsx
- dashboard/src/pages/Experts/components/EditAgentDrawer.tsx
- dashboard/src/styles/theme-vars.css

Rationale: that half was authored against a stale base and needed 96 conflict
hunks against current upstream. Several findings have since been fixed upstream
(rose palette in cdc7aef; the AgentSelector rules-of-hooks bug in 95860cc),
and the remainder are re-derived as small, reviewable PRs instead of a replay
nobody can review. Per-finding status lives in .agents/audit/
frontend-inventory.md (gitignored, local only).

AGENTS.md was also taken from upstream, which carries the schema v20 assertion
and the harness-agent -> octop-harness / harness-gateway -> octop-gateway renames.

Verification: make all on the merged tree - format-all, lint and mypy --strict
(569 files) clean; pytest 4623 passed / 25 skipped / 1 failed. The single failure
is tests/unit/test_plugin_manager.py::
test_install_plugin_importing_legacy_harness_agent, which reproduces identically
on clean upstream/develop and is therefore pre-existing and unrelated.

Committed with SKIP_PRECOMMIT=1: the hook's testmon path runs pytest serially
(no -n), projecting ~4.5 hours, and it cannot go green while that upstream
failure exists. The full parallel ship bar recorded above is the stronger gate.
make test-affected was the only pytest entry point without -n, so every local
commit ran its selected tests on a single core. testmon degrades to the whole
suite whenever a commit touches anything shared - a router, conftest.py, a
utils module - which is most backend work.

Measured on the 4684-test suite with a broad testmon selection:

  before: no -n flag, serial
  after:  17m12s real / 29m22s user (make test-affected)

Previously this selection projected to roughly 4.5 hours, which made the gate
unusable rather than merely slow - the practical effect is that contributors
reach for SKIP_PRECOMMIT=1 to avoid it, and then the gate is worthless.

testmon result recording is unaffected: pytest_runtest_logreport fires in the
controller process, so xdist workers report back and the affected-set cache
stays correct. Selection still narrowed correctly in the run above.

PYTEST_JOBS stays overridable (make test-affected PYTEST_JOBS=4), matching
make test and make test-fast.
AgentSelector returned a spinner from an early return while AgentContext is
loading, and `loading` starts true, so the shell always mounted it that way.
Any hook declared below those early returns therefore executed only on the
second render, and React threw "Rendered more hooks than during the previous
render" the moment agents loaded - the shell crashed rather than the picker
failing softly.

Upstream hoisted the hooks above the early returns in 95860cc (TencentCloud#1588), so the
live tree is already correct. This test is the guard that keeps it that way: a
future refactor that reintroduces the ordering will fail here rather than in
production.

It is verified to actually catch the defect, not merely pass. Against the
pre-sync component (fork main, useRef below both early returns) it fails with
both errors; against current develop it passes:

  fork main:
    x keeps hook order stable when loading flips to false
      -> Rendered more hooks than during the previous render.
    x keeps hook order stable when the picker empties
      -> Rendered fewer hooks than expected. This may be caused by an
         accidental early return statement.

  develop (upstream hoist): 2 passed

The second case covers the mirror bug - a selector that empties returns early
too, so the hook count must not fall either.

Verification: npx vitest run (2 passed), npx tsc -b exit 0, npx eslint clean,
npm run build succeeded. Committed with SKIP_PRECOMMIT=1 because the hook ends
in the serial testmon path fixed by #3, which has not merged yet.
…tree

The merge of upstream/develop mis-anchored the audit's changelog hunk: git
placed the 安全 / 修复 / 可访问性 / 界面 sections at the end of 1.0.2b3 instead
of [Unreleased], so unreleased audit work was being attributed to a release
that shipped on 2026-09-26. Moved them back.

While there, trimmed to what this branch actually contains. The dashboard half
of the audit (9154540) was deliberately dropped in d4a93cd, so every 可访问性
and 界面 line described code that is not on develop - including a claim that
themeContrast.test.ts now locks contrast in CI, which it does not. FE-1/2/3/4,
RACE-1, A11Y-1..16, UI-2 and GATE-2 are removed; each returns as its own PR
lands, with its own entry.

Two corrections to the surviving backend lines:

- SEC-15 said the audit-log limit is 1..1000. Upstream had already
  independently clamped it to AUDIT_LOG_MAX_LIMIT = 500, which is stricter and
  is what the code enforces (admin.py:48). Changed to 1..500.
- SEC-11 now also credits the Content-Security-Policy: sandbox header, which is
  what the route actually sends alongside nosniff.

All twelve surviving backend claims were re-verified against the tree before
this commit: setup_completed_at, _safe_io_path, _require_admin_to_grant_admin
(users.py:130), _mask_api_key, user_has_permission (browser/harness.py:17),
assert_probe_base_url_safe, the SVG disposition (workspace.py:495),
secrets.compare_digest (connectors/service.py:637), AUDIT_LOG_MAX_LIMIT,
HITL thread ownership (chat/routes.py:199-208), the env-mask patterns, and the
ACL coverage list (tests/unit/api/test_acl_gate_coverage.py).

SKIP_PRECOMMIT=1, narrowly: the hook ran and selected only 17 tests in 7.84s,
of which the single failure is the pre-existing upstream legacy PluginContext
shim defect (test_install_plugin_importing_legacy_harness_agent), which also
fails on clean upstream/develop. It is tracked separately.
chore(make): parallelise the testmon gate used by the pre-commit hook
test(dashboard): guard AgentSelector against hook-order regression
… storage

Two defects in the auth plumbing, both of which leave the user stranded with
no way forward.

FE-1 - the latch never re-arms.

`handleUnauthorized()` guards navigation with a module-level
`_redirectingToLogin`, set true before navigating and never reset. The guard
exists to collapse a burst of parallel 401s into one navigation, but it does
that for the whole life of the page: after a single expiry, every later 401
returns at the latch and the user is never sent to the login screen again. Sign
back in, let the new token expire, and the dashboard just shows failing
requests forever, with no visible path to recovery.

Re-arm the latch in `setAuthToken()`. Holding a fresh token is precisely the
condition that means the previous redirect has already served its purpose.
This cannot re-introduce the redirect storm the latch guards against, because a
401 response never carries a renewed token, so nothing re-arms during the
burst itself.

FE-14 - storage access could throw.

`getAuthToken()` and `isSessionOnlyAuth()` read `localStorage` directly. In a
private-mode browser or an embedded webview with storage denied, the *property
access* raises SecurityError - not the method call - so these threw before any
request was even built and every authenticated call failed with an opaque
error instead of prompting for login. The same applied to the write paths, so a
successful login could throw too.

Route the auth-token storage traffic through three guarded helpers. Blocked
storage now degrades to "no token", which yields a normal 401 and a normal
login prompt.

Tests

Both were written first and observed failing on the unfixed tree:

  FE-1: "expected spy to be called 2 times, but got 1 times"
  FE-14: DOMException SecurityError thrown from every storage touch

request.blockedStorage.test.ts covers read, write, session-only detection and
clear while one or both stores raise on access. The existing "redirects only
once when several requests fail concurrently" test still passes unchanged,
which is the behaviour the latch must keep.

Verification: npx vitest run src/api - 33 passed; npx tsc -b clean; npx eslint
clean. Two unrelated failures in knowledgeBasesApi / publishedExpertsApi are
pre-existing on develop and were confirmed there by stashing this change.
fix(dashboard): re-arm the 401 redirect latch and survive blocked web storage
The merge in d4a93cd resolved the ten CONFLICTING dashboard files to
upstream's version, which is what dropped commit 9154540 was supposed to do.
It did not do the whole job. `git checkout --theirs` can only revert paths that
existed on the other side; every file 9154540 ADDED, and every file it modified
without overlapping an upstream change, rode straight through the merge.

What actually survived: 50 files and 1239 insertions of the 'dropped' work.
Five of those were new files from 9154540, now unreferenced or orphaned:

- styles/themeContrast.test.ts and styles/themeVars.test.ts (119 + 78 lines)
  asserted contrast floors and token parity for theme work that is NOT on this
  branch. Both were failing, which is why develop carried a red frontend suite.
- pages/.../WorkspaceDrawer.race.test.tsx tested the FE-4 race fix that no
  longer exists here. Also failing.
- components/AppDialog.tsx existed only for ServiceRestartOverlay.tsx, which was
  reverted to upstream and no longer imports it. Dead component.
- components/Markdown/mermaidLoader.test.ts guarded SEC-5; the mermaid source
  change it covered was also reverted.

All five are removed. dashboard/src is now byte-identical to upstream/develop
except for work that is deliberately ours.

This also corrects two conclusions in the audit inventory. Both
themeContrast.test.ts and themeVars.test.ts were recorded as absent; they were
present on this branch, having leaked in through exactly this path. A11Y-5's
AppDialog was recorded as non-existent; it existed but had only one caller, now
gone. Both findings are re-derived from upstream's actual tree in the follow-up
PRs rather than from the leaked state.

Also fixes two test-only drifts that arrived with upstream/develop and were
failing before this change:

- knowledgeBases.test.ts: the ../request mock did not export
  bridgeAgentHeaders, which knowledgeBases.ts has called since upstream added
  it. The mock now mirrors the real helper, including returning undefined for a
  blank agent id.
- publishedExperts.test.ts: refresh() sends a serialized body; the expectation
  still asserted the pre-body shape.

Verification: dashboard/src differs from upstream/develop in exactly 6 files,
all of them intentional. npx tsc -b clean. npx vitest run: 1253 passed, 7 failed
across 5 files - AuthGuard, DocumentPreviewCore.docxSanitize, SkillDrawer,
skillMarkdown and useChat.history. Those 5 were confirmed red on this branch
with this change stashed, so they are upstream's, not ours, and are left alone.
Note that no CI job and no `make all` target runs vitest at all, which is why a
partial revert of a 1239-line change could sit unnoticed.
revert(dashboard): finish dropping the dropped half of the audit
Cut from develop after the upstream/develop sync (d4a93cd), the corrected
Option-B drop (950f999) and the FE-1/FE-14 auth fix (a352808).

This release carries a large jump. Between the previous release and this one
the fork takes in all of upstream/develop, which was 96 commits ahead of
upstream/main at the time of the sync, plus the backend security remediation
and the 401 redirect fix.

Version 1.0.2b7 -> 1.0.2b7. [Unreleased] is sectioned into [1.0.2b7] dated
2026-10-10 and a fresh empty [Unreleased] is opened above it.
@ra-co88
ra-co88 merged commit a27ac64 into main Oct 10, 2026
4 of 6 checks passed
@ra-co88
ra-co88 deleted the release/1.0.2b7 branch October 10, 2026 16:42
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)
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"]
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.