From 0191361858145b2ef2277e581856ea68f377a367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Blake=E5=BE=90?= <488137@qq.com> Date: Fri, 31 Jul 2026 02:28:53 +0800 Subject: [PATCH 1/2] fix: prevent silent agent overwrites --- .github/workflows/ci.yml | 2 ++ CHANGELOG.md | 7 +++++++ README.md | 2 ++ SECURITY.md | 9 +++++++++ package.json | 5 +++-- scripts/install.sh | 31 +++++++++++++++++++++---------- tests/install.test.mjs | 37 +++++++++++++++++++++++++++++++++++++ 7 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 SECURITY.md create mode 100644 tests/install.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38918b9..db87db0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,3 +17,5 @@ jobs: run: node build/validate.mjs - name: 构建三工具产物 run: node build/build.mjs + - name: 安装冲突回归 + run: node --test tests/*.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d929384 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [0.1.1] - 2026-07-31 + +- 安装前预检同名 agent,默认拒绝冲突且不写入任何文件。 +- 需要覆盖时必须显式追加 `--force`。 +- 新增安装回归测试、CI 门禁与私密安全报告入口。 diff --git a/README.md b/README.md index dd34028..67b7e74 100644 --- a/README.md +++ b/README.md @@ -413,6 +413,8 @@ bash scripts/install.sh codex # 复制到 ~/.codex/prompts/,用 / bash scripts/install.sh openclaw # 复制到 ~/.openclaw/agents/ ``` +安装器遇到同名文件会在写入前退出,避免静默覆盖。确认需要覆盖时,在命令末尾追加 `--force`。 + ## 目录结构 ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ef5ac62 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +请不要在公开 Issue 中披露未修复的安全漏洞。 + +请通过 GitHub 的私密漏洞报告功能提交问题: + +https://github.com/HeiGeAi/200-agent/security/advisories/new + +报告中请包含复现步骤、影响范围和建议修复方式。维护者会在确认后协调披露时间。 diff --git a/package.json b/package.json index c246a0b..5726daa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "200-agent", - "version": "0.1.0", + "version": "0.1.1", "description": "200 个面向中国市场的 AI agent,单一源转译适配 Claude Code / Codex / OpenClaw", "type": "module", "private": false, @@ -8,7 +8,8 @@ "scripts": { "build": "node build/build.mjs", "validate": "node build/validate.mjs", - "check": "node build/validate.mjs && node build/build.mjs" + "test": "node --test tests/*.test.mjs", + "check": "node build/validate.mjs && node build/build.mjs && node --test tests/*.test.mjs" }, "engines": { "node": ">=18" } } diff --git a/scripts/install.sh b/scripts/install.sh index a62ceb7..aa4f5c4 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,30 +1,41 @@ #!/usr/bin/env bash -# 把构建产物安装到对应工具目录。用法: bash scripts/install.sh [claude-code|codex|openclaw] +# 把构建产物安装到对应工具目录。用法: bash scripts/install.sh [claude-code|codex|openclaw] [--force] set -euo pipefail cd "$(dirname "$0")/.." [ -d dist ] || { echo "未发现 dist/,先运行: npm run build"; exit 1; } TARGET="${1:-claude-code}" +OPTION="${2:-}" +[ -z "${3:-}" ] || { echo "参数过多"; exit 1; } +[ -z "$OPTION" ] || [ "$OPTION" = "--force" ] || { echo "未知选项: $OPTION"; exit 1; } case "$TARGET" in claude-code) + SRC="dist/claude-code/agents" DEST="$HOME/.claude/agents" - mkdir -p "$DEST" - cp -v dist/claude-code/agents/*.md "$DEST"/ - echo "已安装 200 agent 到 $DEST" ;; codex) + SRC="dist/codex/prompts" DEST="$HOME/.codex/prompts" - mkdir -p "$DEST" - cp -v dist/codex/prompts/*.md "$DEST"/ - echo "已安装到 $DEST,用 / 调用" ;; openclaw) + SRC="dist/openclaw/agents" DEST="$HOME/.openclaw/agents" - mkdir -p "$DEST" - cp -v dist/openclaw/agents/*.md "$DEST"/ - echo "已安装到 $DEST" ;; *) echo "未知目标: $TARGET (支持 claude-code | codex | openclaw)"; exit 1 ;; esac + +if [ "$OPTION" != "--force" ] && [ -d "$DEST" ]; then + for SOURCE in "$SRC"/*.md; do + if [ -e "$DEST/${SOURCE##*/}" ]; then + echo "检测到同名文件冲突: $DEST/${SOURCE##*/}" >&2 + echo "未写入任何文件。确认覆盖时请追加 --force。" >&2 + exit 1 + fi + done +fi + +mkdir -p "$DEST" +cp -v "$SRC"/*.md "$DEST"/ +echo "已安装 200 agent 到 $DEST" diff --git a/tests/install.test.mjs b/tests/install.test.mjs new file mode 100644 index 0000000..61300bf --- /dev/null +++ b/tests/install.test.mjs @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; + +const ROOT = path.resolve(import.meta.dirname, '..'); +const INSTALL = path.join(ROOT, 'scripts', 'install.sh'); +const SOURCE = path.join(ROOT, 'dist', 'claude-code', 'agents'); + +test('install refuses conflicts before writing unless --force is explicit', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), '200-agent-install-')); + const destination = path.join(home, '.claude', 'agents'); + const names = fs.readdirSync(SOURCE).filter((name) => name.endsWith('.md')).sort(); + fs.mkdirSync(destination, { recursive: true }); + fs.writeFileSync(path.join(destination, names[0]), 'keep me\n'); + + const blocked = spawnSync('bash', [INSTALL, 'claude-code'], { + cwd: ROOT, + env: { ...process.env, HOME: home }, + encoding: 'utf8', + }); + assert.notEqual(blocked.status, 0); + assert.match(`${blocked.stdout}${blocked.stderr}`, /冲突/); + assert.equal(fs.readFileSync(path.join(destination, names[0]), 'utf8'), 'keep me\n'); + assert.equal(fs.existsSync(path.join(destination, names[1])), false); + + const forced = spawnSync('bash', [INSTALL, 'claude-code', '--force'], { + cwd: ROOT, + env: { ...process.env, HOME: home }, + encoding: 'utf8', + }); + assert.equal(forced.status, 0, forced.stderr); + assert.equal(fs.readdirSync(destination).filter((name) => name.endsWith('.md')).length, 200); + assert.notEqual(fs.readFileSync(path.join(destination, names[0]), 'utf8'), 'keep me\n'); +}); From a45705c038b6c800fe3dc35a66b69104bdf65497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Blake=E5=BE=90?= <488137@qq.com> Date: Fri, 31 Jul 2026 02:47:35 +0800 Subject: [PATCH 2/2] fix: reject installer destination symlinks --- scripts/install.sh | 9 +++++++-- tests/install.test.mjs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index aa4f5c4..472af19 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -26,9 +26,14 @@ case "$TARGET" in echo "未知目标: $TARGET (支持 claude-code | codex | openclaw)"; exit 1 ;; esac -if [ "$OPTION" != "--force" ] && [ -d "$DEST" ]; then +if [ -d "$DEST" ]; then for SOURCE in "$SRC"/*.md; do - if [ -e "$DEST/${SOURCE##*/}" ]; then + TARGET_PATH="$DEST/${SOURCE##*/}" + if [ -L "$TARGET_PATH" ]; then + echo "拒绝写入符号链接: $TARGET_PATH" >&2 + exit 1 + fi + if [ "$OPTION" != "--force" ] && [ -e "$TARGET_PATH" ]; then echo "检测到同名文件冲突: $DEST/${SOURCE##*/}" >&2 echo "未写入任何文件。确认覆盖时请追加 --force。" >&2 exit 1 diff --git a/tests/install.test.mjs b/tests/install.test.mjs index 61300bf..d2f0de9 100644 --- a/tests/install.test.mjs +++ b/tests/install.test.mjs @@ -35,3 +35,22 @@ test('install refuses conflicts before writing unless --force is explicit', () = assert.equal(fs.readdirSync(destination).filter((name) => name.endsWith('.md')).length, 200); assert.notEqual(fs.readFileSync(path.join(destination, names[0]), 'utf8'), 'keep me\n'); }); + +test('--force never follows a destination symlink outside the agent directory', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), '200-agent-symlink-')); + const destination = path.join(home, '.claude', 'agents'); + const name = fs.readdirSync(SOURCE).find((item) => item.endsWith('.md')); + const outside = path.join(home, 'outside.md'); + fs.mkdirSync(destination, { recursive: true }); + fs.writeFileSync(outside, 'outside\n'); + fs.symlinkSync(outside, path.join(destination, name)); + + const result = spawnSync('bash', [INSTALL, 'claude-code', '--force'], { + cwd: ROOT, + env: { ...process.env, HOME: home }, + encoding: 'utf8', + }); + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}${result.stderr}`, /符号链接/); + assert.equal(fs.readFileSync(outside, 'utf8'), 'outside\n'); +});