From 91ecab05ac23bbfda83f4960fdb51718199dd1e4 Mon Sep 17 00:00:00 2001 From: pt-act <211776491+pt-act@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:09:58 +0100 Subject: [PATCH 1/2] fix(release): bump the version everywhere CI checks, not just pyproject v1.0.2b7 shipped with pyproject.toml, uv.lock and CHANGELOG bumped, but the FnOS packaging manifests still said 1.0.2b6, so tests/unit/test_fnos_docker_persist.py:: test_fnos_manifest_version_matches_pyproject failed and the Release workflow's "Build distributions" job died at Run tests. Publish to PyPI, Create GitHub Release, Trigger FnOS FPK build and Trigger sync main into develop were all skipped, so nothing was published under the Octop name. main was left internally inconsistent: pyproject 1.0.2b7 against manifests 1.0.2b6, with the v1.0.2b7 tag sitting on it. Bumps the remaining five locations that .cursor/skills/publish/SKILL.md lists for a release: src/octop/__init__.py __version__, fnos/docker/manifest, fnos/native/manifest, the FnOS docker-compose image tag, and the shields.io version badges in README.md and README_CN.md. Why the pre-commit hook did not catch this: the release commit changed only pyproject.toml, uv.lock and CHANGELOG.md. pytest-testmon selects tests by import graph, and nothing imports pyproject.toml, so the affected set was empty and the gate ran zero tests. That is the documented trade-off in the Makefile - "this is local feedback only, so a missed cross-module impact is caught there" - and here CI is what caught it. Worth remembering that a data-only change to a manifest or lockfile is invisible to testmon by construction. Verification: make all = 4624 passed, 25 skipped, 0 failed, exit 0. tests/unit/test_fnos_docker_persist.py and test_self_update.py: 165 passed. --- README.md | 2 +- README_CN.md | 2 +- fnos/docker/app/docker/docker-compose.yaml | 2 +- fnos/docker/manifest | 2 +- fnos/native/manifest | 2 +- src/octop/__init__.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e4c63e1569..5c9af5e69e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@
-
+
diff --git a/README_CN.md b/README_CN.md
index bf6d49a47e..f868a23bf8 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -15,7 +15,7 @@
-
+
diff --git a/fnos/docker/app/docker/docker-compose.yaml b/fnos/docker/app/docker/docker-compose.yaml
index e7777579ee..ec313a5e2e 100644
--- a/fnos/docker/app/docker/docker-compose.yaml
+++ b/fnos/docker/app/docker/docker-compose.yaml
@@ -1,6 +1,6 @@
services:
octop:
- image: ghcr.io/tencentcloud/octop:1.0.2b6
+ image: ghcr.io/tencentcloud/octop:1.0.2b7
container_name: octop
restart: unless-stopped
pull_policy: missing
diff --git a/fnos/docker/manifest b/fnos/docker/manifest
index 5f8c16888f..21b6ca0f32 100644
--- a/fnos/docker/manifest
+++ b/fnos/docker/manifest
@@ -1,5 +1,5 @@
appname=octop
-version=1.0.2b6
+version=1.0.2b7
display_name=Octop
desc=腾讯开源的多用户多智能体 AI 助手平台,支持 Web 控制台、CLI、IM 渠道与定时任务,数据全部本地存储。
安装后使用:
首次会从 ghcr.io 下载与本包版本对应的镜像(设备需能访问 GitHub Container Registry),请等到应用中心显示「运行中」再点「打开」,或访问 http://设备IP:8088,用安装向导里设置的账号登录。
忘记密码可到数据目录 octop-login.txt 查看备份(仅记录安装或应用「设置」改密;网页改密后不会自动更新)。
首次登录后:
请在控制台「设置 → 模型」配置 API Key,并按需配置 IM 渠道、远程桌面、浏览器等。
注意:Docker 版通过容器部署,会拉取 ghcr.io/tencentcloud/octop 上与本包版本相同的镜像。
source=thirdparty
diff --git a/fnos/native/manifest b/fnos/native/manifest
index 7a8265caf6..f2fe0a538f 100644
--- a/fnos/native/manifest
+++ b/fnos/native/manifest
@@ -1,5 +1,5 @@
appname=octop-native
-version=1.0.2b6
+version=1.0.2b7
display_name=Octop
desc=腾讯开源的多用户多智能体 AI 助手平台,支持 Web 控制台、CLI、IM 渠道与定时任务,数据全部本地存储。
安装后使用:
请等到应用中心显示「运行中」再点「打开」,或访问 http://设备IP:8089,用安装向导里设置的用户名或邮箱登录。
忘记密码时打开飞牛「文件管理」,进入应用共享里的 Octop 数据目录,查看 octop-login.txt(仅备份安装或应用「设置」改密;网页改密后不会自动更新);也可在应用「设置」中改密。
命令行管理:
支持官方 octop CLI(octop provider list / agent list / user list / skills / backup 等,详见 octop --help)。
首次登录后:
请在控制台「设置 → 模型」配置 API Key,并按需配置 IM 渠道、远程桌面、浏览器等。
注意:本地版直接运行在飞牛主机上,安装前会自动关联安装「Python 3.12」开发工具。
source=thirdparty
diff --git a/src/octop/__init__.py b/src/octop/__init__.py
index 847a5c322a..3836b89559 100644
--- a/src/octop/__init__.py
+++ b/src/octop/__init__.py
@@ -4,7 +4,7 @@
from octop.infra.utils.httpx_proxy import install_httpx_cidr_no_proxy
-__version__ = "1.0.2b6"
+__version__ = "1.0.2b7"
# httpx 0.28 treats NO_PROXY CIDR as an exact IP (see issue #1347). Patch once
# at import so CLI, ``octop run``, and in-process harness clients all honor it.
From c7e4dac83e37756df6a116d4955daf47e55fd500 Mon Sep 17 00:00:00 2001
From: pt-act <211776491+pt-act@users.noreply.github.com>
Date: Sun, 11 Oct 2026 05:23:09 +0100
Subject: [PATCH 2/2] fix(security): keep the drive letter when a file:// URL
carries it in netloc
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
SEC-2, Windows only. Caught by Windows CI, not by anything local.
`file_url_to_abs_path` read only `urlparse(...).path`. For the canonical
three-slash form `file:///C:/…` that is correct — the drive is in the path. But
Windows CI builds these URLs from `Path.as_posix()`, which produces the
two-slash form `file://C:/…`. There `urlparse` puts `C:` in **netloc** and the
path is just `/Users/…`, so the drive was silently dropped.
The result is a drive-relative path. On the GitHub Actions Windows runner the
workdir is on D:, so `/Users/runneradmin/.octop/…` resolved to
`D:/Users/runneradmin/.octop/…` rather than the file the caller named.
That breaks the SEC-2 guarantee in two ways at once:
- `test_file_url_read_outside_allowlist_forbidden` read D:/… , found nothing,
and returned 404 instead of 403.
- `test_file_url_write_outside_allowlist_forbidden` **wrote the file and
returned 200**. The containment check ran against the mangled path, which was
inside an allowed root, so it passed.
A user-controlled `path` query parameter could therefore cause a write outside
the intended absolute location on Windows, with no 403.
The fix reads netloc and, when it is a bare drive letter, prepends it to the
path. A UNC host (`file://server/share/…`) is not a drive letter and is left to
the existing branch.
This is a defect in our own SEC-2 work (d08a6928), not an upstream regression.
Regression test is platform-independent so it runs on Linux too — it asserts
the URL shape, not the filesystem. Verified it fails on the old code
(1 failed, 12 passed) and passes on the new (13 passed).
make all: 4625 passed, 25 skipped, 0 failed, exit 0.
---
.../infra/gateway/media/backend_files.py | 7 ++++++
tests/unit/test_host_download_allow.py | 25 +++++++++++++++++++
2 files changed, 32 insertions(+)
diff --git a/src/octop/infra/gateway/media/backend_files.py b/src/octop/infra/gateway/media/backend_files.py
index 15f63316fe..5e76369a26 100644
--- a/src/octop/infra/gateway/media/backend_files.py
+++ b/src/octop/infra/gateway/media/backend_files.py
@@ -145,7 +145,14 @@ def is_allowed_host_temp_path(resolved: Path) -> bool:
def file_url_to_abs_path(file_url: str) -> str:
parsed = urllib.parse.urlparse(file_url)
+ netloc = parsed.netloc
path = parsed.path
+ # file://C:/… — two slashes, so urlparse puts the drive letter in netloc
+ # and path alone loses it. Without this the result is drive-relative
+ # (D:\Users\… when the workdir is on D:), which both escapes the
+ # containment check and writes somewhere the caller did not name.
+ if len(netloc) == 2 and netloc[1] == ":" and netloc[0].isalpha():
+ return str(Path(f"{netloc}{urllib.parse.unquote(path)}"))
# file:///C:/… — Windows drive in URL path
if len(path) >= 3 and path[0] == "/" and path[2] == ":":
return str(Path(urllib.parse.unquote(path[1:])))
diff --git a/tests/unit/test_host_download_allow.py b/tests/unit/test_host_download_allow.py
index 4add0cf25c..b08cb10daf 100644
--- a/tests/unit/test_host_download_allow.py
+++ b/tests/unit/test_host_download_allow.py
@@ -66,3 +66,28 @@ def test_file_url_windows_drive_decodes_unicode() -> None:
"""file:///C:/… must unquote and match native Path form (Windows CI)."""
url = "file:///C:/Users/me/out/%E4%BF%9D%E6%8A%A4.pptx"
assert file_url_to_abs_path(url) == str(Path("C:/Users/me/out/保护.pptx"))
+
+
+def test_file_url_windows_drive_in_netloc_keeps_drive() -> None:
+ """``file://C:/…`` — two slashes, so the drive lands in netloc, not path.
+
+ Windows CI builds these URLs from ``Path.as_posix()``, which yields
+ ``file://C:/…``. ``urlparse`` puts ``C:`` in netloc, so reading ``path``
+ alone loses the drive and returns ``/Users/…``. On a runner whose workdir
+ is on D: that resolves drive-relative, which both escapes the containment
+ check and writes somewhere the caller did not name — SEC-2's file://
+ write test came back 200 instead of 403.
+ """
+ assert file_url_to_abs_path("file://C:/Users/me/out/a.pptx") == str(
+ Path("C:/Users/me/out/a.pptx")
+ )
+ # Percent-decoding still applies on this branch.
+ assert file_url_to_abs_path("file://C:/Users/me/out/%E4%BF%9D%E6%8A%A4.pptx") == str(
+ Path("C:/Users/me/out/保护.pptx")
+ )
+ # The three-slash form is unchanged by the fix.
+ assert file_url_to_abs_path("file:///C:/Users/me/out/a.pptx") == str(
+ Path("C:/Users/me/out/a.pptx")
+ )
+ # A UNC host is still a host, not a drive letter.
+ assert file_url_to_abs_path("file://server/share/a.pptx") != str(Path("C:/share/a.pptx"))