musubi-sdd 3.8.0 → 3.10.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/README.ja.md +25 -1
- package/README.md +25 -1
- package/bin/musubi-validate.js +234 -0
- package/package.json +1 -1
- package/src/orchestration/guardrails/base-guardrail.js +358 -0
- package/src/orchestration/guardrails/guardrail-rules.js +532 -0
- package/src/orchestration/guardrails/index.js +61 -0
- package/src/orchestration/guardrails/input-guardrail.js +378 -0
- package/src/orchestration/guardrails/output-guardrail.js +514 -0
- package/src/orchestration/guardrails/safety-check.js +471 -0
- package/src/orchestration/index.js +43 -0
- package/src/templates/agents/claude-code/CLAUDE.md +9 -0
- package/src/templates/agents/claude-code/skills/constitution-enforcer/SKILL.md +26 -0
- package/src/templates/agents/claude-code/skills/orchestrator/SKILL.md +9 -0
- package/src/templates/agents/claude-code/skills/security-auditor/SKILL.md +34 -0
- package/src/templates/agents/codex/AGENTS.md +9 -0
- package/src/templates/agents/cursor/AGENTS.md +9 -0
- package/src/templates/agents/gemini-cli/GEMINI.md +9 -0
- package/src/templates/agents/github-copilot/AGENTS.md +9 -0
- package/src/templates/agents/qwen-code/QWEN.md +9 -0
- package/src/templates/agents/windsurf/AGENTS.md +9 -0
package/README.ja.md
CHANGED
|
@@ -71,7 +71,31 @@ musubi init --windsurf # Windsurf IDE
|
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
74
|
-
## 📊 v3.
|
|
74
|
+
## 📊 v3.9.0 の新機能
|
|
75
|
+
|
|
76
|
+
- 🛡️ **Guardrailsシステム** - OpenAI Agents SDK inspired 入出力検証とセーフティチェック
|
|
77
|
+
- ✅ **InputGuardrail** - 入力検証、PII検出、インジェクション攻撃防止
|
|
78
|
+
- ✅ **OutputGuardrail** - 出力サニタイズ、機密データ墨消し、コンテンツポリシー適用
|
|
79
|
+
- ⚖️ **SafetyCheckGuardrail** - 憲法条項準拠、コンテンツ安全性分析
|
|
80
|
+
- 🔧 **GuardrailRules DSL** - RuleBuilderによる検証ルール構築のFluent API
|
|
81
|
+
- 🔗 **GuardrailChain** - 複数Guardrailの順次/並列実行
|
|
82
|
+
- 🖥️ **CLIコマンド** - `musubi-validate guardrails` と `guardrails-chain` コマンド
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# セキュリティプリセットで入力検証
|
|
86
|
+
npx musubi-validate guardrails "user input" --type input --preset security
|
|
87
|
+
|
|
88
|
+
# PII墨消しで出力検証
|
|
89
|
+
npx musubi-validate guardrails "output" --type output --redact
|
|
90
|
+
|
|
91
|
+
# 憲法準拠でセーフティチェック
|
|
92
|
+
npx musubi-validate guardrails "code" --type safety --constitutional --level high
|
|
93
|
+
|
|
94
|
+
# Guardrailチェーンを並列実行
|
|
95
|
+
npx musubi-validate guardrails-chain "content" --parallel
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 以前のバージョン (v3.7.1)
|
|
75
99
|
|
|
76
100
|
- 🌐 **WebSocketリアルタイムGUI** - `musubi-browser`ダッシュボードでライブ更新
|
|
77
101
|
- 📋 **GUIクイックアクション** - 新規要件モーダル、プロジェクト検証、レポートエクスポート
|
package/README.md
CHANGED
|
@@ -71,7 +71,31 @@ musubi init --windsurf # Windsurf IDE
|
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
74
|
-
## 📊 What's New in v3.
|
|
74
|
+
## 📊 What's New in v3.9.0
|
|
75
|
+
|
|
76
|
+
- 🛡️ **Guardrails System** - OpenAI Agents SDK inspired input/output validation and safety checks
|
|
77
|
+
- ✅ **InputGuardrail** - Input validation, PII detection, injection attack prevention
|
|
78
|
+
- ✅ **OutputGuardrail** - Output sanitization, sensitive data redaction, content policy enforcement
|
|
79
|
+
- ⚖️ **SafetyCheckGuardrail** - Constitutional Articles compliance, content safety analysis
|
|
80
|
+
- 🔧 **GuardrailRules DSL** - Fluent API for building validation rules with RuleBuilder
|
|
81
|
+
- 🔗 **GuardrailChain** - Compose multiple guardrails with sequential/parallel execution
|
|
82
|
+
- 🖥️ **CLI Commands** - `musubi-validate guardrails` and `guardrails-chain` commands
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Input validation with security preset
|
|
86
|
+
npx musubi-validate guardrails "user input" --type input --preset security
|
|
87
|
+
|
|
88
|
+
# Output validation with PII redaction
|
|
89
|
+
npx musubi-validate guardrails "output" --type output --redact
|
|
90
|
+
|
|
91
|
+
# Safety check with constitutional compliance
|
|
92
|
+
npx musubi-validate guardrails "code" --type safety --constitutional --level high
|
|
93
|
+
|
|
94
|
+
# Run guardrail chain in parallel
|
|
95
|
+
npx musubi-validate guardrails-chain "content" --parallel
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Previous (v3.7.1)
|
|
75
99
|
|
|
76
100
|
- 🌐 **WebSocket Real-time GUI** - Live replanning updates with `musubi-browser` dashboard
|
|
77
101
|
- 📋 **GUI Quick Actions** - Modal dialog for New Requirement, Validate Project, Export Report
|
package/bin/musubi-validate.js
CHANGED
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
* musubi-validate article <1-9> # Validate specific article
|
|
12
12
|
* musubi-validate gates # Validate Phase -1 Gates
|
|
13
13
|
* musubi-validate complexity # Validate complexity limits
|
|
14
|
+
* musubi-validate guardrails # Validate content with guardrails
|
|
14
15
|
* musubi-validate all # Run all validations
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
const { Command } = require('commander');
|
|
18
19
|
const chalk = require('chalk');
|
|
19
20
|
const ConstitutionValidator = require('../src/validators/constitution');
|
|
21
|
+
const {
|
|
22
|
+
createInputGuardrail,
|
|
23
|
+
createOutputGuardrail,
|
|
24
|
+
createSafetyCheckGuardrail,
|
|
25
|
+
GuardrailChain,
|
|
26
|
+
SafetyLevel
|
|
27
|
+
} = require('../src/orchestration/guardrails');
|
|
20
28
|
|
|
21
29
|
const program = new Command();
|
|
22
30
|
|
|
@@ -107,6 +115,134 @@ program
|
|
|
107
115
|
}
|
|
108
116
|
});
|
|
109
117
|
|
|
118
|
+
// Guardrails validation
|
|
119
|
+
program
|
|
120
|
+
.command('guardrails')
|
|
121
|
+
.description('Validate content with input/output guardrails')
|
|
122
|
+
.argument('[content]', 'Content to validate (or use --file)')
|
|
123
|
+
.option('--file <path>', 'Read content from file')
|
|
124
|
+
.option('-t, --type <type>', 'Guardrail type (input|output|safety)', 'input')
|
|
125
|
+
.option('-l, --level <level>', 'Safety level (basic|standard|strict|paranoid)', 'standard')
|
|
126
|
+
.option('--constitutional', 'Enable constitutional compliance checks')
|
|
127
|
+
.option('--redact', 'Enable redaction for output guardrails')
|
|
128
|
+
.option('-f, --format <type>', 'Output format (console|json)', 'console')
|
|
129
|
+
.action(async (content, options) => {
|
|
130
|
+
try {
|
|
131
|
+
// Get content from argument, file, or stdin
|
|
132
|
+
let inputContent = content;
|
|
133
|
+
|
|
134
|
+
if (options.file) {
|
|
135
|
+
const fs = require('fs');
|
|
136
|
+
const path = require('path');
|
|
137
|
+
const filePath = path.resolve(process.cwd(), options.file);
|
|
138
|
+
inputContent = fs.readFileSync(filePath, 'utf-8');
|
|
139
|
+
} else if (!inputContent && !process.stdin.isTTY) {
|
|
140
|
+
// Read from stdin
|
|
141
|
+
const chunks = [];
|
|
142
|
+
for await (const chunk of process.stdin) {
|
|
143
|
+
chunks.push(chunk);
|
|
144
|
+
}
|
|
145
|
+
inputContent = Buffer.concat(chunks).toString('utf-8');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!inputContent) {
|
|
149
|
+
console.error(chalk.red('✗ No content provided. Use --file or pipe content.'));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let guardrail;
|
|
154
|
+
const guardrailType = options.type.toLowerCase();
|
|
155
|
+
|
|
156
|
+
switch (guardrailType) {
|
|
157
|
+
case 'input':
|
|
158
|
+
guardrail = createInputGuardrail('userInput', {
|
|
159
|
+
sanitize: true
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case 'output':
|
|
164
|
+
guardrail = createOutputGuardrail(options.redact ? 'redact' : 'safe');
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'safety':
|
|
168
|
+
guardrail = createSafetyCheckGuardrail(options.level, {
|
|
169
|
+
enforceConstitution: options.constitutional
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
default:
|
|
174
|
+
console.error(chalk.red(`✗ Unknown guardrail type: ${guardrailType}`));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(chalk.dim(`\n🛡️ Running ${guardrailType} guardrail validation...\n`));
|
|
179
|
+
|
|
180
|
+
const result = await guardrail.run(inputContent);
|
|
181
|
+
|
|
182
|
+
displayGuardrailResults(result, options);
|
|
183
|
+
process.exit(result.passed ? 0 : 1);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(chalk.red('✗ Guardrail validation error:'), error.message);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Guardrails chain validation
|
|
191
|
+
program
|
|
192
|
+
.command('guardrails-chain')
|
|
193
|
+
.description('Run a chain of guardrails on content')
|
|
194
|
+
.argument('[content]', 'Content to validate')
|
|
195
|
+
.option('--file <path>', 'Read content from file')
|
|
196
|
+
.option('--parallel', 'Run guardrails in parallel')
|
|
197
|
+
.option('--stop-on-failure', 'Stop on first failure')
|
|
198
|
+
.option('-f, --format <type>', 'Output format (console|json)', 'console')
|
|
199
|
+
.action(async (content, options) => {
|
|
200
|
+
try {
|
|
201
|
+
// Get content
|
|
202
|
+
let inputContent = content;
|
|
203
|
+
|
|
204
|
+
if (options.file) {
|
|
205
|
+
const fs = require('fs');
|
|
206
|
+
const path = require('path');
|
|
207
|
+
const filePath = path.resolve(process.cwd(), options.file);
|
|
208
|
+
inputContent = fs.readFileSync(filePath, 'utf-8');
|
|
209
|
+
} else if (!inputContent && !process.stdin.isTTY) {
|
|
210
|
+
const chunks = [];
|
|
211
|
+
for await (const chunk of process.stdin) {
|
|
212
|
+
chunks.push(chunk);
|
|
213
|
+
}
|
|
214
|
+
inputContent = Buffer.concat(chunks).toString('utf-8');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!inputContent) {
|
|
218
|
+
console.error(chalk.red('✗ No content provided. Use --file or pipe content.'));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create guardrail chain
|
|
223
|
+
const chain = new GuardrailChain({
|
|
224
|
+
name: 'ValidationChain',
|
|
225
|
+
parallel: options.parallel || false,
|
|
226
|
+
stopOnFirstFailure: options.stopOnFailure || false
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Add default guardrails
|
|
230
|
+
chain.add(createInputGuardrail('security'));
|
|
231
|
+
chain.add(createSafetyCheckGuardrail('standard'));
|
|
232
|
+
chain.add(createOutputGuardrail('safe'));
|
|
233
|
+
|
|
234
|
+
console.log(chalk.dim('\n🔗 Running guardrail chain validation...\n'));
|
|
235
|
+
|
|
236
|
+
const result = await chain.run(inputContent);
|
|
237
|
+
|
|
238
|
+
displayChainResults(result, options);
|
|
239
|
+
process.exit(result.passed ? 0 : 1);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error(chalk.red('✗ Guardrail chain error:'), error.message);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
110
246
|
// Score validation (numeric score output for CI/CD)
|
|
111
247
|
program
|
|
112
248
|
.command('score')
|
|
@@ -345,6 +481,104 @@ function displayMarkdown(title, results) {
|
|
|
345
481
|
}
|
|
346
482
|
}
|
|
347
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Display guardrail validation results
|
|
486
|
+
*/
|
|
487
|
+
function displayGuardrailResults(result, options) {
|
|
488
|
+
if (options.format === 'json') {
|
|
489
|
+
console.log(JSON.stringify(result, null, 2));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
console.log(chalk.bold('Guardrail Validation Results'));
|
|
494
|
+
console.log(chalk.bold('━'.repeat(50)));
|
|
495
|
+
|
|
496
|
+
if (result.passed) {
|
|
497
|
+
console.log(chalk.green(`\n✓ PASSED - ${result.guardrailName}\n`));
|
|
498
|
+
} else {
|
|
499
|
+
console.log(chalk.red(`\n✗ FAILED - ${result.guardrailName}\n`));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (result.message) {
|
|
503
|
+
console.log(chalk.dim(`Message: ${result.message}`));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
console.log(chalk.dim(`Execution time: ${result.executionTimeMs}ms\n`));
|
|
507
|
+
|
|
508
|
+
if (result.violations && result.violations.length > 0) {
|
|
509
|
+
console.log(chalk.bold.red('Violations:'));
|
|
510
|
+
result.violations.forEach(violation => {
|
|
511
|
+
const severityIcon = violation.severity === 'error' ? '✗' :
|
|
512
|
+
violation.severity === 'warning' ? '⚠' : 'ℹ';
|
|
513
|
+
const severityColor = violation.severity === 'error' ? chalk.red :
|
|
514
|
+
violation.severity === 'warning' ? chalk.yellow : chalk.blue;
|
|
515
|
+
console.log(severityColor(` ${severityIcon} [${violation.code}] ${violation.message}`));
|
|
516
|
+
});
|
|
517
|
+
console.log();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Show metadata if redaction was applied
|
|
521
|
+
if (result.metadata?.redactionApplied) {
|
|
522
|
+
console.log(chalk.bold.blue('Redaction Applied:'));
|
|
523
|
+
console.log(chalk.dim(` ${result.metadata.redactionCount} item(s) redacted`));
|
|
524
|
+
if (result.metadata.redactions) {
|
|
525
|
+
result.metadata.redactions.forEach(r => {
|
|
526
|
+
console.log(chalk.dim(` - ${r.type}: ${r.count}`));
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
console.log();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Show quality scores if available
|
|
533
|
+
if (result.metadata?.qualityScores) {
|
|
534
|
+
console.log(chalk.bold.blue('Quality Scores:'));
|
|
535
|
+
Object.entries(result.metadata.qualityScores).forEach(([name, score]) => {
|
|
536
|
+
console.log(chalk.dim(` ${name}: ${(score * 100).toFixed(1)}%`));
|
|
537
|
+
});
|
|
538
|
+
console.log();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Display guardrail chain results
|
|
544
|
+
*/
|
|
545
|
+
function displayChainResults(result, options) {
|
|
546
|
+
if (options.format === 'json') {
|
|
547
|
+
console.log(JSON.stringify(result, null, 2));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
console.log(chalk.bold('Guardrail Chain Results'));
|
|
552
|
+
console.log(chalk.bold('━'.repeat(50)));
|
|
553
|
+
|
|
554
|
+
if (result.passed) {
|
|
555
|
+
console.log(chalk.green(`\n✓ PASSED - ${result.chainName}\n`));
|
|
556
|
+
} else {
|
|
557
|
+
console.log(chalk.red(`\n✗ FAILED - ${result.chainName}\n`));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log(chalk.dim(`Guardrails executed: ${result.executedCount}/${result.guardrailCount}`));
|
|
561
|
+
console.log(chalk.dim(`Total time: ${result.executionTimeMs}ms\n`));
|
|
562
|
+
|
|
563
|
+
// Show individual guardrail results
|
|
564
|
+
console.log(chalk.bold('Individual Results:'));
|
|
565
|
+
result.results.forEach((r, index) => {
|
|
566
|
+
const icon = r.passed ? chalk.green('✓') : chalk.red('✗');
|
|
567
|
+
console.log(` ${icon} ${r.guardrailName} (${r.executionTimeMs}ms)`);
|
|
568
|
+
});
|
|
569
|
+
console.log();
|
|
570
|
+
|
|
571
|
+
// Show all violations
|
|
572
|
+
if (result.violations && result.violations.length > 0) {
|
|
573
|
+
console.log(chalk.bold.red('All Violations:'));
|
|
574
|
+
result.violations.forEach(violation => {
|
|
575
|
+
const severityColor = violation.severity === 'error' ? chalk.red : chalk.yellow;
|
|
576
|
+
console.log(severityColor(` • [${violation.code}] ${violation.message}`));
|
|
577
|
+
});
|
|
578
|
+
console.log();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
348
582
|
// Parse arguments
|
|
349
583
|
program.parse(process.argv);
|
|
350
584
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Ultimate Specification Driven Development Tool with 27 Agents for 7 AI Coding Platforms + MCP Integration (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Base Guardrail class for MUSUBI Orchestration
|
|
3
|
+
*
|
|
4
|
+
* Guardrails provide safety checks for agent inputs and outputs.
|
|
5
|
+
* Inspired by OpenAI Agents SDK guardrails pattern.
|
|
6
|
+
*
|
|
7
|
+
* @module orchestration/guardrails/base-guardrail
|
|
8
|
+
* @version 3.9.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Guardrail execution result
|
|
15
|
+
* @typedef {Object} GuardrailResult
|
|
16
|
+
* @property {boolean} passed - Whether the guardrail check passed
|
|
17
|
+
* @property {string} guardrailName - Name of the guardrail that was executed
|
|
18
|
+
* @property {string} [message] - Optional message describing the result
|
|
19
|
+
* @property {Array<GuardrailViolation>} violations - List of violations found
|
|
20
|
+
* @property {Object} [metadata] - Additional metadata from the check
|
|
21
|
+
* @property {number} executionTimeMs - Time taken to execute the guardrail
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Guardrail violation details
|
|
26
|
+
* @typedef {Object} GuardrailViolation
|
|
27
|
+
* @property {string} code - Violation code (e.g., 'PII_DETECTED', 'PROHIBITED_CONTENT')
|
|
28
|
+
* @property {string} message - Human-readable description
|
|
29
|
+
* @property {string} severity - 'error' | 'warning' | 'info'
|
|
30
|
+
* @property {Object} [context] - Additional context about the violation
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Guardrail configuration options
|
|
35
|
+
* @typedef {Object} GuardrailConfig
|
|
36
|
+
* @property {string} name - Unique name for the guardrail
|
|
37
|
+
* @property {string} [description] - Description of what the guardrail checks
|
|
38
|
+
* @property {boolean} [enabled=true] - Whether the guardrail is enabled
|
|
39
|
+
* @property {boolean} [failFast=false] - Stop on first violation
|
|
40
|
+
* @property {string} [severity='error'] - Default severity level
|
|
41
|
+
* @property {Object} [options] - Guardrail-specific options
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tripwire exception - thrown when guardrail fails and tripwire is enabled
|
|
46
|
+
* @class
|
|
47
|
+
*/
|
|
48
|
+
class GuardrailTripwireException extends Error {
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} message - Error message
|
|
51
|
+
* @param {GuardrailResult} result - The guardrail result that triggered the tripwire
|
|
52
|
+
*/
|
|
53
|
+
constructor(message, result) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'GuardrailTripwireException';
|
|
56
|
+
this.result = result;
|
|
57
|
+
this.violations = result.violations;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Base class for all guardrails
|
|
63
|
+
* @abstract
|
|
64
|
+
*/
|
|
65
|
+
class BaseGuardrail {
|
|
66
|
+
/**
|
|
67
|
+
* @param {GuardrailConfig} config - Guardrail configuration
|
|
68
|
+
*/
|
|
69
|
+
constructor(config) {
|
|
70
|
+
if (new.target === BaseGuardrail) {
|
|
71
|
+
throw new Error('BaseGuardrail is abstract and cannot be instantiated directly');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.name = config.name || this.constructor.name;
|
|
75
|
+
this.description = config.description || '';
|
|
76
|
+
this.enabled = config.enabled !== false;
|
|
77
|
+
this.failFast = config.failFast || false;
|
|
78
|
+
this.defaultSeverity = config.severity || 'error';
|
|
79
|
+
this.options = config.options || {};
|
|
80
|
+
|
|
81
|
+
// Tripwire: if true, throws exception on failure instead of returning result
|
|
82
|
+
this.tripwireEnabled = config.tripwireEnabled || false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute the guardrail check
|
|
87
|
+
* @abstract
|
|
88
|
+
* @param {*} input - Input to validate
|
|
89
|
+
* @param {Object} [context] - Execution context
|
|
90
|
+
* @returns {Promise<GuardrailResult>}
|
|
91
|
+
*/
|
|
92
|
+
async check(input, context = {}) {
|
|
93
|
+
throw new Error('Subclasses must implement check() method');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run the guardrail with timing and error handling
|
|
98
|
+
* @param {*} input - Input to validate
|
|
99
|
+
* @param {Object} [context] - Execution context
|
|
100
|
+
* @returns {Promise<GuardrailResult>}
|
|
101
|
+
*/
|
|
102
|
+
async run(input, context = {}) {
|
|
103
|
+
if (!this.enabled) {
|
|
104
|
+
return this.createResult(true, [], 'Guardrail is disabled');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await this.check(input, context);
|
|
111
|
+
result.executionTimeMs = Date.now() - startTime;
|
|
112
|
+
result.guardrailName = this.name;
|
|
113
|
+
|
|
114
|
+
// Handle tripwire
|
|
115
|
+
if (this.tripwireEnabled && !result.passed) {
|
|
116
|
+
throw new GuardrailTripwireException(
|
|
117
|
+
`Guardrail '${this.name}' triggered tripwire: ${result.message || 'Validation failed'}`,
|
|
118
|
+
result
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof GuardrailTripwireException) {
|
|
125
|
+
throw error; // Re-throw tripwire exceptions
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Wrap unexpected errors
|
|
129
|
+
return this.createResult(
|
|
130
|
+
false,
|
|
131
|
+
[{
|
|
132
|
+
code: 'GUARDRAIL_ERROR',
|
|
133
|
+
message: error.message,
|
|
134
|
+
severity: 'error',
|
|
135
|
+
context: { errorName: error.name, stack: error.stack }
|
|
136
|
+
}],
|
|
137
|
+
`Guardrail execution failed: ${error.message}`,
|
|
138
|
+
Date.now() - startTime
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a standardized guardrail result
|
|
145
|
+
* @protected
|
|
146
|
+
* @param {boolean} passed - Whether the check passed
|
|
147
|
+
* @param {Array<GuardrailViolation>} [violations=[]] - List of violations
|
|
148
|
+
* @param {string} [message] - Optional message
|
|
149
|
+
* @param {number} [executionTimeMs=0] - Execution time
|
|
150
|
+
* @param {Object} [metadata] - Additional metadata
|
|
151
|
+
* @returns {GuardrailResult}
|
|
152
|
+
*/
|
|
153
|
+
createResult(passed, violations = [], message = undefined, executionTimeMs = 0, metadata = {}) {
|
|
154
|
+
return {
|
|
155
|
+
passed,
|
|
156
|
+
guardrailName: this.name,
|
|
157
|
+
message,
|
|
158
|
+
violations,
|
|
159
|
+
metadata,
|
|
160
|
+
executionTimeMs
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a violation object
|
|
166
|
+
* @protected
|
|
167
|
+
* @param {string} code - Violation code
|
|
168
|
+
* @param {string} message - Human-readable message
|
|
169
|
+
* @param {string} [severity] - Severity level
|
|
170
|
+
* @param {Object} [context] - Additional context
|
|
171
|
+
* @returns {GuardrailViolation}
|
|
172
|
+
*/
|
|
173
|
+
createViolation(code, message, severity = null, context = {}) {
|
|
174
|
+
return {
|
|
175
|
+
code,
|
|
176
|
+
message,
|
|
177
|
+
severity: severity || this.defaultSeverity,
|
|
178
|
+
context
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Enable the guardrail
|
|
184
|
+
*/
|
|
185
|
+
enable() {
|
|
186
|
+
this.enabled = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Disable the guardrail
|
|
191
|
+
*/
|
|
192
|
+
disable() {
|
|
193
|
+
this.enabled = false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Enable tripwire mode
|
|
198
|
+
*/
|
|
199
|
+
enableTripwire() {
|
|
200
|
+
this.tripwireEnabled = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Disable tripwire mode
|
|
205
|
+
*/
|
|
206
|
+
disableTripwire() {
|
|
207
|
+
this.tripwireEnabled = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get guardrail info
|
|
212
|
+
* @returns {Object}
|
|
213
|
+
*/
|
|
214
|
+
getInfo() {
|
|
215
|
+
return {
|
|
216
|
+
name: this.name,
|
|
217
|
+
description: this.description,
|
|
218
|
+
enabled: this.enabled,
|
|
219
|
+
failFast: this.failFast,
|
|
220
|
+
tripwireEnabled: this.tripwireEnabled,
|
|
221
|
+
defaultSeverity: this.defaultSeverity
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Guardrail chain for running multiple guardrails
|
|
228
|
+
*/
|
|
229
|
+
class GuardrailChain {
|
|
230
|
+
/**
|
|
231
|
+
* @param {Object} [options] - Chain options
|
|
232
|
+
* @param {boolean} [options.parallel=false] - Run guardrails in parallel
|
|
233
|
+
* @param {boolean} [options.stopOnFirstFailure=false] - Stop on first failure
|
|
234
|
+
* @param {string} [options.name='GuardrailChain'] - Chain name
|
|
235
|
+
*/
|
|
236
|
+
constructor(options = {}) {
|
|
237
|
+
this.name = options.name || 'GuardrailChain';
|
|
238
|
+
this.parallel = options.parallel || false;
|
|
239
|
+
this.stopOnFirstFailure = options.stopOnFirstFailure || false;
|
|
240
|
+
this.guardrails = [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Add a guardrail to the chain
|
|
245
|
+
* @param {BaseGuardrail} guardrail - Guardrail to add
|
|
246
|
+
* @returns {GuardrailChain} this for chaining
|
|
247
|
+
*/
|
|
248
|
+
add(guardrail) {
|
|
249
|
+
if (!(guardrail instanceof BaseGuardrail)) {
|
|
250
|
+
throw new Error('Can only add BaseGuardrail instances to the chain');
|
|
251
|
+
}
|
|
252
|
+
this.guardrails.push(guardrail);
|
|
253
|
+
return this;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add multiple guardrails to the chain
|
|
258
|
+
* @param {Array<BaseGuardrail>} guardrails - Guardrails to add
|
|
259
|
+
* @returns {GuardrailChain} this for chaining
|
|
260
|
+
*/
|
|
261
|
+
addAll(guardrails) {
|
|
262
|
+
guardrails.forEach(g => this.add(g));
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Run all guardrails in the chain
|
|
268
|
+
* @param {*} input - Input to validate
|
|
269
|
+
* @param {Object} [context] - Execution context
|
|
270
|
+
* @returns {Promise<Object>} Combined result
|
|
271
|
+
*/
|
|
272
|
+
async run(input, context = {}) {
|
|
273
|
+
const startTime = Date.now();
|
|
274
|
+
const results = [];
|
|
275
|
+
const allViolations = [];
|
|
276
|
+
let overallPassed = true;
|
|
277
|
+
|
|
278
|
+
if (this.parallel) {
|
|
279
|
+
// Parallel execution with optional early termination
|
|
280
|
+
const promises = this.guardrails.map(g => g.run(input, context));
|
|
281
|
+
|
|
282
|
+
if (this.stopOnFirstFailure) {
|
|
283
|
+
// Use Promise.race pattern for early termination
|
|
284
|
+
const settledResults = await Promise.allSettled(promises);
|
|
285
|
+
|
|
286
|
+
for (const settled of settledResults) {
|
|
287
|
+
if (settled.status === 'fulfilled') {
|
|
288
|
+
const result = settled.value;
|
|
289
|
+
results.push(result);
|
|
290
|
+
|
|
291
|
+
if (!result.passed) {
|
|
292
|
+
overallPassed = false;
|
|
293
|
+
allViolations.push(...result.violations);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// Handle rejected promise (tripwire or error)
|
|
297
|
+
throw settled.reason;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
const resolvedResults = await Promise.all(promises);
|
|
302
|
+
for (const result of resolvedResults) {
|
|
303
|
+
results.push(result);
|
|
304
|
+
if (!result.passed) {
|
|
305
|
+
overallPassed = false;
|
|
306
|
+
allViolations.push(...result.violations);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// Sequential execution
|
|
312
|
+
for (const guardrail of this.guardrails) {
|
|
313
|
+
const result = await guardrail.run(input, context);
|
|
314
|
+
results.push(result);
|
|
315
|
+
|
|
316
|
+
if (!result.passed) {
|
|
317
|
+
overallPassed = false;
|
|
318
|
+
allViolations.push(...result.violations);
|
|
319
|
+
|
|
320
|
+
if (this.stopOnFirstFailure) {
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
passed: overallPassed,
|
|
329
|
+
chainName: this.name,
|
|
330
|
+
results,
|
|
331
|
+
violations: allViolations,
|
|
332
|
+
guardrailCount: this.guardrails.length,
|
|
333
|
+
executedCount: results.length,
|
|
334
|
+
executionTimeMs: Date.now() - startTime
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get list of guardrails in the chain
|
|
340
|
+
* @returns {Array<Object>}
|
|
341
|
+
*/
|
|
342
|
+
getGuardrails() {
|
|
343
|
+
return this.guardrails.map(g => g.getInfo());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Clear all guardrails from the chain
|
|
348
|
+
*/
|
|
349
|
+
clear() {
|
|
350
|
+
this.guardrails = [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = {
|
|
355
|
+
BaseGuardrail,
|
|
356
|
+
GuardrailChain,
|
|
357
|
+
GuardrailTripwireException
|
|
358
|
+
};
|