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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ jobs:
run: node build/validate.mjs
- name: 构建三工具产物
run: node build/build.mjs
- name: 安装冲突回归
run: node --test tests/*.test.mjs
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## [0.1.1] - 2026-07-31

- 安装前预检同名 agent,默认拒绝冲突且不写入任何文件。
- 需要覆盖时必须显式追加 `--force`。
- 即使使用 `--force`,仍拒绝目标文件、agent 目录或工具目录 symlink。
- 新增安装回归测试、CI 门禁与私密安全报告入口。
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,8 @@ bash scripts/install.sh codex # 复制到 ~/.codex/prompts/,用 /<slug>
bash scripts/install.sh openclaw # 复制到 ~/.openclaw/agents/
```

安装器遇到同名文件会在写入前退出,避免静默覆盖。确认需要覆盖时,在命令末尾追加 `--force`。

## 目录结构

```
Expand Down
9 changes: 9 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Security Policy

请不要在公开 Issue 中披露未修复的安全漏洞。

请通过 GitHub 的私密漏洞报告功能提交问题:

https://github.com/HeiGeAi/200-agent/security/advisories/new

报告中请包含复现步骤、影响范围和建议修复方式。维护者会在确认后协调披露时间。
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "200-agent",
"version": "0.1.0",
"version": "0.1.1",
"description": "200 个面向中国市场的 AI agent,单一源转译适配 Claude Code / Codex / OpenClaw",
"type": "module",
"private": false,
"license": "MIT",
"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" }
}
55 changes: 45 additions & 10 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
#!/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,用 /<slug> 调用"
;;
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

TOOL_HOME="${DEST%/*}"
if [ -L "$TOOL_HOME" ] || [ -L "$DEST" ]; then
echo "拒绝写入符号链接目录: $DEST" >&2
exit 1
fi

SOURCE_PARENT="${SRC%/*}"
SOURCE_ROOT="${SOURCE_PARENT%/*}"
if [ -L "$SOURCE_ROOT" ] || [ -L "$SOURCE_PARENT" ] || [ -L "$SRC" ]; then
echo "拒绝从符号链接源目录复制: $SRC" >&2
exit 1
fi
for SOURCE in "$SRC"/*.md; do
if [ -L "$SOURCE" ]; then
echo "拒绝从符号链接源文件复制: $SOURCE" >&2
exit 1
fi
done

if [ -d "$DEST" ]; then
for SOURCE in "$SRC"/*.md; do
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
fi
done
fi

mkdir -p "$DEST"
cp -v "$SRC"/*.md "$DEST"/
echo "已安装 200 agent 到 $DEST"
126 changes: 126 additions & 0 deletions tests/install.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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');

function sourceFixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), '200-agent-source-'));
const install = path.join(root, 'scripts', 'install.sh');
fs.mkdirSync(path.dirname(install), { recursive: true });
fs.copyFileSync(INSTALL, install);
fs.chmodSync(install, 0o755);
return { root, install };
}

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');
});

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');
});

test('--force rejects a symlinked agent directory before writing', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), '200-agent-dir-symlink-'));
const toolHome = path.join(home, '.claude');
const destination = path.join(toolHome, 'agents');
const outside = path.join(home, 'outside');
fs.mkdirSync(toolHome, { recursive: true });
fs.mkdirSync(outside);
fs.symlinkSync(outside, destination, 'dir');

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.readdirSync(outside).length, 0);
});

test('installer refuses a symlinked source agent file before copying', () => {
const { root, install } = sourceFixture();
const home = path.join(root, 'home');
const source = path.join(root, 'dist', 'claude-code', 'agents');
const outside = path.join(root, 'outside.md');
fs.mkdirSync(source, { recursive: true });
fs.writeFileSync(outside, 'outside\n');
fs.symlinkSync(outside, path.join(source, 'agent.md'));

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.existsSync(path.join(home, '.claude', 'agents', 'agent.md')), false);
});

test('installer refuses a symlinked source agent directory before copying', () => {
const { root, install } = sourceFixture();
const home = path.join(root, 'home');
const sourceParent = path.join(root, 'dist', 'claude-code');
const outside = path.join(root, 'outside');
fs.mkdirSync(sourceParent, { recursive: true });
fs.mkdirSync(outside);
fs.writeFileSync(path.join(outside, 'agent.md'), 'outside\n');
fs.symlinkSync(outside, path.join(sourceParent, 'agents'), 'dir');

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.existsSync(path.join(home, '.claude', 'agents', 'agent.md')), false);
});
Loading