agent-guardrails 0.3.4 → 0.3.6
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/README.md +31 -0
- package/lib/commands/daemon.js +100 -0
- package/lib/daemon/hooks/daemon-check.js +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,6 +138,20 @@ npx agent-guardrails setup --agent <your-agent>
|
|
|
138
138
|
- `openhands`
|
|
139
139
|
- `openclaw`
|
|
140
140
|
|
|
141
|
+
### 更新 / Update
|
|
142
|
+
|
|
143
|
+
如果通过 **npm 全局安装**:
|
|
144
|
+
```bash
|
|
145
|
+
npm update -g agent-guardrails
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
如果通过 **npx / MCP 使用**(不需要改配置文件):
|
|
149
|
+
```bash
|
|
150
|
+
# 清除 npx 缓存,下次启动时自动拉取最新版
|
|
151
|
+
npm cache clean --force
|
|
152
|
+
# 然后重启你的 AI 工具(Claude Code / Cursor 等)即可
|
|
153
|
+
```
|
|
154
|
+
|
|
141
155
|
---
|
|
142
156
|
|
|
143
157
|
## 文档 / Documentation
|
|
@@ -573,6 +587,23 @@ The daemon monitors file changes and automatically runs guardrail checks:
|
|
|
573
587
|
| Background process | Foreground process |
|
|
574
588
|
| Best for active development | Best for pre-commit/CI |
|
|
575
589
|
|
|
590
|
+
### MCP Integration / MCP 集成
|
|
591
|
+
|
|
592
|
+
AI agents can read daemon results via the `read_daemon_status` MCP tool — no polling needed:
|
|
593
|
+
|
|
594
|
+
```json
|
|
595
|
+
{
|
|
596
|
+
"mcpServers": {
|
|
597
|
+
"agent-guardrails": {
|
|
598
|
+
"command": "npx",
|
|
599
|
+
"args": ["agent-guardrails", "mcp"]
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
The agent calls `read_daemon_status` after code changes to check the latest guardrail result, including findings, risks, and cost hints.
|
|
606
|
+
|
|
576
607
|
---
|
|
577
608
|
|
|
578
609
|
## CLI Fallback Quick Start
|
package/lib/commands/daemon.js
CHANGED
|
@@ -23,6 +23,101 @@ const DAEMON_LOG_FILE = ".agent-guardrails/daemon.log";
|
|
|
23
23
|
const DAEMON_CONFIG_FILE = ".agent-guardrails/daemon.json";
|
|
24
24
|
const DAEMON_INFO_FILE = ".agent-guardrails/daemon-info.json";
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 注入/移除 AGENTS.md 中的守护进程规则
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const DAEMON_AGENTS_MARKER_START = "<!-- agent-guardrails:daemon:start -->";
|
|
31
|
+
const DAEMON_AGENTS_MARKER_END = "<!-- agent-guardrails:daemon:end -->";
|
|
32
|
+
const DAEMON_AGENTS_BLOCK = `\n${DAEMON_AGENTS_MARKER_START}
|
|
33
|
+
## Daemon Mode / 守护模式
|
|
34
|
+
|
|
35
|
+
Daemon 正在运行。每次修改文件后,请调用 \`read_daemon_status\` MCP 工具检查 guardrail 结果。如果有 error 级别的 finding,在继续之前先修复。
|
|
36
|
+
${DAEMON_AGENTS_MARKER_END}\n`;
|
|
37
|
+
|
|
38
|
+
function injectDaemonRule(repoRoot) {
|
|
39
|
+
const agentsFile = path.join(repoRoot, "AGENTS.md");
|
|
40
|
+
if (!fs.existsSync(agentsFile)) return;
|
|
41
|
+
let content = fs.readFileSync(agentsFile, "utf8");
|
|
42
|
+
if (content.includes(DAEMON_AGENTS_MARKER_START)) return; // 已存在
|
|
43
|
+
content = content.trimEnd() + DAEMON_AGENTS_BLOCK;
|
|
44
|
+
fs.writeFileSync(agentsFile, content, "utf8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeDaemonRule(repoRoot) {
|
|
48
|
+
const agentsFile = path.join(repoRoot, "AGENTS.md");
|
|
49
|
+
if (!fs.existsSync(agentsFile)) return;
|
|
50
|
+
let content = fs.readFileSync(agentsFile, "utf8");
|
|
51
|
+
const startIdx = content.indexOf(DAEMON_AGENTS_MARKER_START);
|
|
52
|
+
const endIdx = content.indexOf(DAEMON_AGENTS_MARKER_END);
|
|
53
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
54
|
+
content = content.slice(0, startIdx).trimEnd() + "\n" + content.slice(endIdx + DAEMON_AGENTS_MARKER_END.length).trimStart() + "\n";
|
|
55
|
+
fs.writeFileSync(agentsFile, content, "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 注入/移除 Claude Code PostToolUse hook(daemon-check)
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
const DAEMON_HOOK_ID = "agent-guardrails:daemon-check";
|
|
63
|
+
const DAEMON_HOOK_SCRIPT = ".claude/hooks/daemon-check.js";
|
|
64
|
+
|
|
65
|
+
function injectClaudeHook(repoRoot) {
|
|
66
|
+
const settingsPath = path.join(repoRoot, ".claude", "settings.json");
|
|
67
|
+
const hookScriptPath = path.join(repoRoot, DAEMON_HOOK_SCRIPT);
|
|
68
|
+
const scriptSource = path.resolve(__dirname, "..", "daemon", "hooks", "daemon-check.js");
|
|
69
|
+
|
|
70
|
+
// 复制 hook 脚本到项目
|
|
71
|
+
const hookDir = path.dirname(hookScriptPath);
|
|
72
|
+
if (!fs.existsSync(hookDir)) fs.mkdirSync(hookDir, { recursive: true });
|
|
73
|
+
fs.copyFileSync(scriptSource, hookScriptPath);
|
|
74
|
+
|
|
75
|
+
// 合并到 settings.json
|
|
76
|
+
let settings = {};
|
|
77
|
+
if (fs.existsSync(settingsPath)) {
|
|
78
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!settings.hooks) settings.hooks = {};
|
|
82
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
83
|
+
|
|
84
|
+
// 幂等检查
|
|
85
|
+
if (settings.hooks.PostToolUse.some(h => h.id === DAEMON_HOOK_ID)) return;
|
|
86
|
+
|
|
87
|
+
settings.hooks.PostToolUse.push({
|
|
88
|
+
id: DAEMON_HOOK_ID,
|
|
89
|
+
matcher: "Edit|Write|MultiEdit",
|
|
90
|
+
hooks: [{
|
|
91
|
+
type: "command",
|
|
92
|
+
command: `node "${hookScriptPath}"`,
|
|
93
|
+
timeout: 10
|
|
94
|
+
}]
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const settingsDir = path.dirname(settingsPath);
|
|
98
|
+
if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
|
|
99
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function removeClaudeHook(repoRoot) {
|
|
103
|
+
const settingsPath = path.join(repoRoot, ".claude", "settings.json");
|
|
104
|
+
const hookScriptPath = path.join(repoRoot, DAEMON_HOOK_SCRIPT);
|
|
105
|
+
|
|
106
|
+
// 删除 hook 脚本
|
|
107
|
+
if (fs.existsSync(hookScriptPath)) fs.unlinkSync(hookScriptPath);
|
|
108
|
+
|
|
109
|
+
// 从 settings.json 移除
|
|
110
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
111
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
112
|
+
if (!settings.hooks?.PostToolUse) return;
|
|
113
|
+
|
|
114
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(h => h.id !== DAEMON_HOOK_ID);
|
|
115
|
+
if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
|
|
116
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
119
|
+
}
|
|
120
|
+
|
|
26
121
|
/**
|
|
27
122
|
* 默认守护配置
|
|
28
123
|
*/
|
|
@@ -219,6 +314,8 @@ export async function startDaemon(repoRoot, options = {}) {
|
|
|
219
314
|
const pid = await waitForPidFile(pidFile, 5000);
|
|
220
315
|
|
|
221
316
|
if (pid) {
|
|
317
|
+
injectDaemonRule(repoRoot);
|
|
318
|
+
injectClaudeHook(repoRoot);
|
|
222
319
|
console.log(`${t("daemon.started")}`);
|
|
223
320
|
console.log(` PID: ${pid}`);
|
|
224
321
|
console.log(`\n${t("daemon.logFile")}: ${DAEMON_LOG_FILE}`);
|
|
@@ -276,6 +373,9 @@ export async function stopDaemon(repoRoot, options = {}) {
|
|
|
276
373
|
fs.unlinkSync(pidFile);
|
|
277
374
|
}
|
|
278
375
|
|
|
376
|
+
removeDaemonRule(repoRoot);
|
|
377
|
+
removeClaudeHook(repoRoot);
|
|
378
|
+
|
|
279
379
|
console.log(`\n${t("daemon.stopped")}`);
|
|
280
380
|
return { success: true };
|
|
281
381
|
} catch (error) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code PostToolUse Hook — daemon-check
|
|
4
|
+
*
|
|
5
|
+
* Reads the latest daemon check result. If error-level findings exist,
|
|
6
|
+
* outputs them to stderr with exit code 2 so Claude Code sees them.
|
|
7
|
+
* Otherwise exits silently with code 0.
|
|
8
|
+
*
|
|
9
|
+
* Triggered by: Edit, Write, MultiEdit tool usage
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
16
|
+
const resultFile = path.join(projectDir, ".agent-guardrails", "daemon-result.json");
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(resultFile)) process.exit(0);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const data = JSON.parse(fs.readFileSync(resultFile, "utf8"));
|
|
22
|
+
|
|
23
|
+
if (!data.ok && data.result?.findings?.length > 0) {
|
|
24
|
+
const errors = data.result.findings
|
|
25
|
+
.filter(f => f.severity === "error")
|
|
26
|
+
.map(f => `[${f.code}] ${f.message}`)
|
|
27
|
+
.join("\n");
|
|
28
|
+
|
|
29
|
+
if (errors) {
|
|
30
|
+
process.stderr.write(`Guardrails daemon detected issues:\n${errors}\n`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch { /* ignore parse errors */ }
|
|
35
|
+
|
|
36
|
+
process.exit(0);
|