guardrails-ref 1.0.0
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/dist/add.d.ts +1 -0
- package/dist/add.js +29 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +83 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.js +32 -0
- package/dist/parse.d.ts +23 -0
- package/dist/parse.js +121 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +81 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.js +147 -0
- package/dist/validate.d.ts +17 -0
- package/dist/validate.js +99 -0
- package/package.json +46 -0
package/dist/add.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runAdd(name: string, projectPath?: string): boolean;
|
package/dist/add.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { TEMPLATES, TEMPLATE_NAMES } from "./templates.js";
|
|
5
|
+
export function runAdd(name, projectPath = ".") {
|
|
6
|
+
const normalized = name.toLowerCase().replace(/\s+/g, "-");
|
|
7
|
+
const content = TEMPLATES[normalized];
|
|
8
|
+
if (!content) {
|
|
9
|
+
console.log(chalk.red("Unknown guardrail:") + " " + name);
|
|
10
|
+
console.log(chalk.gray("Available: " + TEMPLATE_NAMES.join(", ")));
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const root = resolve(projectPath);
|
|
14
|
+
const guardrailsDir = resolve(root, ".agents", "guardrails");
|
|
15
|
+
const exampleDir = resolve(guardrailsDir, normalized);
|
|
16
|
+
const exampleFile = resolve(exampleDir, "GUARDRAIL.md");
|
|
17
|
+
if (existsSync(exampleFile)) {
|
|
18
|
+
console.log(chalk.yellow(".agents/guardrails/" + normalized + "/GUARDRAIL.md already exists"));
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (!existsSync(guardrailsDir)) {
|
|
22
|
+
mkdirSync(guardrailsDir, { recursive: true });
|
|
23
|
+
console.log(chalk.green("✓") + " Created .agents/guardrails/");
|
|
24
|
+
}
|
|
25
|
+
mkdirSync(exampleDir, { recursive: true });
|
|
26
|
+
writeFileSync(exampleFile, content);
|
|
27
|
+
console.log(chalk.green("✓") + " Added .agents/guardrails/" + normalized + "/GUARDRAIL.md");
|
|
28
|
+
return true;
|
|
29
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { validatePath, listGuardrails } from "./validate.js";
|
|
5
|
+
import { runSetup } from "./setup.js";
|
|
6
|
+
import { runInit } from "./init.js";
|
|
7
|
+
import { runAdd } from "./add.js";
|
|
8
|
+
program
|
|
9
|
+
.name("guardrails-ref")
|
|
10
|
+
.description("Validate and list Agent Guardrails (GUARDRAIL.md) files")
|
|
11
|
+
.version("1.0.0");
|
|
12
|
+
program
|
|
13
|
+
.command("validate [path]")
|
|
14
|
+
.description("Validate GUARDRAIL.md files in a directory or a single file")
|
|
15
|
+
.action((path = ".") => {
|
|
16
|
+
const result = validatePath(path);
|
|
17
|
+
let hasErrors = false;
|
|
18
|
+
for (const r of result.results) {
|
|
19
|
+
if (r.success) {
|
|
20
|
+
const warnStr = r.warnings.length > 0 ? chalk.yellow(` (${r.warnings.length} warnings)`) : "";
|
|
21
|
+
console.log(chalk.green("✓"), r.path, warnStr);
|
|
22
|
+
for (const w of r.warnings) {
|
|
23
|
+
console.log(chalk.yellow(" warning:"), w);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
hasErrors = true;
|
|
28
|
+
console.log(chalk.red("✗"), r.path);
|
|
29
|
+
for (const e of r.errors) {
|
|
30
|
+
console.log(chalk.red(" error:"), e);
|
|
31
|
+
}
|
|
32
|
+
for (const w of r.warnings) {
|
|
33
|
+
console.log(chalk.yellow(" warning:"), w);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (result.total === 0) {
|
|
38
|
+
console.log(chalk.yellow("No GUARDRAIL.md or GUARDRAILS.md files found"));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(`Valid: ${result.valid}/${result.total} Invalid: ${result.invalid}/${result.total}`);
|
|
43
|
+
}
|
|
44
|
+
process.exit(hasErrors ? 1 : 0);
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command("init [path]")
|
|
48
|
+
.description("Create .agents/guardrails/, add no-plaintext-secrets, and run setup (one command to get started)")
|
|
49
|
+
.action((path = ".") => {
|
|
50
|
+
runInit(path);
|
|
51
|
+
});
|
|
52
|
+
program
|
|
53
|
+
.command("add <name> [path]")
|
|
54
|
+
.description("Add an example guardrail by name (e.g. no-destructive-commands, no-new-deps-without-approval)")
|
|
55
|
+
.action((name, path = ".") => {
|
|
56
|
+
const ok = runAdd(name, path);
|
|
57
|
+
process.exit(ok ? 0 : 1);
|
|
58
|
+
});
|
|
59
|
+
program
|
|
60
|
+
.command("setup [path]")
|
|
61
|
+
.description("Add the guardrail one-liner to Cursor rules and Claude instructions (required until IDEs support guardrails natively)")
|
|
62
|
+
.action((path = ".") => {
|
|
63
|
+
const result = runSetup(path);
|
|
64
|
+
console.log(result.message);
|
|
65
|
+
});
|
|
66
|
+
program
|
|
67
|
+
.command("list [path]")
|
|
68
|
+
.description("List discovered guardrails")
|
|
69
|
+
.action((path = ".") => {
|
|
70
|
+
const guardrails = listGuardrails(path);
|
|
71
|
+
if (guardrails.length === 0) {
|
|
72
|
+
console.log(chalk.yellow("No guardrails found"));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const g of guardrails) {
|
|
76
|
+
console.log(chalk.cyan(g.name));
|
|
77
|
+
console.log(" ", g.description.slice(0, 80) + (g.description.length > 80 ? "..." : ""));
|
|
78
|
+
console.log(" ", chalk.gray(g.path));
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
console.log(`Total: ${guardrails.length} guardrail(s)`);
|
|
82
|
+
});
|
|
83
|
+
program.parse();
|
package/dist/init.d.ts
ADDED
package/dist/init.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { runSetup } from "./setup.js";
|
|
5
|
+
import { TEMPLATES } from "./templates.js";
|
|
6
|
+
export function runInit(projectPath = ".") {
|
|
7
|
+
const root = resolve(projectPath);
|
|
8
|
+
const guardrailsDir = resolve(root, ".agents", "guardrails");
|
|
9
|
+
const exampleDir = resolve(guardrailsDir, "no-plaintext-secrets");
|
|
10
|
+
const exampleFile = resolve(exampleDir, "GUARDRAIL.md");
|
|
11
|
+
let exampleCreated = false;
|
|
12
|
+
if (!existsSync(guardrailsDir)) {
|
|
13
|
+
mkdirSync(guardrailsDir, { recursive: true });
|
|
14
|
+
console.log(chalk.green("✓") + " Created .agents/guardrails/");
|
|
15
|
+
}
|
|
16
|
+
if (!existsSync(exampleFile)) {
|
|
17
|
+
mkdirSync(exampleDir, { recursive: true });
|
|
18
|
+
writeFileSync(exampleFile, TEMPLATES["no-plaintext-secrets"]);
|
|
19
|
+
exampleCreated = true;
|
|
20
|
+
console.log(chalk.green("✓") + " Created .agents/guardrails/no-plaintext-secrets/GUARDRAIL.md");
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log(chalk.yellow(" .agents/guardrails/no-plaintext-secrets/GUARDRAIL.md already exists"));
|
|
24
|
+
}
|
|
25
|
+
const setupResult = runSetup(projectPath);
|
|
26
|
+
console.log(setupResult.message);
|
|
27
|
+
return {
|
|
28
|
+
guardrailsDir,
|
|
29
|
+
exampleCreated,
|
|
30
|
+
setupDone: setupResult.message,
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface GuardrailFrontmatter {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
scope?: "global" | "project" | "session";
|
|
5
|
+
severity?: "critical" | "warning" | "advisory";
|
|
6
|
+
triggers?: string[];
|
|
7
|
+
license?: string;
|
|
8
|
+
metadata?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export interface ParsedGuardrail {
|
|
11
|
+
path: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
frontmatter: GuardrailFrontmatter;
|
|
15
|
+
body: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ParseResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
guardrail?: ParsedGuardrail;
|
|
20
|
+
errors: string[];
|
|
21
|
+
warnings: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function parseGuardrailFile(filePath: string): ParseResult;
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
const NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5
|
+
const MAX_NAME_LENGTH = 64;
|
|
6
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
7
|
+
const VALID_SCOPES = new Set(["global", "project", "session"]);
|
|
8
|
+
const VALID_SEVERITIES = new Set(["critical", "warning", "advisory"]);
|
|
9
|
+
export function parseGuardrailFile(filePath) {
|
|
10
|
+
const errors = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
let content;
|
|
13
|
+
try {
|
|
14
|
+
content = readFileSync(filePath, "utf-8");
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
errors: [`Cannot read file: ${filePath}`],
|
|
20
|
+
warnings: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = matter(content);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
errors: [`Invalid YAML frontmatter: ${err.message}`],
|
|
31
|
+
warnings: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const data = parsed.data;
|
|
35
|
+
const body = parsed.content.trim();
|
|
36
|
+
// Required: name
|
|
37
|
+
const name = data.name;
|
|
38
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
39
|
+
errors.push("Missing or empty required field: name");
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
43
|
+
warnings.push(`name exceeds ${MAX_NAME_LENGTH} characters`);
|
|
44
|
+
}
|
|
45
|
+
if (!NAME_REGEX.test(name)) {
|
|
46
|
+
errors.push("name must be lowercase letters, numbers, and hyphens only; no consecutive hyphens; cannot start or end with hyphen");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Required: description
|
|
50
|
+
const description = data.description;
|
|
51
|
+
if (typeof description !== "string" || !description.trim()) {
|
|
52
|
+
errors.push("Missing or empty required field: description");
|
|
53
|
+
}
|
|
54
|
+
else if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
55
|
+
warnings.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`);
|
|
56
|
+
}
|
|
57
|
+
// Optional: scope
|
|
58
|
+
const scope = data.scope;
|
|
59
|
+
if (scope !== undefined) {
|
|
60
|
+
if (typeof scope !== "string" || !VALID_SCOPES.has(scope)) {
|
|
61
|
+
warnings.push(`scope must be one of: global, project, session`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Optional: severity
|
|
65
|
+
const severity = data.severity;
|
|
66
|
+
if (severity !== undefined) {
|
|
67
|
+
if (typeof severity !== "string" || !VALID_SEVERITIES.has(severity)) {
|
|
68
|
+
warnings.push(`severity must be one of: critical, warning, advisory`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Optional: triggers
|
|
72
|
+
const triggers = data.triggers;
|
|
73
|
+
if (triggers !== undefined) {
|
|
74
|
+
if (!Array.isArray(triggers) || !triggers.every((t) => typeof t === "string")) {
|
|
75
|
+
warnings.push("triggers must be a list of strings");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Check directory name matches (when in a directory)
|
|
79
|
+
const dirName = filePath.split(/[/\\]/).slice(-2)[0];
|
|
80
|
+
if (dirName && dirName !== "." && typeof name === "string" && name !== dirName) {
|
|
81
|
+
warnings.push(`name "${name}" does not match parent directory "${dirName}"`);
|
|
82
|
+
}
|
|
83
|
+
if (errors.length > 0) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
errors,
|
|
87
|
+
warnings,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const frontmatter = {
|
|
91
|
+
name: name.trim(),
|
|
92
|
+
description: description.trim(),
|
|
93
|
+
};
|
|
94
|
+
if (scope && typeof scope === "string" && VALID_SCOPES.has(scope)) {
|
|
95
|
+
frontmatter.scope = scope;
|
|
96
|
+
}
|
|
97
|
+
if (severity && typeof severity === "string" && VALID_SEVERITIES.has(severity)) {
|
|
98
|
+
frontmatter.severity = severity;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(triggers) && triggers.every((t) => typeof t === "string")) {
|
|
101
|
+
frontmatter.triggers = triggers;
|
|
102
|
+
}
|
|
103
|
+
if (data.license && typeof data.license === "string") {
|
|
104
|
+
frontmatter.license = data.license;
|
|
105
|
+
}
|
|
106
|
+
if (data.metadata && typeof data.metadata === "object" && !Array.isArray(data.metadata)) {
|
|
107
|
+
frontmatter.metadata = data.metadata;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
guardrail: {
|
|
112
|
+
path: resolve(filePath),
|
|
113
|
+
name: frontmatter.name,
|
|
114
|
+
description: frontmatter.description,
|
|
115
|
+
frontmatter,
|
|
116
|
+
body,
|
|
117
|
+
},
|
|
118
|
+
errors: [],
|
|
119
|
+
warnings,
|
|
120
|
+
};
|
|
121
|
+
}
|
package/dist/setup.d.ts
ADDED
package/dist/setup.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
const GUARDRAIL_RULE = "You MUST read and follow all constraints in .agents/guardrails/. Never violate a guardrail without explicit human approval.";
|
|
5
|
+
const GUARDRAIL_RULE_MARKER = "read and follow all constraints in .agents/guardrails";
|
|
6
|
+
function hasRule(content) {
|
|
7
|
+
return content.includes(GUARDRAIL_RULE_MARKER);
|
|
8
|
+
}
|
|
9
|
+
export function runSetup(projectPath = ".") {
|
|
10
|
+
const root = resolve(projectPath);
|
|
11
|
+
let cursorDone = false;
|
|
12
|
+
let claudeDone = false;
|
|
13
|
+
// Cursor: .cursor/rules/agent-guardrails.md (preferred) or .cursorrules (legacy)
|
|
14
|
+
const cursorRulesDir = resolve(root, ".cursor", "rules");
|
|
15
|
+
const cursorRuleFile = resolve(cursorRulesDir, "agent-guardrails.md");
|
|
16
|
+
const cursorRuleContent = `---
|
|
17
|
+
description: Load and follow Agent Guardrails
|
|
18
|
+
alwaysApply: true
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
${GUARDRAIL_RULE}
|
|
22
|
+
`;
|
|
23
|
+
const cursorrulesPath = resolve(root, ".cursorrules");
|
|
24
|
+
if (existsSync(cursorRuleFile)) {
|
|
25
|
+
const existing = readFileSync(cursorRuleFile, "utf-8");
|
|
26
|
+
if (!hasRule(existing)) {
|
|
27
|
+
writeFileSync(cursorRuleFile, cursorRuleContent);
|
|
28
|
+
cursorDone = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (existsSync(cursorrulesPath)) {
|
|
32
|
+
// Legacy: append to .cursorrules
|
|
33
|
+
const existing = readFileSync(cursorrulesPath, "utf-8");
|
|
34
|
+
if (!hasRule(existing)) {
|
|
35
|
+
const appended = existing.trimEnd() + "\n\n" + GUARDRAIL_RULE + "\n";
|
|
36
|
+
writeFileSync(cursorrulesPath, appended);
|
|
37
|
+
cursorDone = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Create .cursor/rules/agent-guardrails.md
|
|
42
|
+
if (!existsSync(cursorRulesDir)) {
|
|
43
|
+
mkdirSync(cursorRulesDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
writeFileSync(cursorRuleFile, cursorRuleContent);
|
|
46
|
+
cursorDone = true;
|
|
47
|
+
}
|
|
48
|
+
// Claude Code: .claude/instructions.md
|
|
49
|
+
const claudeDir = resolve(root, ".claude");
|
|
50
|
+
const claudeInstructions = resolve(claudeDir, "instructions.md");
|
|
51
|
+
if (existsSync(claudeInstructions)) {
|
|
52
|
+
const existing = readFileSync(claudeInstructions, "utf-8");
|
|
53
|
+
if (!hasRule(existing)) {
|
|
54
|
+
const appended = existing.trimEnd() + "\n\n" + GUARDRAIL_RULE + "\n";
|
|
55
|
+
writeFileSync(claudeInstructions, appended);
|
|
56
|
+
claudeDone = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (!existsSync(claudeDir)) {
|
|
61
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
writeFileSync(claudeInstructions, GUARDRAIL_RULE + "\n");
|
|
64
|
+
claudeDone = true;
|
|
65
|
+
}
|
|
66
|
+
const messages = [];
|
|
67
|
+
if (cursorDone) {
|
|
68
|
+
messages.push(chalk.green("✓") + " Added guardrail rule for Cursor");
|
|
69
|
+
}
|
|
70
|
+
if (claudeDone) {
|
|
71
|
+
messages.push(chalk.green("✓") + " Added guardrail rule for Claude Code");
|
|
72
|
+
}
|
|
73
|
+
if (messages.length === 0) {
|
|
74
|
+
messages.push(chalk.yellow("Guardrail rule already present in all config files."));
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
cursor: cursorDone,
|
|
78
|
+
claude: claudeDone,
|
|
79
|
+
message: messages.join("\n"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled example guardrails. Used by init and add commands.
|
|
3
|
+
*/
|
|
4
|
+
export const TEMPLATES = {
|
|
5
|
+
"no-plaintext-secrets": `---
|
|
6
|
+
name: no-plaintext-secrets
|
|
7
|
+
description: Never log, commit, or expose API keys, passwords, or tokens. Use environment variables or secrets managers. Apply when adding logging, implementing auth, or integrating third-party APIs.
|
|
8
|
+
scope: project
|
|
9
|
+
severity: critical
|
|
10
|
+
triggers:
|
|
11
|
+
- "Adding logging"
|
|
12
|
+
- "Implementing authentication"
|
|
13
|
+
- "API integration"
|
|
14
|
+
- "Handling credentials"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# No Plaintext Secrets
|
|
18
|
+
|
|
19
|
+
## Trigger
|
|
20
|
+
Implementing authentication endpoints, adding logging, integrating third-party APIs, or handling any user credentials or API keys.
|
|
21
|
+
|
|
22
|
+
## Instruction
|
|
23
|
+
- Never store, log, or commit plaintext credentials (API keys, passwords, tokens, sessions)
|
|
24
|
+
- Always use environment variables or secrets managers (e.g. 1Password, Vault)
|
|
25
|
+
- Use bcrypt with 12 salt rounds for password hashing
|
|
26
|
+
- Always require HTTPS for authentication endpoints
|
|
27
|
+
- Use a \`redactSensitive()\` helper when logging objects that may contain secrets
|
|
28
|
+
|
|
29
|
+
## Reason
|
|
30
|
+
API keys were exposed in git during a 2025 security audit. Plaintext credentials in logs led to emergency key rotation.
|
|
31
|
+
`,
|
|
32
|
+
"database-migrations": `---
|
|
33
|
+
name: database-migrations
|
|
34
|
+
description: Always use migration files for schema changes. Never modify production schema directly. Apply when modifying database schema, adding tables, or changing columns.
|
|
35
|
+
scope: project
|
|
36
|
+
severity: critical
|
|
37
|
+
triggers:
|
|
38
|
+
- "Modifying database schema"
|
|
39
|
+
- "Adding or changing tables"
|
|
40
|
+
- "Database migrations"
|
|
41
|
+
- "Prisma schema"
|
|
42
|
+
- "SQL migrations"
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
# Database Migrations
|
|
46
|
+
|
|
47
|
+
## Trigger
|
|
48
|
+
Modifying database schema, adding tables, changing columns, or optimizing queries that affect schema.
|
|
49
|
+
|
|
50
|
+
## Instruction
|
|
51
|
+
- Always create a migration file (e.g. \`prisma migrate dev\`, \`rails db:migrate\`, or equivalent)
|
|
52
|
+
- Never modify production schema directly without a migration
|
|
53
|
+
- Test migrations in staging before applying to production
|
|
54
|
+
- Require explicit human approval before running migrations in production
|
|
55
|
+
|
|
56
|
+
## Reason
|
|
57
|
+
Direct schema changes caused 4-hour downtime and data inconsistencies. Migrations provide rollback capability and audit trail.
|
|
58
|
+
`,
|
|
59
|
+
"no-destructive-commands": `---
|
|
60
|
+
name: no-destructive-commands
|
|
61
|
+
description: Never run destructive shell commands or SQL without explicit human approval. Apply when deleting files, dropping tables, truncating data, or modifying production.
|
|
62
|
+
scope: project
|
|
63
|
+
severity: critical
|
|
64
|
+
triggers:
|
|
65
|
+
- "Deleting files"
|
|
66
|
+
- "Cleaning up"
|
|
67
|
+
- "Dropping tables"
|
|
68
|
+
- "Truncating data"
|
|
69
|
+
- "Database cleanup"
|
|
70
|
+
- "rm -rf"
|
|
71
|
+
- "DROP TABLE"
|
|
72
|
+
- "TRUNCATE"
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
# No Destructive Commands
|
|
76
|
+
|
|
77
|
+
## Trigger
|
|
78
|
+
Running shell commands that delete files, database commands that drop or truncate data, or any operation that irreversibly removes content.
|
|
79
|
+
|
|
80
|
+
## Instruction
|
|
81
|
+
- Never run \`rm -rf\`, \`rm -r\`, or recursive deletes without explicit human approval
|
|
82
|
+
- Never run \`DROP TABLE\`, \`DROP DATABASE\`, \`TRUNCATE\`, or \`DELETE\` without a WHERE clause on production data without explicit approval
|
|
83
|
+
- For cleanup tasks: propose the command, explain the impact, and wait for confirmation before executing
|
|
84
|
+
- Prefer moving to trash or using \`--dry-run\` when available
|
|
85
|
+
- If the user asks to "delete everything" or "clean slate": stop and confirm scope before proceeding
|
|
86
|
+
|
|
87
|
+
## Reason
|
|
88
|
+
Agents have run \`rm -rf\` on wrong directories and \`DROP TABLE\` in production. Destructive operations must never execute without explicit human confirmation.
|
|
89
|
+
`,
|
|
90
|
+
"no-new-deps-without-approval": `---
|
|
91
|
+
name: no-new-deps-without-approval
|
|
92
|
+
description: Do not add new npm, pip, or other package dependencies without explicit human approval. Apply when installing packages, adding imports, or suggesting new libraries.
|
|
93
|
+
scope: project
|
|
94
|
+
severity: warning
|
|
95
|
+
triggers:
|
|
96
|
+
- "Installing packages"
|
|
97
|
+
- "Adding dependencies"
|
|
98
|
+
- "npm install"
|
|
99
|
+
- "pip install"
|
|
100
|
+
- "New library"
|
|
101
|
+
- "Use package X"
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# No New Dependencies Without Approval
|
|
105
|
+
|
|
106
|
+
## Trigger
|
|
107
|
+
Adding a new package to package.json, requirements.txt, pyproject.toml, Cargo.toml, or any dependency manifest.
|
|
108
|
+
|
|
109
|
+
## Instruction
|
|
110
|
+
- Before running \`npm install <pkg>\`, \`pip install <pkg>\`, or equivalent: list the package and why it's needed, then ask for approval
|
|
111
|
+
- Prefer using existing project dependencies over adding new ones
|
|
112
|
+
- If a built-in or stdlib solution exists, use it instead of a new dependency
|
|
113
|
+
- When suggesting a new dependency: include package name, purpose, and alternative (e.g. "or we could use the built-in X")
|
|
114
|
+
- Never add dependencies "to fix" a problem without confirming the user wants new packages in the project
|
|
115
|
+
|
|
116
|
+
## Reason
|
|
117
|
+
Uncontrolled dependency growth leads to security risk, bundle bloat, and maintenance burden. Teams want to review what gets added to their lockfile.
|
|
118
|
+
`,
|
|
119
|
+
"rate-limiting": `---
|
|
120
|
+
name: rate-limiting
|
|
121
|
+
description: Limit tool calls and API requests to prevent runaway loops. Max 50 tool calls per session, max 10 per iteration. Stop after 3 consecutive errors.
|
|
122
|
+
scope: session
|
|
123
|
+
severity: warning
|
|
124
|
+
triggers:
|
|
125
|
+
- "Debugging API integrations"
|
|
126
|
+
- "Stripe or payment API calls"
|
|
127
|
+
- "External API calls in loops"
|
|
128
|
+
- "Context window approaching capacity"
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
# Rate Limiting
|
|
132
|
+
|
|
133
|
+
## Trigger
|
|
134
|
+
Debugging API integrations, making repeated external API calls, or when context exceeds 80% capacity or 10+ consecutive errors.
|
|
135
|
+
|
|
136
|
+
## Instruction
|
|
137
|
+
- Max 50 tool calls per session
|
|
138
|
+
- Max 10 tool calls per iteration
|
|
139
|
+
- Stop after 3 consecutive errors and request context rotation
|
|
140
|
+
- For payment APIs (Stripe, etc.): always use test mode when debugging
|
|
141
|
+
- When context exceeds 80% capacity: re-inject GUARDRAILS.md + summary + objective, then reset context
|
|
142
|
+
|
|
143
|
+
## Reason
|
|
144
|
+
Agent debugging Stripe entered an infinite loop of test calls, resulting in 2000+ requests in 30 minutes, $200 API costs, and account suspension.
|
|
145
|
+
`,
|
|
146
|
+
};
|
|
147
|
+
export const TEMPLATE_NAMES = Object.keys(TEMPLATES);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ValidateResult {
|
|
2
|
+
valid: number;
|
|
3
|
+
invalid: number;
|
|
4
|
+
total: number;
|
|
5
|
+
results: Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
success: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
warnings: string[];
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function validatePath(inputPath: string): ValidateResult;
|
|
13
|
+
export declare function listGuardrails(inputPath: string): Array<{
|
|
14
|
+
path: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
}>;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "fs";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { parseGuardrailFile } from "./parse.js";
|
|
4
|
+
const GUARDRAIL_FILENAMES = ["GUARDRAIL.md", "GUARDRAILS.md"];
|
|
5
|
+
const MAX_DEPTH = 6;
|
|
6
|
+
const MAX_DIRS = 2000;
|
|
7
|
+
function findGuardrailFiles(dir, depth = 0, count = { dirs: 0 }) {
|
|
8
|
+
if (depth > MAX_DEPTH || count.dirs > MAX_DIRS) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const files = [];
|
|
12
|
+
try {
|
|
13
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (entry.name === ".git" || entry.name === "node_modules") {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const fullPath = join(dir, entry.name);
|
|
19
|
+
if (entry.isFile()) {
|
|
20
|
+
if (GUARDRAIL_FILENAMES.includes(entry.name)) {
|
|
21
|
+
files.push(fullPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isDirectory()) {
|
|
25
|
+
count.dirs++;
|
|
26
|
+
files.push(...findGuardrailFiles(fullPath, depth + 1, count));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Skip directories we can't read
|
|
32
|
+
}
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
export function validatePath(inputPath) {
|
|
36
|
+
const resolved = resolve(inputPath);
|
|
37
|
+
const results = [];
|
|
38
|
+
let valid = 0;
|
|
39
|
+
let invalid = 0;
|
|
40
|
+
let filesToValidate = [];
|
|
41
|
+
try {
|
|
42
|
+
const stat = statSync(resolved);
|
|
43
|
+
if (stat.isFile()) {
|
|
44
|
+
if (GUARDRAIL_FILENAMES.includes(resolved.split(/[/\\]/).pop() || "")) {
|
|
45
|
+
filesToValidate = [resolved];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (stat.isDirectory()) {
|
|
49
|
+
filesToValidate = findGuardrailFiles(resolved);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
results.push({
|
|
54
|
+
path: resolved,
|
|
55
|
+
success: false,
|
|
56
|
+
errors: ["Path does not exist or is not accessible"],
|
|
57
|
+
warnings: [],
|
|
58
|
+
});
|
|
59
|
+
invalid++;
|
|
60
|
+
}
|
|
61
|
+
for (const filePath of filesToValidate) {
|
|
62
|
+
const parseResult = parseGuardrailFile(filePath);
|
|
63
|
+
results.push({
|
|
64
|
+
path: filePath,
|
|
65
|
+
success: parseResult.success,
|
|
66
|
+
errors: parseResult.errors,
|
|
67
|
+
warnings: parseResult.warnings,
|
|
68
|
+
});
|
|
69
|
+
if (parseResult.success) {
|
|
70
|
+
valid++;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
invalid++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
valid,
|
|
78
|
+
invalid,
|
|
79
|
+
total: valid + invalid,
|
|
80
|
+
results,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function listGuardrails(inputPath) {
|
|
84
|
+
const validateResult = validatePath(inputPath);
|
|
85
|
+
const guardrails = [];
|
|
86
|
+
for (const r of validateResult.results) {
|
|
87
|
+
if (r.success) {
|
|
88
|
+
const parseResult = parseGuardrailFile(r.path);
|
|
89
|
+
if (parseResult.guardrail) {
|
|
90
|
+
guardrails.push({
|
|
91
|
+
path: parseResult.guardrail.path,
|
|
92
|
+
name: parseResult.guardrail.name,
|
|
93
|
+
description: parseResult.guardrail.description,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return guardrails;
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guardrails-ref",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Validate and manage Agent Guardrails (GUARDRAIL.md) — init, setup, add, validate",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/validate.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"guardrails-ref": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"validate": "node dist/cli.js validate",
|
|
13
|
+
"list": "node dist/cli.js list",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"agent",
|
|
18
|
+
"guardrails",
|
|
19
|
+
"ai",
|
|
20
|
+
"cursor",
|
|
21
|
+
"claude"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/9atar6/agent-guardrails.git",
|
|
27
|
+
"directory": "guardrails-ref"
|
|
28
|
+
},
|
|
29
|
+
"bugs": "https://github.com/9atar6/agent-guardrails/issues",
|
|
30
|
+
"homepage": "https://github.com/9atar6/agent-guardrails#readme",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"gray-matter": "^4.0.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.10.0",
|
|
38
|
+
"typescript": "^5.3.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
]
|
|
46
|
+
}
|