Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,46 @@ jobs:
- name: Run clean npm and Python delivery flow
run: python scripts/check_clean_install.py --json

ubuntu-distro-python-install:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Ubuntu pip package
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
- name: Install with externally managed distro Python
shell: bash
run: |
set -euo pipefail
compgen -G '/usr/lib/python3*/EXTERNALLY-MANAGED' >/dev/null
project="$RUNNER_TEMP/probhub-distro-project"
home="$RUNNER_TEMP/probhub-distro-home"
user_base="$RUNNER_TEMP/probhub-distro-user-base"
mkdir -p "$project" "$home" "$user_base"
cd "$project"
env \
HOME="$home" \
PYTHON=/usr/bin/python3 \
PYTHONUSERBASE="$user_base" \
PROBHUB_ALLOW_SYSTEM_PYTHON=1 \
node "$GITHUB_WORKSPACE/bin/init.js" --local
env \
HOME="$home" \
PYTHON=/usr/bin/python3 \
PYTHONUSERBASE="$user_base" \
node "$GITHUB_WORKSPACE/bin/probhub.js" --version
env PYTHONUSERBASE="$user_base" /usr/bin/python3 -c \
'import flask, pypdf, yaml; print(flask.__name__, pypdf.__version__)'
test -f "$project/.agents/skills/probhub/.probhub-version.json"
test -n "$(find "$user_base" -type f -print -quit)"

python-dependency-audit:
strategy:
fail-fast: false
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## [Unreleased]

- 主包 README、兼容包 README、Agent Skill 与 Release 安装说明统一为 Node.js 18+ / Python 3.10+ 的系统 Python 显式授权流程;非虚拟环境依赖只写入用户目录并兼容 Ubuntu PEP 668,pip 子进程清除 Python 环境污染;临时 `npx`、Doctor 修复和 WebUI 检查不再遗漏 `PROBHUB_ALLOW_SYSTEM_PYTHON=1`,安装器报错不再引导用户创建虚拟环境。
- 空工作区锁文件只在取得 OS 文件锁后初始化,消除 Windows 并发 generation 首次启动时的写入、刷新与关闭竞态。
- Build Manifest 升至 schema v4、试卷 generation 升至 schema v3,统一记录 ProbHub/Core、Typst、pypdf、模板与固定字体的 `builder_fingerprint`;`status` 提供字段级 stale 原因,旧 schema 和不可探测工具链不再误报 `current`,build/seal/generation 在发布前以 `builder_changed` 阻断身份漂移。Noto Sans CJK SC 与许可证改为随 npm 包发布,Typst 正式编译只使用校验后的包内字体。
- Python 运行时升级并锁定 Flask 3.1.3 与 pypdf 6.14.2;Windows/Ubuntu CI 使用固定 pip-audit 审计完整依赖闭包,临时例外必须绑定包名、原因、到期日和追踪链接。PDF 页数读取、文本边界扫描与切页迁入受 timeout、内存、输出和进程数限制的独立 worker,损坏或异常 PDF 不再无界占用构建进程或 WebUI 请求线程。
- 非交互 stdout/stderr 及 Checker feedback 改为原子共享单一输出预算,完成、超时、取消和资源超限均在进程树终止后执行确定性公平前缀截断;无法测量或截断时 fail closed,沙箱缓存 Schema 升至 6。stress schema 2 反例以 `E + min(E, 8 MiB)` 限制单次持久化,消除 `generator.out` 输入副本,记录逐文件预算与截断证据,并拒绝重放不完整的 Generator OLE 输入。
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ ProbHub 会在这条流程中提供:

### 1. 安装 Skill

安装前请确认已安装 Node.js 18 或更高版本(包含 npm),以及 Python 3.10 或更高版本。安装 Skill 时会把 Flask、PyYAML 和 pypdf 等依赖安装到当前 `python` 指向的环境;下面的命令显式允许这次安装。
安装前请确认已安装 Node.js 18 或更高版本(包含 npm),以及 Python 3.10 或更高版本。Ubuntu 的系统 Python 还需安装 `python3-pip`。安装 Skill 时会把 Flask、PyYAML 和 pypdf 等固定版本依赖安装到当前 Python 的用户依赖目录;下面的命令显式允许这次安装。

Windows PowerShell:

Expand All @@ -53,14 +53,32 @@ probhub doctor

`probhub doctor` 会列出 Python、Node.js、npm、`g++`、Typst、字体和 Python 依赖的实际状态。先修复其中的错误,再继续创建题目。

`PROBHUB_ALLOW_SYSTEM_PYTHON=1` 只表示你同意本次安装写入当前 Python 的用户依赖目录;不会覆盖 Ubuntu 由系统包管理器维护的 Python 包。PowerShell 中的设置只在当前终端会话生效,它也不会关闭 ProbHub 的资源限制。

`probhub-skill` 还会把 Agent Skill 安装到:

```text
~/.claude/skills/probhub
~/.agents/skills/probhub
```

只想临时安装 Skill 时可以运行 `npx probhub-skill`;只想安装到当前项目时使用 `npx probhub-skill --local`。
<details>
<summary>临时运行或只安装到当前项目</summary>

不全局安装 npm 包时,同样需要显式允许写入当前 Python:

```powershell
$env:PROBHUB_ALLOW_SYSTEM_PYTHON = "1"
npx probhub-skill
```

```bash
PROBHUB_ALLOW_SYSTEM_PYTHON=1 npx probhub-skill
```

只安装到当前项目的 Agent Skill 目录时,在命令末尾增加 `--local`。

</details>

### 2. 调用 Agent

Expand Down Expand Up @@ -117,7 +135,7 @@ Ubuntu 可以安装编译器:

```bash
sudo apt update
sudo apt install -y g++
sudo apt install -y g++ python3-pip
```

Typst 请使用上方链接中的 0.14.2 固定版本。Noto Sans CJK SC 已随 npm 主包发布,正式编译会校验字体字节并忽略系统字体,无需单独下载安装。
Expand Down Expand Up @@ -267,7 +285,7 @@ npx probhub --version
- `g++` 或 Typst 不在 `PATH`;
- Typst 不是 0.14.2;
- npm 包内的固定字体缺失或校验失败,可重新安装 `probhub`;
- Python 依赖没有安装,可重新运行 `probhub-skill`。
- Python 依赖没有安装,可按“快速开始”使用带 `PROBHUB_ALLOW_SYSTEM_PYTHON=1` 的同一条 `probhub-skill` 命令重新安装

### WebUI 打不开

Expand Down
2 changes: 2 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ description: 当用户需要创作或维护算法竞赛题目、选择快速/普

# 3. CLI 操作规则

安装、升级、排查 `doctor` / WebUI 启动问题,或准备 GitHub Release 安装说明时,先读取 `references/installation.md`。不得推荐虚拟环境、遗漏 Node.js/Python 版本要求,或在系统 Python 路径中省略显式允许开关。

## 3.1 入口和工作区定位

在工作区根目录或其任意子目录中运行:
Expand Down
9 changes: 7 additions & 2 deletions bin/python.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
const { spawnSync } = require('child_process');

const MODULE_BOOTSTRAP = [
'import runpy, sys',
'import runpy, site, sys',
'root = sys.argv.pop(1)',
'module = sys.argv.pop(1)',
"stdout = getattr(sys, 'stdout', None)",
"stderr = getattr(sys, 'stderr', None)",
"getattr(stdout, 'reconfigure', lambda **kwargs: None)(encoding='utf-8', errors='backslashreplace')",
"getattr(stderr, 'reconfigure', lambda **kwargs: None)(encoding='utf-8', errors='backslashreplace')",
"sys.path[:] = [root] + [entry for entry in sys.path if entry not in ('', root)]",
"inside_venv = bool(getattr(sys, 'real_prefix', None) or sys.prefix != getattr(sys, 'base_prefix', sys.prefix))",
'user_sites = [] if inside_venv else site.getusersitepackages()',
'user_sites = [user_sites] if isinstance(user_sites, str) else list(user_sites)',
"base_paths = [entry for entry in sys.path if entry not in ('', root) and entry not in user_sites]",
"package_index = next((index for index, entry in enumerate(base_paths) if entry.replace('\\\\', '/').lower().endswith(('/site-packages', '/dist-packages'))), len(base_paths))",
"sys.path[:] = [root] + base_paths[:package_index] + [entry for entry in user_sites if entry and entry != root] + base_paths[package_index:]",
"runpy.run_module(module, run_name='__main__', alter_sys=True)",
].join('; ');

Expand Down
38 changes: 27 additions & 11 deletions compat/probhub-skill/README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
# probhub-skill

`probhub-skill` 是 ProbHub 的轻量 npm 入口包:
`probhub-skill` 是 ProbHub 的轻量 npm 入口包。完整实现由同版本的 [`probhub`](https://www.npmjs.com/package/probhub) 主包提供;本包只保留命令转发,不复制 Python Core、WebUI、Skill 或 references。

安装前需要 Node.js 18 或更高版本(包含 npm),以及 Python 3.10 或更高版本;Ubuntu 的系统 Python 还需安装 `python3-pip`。

Windows PowerShell:

```powershell
npm install -g probhub
$env:PROBHUB_ALLOW_SYSTEM_PYTHON = "1"
probhub-skill
probhub doctor
```

Ubuntu/Linux:

```bash
python3 -m venv ~/.probhub/venv
source ~/.probhub/venv/bin/activate
npx probhub-skill
npx probhub-skill --local
npm install -g probhub
PROBHUB_ALLOW_SYSTEM_PYTHON=1 probhub-skill
probhub doctor
```

Windows PowerShell 使用 `py -3 -m venv "$HOME\.probhub\venv"`,然后运行 `& "$HOME\.probhub\venv\Scripts\Activate.ps1"`。后续也可把 `PYTHON` 指向该虚拟环境解释器;只有明确接受修改当前系统 Python 时才设置 `PROBHUB_ALLOW_SYSTEM_PYTHON=1`
`PROBHUB_ALLOW_SYSTEM_PYTHON=1` 明确授权安装器把固定版本的 Python 依赖安装到当前 Python 的用户依赖目录,不会覆盖 Ubuntu 由系统包管理器维护的 Python 包。PowerShell 中的设置只在当前终端会话生效。需要指定另一套 Python 3.10+ 时,先设置 `PYTHON` 指向该解释器

完整实现由同版本的 [`probhub`](https://www.npmjs.com/package/probhub) 主包提供。本包只保留 `probhub-skill` 与 `probhub` 两个命令转发入口,不复制 Python Core、WebUI、Skill 或 references。
临时运行时使用:

需要持久使用 CLI 时,安装完整主包:
```powershell
$env:PROBHUB_ALLOW_SYSTEM_PYTHON = "1"
npx probhub-skill
```

```bash
npm install -g probhub
probhub-skill
probhub --version
PROBHUB_ALLOW_SYSTEM_PYTHON=1 npx probhub-skill
```

只安装到当前项目的 Agent Skill 目录时增加 `--local`。安装后在包含 `.probhub/workspace.yaml` 的目录运行 `probhub --json ui --check` 检查 WebUI,运行 `probhub ui` 启动它。

## 维护规则

- 本包版本必须与 `probhub` 主包版本完全一致。
- 必须先发布 `probhub`,确认 npm registry 可安装后,再发布本包。
- 两个包的同版本均可从 npm 安装后,才能创建对应 GitHub Release。
- 本包的 `dependencies.probhub` 必须锁定精确版本,不能使用 `^` 或 `~`。
- 功能代码只在 `probhub` 主包中维护,本包不得复制实现。
6 changes: 3 additions & 3 deletions probhub/build_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ def workspace_file_lock(
path.parent.mkdir(parents=True, exist_ok=True)
stream = _open_lock_stream(path, no_follow=no_follow)
try:
if path.stat().st_size == 0:
stream.write(b"\0")
stream.flush()
deadline = time.monotonic() + max(0, float(wait_timeout))
while True:
try:
Expand All @@ -127,6 +124,9 @@ def workspace_file_lock(
else f"failed to acquire ProbHub workspace lock {path}: {exc}"
)
raise ProbHubError(message, code=code) from exc
if path.stat().st_size == 0:
stream.write(b"\0")
stream.flush()
try:
yield path
finally:
Expand Down
90 changes: 66 additions & 24 deletions probhub/install_deps.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
"""Bounded Python dependency installation used by the npm Skill entry."""

import os
import shutil
import sys
import tempfile
from pathlib import Path

from .process_control import run_managed_to_files


WINDOWS_NODE_CHILD_LAUNCHER = (
"const {spawnSync}=require('child_process');"
"const child=spawnSync(process.argv[1],process.argv.slice(2),{stdio:'inherit'});"
"if(child.error){console.error(child.error.message);process.exit(1);}"
"process.exit(child.status===null?1:child.status);"
)


for stream in (sys.stdout, sys.stderr):
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure:
Expand All @@ -24,13 +33,37 @@ def _inside_virtual_environment():
)


def _pip_install_command(requirements, *, user_install):
python = os.environ.get("PYTHON") or sys.executable
command = [
python,
"-m",
"pip",
"install",
"--disable-pip-version-check",
]
if user_install:
command.append("--user")
command.extend(["-r", str(requirements)])
if os.name != "nt":
return command
node = shutil.which("node")
if not node:
raise OSError("Node.js >= 18 is required to contain the Windows Python installer")
# Windows Store Python uses an App Execution Alias that cannot itself be
# created suspended. Start a bounded Node supervisor first; its Python and
# pip descendants inherit the already-assigned Job Object.
return [node, "-e", WINDOWS_NODE_CHILD_LAUNCHER, *command]


def main():
if not _inside_virtual_environment() and os.environ.get("PROBHUB_ALLOW_SYSTEM_PYTHON") != "1":
user_install = not _inside_virtual_environment()
if user_install and os.environ.get("PROBHUB_ALLOW_SYSTEM_PYTHON") != "1":
print(
"ProbHub Skill dependency installation requires an activated Python "
"virtual environment. Activate one or set PYTHON to its interpreter. "
"Set PROBHUB_ALLOW_SYSTEM_PYTHON=1 only when modifying this Python "
"installation is intentional.",
"ProbHub refused to modify the selected Python installation without "
"explicit consent. Set PROBHUB_ALLOW_SYSTEM_PYTHON=1 when installing "
"the pinned dependencies into this Python is intentional, or set "
"PYTHON to another prepared Python 3.10+ interpreter.",
file=sys.stderr,
)
return 1
Expand All @@ -44,34 +77,43 @@ def main():
stdout_path = Path(temp) / "stdout"
stderr_path = Path(temp) / "stderr"
env = os.environ.copy()
for variable in ("PYTHONHOME", "PYTHONPATH", "PYTHONSTARTUP"):
env.pop(variable, None)
env["PYTHONIOENCODING"] = "utf-8"
env["PYTHONUTF8"] = "1"
result = run_managed_to_files(
[
sys.executable,
"-m",
"pip",
"install",
"--disable-pip-version-check",
"-r",
str(requirements),
],
stdout_path=stdout_path,
stderr_path=stderr_path,
timeout=600,
memory_limit_mb=2048,
output_limit_bytes=16 * 1024 * 1024,
process_limit=64,
cwd=package_root,
env=env,
)
if user_install:
# Distro Python remains externally managed; pinned packages are
# installed only into the selected interpreter's user site.
env["PIP_BREAK_SYSTEM_PACKAGES"] = "1"
try:
command = _pip_install_command(requirements, user_install=user_install)
result = run_managed_to_files(
command,
stdout_path=stdout_path,
stderr_path=stderr_path,
timeout=600,
memory_limit_mb=2048,
output_limit_bytes=16 * 1024 * 1024,
process_limit=64,
cwd=package_root,
env=env,
)
except OSError as exc:
print(f"ProbHub dependency installation failed: {exc}", file=sys.stderr)
return 1
stdout = stdout_path.read_text(encoding="utf-8", errors="backslashreplace") if stdout_path.is_file() else ""
stderr = stderr_path.read_text(encoding="utf-8", errors="backslashreplace") if stderr_path.is_file() else ""
if stdout:
print(stdout, end="" if stdout.endswith("\n") else "\n")
if stderr:
print(stderr, end="" if stderr.endswith("\n") else "\n", file=sys.stderr)
if result["reason"] != "completed" or result["returncode"] != 0:
if user_install and "No module named pip" in stderr:
print(
"The selected Python does not provide pip. On Ubuntu, install "
"it with: sudo apt install python3-pip",
file=sys.stderr,
)
print(
f"ProbHub dependency installation failed: "
f"{result.get('message') or result['reason']}",
Expand Down
Loading
Loading