@yottameta/yotta-guardian 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/SKILL.md +14 -4
- package/bin/install.js +246 -73
- package/install.sh +216 -40
- package/package.json +1 -1
- package/references/intent-verifier.md +6 -6
- package/references/policies.md +10 -10
- package/references/rules.md +2 -2
- package/scripts/guardian_rules.py +1 -1
- package/scripts/yotta_guardian.py +23 -4
- package/skill-manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## v0.1.8 (2026-10-01)
|
|
4
|
+
|
|
5
|
+
- 安装器卫生批次:`bin/install.js` / `install.sh` 统一(未知参数报错 exit 2、`--help` / `--version`、残留清理白名单、嵌套载荷保留);由模板单一真源渲染,接入漂移门禁。
|
|
6
|
+
|
|
7
|
+
## v0.1.7 (2026-09-28)
|
|
8
|
+
|
|
9
|
+
文档格式修复 + 两处引擎健壮性修复(规则判定行为不变)。
|
|
10
|
+
|
|
11
|
+
- **修复代码围栏**:`SKILL.md` 与 `references/`(intent-verifier / policies / rules)中被写作管线占位符顶替的代码围栏,全部还原为标准三个反引号,共 20 处;此前自 0.1.0 起一直未还原,Markdown 渲染异常(命令与配置示例无法按代码块显示)。
|
|
12
|
+
- **修复非法转义(0.1.0 起既有)**:`scripts/guardian_rules.py` 的模块 docstring 里 `\s`、`\b` 属非法转义 —— Python 3.12+ 每次调用都向 stderr 报 2 行 SyntaxWarning,严格告警(`PYTHONWARNINGS=error`)下引擎直接加载失败。该 docstring 改为 raw 字符串(与规则表内 `r"…"` 写法一致),告警消失。
|
|
13
|
+
- **修复致命异常退出码(fail-closed,0.1.0 起既有)**:此前未捕获异常走 Python 默认退出码 1,而契约里 1 = 允许但带警告 —— 按退出码判定的宿主会把「引擎崩溃」读成放行(fail-open)。现在规则表加载失败与运行期未预期异常统一以**退出码 4**(用法错误 / 致命异常)结束并在 stderr 写明原因;新增 3 项回归用例(导入失败 → 4、运行期异常 → 4、严格告警下仍正常判定,共 86 项)。
|
|
14
|
+
- **修复彩色输出必崩(0.1.6 起既有)**:`check` 的彩色分支 `c()` 格式串只有 1 个 `%s` 却喂 2 个参数 —— 真人终端(`sys.stdout.isatty()` 为真)必抛 `TypeError`,以退出码 4 结束且**判定与命中规则完全不输出**;`--json` / `--no-color` / 管道不受影响(故此前测试未覆盖)。已修正格式串,并新增「假 TTY」回归用例(共 88 项)。
|
|
15
|
+
- **文档**:`SKILL.md` 新增「宿主接入」一节(放行不代批 / 用文件接引擎输出 / stdin 按 UTF-8 读),并把退出码 4 的语义写成「用法错误 / 致命异常(绝不要当作放行)」。
|
|
16
|
+
- **发布门禁联动**:`validate-skill` 与 `preflight-publish` 的交付面检查扩到 Python 一侧(语法错误 / 非法转义等编译期告警),与 Markdown 检查共用同一入口,命中即拦。
|
|
17
|
+
- 规则判定、策略档位、CLI 输出结构与审计格式不变;版本四处对齐 0.1.7。
|
|
18
|
+
|
|
3
19
|
## v0.1.6 (2026-09-25)
|
|
4
20
|
|
|
5
21
|
安全修复:复合命令拆分,堵住分隔符后的危险段绕过。
|
|
@@ -50,7 +66,7 @@
|
|
|
50
66
|
|
|
51
67
|
- **README.md 改为英文**:作为 GitHub / npm / ClawHub 首页的英文门面(翻译 + 精简,覆盖定位 / 核心价值 / 命令 / 快速使用 / 安装 / 使用示例 / 边界 / 开发校验全流程)。
|
|
52
68
|
- **新增 README.zh-CN.md**:原中文完整主文档整体平移,顶部加语言切换链接。
|
|
53
|
-
- **修复代码围栏**:README
|
|
69
|
+
- **修复代码围栏**:README 中的围栏占位符(写作管线留下的标记)全部改为标准 ```bash / ```(Markdown 渲染修复)。
|
|
54
70
|
- **package.json**:description 改英文;files 加 README.zh-CN.md;版本 0.1.0 → 0.1.1。
|
|
55
71
|
- 版本四处对齐:package.json / SKILL frontmatter / 引擎 VERSION / 文档。
|
|
56
72
|
- 边界(B 方案):references / CHANGELOG / 测试注释不翻译;SKILL 触发描述保持中文。
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-guardian
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.8
|
|
4
4
|
description: 元盾 —— 跨智能体的危险调用拦截护栏:确定性规则引擎 + 可插拔意图验证(不绑模型),拦截危险 exec / write / edit / read / run / shell 工具调用,提供审计日志。触发:代理要执行高风险命令(递归删除、磁盘格式化、提权、防火墙改动、反向 shell、下载即执行等)、要写入系统敏感路径或修改系统配置、要在执行危险操作前做安全检查、或用户说 护栏/拦截/危险操作/安全检查 等。边界:默认只读评估,不自动执行也不放行危险操作;不替代用户决策;不隐藏审计记录;规则可配置。
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
@@ -27,7 +27,7 @@ license: MIT
|
|
|
27
27
|
|
|
28
28
|
Windows 用 python,Linux/macOS 用 python3。
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
```bash
|
|
31
31
|
# 检查一条 exec(0 = 允许)
|
|
32
32
|
python3 scripts/yotta_guardian.py check exec --cmd "git status"
|
|
33
33
|
|
|
@@ -43,15 +43,25 @@ python3 scripts/yotta_guardian.py check --batch calls.json --json
|
|
|
43
43
|
# 审计
|
|
44
44
|
python3 scripts/yotta_guardian.py check exec --cmd "..." --audit-log .yotta-guardian/audit.jsonl
|
|
45
45
|
python3 scripts/yotta_guardian.py audit --file .yotta-guardian/audit.jsonl --tail 20
|
|
46
|
-
|
|
46
|
+
```
|
|
47
47
|
|
|
48
48
|
## 工作流程(AI 智能体执行危险操作前)
|
|
49
49
|
|
|
50
50
|
1. **先检查**:把将要执行的工具调用交给护栏 `check`(单条或 `--batch` 批量)。
|
|
51
|
-
2. **看退出码**:0 = 允许;1 = 允许但带警告(建议人工复核);2 / 3 = 拒绝(high / critical,不要执行);4 =
|
|
51
|
+
2. **看退出码**:0 = 允许;1 = 允许但带警告(建议人工复核);2 / 3 = 拒绝(high / critical,不要执行);4 = 用法错误 / 致命异常(引擎没跑起来 —— 绝不要当作放行)。
|
|
52
52
|
3. **被拒绝怎么办**:如实向用户报告原因与命中规则;确有授权的操作,用 `--allow` / `--allow-path` / 自定义规则文件放行并留审计记录;**不要绕过检查**。
|
|
53
53
|
4. **留痕**:高风险场景用 `--audit-log` 落审计日志,供追溯。
|
|
54
54
|
|
|
55
|
+
## 宿主接入(把护栏挂到「工具调用前」事件时)
|
|
56
|
+
|
|
57
|
+
`skill-manifest.json` 声明 `before_tool` + `fallback: explicit-unverified`。接入方按下面三条做,可避开实测踩过的坑:
|
|
58
|
+
|
|
59
|
+
1. **放行不要回「允许」**:只在拦截时输出拒绝判定(如 `permissionDecision: "deny"`);放行时**不输出任何判定**,把决定权交回宿主。若放行时回「allow」,会覆盖宿主自身的权限流程,护栏就变成自动放行器 —— 护栏只该「拦」,不该「代批」。
|
|
60
|
+
2. **用文件接引擎输出**:部分 hook 进程环境里,用管道捕获子进程 stdout 会拿到空值(同一写法跑 `python -c "print(1)"` 却正常);把引擎 stdout / stderr 重定向到临时文件再读回,判定即稳定(引擎自身的 stdout 是纯净的,`--json` 可直接解析)。
|
|
61
|
+
3. **stdin 先按 UTF-8 读**:hook 载荷里的命令文本常含中文,按本机默认码页解码会乱码;读 stdin 前先 `sys.stdin.reconfigure(encoding="utf-8")`(引擎已对自身 stdout / stderr 做 UTF-8 处理,stdin 由接入方负责)。
|
|
62
|
+
|
|
63
|
+
另外:引擎对「没跑起来」是 fail-closed 的 —— 规则表加载失败或运行期未预期异常一律以退出码 4 结束并在 stderr 写「致命异常: …」。接入方应把 4 当作**不放行 + 报告**处理,而不是放行。
|
|
64
|
+
|
|
55
65
|
## 策略(policy)
|
|
56
66
|
|
|
57
67
|
| 策略 | 行为 |
|
package/bin/install.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* yotta-guardian 跨平台安装器(YottaSkills)
|
|
4
|
+
*
|
|
4
5
|
* 用法:
|
|
5
|
-
* npx -y @yottameta/yotta-guardian --agent <name>
|
|
6
|
-
* npx -y @yottameta/yotta-guardian --dir
|
|
7
|
-
* npx -y @yottameta/yotta-guardian -g
|
|
8
|
-
* npx -y @yottameta/yotta-guardian
|
|
9
|
-
* npx -y @yottameta/yotta-guardian --list
|
|
6
|
+
* npx -y @yottameta/yotta-guardian --agent <name> # 按智能体默认用户级目录安装(推荐)
|
|
7
|
+
* npx -y @yottameta/yotta-guardian --dir <path> # 装到指定目录(用户改了目录的智能体)
|
|
8
|
+
* npx -y @yottameta/yotta-guardian -g # 安装到全部已知智能体用户级目录
|
|
9
|
+
* npx -y @yottameta/yotta-guardian # 安装到检测到的项目级目录
|
|
10
|
+
* npx -y @yottameta/yotta-guardian --list # 列出智能体 -> 默认目录
|
|
11
|
+
* npx -y @yottameta/yotta-guardian --dry-run # 只显示将写入的目标,不写文件
|
|
12
|
+
* npx -y @yottameta/yotta-guardian --version # 显示版本
|
|
13
|
+
* npx -y @yottameta/yotta-guardian --help # 显示帮助
|
|
14
|
+
*
|
|
15
|
+
* 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
|
|
10
16
|
*/
|
|
11
17
|
'use strict';
|
|
12
18
|
const fs = require('fs');
|
|
@@ -16,6 +22,21 @@ const os = require('os');
|
|
|
16
22
|
const SKILL_NAME = 'yotta-guardian';
|
|
17
23
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
18
24
|
|
|
25
|
+
// 顶层开发文件 / 目录:只在安装包顶层跳过;技能包内嵌套同名载荷必须保留。
|
|
26
|
+
// 与 install.sh 同口径:开发件与运行时不相关目录一律不落目标。
|
|
27
|
+
const TOP_SKIP = [
|
|
28
|
+
'package.json', 'package-lock.json', 'bin', 'lib', 'test',
|
|
29
|
+
'.github', '.git', '.gitignore', '.npmignore', '.gitattributes',
|
|
30
|
+
'.yotta', '.tmp', 'install.sh', 'node_modules',
|
|
31
|
+
];
|
|
32
|
+
const TOP_SKIP_SET = new Set(TOP_SKIP);
|
|
33
|
+
// 缓存 / 编译产物:任意层级跳过,安装时同样清理。
|
|
34
|
+
const CACHE_DIRS = new Set(['__pycache__', '.pytest_cache', '.mypy_cache']);
|
|
35
|
+
|
|
36
|
+
class UsageError extends Error {}
|
|
37
|
+
class TargetError extends Error {}
|
|
38
|
+
class InstallError extends Error {}
|
|
39
|
+
|
|
19
40
|
// 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
|
|
20
41
|
// 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
|
|
21
42
|
// Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
|
|
@@ -39,6 +60,26 @@ const AGENT_DIRS = {
|
|
|
39
60
|
agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
|
|
40
61
|
};
|
|
41
62
|
|
|
63
|
+
const PROJECT_DIRS = [
|
|
64
|
+
'.claude/skills',
|
|
65
|
+
'.cursor/skills',
|
|
66
|
+
'.codex/skills',
|
|
67
|
+
'.config/goose/skills',
|
|
68
|
+
'.config/agents/skills',
|
|
69
|
+
'.opencode/skills',
|
|
70
|
+
'.codeium/windsurf/skills',
|
|
71
|
+
'.workbuddy/skills',
|
|
72
|
+
'.kiro/skills',
|
|
73
|
+
'.traecli/skills',
|
|
74
|
+
'.gemini/skills',
|
|
75
|
+
'.trae-cn/skills',
|
|
76
|
+
'.qwen/skills',
|
|
77
|
+
'.comate/skills',
|
|
78
|
+
'.codebuddy/skills',
|
|
79
|
+
'.kimi/skills',
|
|
80
|
+
'.agents/skills',
|
|
81
|
+
];
|
|
82
|
+
|
|
42
83
|
// Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
|
|
43
84
|
function codexUserDir() {
|
|
44
85
|
const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
@@ -57,107 +98,239 @@ function resolveUserDir(rel) {
|
|
|
57
98
|
return path.join(os.homedir(), rel);
|
|
58
99
|
}
|
|
59
100
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
101
|
+
function displayDir(rel) {
|
|
102
|
+
if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
|
|
103
|
+
return '~/' + rel;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function skillVersion() {
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
109
|
+
return data.version || null;
|
|
110
|
+
} catch (_) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function usage() {
|
|
116
|
+
console.log(SKILL_NAME + ' 安装器(YottaSkills)');
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log('用法:');
|
|
119
|
+
console.log(' node bin/install.js --agent <name> 按智能体默认用户级目录安装(推荐)');
|
|
120
|
+
console.log(' node bin/install.js --dir <path> 装到指定目录(用户改了目录的智能体)');
|
|
121
|
+
console.log(' node bin/install.js -g 安装到全部已知智能体用户级目录');
|
|
122
|
+
console.log(' node bin/install.js 安装到检测到的项目级目录');
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log('参数:');
|
|
125
|
+
console.log(' --agent <name> 智能体键名,见 --list');
|
|
126
|
+
console.log(' --dir <path> 自定义技能目录');
|
|
127
|
+
console.log(' -g, --global 安装到全部已知用户级目录');
|
|
128
|
+
console.log(' --list, -l 列出支持的智能体目录');
|
|
129
|
+
console.log(' --dry-run 只显示将写入的目标,不写文件');
|
|
130
|
+
console.log(' --version, -v 显示版本');
|
|
131
|
+
console.log(' --help, -h 显示帮助');
|
|
132
|
+
console.log(' --yes, -y 兼容参数(-g 不再强制要求)');
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log('退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseArgs(argv) {
|
|
138
|
+
const opts = { help: false, version: false, list: false, global: false, dryRun: false, yes: false, dir: null, agent: null };
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const arg = argv[i];
|
|
141
|
+
if (arg === '--help' || arg === '-h') opts.help = true;
|
|
142
|
+
else if (arg === '--version' || arg === '-v') opts.version = true;
|
|
143
|
+
else if (arg === '--list' || arg === '-l') opts.list = true;
|
|
144
|
+
else if (arg === '--global' || arg === '-g') opts.global = true;
|
|
145
|
+
else if (arg === '--dry-run') opts.dryRun = true;
|
|
146
|
+
else if (arg === '--yes' || arg === '-y') opts.yes = true;
|
|
147
|
+
else if (arg === '--dir') {
|
|
148
|
+
const value = argv[++i];
|
|
149
|
+
if (!value) throw new UsageError('--dir 需要一个非空路径');
|
|
150
|
+
opts.dir = value;
|
|
151
|
+
} else if (arg === '--agent') {
|
|
152
|
+
const value = argv[++i];
|
|
153
|
+
if (!value) throw new UsageError('--agent 需要一个非空名称');
|
|
154
|
+
opts.agent = String(value).toLowerCase();
|
|
155
|
+
} else {
|
|
156
|
+
throw new UsageError('未知参数: ' + arg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!opts.help && !opts.version) {
|
|
160
|
+
const selected = [opts.dir, opts.agent, opts.global].filter(Boolean).length;
|
|
161
|
+
if (selected > 1) throw new UsageError('--dir / --agent / -g 只能选一个');
|
|
162
|
+
}
|
|
163
|
+
return opts;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function printList() {
|
|
167
|
+
console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
|
|
168
|
+
for (const [key, value] of Object.entries(AGENT_DIRS)) {
|
|
169
|
+
const resolved = value.dirs.map(displayDir);
|
|
170
|
+
console.log(' ' + key.padEnd(10) + value.label.padEnd(18) + resolved.join('、'));
|
|
171
|
+
}
|
|
172
|
+
console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
|
|
173
|
+
console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function assertSafeTarget(target) {
|
|
177
|
+
const rel = path.relative(PKG_ROOT, target);
|
|
178
|
+
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
|
|
179
|
+
throw new UsageError('目标目录不能在技能源目录内(防止自装自毁)');
|
|
180
|
+
}
|
|
65
181
|
}
|
|
66
182
|
|
|
67
|
-
function
|
|
183
|
+
function shouldSkipCache(name, isFile) {
|
|
184
|
+
if (CACHE_DIRS.has(name)) return true;
|
|
185
|
+
if (isFile && (name.endsWith('.pyc') || name.endsWith('.pyo'))) return true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function copyDir(src, dst, topLevel) {
|
|
68
190
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
69
|
-
|
|
191
|
+
// 只跳过安装包顶层开发文件;template/ 等嵌套同名载荷必须保留。
|
|
192
|
+
if (topLevel && TOP_SKIP_SET.has(entry.name)) continue;
|
|
193
|
+
if (shouldSkipCache(entry.name, entry.isFile())) continue;
|
|
70
194
|
const s = path.join(src, entry.name);
|
|
71
195
|
const d = path.join(dst, entry.name);
|
|
72
196
|
if (entry.isDirectory()) {
|
|
73
197
|
fs.mkdirSync(d, { recursive: true });
|
|
74
|
-
copyDir(s, d,
|
|
198
|
+
copyDir(s, d, false);
|
|
75
199
|
} else if (entry.isFile()) {
|
|
76
200
|
fs.copyFileSync(s, d);
|
|
77
201
|
}
|
|
78
202
|
}
|
|
79
203
|
}
|
|
80
204
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
205
|
+
// 清理旧版安装残留(fail-closed 白名单):
|
|
206
|
+
// 仅在目标目录已存在且含 SKILL.md 时触发;只删顶层开发项 + 任意层级缓存;
|
|
207
|
+
// 不整目录删除、不跟随符号链接。
|
|
208
|
+
function cleanResidue(target) {
|
|
209
|
+
const removed = [];
|
|
210
|
+
let stat = null;
|
|
211
|
+
try { stat = fs.statSync(target); } catch (_) { return removed; }
|
|
212
|
+
if (!stat.isDirectory()) return removed;
|
|
213
|
+
if (!fs.existsSync(path.join(target, 'SKILL.md'))) return removed;
|
|
214
|
+
for (const name of TOP_SKIP) {
|
|
215
|
+
const p = path.join(target, name);
|
|
216
|
+
let entry = null;
|
|
217
|
+
try { entry = fs.lstatSync(p); } catch (_) { continue; }
|
|
218
|
+
if (entry.isSymbolicLink()) continue;
|
|
219
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
220
|
+
removed.push(name);
|
|
221
|
+
}
|
|
222
|
+
const walk = (dir) => {
|
|
223
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
224
|
+
const p = path.join(dir, entry.name);
|
|
225
|
+
if (entry.isSymbolicLink()) continue;
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
if (CACHE_DIRS.has(entry.name)) {
|
|
228
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
229
|
+
removed.push(path.relative(target, p));
|
|
230
|
+
} else {
|
|
231
|
+
walk(p);
|
|
232
|
+
}
|
|
233
|
+
} else if (entry.isFile() && (entry.name.endsWith('.pyc') || entry.name.endsWith('.pyo'))) {
|
|
234
|
+
fs.unlinkSync(p);
|
|
235
|
+
removed.push(path.relative(target, p));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
walk(target);
|
|
240
|
+
return removed;
|
|
84
241
|
}
|
|
85
242
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
243
|
+
function installTo(dest, opts) {
|
|
244
|
+
if (!dest || typeof dest !== 'string') throw new UsageError('目标目录不能为空');
|
|
245
|
+
const target = path.resolve(dest, SKILL_NAME);
|
|
246
|
+
assertSafeTarget(target);
|
|
247
|
+
if (opts.dryRun) {
|
|
248
|
+
console.log('[dry-run] 将安装到 -> ' + target);
|
|
249
|
+
return target;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
for (const rel of cleanResidue(target)) console.log('已清理残留: ' + rel);
|
|
253
|
+
fs.mkdirSync(target, { recursive: true });
|
|
254
|
+
copyDir(PKG_ROOT, target, true);
|
|
255
|
+
if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
|
|
256
|
+
throw new InstallError('安装结果缺少 SKILL.md');
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (err instanceof UsageError || err instanceof TargetError || err instanceof InstallError) throw err;
|
|
260
|
+
throw new InstallError('安装到 ' + target + ' 失败: ' + err.message);
|
|
261
|
+
}
|
|
262
|
+
console.log('installed -> ' + target);
|
|
263
|
+
return target;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function run() {
|
|
267
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
268
|
+
if (opts.help) { usage(); return; }
|
|
269
|
+
if (opts.version) {
|
|
270
|
+
const version = skillVersion();
|
|
271
|
+
if (!version) {
|
|
272
|
+
console.error(SKILL_NAME + ' 版本未知(未找到 package.json)');
|
|
273
|
+
process.exitCode = 1;
|
|
274
|
+
return;
|
|
102
275
|
}
|
|
103
|
-
console.log('
|
|
104
|
-
console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
|
|
276
|
+
console.log(SKILL_NAME + ' v' + version);
|
|
105
277
|
return;
|
|
106
278
|
}
|
|
279
|
+
if (opts.list) { printList(); return; }
|
|
107
280
|
|
|
108
|
-
if (
|
|
281
|
+
if (opts.dir) { installTo(opts.dir, opts); return; }
|
|
109
282
|
|
|
110
|
-
if (agent) {
|
|
111
|
-
const info = AGENT_DIRS[agent];
|
|
283
|
+
if (opts.agent) {
|
|
284
|
+
const info = AGENT_DIRS[opts.agent];
|
|
112
285
|
if (!info) {
|
|
113
|
-
|
|
114
|
-
console.log('可用: ' + Object.keys(AGENT_DIRS).join(', '));
|
|
115
|
-
return;
|
|
286
|
+
throw new UsageError('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';自定义目录请用 --dir <路径>。');
|
|
116
287
|
}
|
|
117
|
-
installTo(resolveUserDir(info.dirs[0]));
|
|
288
|
+
installTo(resolveUserDir(info.dirs[0]), opts);
|
|
118
289
|
console.log('完成。');
|
|
119
290
|
return;
|
|
120
291
|
}
|
|
121
292
|
|
|
122
|
-
if (
|
|
293
|
+
if (opts.global) {
|
|
123
294
|
const seen = new Set();
|
|
124
|
-
for (const
|
|
125
|
-
for (const
|
|
126
|
-
if (seen.has(
|
|
127
|
-
seen.add(
|
|
128
|
-
installTo(resolveUserDir(
|
|
295
|
+
for (const value of Object.values(AGENT_DIRS)) {
|
|
296
|
+
for (const rel of value.dirs) {
|
|
297
|
+
if (seen.has(rel)) continue;
|
|
298
|
+
seen.add(rel);
|
|
299
|
+
installTo(resolveUserDir(rel), opts);
|
|
129
300
|
}
|
|
130
301
|
}
|
|
131
|
-
console.log('完成。');
|
|
302
|
+
if (!opts.dryRun) console.log('完成。');
|
|
132
303
|
return;
|
|
133
304
|
}
|
|
134
305
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
'
|
|
138
|
-
'.codex/skills',
|
|
139
|
-
'.config/goose/skills',
|
|
140
|
-
'.config/agents/skills',
|
|
141
|
-
'.opencode/skills',
|
|
142
|
-
'.codeium/windsurf/skills',
|
|
143
|
-
'.workbuddy/skills',
|
|
144
|
-
'.kiro/skills',
|
|
145
|
-
'.traecli/skills',
|
|
146
|
-
'.gemini/skills',
|
|
147
|
-
'.trae-cn/skills',
|
|
148
|
-
'.qwen/skills',
|
|
149
|
-
'.comate/skills',
|
|
150
|
-
'.codebuddy/skills',
|
|
151
|
-
'.kimi/skills',
|
|
152
|
-
'.agents/skills',
|
|
153
|
-
];
|
|
154
|
-
let installedAny = false;
|
|
155
|
-
for (const d of PROJECT_DIRS) {
|
|
156
|
-
if (fs.existsSync(d)) { installTo(d); installedAny = true; }
|
|
306
|
+
const dirs = PROJECT_DIRS.filter((d) => fs.existsSync(d));
|
|
307
|
+
if (!dirs.length) {
|
|
308
|
+
throw new TargetError('未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。');
|
|
157
309
|
}
|
|
158
|
-
|
|
159
|
-
|
|
310
|
+
for (const dir of dirs) installTo(dir, opts);
|
|
311
|
+
if (!opts.dryRun) console.log('完成。');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function main() {
|
|
315
|
+
try {
|
|
316
|
+
run();
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (err instanceof UsageError) {
|
|
319
|
+
console.error('用法错误: ' + err.message);
|
|
320
|
+
usage();
|
|
321
|
+
process.exitCode = 2;
|
|
322
|
+
} else if (err instanceof TargetError) {
|
|
323
|
+
console.error('目标错误: ' + err.message);
|
|
324
|
+
process.exitCode = 4;
|
|
325
|
+
} else if (err instanceof InstallError) {
|
|
326
|
+
console.error('安装失败: ' + err.message);
|
|
327
|
+
console.error('修复建议: 检查目录权限与磁盘空间后重试,或用 --dir 换一个目录。');
|
|
328
|
+
process.exitCode = 1;
|
|
329
|
+
} else {
|
|
330
|
+
console.error('未知错误: ' + (err && err.message ? err.message : String(err)));
|
|
331
|
+
process.exitCode = 1;
|
|
332
|
+
}
|
|
160
333
|
}
|
|
161
334
|
}
|
|
162
335
|
|
|
163
|
-
main();
|
|
336
|
+
main();
|
package/install.sh
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# yotta-guardian 多智能体安装脚本(YottaSkills
|
|
2
|
+
# yotta-guardian 多智能体安装脚本(YottaSkills)
|
|
3
3
|
# 用法:
|
|
4
4
|
# bash install.sh --agent <name> # 按智能体默认用户级目录安装
|
|
5
5
|
# bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
|
|
6
6
|
# bash install.sh -g # 装到全部已知智能体用户级目录
|
|
7
|
-
# bash install.sh
|
|
7
|
+
# bash install.sh # 检测并安装到已存在的项目级目录
|
|
8
8
|
# bash install.sh --list # 列出智能体 -> 默认目录
|
|
9
|
+
# bash install.sh --dry-run # 只显示将写入的目标,不写文件
|
|
10
|
+
# bash install.sh --version # 显示版本
|
|
11
|
+
# bash install.sh --help # 显示帮助
|
|
12
|
+
# 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
|
|
9
13
|
set -euo pipefail
|
|
10
14
|
|
|
11
15
|
SKILL_NAME="yotta-guardian"
|
|
12
16
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
_IS_WINDOWS=0
|
|
13
18
|
case "$(uname -s)" in
|
|
14
19
|
MINGW*|MSYS*)
|
|
15
20
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
|
|
21
|
+
_IS_WINDOWS=1
|
|
16
22
|
;;
|
|
23
|
+
CYGWIN*) _IS_WINDOWS=1 ;;
|
|
17
24
|
esac
|
|
18
25
|
|
|
19
26
|
# 智能体 -> 用户级默认目录(--agent 装到第一个)
|
|
20
27
|
# .agents/skills 并非通用目录:OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 读取。
|
|
21
|
-
# 判断当前环境:Windows Git Bash 用 %USERPROFILE%,Unix 用 ~
|
|
22
|
-
_IS_WINDOWS=0
|
|
23
|
-
case "$(uname -s)" in
|
|
24
|
-
MINGW*|MSYS*|CYGWIN*) _IS_WINDOWS=1 ;;
|
|
25
|
-
esac
|
|
26
28
|
dirs_for() {
|
|
27
29
|
case "$1" in
|
|
28
30
|
claude) echo ".claude/skills" ;;
|
|
@@ -60,36 +62,153 @@ resolve_user() {
|
|
|
60
62
|
esac
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
local
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
""
|
|
65
|
+
skill_version() {
|
|
66
|
+
local pkg="$SOURCE_DIR/package.json" v=""
|
|
67
|
+
[ -f "$pkg" ] || return 0
|
|
68
|
+
if command -v awk >/dev/null 2>&1; then
|
|
69
|
+
v="$(awk -F'"' '{for (i=1;i<=NF;i++) if ($i=="version") {print $(i+2); exit}}' "$pkg" 2>/dev/null || true)"
|
|
70
|
+
fi
|
|
71
|
+
if [ -z "$v" ]; then
|
|
72
|
+
v="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pkg" 2>/dev/null | head -n 1 || true)"
|
|
73
|
+
fi
|
|
74
|
+
printf '%s' "$v"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
usage() {
|
|
78
|
+
echo "$SKILL_NAME 安装脚本(YottaSkills)"
|
|
79
|
+
echo ""
|
|
80
|
+
echo "用法:"
|
|
81
|
+
echo " bash install.sh --agent <name> 按智能体默认用户级目录安装(推荐)"
|
|
82
|
+
echo " bash install.sh --dir <path> 装到指定目录(用户改了目录的智能体)"
|
|
83
|
+
echo " bash install.sh -g 安装到全部已知智能体用户级目录"
|
|
84
|
+
echo " bash install.sh 安装到检测到的项目级目录"
|
|
85
|
+
echo ""
|
|
86
|
+
echo "参数:"
|
|
87
|
+
echo " --agent <name> 智能体键名,见 --list"
|
|
88
|
+
echo " --dir <path> 自定义技能目录"
|
|
89
|
+
echo " -g, --global 安装到全部已知用户级目录"
|
|
90
|
+
echo " --list, -l 列出支持的智能体目录"
|
|
91
|
+
echo " --dry-run 只显示将写入的目标,不写文件"
|
|
92
|
+
echo " --version, -v 显示版本"
|
|
93
|
+
echo " --help, -h 显示帮助"
|
|
94
|
+
echo " --yes, -y 兼容参数(-g 不再强制要求)"
|
|
95
|
+
echo ""
|
|
96
|
+
echo "退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resolve_path() {
|
|
100
|
+
# 与 SOURCE_DIR 同一风格(Windows Git Bash 用 pwd -W),供路径包含判断使用
|
|
101
|
+
local p="$1"
|
|
102
|
+
if [ "$_IS_WINDOWS" = "1" ]; then
|
|
103
|
+
(cd "$p" 2>/dev/null && pwd -W)
|
|
104
|
+
else
|
|
105
|
+
(cd "$p" 2>/dev/null && pwd -P)
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assert_safe_target() {
|
|
110
|
+
local dest="$1" src_real dest_real parent
|
|
111
|
+
src_real="$(resolve_path "$SOURCE_DIR")" || return 0
|
|
112
|
+
if [ -d "$dest" ]; then
|
|
113
|
+
dest_real="$(resolve_path "$dest")" || return 0
|
|
114
|
+
else
|
|
115
|
+
parent="$(dirname "$dest")"
|
|
116
|
+
dest_real="$(resolve_path "$parent")" || return 0
|
|
117
|
+
dest_real="$dest_real/$(basename "$dest")"
|
|
118
|
+
fi
|
|
119
|
+
case "$dest_real" in
|
|
120
|
+
"$src_real"|"$src_real"/*)
|
|
121
|
+
echo "安装失败:目标目录不能在技能源目录内(防止自装自毁)" >&2
|
|
122
|
+
return 1
|
|
123
|
+
;;
|
|
68
124
|
esac
|
|
125
|
+
return 0
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# 清理旧版安装残留(fail-closed 白名单):仅在目标目录已存在且含 SKILL.md 时触发;
|
|
129
|
+
# 只删顶层开发项 + 任意层级缓存;不整目录删除、不跟随符号链接。
|
|
130
|
+
DEV_SKIP="package.json package-lock.json bin lib test .github .git .gitignore .npmignore .gitattributes .yotta .tmp install.sh node_modules"
|
|
131
|
+
clean_residue() {
|
|
132
|
+
local dest="$1" name p
|
|
133
|
+
[ -d "$dest" ] || return 0
|
|
134
|
+
[ -f "$dest/SKILL.md" ] || return 0
|
|
135
|
+
for name in $DEV_SKIP; do
|
|
136
|
+
p="$dest/$name"
|
|
137
|
+
[ -L "$p" ] && continue
|
|
138
|
+
if [ -e "$p" ]; then
|
|
139
|
+
rm -rf -- "$p"
|
|
140
|
+
echo "已清理残留: $name"
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
find "$dest" -type d \( -name '__pycache__' -o -name '.pytest_cache' -o -name '.mypy_cache' \) -prune -exec rm -rf -- {} + 2>/dev/null || true
|
|
144
|
+
find "$dest" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete 2>/dev/null || true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# 复制技能本体:顶层跳过开发件与运行时不相关目录(嵌套同名保留);
|
|
148
|
+
# 任意层级跳过缓存(__pycache__ / .pytest_cache / .mypy_cache / *.pyc / *.pyo)。
|
|
149
|
+
copy_tree() {
|
|
150
|
+
local src="$1" dst="$2" top="$3" entry base
|
|
151
|
+
mkdir -p "$dst"
|
|
152
|
+
for entry in "$src"/* "$src"/.[!.]* "$src"/..?*; do
|
|
153
|
+
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
154
|
+
base="$(basename "$entry")"
|
|
155
|
+
case "$base" in
|
|
156
|
+
__pycache__|.pytest_cache|.mypy_cache) continue ;;
|
|
157
|
+
esac
|
|
158
|
+
if [ "$top" = "1" ]; then
|
|
159
|
+
case " $DEV_SKIP " in
|
|
160
|
+
*" $base "*) continue ;;
|
|
161
|
+
esac
|
|
162
|
+
fi
|
|
163
|
+
if [ -d "$entry" ] && [ ! -L "$entry" ]; then
|
|
164
|
+
copy_tree "$entry" "$dst/$base" 0 || return 1
|
|
165
|
+
else
|
|
166
|
+
case "$base" in
|
|
167
|
+
*.pyc|*.pyo) continue ;;
|
|
168
|
+
esac
|
|
169
|
+
cp -RP "$entry" "$dst/$base" || return 1
|
|
170
|
+
fi
|
|
171
|
+
done
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
install_to() {
|
|
175
|
+
local base="$1" dry="$2" dest
|
|
176
|
+
dest="$base/$SKILL_NAME"
|
|
69
177
|
if [ -L "$base" ]; then
|
|
70
|
-
echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2
|
|
178
|
+
echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
if [ -e "$base" ] && [ ! -d "$base" ]; then
|
|
182
|
+
echo "安装失败:目标路径已存在且不是目录:$base" >&2
|
|
183
|
+
return 1
|
|
71
184
|
fi
|
|
72
|
-
mkdir -p "$base"
|
|
73
|
-
dest="$base/$SKILL_NAME"
|
|
74
185
|
if [ -L "$dest" ]; then
|
|
75
|
-
echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
|
|
186
|
+
echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
|
|
187
|
+
return 1
|
|
76
188
|
fi
|
|
77
189
|
if [ -e "$dest" ] && [ ! -d "$dest" ]; then
|
|
78
|
-
echo "安装失败:技能路径已存在且不是目录:$dest" >&2
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
190
|
+
echo "安装失败:技能路径已存在且不是目录:$dest" >&2
|
|
191
|
+
return 1
|
|
192
|
+
fi
|
|
193
|
+
assert_safe_target "$dest" || return 1
|
|
194
|
+
if [ "$dry" = "1" ]; then
|
|
195
|
+
echo "[dry-run] 将安装到 -> $dest"
|
|
196
|
+
return 0
|
|
197
|
+
fi
|
|
198
|
+
clean_residue "$dest"
|
|
199
|
+
mkdir -p "$dest" || return 1
|
|
200
|
+
copy_tree "$SOURCE_DIR" "$dest" 1 || return 1
|
|
201
|
+
if [ ! -f "$dest/SKILL.md" ]; then
|
|
202
|
+
echo "安装失败:结果缺少 SKILL.md" >&2
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
86
205
|
echo "installed -> $dest"
|
|
87
206
|
}
|
|
88
207
|
|
|
89
208
|
list() {
|
|
90
209
|
echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
|
|
210
|
+
local a dirs first
|
|
91
211
|
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
92
|
-
local dirs first
|
|
93
212
|
dirs="$(dirs_for "$a")"
|
|
94
213
|
first="${dirs%% *}"
|
|
95
214
|
case "$first" in
|
|
@@ -108,44 +227,101 @@ list() {
|
|
|
108
227
|
}
|
|
109
228
|
|
|
110
229
|
main() {
|
|
111
|
-
local agent="" dir="" global=0 show_list=0
|
|
230
|
+
local agent="" dir="" global=0 show_list=0 show_help=0 show_version=0 dry=0
|
|
112
231
|
while [ $# -gt 0 ]; do
|
|
113
232
|
case "$1" in
|
|
114
|
-
--
|
|
115
|
-
--
|
|
116
|
-
-g|--global) global=1 ;;
|
|
233
|
+
--help|-h) show_help=1 ;;
|
|
234
|
+
--version|-v) show_version=1 ;;
|
|
117
235
|
--list|-l) show_list=1 ;;
|
|
236
|
+
-g|--global) global=1 ;;
|
|
237
|
+
--dry-run) dry=1 ;;
|
|
238
|
+
--yes|-y) : ;;
|
|
239
|
+
--agent)
|
|
240
|
+
shift
|
|
241
|
+
if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
|
|
242
|
+
echo "用法错误: --agent 需要一个非空名称" >&2
|
|
243
|
+
exit 2
|
|
244
|
+
fi
|
|
245
|
+
agent="$1"
|
|
246
|
+
;;
|
|
247
|
+
--dir)
|
|
248
|
+
shift
|
|
249
|
+
if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
|
|
250
|
+
echo "用法错误: --dir 需要一个非空路径" >&2
|
|
251
|
+
exit 2
|
|
252
|
+
fi
|
|
253
|
+
dir="$1"
|
|
254
|
+
;;
|
|
118
255
|
*) echo "未知参数: $1" >&2; exit 2 ;;
|
|
119
256
|
esac
|
|
120
257
|
shift
|
|
121
258
|
done
|
|
122
259
|
|
|
123
|
-
if [ "$
|
|
124
|
-
if [
|
|
260
|
+
if [ "$show_help" = "1" ]; then usage; return 0; fi
|
|
261
|
+
if [ "$show_version" = "1" ]; then
|
|
262
|
+
local v
|
|
263
|
+
v="$(skill_version)"
|
|
264
|
+
if [ -z "$v" ]; then
|
|
265
|
+
echo "$SKILL_NAME 版本未知(未找到 package.json)" >&2
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
echo "$SKILL_NAME v$v"
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
if [ "$show_list" = "1" ]; then list; return 0; fi
|
|
272
|
+
|
|
273
|
+
local selected=0
|
|
274
|
+
[ -n "$dir" ] && selected=$((selected + 1))
|
|
275
|
+
[ -n "$agent" ] && selected=$((selected + 1))
|
|
276
|
+
[ "$global" = "1" ] && selected=$((selected + 1))
|
|
277
|
+
if [ "$selected" -gt 1 ]; then
|
|
278
|
+
echo "用法错误: --dir / --agent / -g 只能选一个" >&2
|
|
279
|
+
exit 2
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
if [ -n "$dir" ]; then
|
|
283
|
+
install_to "$dir" "$dry" || exit 1
|
|
284
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
125
287
|
if [ -n "$agent" ]; then
|
|
126
288
|
local dirs first
|
|
127
289
|
if ! dirs="$(dirs_for "$agent")"; then
|
|
128
|
-
echo "未收录智能体: $agent
|
|
290
|
+
echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
|
|
291
|
+
exit 2
|
|
129
292
|
fi
|
|
130
293
|
first="${dirs%% *}"
|
|
131
|
-
install_to "$(resolve_user "$first")"
|
|
294
|
+
install_to "$(resolve_user "$first")" "$dry" || exit 1
|
|
295
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
296
|
+
return 0
|
|
132
297
|
fi
|
|
133
298
|
if [ "$global" = "1" ]; then
|
|
134
|
-
|
|
135
|
-
local dirs rel
|
|
299
|
+
local a dirs rel seen=" "
|
|
136
300
|
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
137
|
-
dirs="$(dirs_for "$a")"
|
|
138
|
-
for rel in $dirs; do
|
|
301
|
+
dirs="$(dirs_for "$a")" || continue
|
|
302
|
+
for rel in $dirs; do
|
|
303
|
+
case "$seen" in
|
|
304
|
+
*" $rel "*) continue ;;
|
|
305
|
+
esac
|
|
306
|
+
seen="$seen$rel "
|
|
307
|
+
install_to "$(resolve_user "$rel")" "$dry" || exit 1
|
|
308
|
+
done
|
|
139
309
|
done
|
|
140
|
-
echo "完成。"
|
|
310
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
311
|
+
return 0
|
|
141
312
|
fi
|
|
142
313
|
local installed=0 d
|
|
143
314
|
for d in .claude/skills .cursor/skills .codex/skills .config/goose/skills .config/agents/skills .opencode/skills .codeium/windsurf/skills .workbuddy/skills .kiro/skills .traecli/skills .gemini/skills .trae-cn/skills .qwen/skills .comate/skills .codebuddy/skills .kimi/skills .agents/skills; do
|
|
144
|
-
if [ -d "$d" ]; then
|
|
315
|
+
if [ -d "$d" ]; then
|
|
316
|
+
install_to "$d" "$dry" || exit 1
|
|
317
|
+
installed=1
|
|
318
|
+
fi
|
|
145
319
|
done
|
|
146
320
|
if [ "$installed" = "0" ]; then
|
|
147
|
-
echo "未检测到项目级智能体目录。可用 --agent <name>
|
|
321
|
+
echo "目标错误: 未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。" >&2
|
|
322
|
+
exit 4
|
|
148
323
|
fi
|
|
324
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
149
325
|
}
|
|
150
326
|
|
|
151
327
|
main "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yottameta/yotta-guardian",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Yuandun (元盾) — cross-agent dangerous tool-call guardrail: a deterministic rule engine + pluggable intent verifier (model-agnostic) that evaluates exec/write/edit/read/run/shell calls and provides audit logs. Triggers when an agent is about to run a high-risk command, write sensitive paths, or change system config. Boundaries: read-only evaluation by default, configurable allowances.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
引擎对「允许」结论的调用发起验证,写入 stdin 的 JSON:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```json
|
|
17
17
|
{
|
|
18
18
|
"tool": "exec",
|
|
19
19
|
"cmd": "npm install",
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
"cwd": "/home/user/project",
|
|
25
25
|
"findings": []
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
```
|
|
28
28
|
|
|
29
29
|
字段说明:tool 为工具类型;cmd / path / content_preview(内容前 500 字符)/ target 为调用内容;policy 为当前策略;findings 为确定性规则已命中的条目(允许结论下通常为空或仅低危)。
|
|
30
30
|
|
|
31
31
|
## 三、响应(stdout,JSON)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
```json
|
|
34
34
|
{
|
|
35
35
|
"verdict": "allow",
|
|
36
36
|
"severity": "low",
|
|
37
37
|
"reason": "安装依赖,无恶意信号"
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
```
|
|
40
40
|
|
|
41
41
|
- verdict:allow / deny / review 三选一(必须)。
|
|
42
42
|
- severity:可选,deny 时默认 high;取值 info / low / medium / high / critical。
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
## 六、示例验证器(Python 桩)
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
```python
|
|
63
63
|
#!/usr/bin/env python3
|
|
64
64
|
import json
|
|
65
65
|
import sys
|
|
@@ -71,6 +71,6 @@ if cmd.startswith("danger"):
|
|
|
71
71
|
else:
|
|
72
72
|
out = {"verdict": "allow", "severity": "low", "reason": "无风险信号"}
|
|
73
73
|
json.dump(out, sys.stdout)
|
|
74
|
-
|
|
74
|
+
```
|
|
75
75
|
|
|
76
76
|
真实场景可用该协议把调用摘要发给 LLM 网关做语义判断,返回同样的 JSON 即可接入。
|
package/references/policies.md
CHANGED
|
@@ -34,9 +34,9 @@ AI 智能体判断:退出码非 0 时不要直接执行;2 / 3 一律停下
|
|
|
34
34
|
|
|
35
35
|
### 执行前 gate
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
```bash
|
|
38
38
|
python3 scripts/yotta_guardian.py check exec --cmd "<待执行命令>" --json
|
|
39
|
-
|
|
39
|
+
```
|
|
40
40
|
|
|
41
41
|
- exit 0:执行。
|
|
42
42
|
- exit 1:执行但提示用户「护栏给出中危警告」。
|
|
@@ -45,9 +45,9 @@ python3 scripts/yotta_guardian.py check exec --cmd "<待执行命令>" --json
|
|
|
45
45
|
|
|
46
46
|
### 批量预检
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
```bash
|
|
49
49
|
python3 scripts/yotta_guardian.py check --batch calls.json --json
|
|
50
|
-
|
|
50
|
+
```
|
|
51
51
|
|
|
52
52
|
calls.json 为数组或 {"calls": [...]},每项含 tool / cmd / path / content / old / new / target。任一被拒时整体退出码取最严重值。
|
|
53
53
|
|
|
@@ -55,25 +55,25 @@ calls.json 为数组或 {"calls": [...]},每项含 tool / cmd / path / content
|
|
|
55
55
|
|
|
56
56
|
确有授权的操作(如正规重启、备份清理)用显式放行,而不是绕过检查:
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
```bash
|
|
59
59
|
python3 scripts/yotta_guardian.py check exec --cmd "<命令>" --allow "<模式>"
|
|
60
60
|
python3 scripts/yotta_guardian.py check write --path "<路径>" --allow-path "<前缀>"
|
|
61
|
-
|
|
61
|
+
```
|
|
62
62
|
|
|
63
63
|
### 审计
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
```bash
|
|
66
66
|
# 检查时落审计
|
|
67
67
|
python3 scripts/yotta_guardian.py check exec --cmd "<命令>" --audit-log .yotta-guardian/audit.jsonl
|
|
68
68
|
|
|
69
69
|
# 查询
|
|
70
70
|
python3 scripts/yotta_guardian.py audit --file .yotta-guardian/audit.jsonl --tail 20
|
|
71
71
|
python3 scripts/yotta_guardian.py audit --file .yotta-guardian/audit.jsonl --denied --json
|
|
72
|
-
|
|
72
|
+
```
|
|
73
73
|
|
|
74
74
|
## 五、JSON 输出结构(check --json)
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
```json
|
|
77
77
|
{
|
|
78
78
|
"tool": "yotta-guardian",
|
|
79
79
|
"version": "0.1.0",
|
|
@@ -93,4 +93,4 @@ python3 scripts/yotta_guardian.py audit --file .yotta-guardian/audit.jsonl --den
|
|
|
93
93
|
}
|
|
94
94
|
]
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
```
|
package/references/rules.md
CHANGED
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
|
|
102
102
|
## 六、自定义规则文件(--config)
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
```json
|
|
105
105
|
{
|
|
106
106
|
"policy": "default",
|
|
107
107
|
"allow": ["^git "],
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"deny": ["^evil"],
|
|
110
110
|
"verifier": { "command": ["python3", "/path/verifier.py"], "timeout": 30 }
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
```
|
|
113
113
|
|
|
114
114
|
- policy: default / strict / loose
|
|
115
115
|
- allow: 命令正则放行模式
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""guardian_rules.py — YottaMeta 元盾(yotta-guardian)内置规则表(数据,不含匹配逻辑)。
|
|
2
|
+
r"""guardian_rules.py — YottaMeta 元盾(yotta-guardian)内置规则表(数据,不含匹配逻辑)。
|
|
3
3
|
|
|
4
4
|
本文件只声明规则数据,与 yotta-security-audit(元安)/ yotta-vetter(元审)的
|
|
5
5
|
「源码静态扫描」规则不同:这里是「工具调用护栏」规则,面向 exec / write / edit 等
|
|
@@ -49,9 +49,15 @@ except Exception:
|
|
|
49
49
|
|
|
50
50
|
_HERE = Path(__file__).resolve().parent
|
|
51
51
|
sys.path.insert(0, str(_HERE))
|
|
52
|
-
|
|
52
|
+
try:
|
|
53
|
+
import guardian_rules as GR # noqa: E402
|
|
54
|
+
except BaseException as _import_exc: # 规则表加载失败 = 引擎没跑起来 → 致命异常(4)
|
|
55
|
+
sys.stderr.write(
|
|
56
|
+
"致命异常: 规则表加载失败: %s: %s\n" % (type(_import_exc).__name__, _import_exc)
|
|
57
|
+
)
|
|
58
|
+
raise SystemExit(4)
|
|
53
59
|
|
|
54
|
-
VERSION = "0.1.
|
|
60
|
+
VERSION = "0.1.8"
|
|
55
61
|
TOOL_NAME = "yotta-guardian"
|
|
56
62
|
TOOL_CN = "元盾"
|
|
57
63
|
|
|
@@ -1138,7 +1144,8 @@ def _build_verifier(args):
|
|
|
1138
1144
|
|
|
1139
1145
|
def _print_check_text(call, verdict, color):
|
|
1140
1146
|
def c(s, code):
|
|
1141
|
-
|
|
1147
|
+
# 2026-09-28:原写法只有 1 个 %s 却喂了 2 个参数 → 彩色路径(真人终端)必崩 TypeError。
|
|
1148
|
+
return "\033[%sm%s\033[0m" % (code, s) if color else s
|
|
1142
1149
|
kind = c("DENY", "31;1") if not verdict.allowed else c("ALLOW", "32;1")
|
|
1143
1150
|
sev = verdict.severity.upper()
|
|
1144
1151
|
print("[检查] %s" % call.describe(300))
|
|
@@ -1367,4 +1374,16 @@ def main(argv=None):
|
|
|
1367
1374
|
|
|
1368
1375
|
|
|
1369
1376
|
if __name__ == "__main__":
|
|
1370
|
-
|
|
1377
|
+
# 顶层兜底(2026-09-28):未捕获异常一律按「致命异常」退出码 4 结束。
|
|
1378
|
+
# 绝不能让引擎崩溃落到 1 —— 契约里 1 = 允许但带警告,按退出码判定的宿主会把
|
|
1379
|
+
# 「护栏没跑起来」读成放行(fail-open)。SystemExit / KeyboardInterrupt 原样透传。
|
|
1380
|
+
try:
|
|
1381
|
+
main()
|
|
1382
|
+
except (SystemExit, KeyboardInterrupt):
|
|
1383
|
+
raise
|
|
1384
|
+
except BaseException as exc: # 兜底就是要 catch-all
|
|
1385
|
+
try:
|
|
1386
|
+
sys.stderr.write("致命异常: %s: %s\n" % (type(exc).__name__, exc))
|
|
1387
|
+
except Exception:
|
|
1388
|
+
pass
|
|
1389
|
+
raise SystemExit(4)
|