@yottameta/yotta-agent-hardening 0.2.6 → 0.2.7
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 +4 -0
- package/SKILL.md +1 -1
- package/bin/install.js +247 -74
- package/install.sh +223 -28
- package/package.json +1 -1
- package/scripts/yotta_agent_hardening.py +1 -1
package/CHANGELOG.md
CHANGED
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-agent-hardening
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.7
|
|
4
4
|
description: 元安全 —— 给 AI 智能体 / Agent 技能自身做「体检 + 加固建议」:按 提示注入防护 / 工具调用边界 / 数据隔离 三域,对安装的 skills、MCP 服务器、工具描述、权限与数据读取面做配置面静态加固扫描,输出加固报告与可执行防御守则(零依赖 Python 3.8+,扫描只读、敏感读取检测默认开启、报告用「类」表述、每次扫描默认留痕)。触发:用户要求给智能体或技能环境做安全体检 / 加固、检查 MCP 服务器或技能是否可信、排查提示注入 / 越权 / 数据泄露风险、想了解装了一堆技能后的整体暴露面;或用户说 元安全 / 加固 / 安全体检 / 体检 / hardening / 扫一下我的技能 / 检查 MCP / 防御守则 / guardrails 等。边界(Do NOT trigger):不产出可复制注入串 / 攻击 payload;只扫描用户自有、有权检查的目录与配置,不扫描无权访问的环境;不做运行时拦截(那是元盾);不做单个技能装前审核(那是元审 / 元信);不替代人工安全审计与最终决策。
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
package/bin/install.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* yotta-agent-hardening
|
|
3
|
+
* yotta-agent-hardening 跨平台安装器(YottaSkills)
|
|
4
|
+
*
|
|
4
5
|
* 用法:
|
|
5
|
-
* npx -y @yottameta/yotta-agent-hardening --agent <name>
|
|
6
|
-
* npx -y @yottameta/yotta-agent-hardening --dir
|
|
7
|
-
* npx -y @yottameta/yotta-agent-hardening -g
|
|
8
|
-
* npx -y @yottameta/yotta-agent-hardening
|
|
9
|
-
* npx -y @yottameta/yotta-agent-hardening --list
|
|
6
|
+
* npx -y @yottameta/yotta-agent-hardening --agent <name> # 按智能体默认用户级目录安装(推荐)
|
|
7
|
+
* npx -y @yottameta/yotta-agent-hardening --dir <path> # 装到指定目录(用户改了目录的智能体)
|
|
8
|
+
* npx -y @yottameta/yotta-agent-hardening -g # 安装到全部已知智能体用户级目录
|
|
9
|
+
* npx -y @yottameta/yotta-agent-hardening # 安装到检测到的项目级目录
|
|
10
|
+
* npx -y @yottameta/yotta-agent-hardening --list # 列出智能体 -> 默认目录
|
|
11
|
+
* npx -y @yottameta/yotta-agent-hardening --dry-run # 只显示将写入的目标,不写文件
|
|
12
|
+
* npx -y @yottameta/yotta-agent-hardening --version # 显示版本
|
|
13
|
+
* npx -y @yottameta/yotta-agent-hardening --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-agent-hardening';
|
|
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-agent-hardening
|
|
2
|
+
# yotta-agent-hardening 多智能体安装脚本(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-agent-hardening"
|
|
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,17 +62,153 @@ resolve_user() {
|
|
|
60
62
|
esac
|
|
61
63
|
}
|
|
62
64
|
|
|
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
|
+
;;
|
|
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
|
+
|
|
63
174
|
install_to() {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
175
|
+
local base="$1" dry="$2" dest
|
|
176
|
+
dest="$base/$SKILL_NAME"
|
|
177
|
+
if [ -L "$base" ]; then
|
|
178
|
+
echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
if [ -e "$base" ] && [ ! -d "$base" ]; then
|
|
182
|
+
echo "安装失败:目标路径已存在且不是目录:$base" >&2
|
|
183
|
+
return 1
|
|
184
|
+
fi
|
|
185
|
+
if [ -L "$dest" ]; then
|
|
186
|
+
echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
|
|
187
|
+
return 1
|
|
188
|
+
fi
|
|
189
|
+
if [ -e "$dest" ] && [ ! -d "$dest" ]; then
|
|
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
|
|
205
|
+
echo "installed -> $dest"
|
|
68
206
|
}
|
|
69
207
|
|
|
70
208
|
list() {
|
|
71
209
|
echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
|
|
210
|
+
local a dirs first
|
|
72
211
|
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
73
|
-
local dirs first
|
|
74
212
|
dirs="$(dirs_for "$a")"
|
|
75
213
|
first="${dirs%% *}"
|
|
76
214
|
case "$first" in
|
|
@@ -89,44 +227,101 @@ list() {
|
|
|
89
227
|
}
|
|
90
228
|
|
|
91
229
|
main() {
|
|
92
|
-
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
|
|
93
231
|
while [ $# -gt 0 ]; do
|
|
94
232
|
case "$1" in
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
-g|--global) global=1 ;;
|
|
233
|
+
--help|-h) show_help=1 ;;
|
|
234
|
+
--version|-v) show_version=1 ;;
|
|
98
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
|
+
;;
|
|
99
255
|
*) echo "未知参数: $1" >&2; exit 2 ;;
|
|
100
256
|
esac
|
|
101
257
|
shift
|
|
102
258
|
done
|
|
103
259
|
|
|
104
|
-
if [ "$
|
|
105
|
-
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
|
|
106
287
|
if [ -n "$agent" ]; then
|
|
107
288
|
local dirs first
|
|
108
289
|
if ! dirs="$(dirs_for "$agent")"; then
|
|
109
|
-
echo "未收录智能体: $agent
|
|
290
|
+
echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
|
|
291
|
+
exit 2
|
|
110
292
|
fi
|
|
111
293
|
first="${dirs%% *}"
|
|
112
|
-
install_to "$(resolve_user "$first")"
|
|
294
|
+
install_to "$(resolve_user "$first")" "$dry" || exit 1
|
|
295
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
296
|
+
return 0
|
|
113
297
|
fi
|
|
114
298
|
if [ "$global" = "1" ]; then
|
|
115
|
-
|
|
116
|
-
local dirs rel
|
|
299
|
+
local a dirs rel seen=" "
|
|
117
300
|
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
118
|
-
dirs="$(dirs_for "$a")"
|
|
119
|
-
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
|
|
120
309
|
done
|
|
121
|
-
echo "完成。"
|
|
310
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
311
|
+
return 0
|
|
122
312
|
fi
|
|
123
313
|
local installed=0 d
|
|
124
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
|
|
125
|
-
if [ -d "$d" ]; then
|
|
315
|
+
if [ -d "$d" ]; then
|
|
316
|
+
install_to "$d" "$dry" || exit 1
|
|
317
|
+
installed=1
|
|
318
|
+
fi
|
|
126
319
|
done
|
|
127
320
|
if [ "$installed" = "0" ]; then
|
|
128
|
-
echo "未检测到项目级智能体目录。可用 --agent <name>
|
|
321
|
+
echo "目标错误: 未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。" >&2
|
|
322
|
+
exit 4
|
|
129
323
|
fi
|
|
324
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
130
325
|
}
|
|
131
326
|
|
|
132
|
-
main "$@"
|
|
327
|
+
main "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yottameta/yotta-agent-hardening",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "YuanSafe (元安全) — a defensive AI-agent hardening skill: static configuration-facing hardening scan of an agent's own runtime (installed skills, MCP servers, tool descriptions, permissions and data surfaces) across three domains — prompt-injection defense / tool-call boundaries / data isolation — producing a hardening report and enforceable defense guardrails, with zero-dependency Python 3.8+, read-only scanning, sensitive-read detection on by default, \"class\" phrasing in reports (no copy-paste injection strings) and an audit trail on every scan. Triggers: user asks for a security check / hardening of an agent or skill environment, whether MCP servers or skills are trustworthy, prompt-injection / over-privilege / data-leak risk investigation, or the overall exposure after installing many skills. Boundaries: no executable injection strings or attack payloads; scans only directories and configs the user owns or is authorized to inspect; no runtime interception (that is yotta-guardian); no per-skill pre-install review (that is yotta-vetter / yotta-verify); human review and final decisions are not replaced.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|