wogiflow 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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Damage Control System
|
|
5
|
+
*
|
|
6
|
+
* Event-based pattern matching for destructive command protection.
|
|
7
|
+
* Supports multiple event types: bash, file, stop, prompt.
|
|
8
|
+
*
|
|
9
|
+
* Inspired by Hookify plugin patterns, adapted for multi-CLI compatibility.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* flow damage-control check "<command>" Check if command is allowed
|
|
13
|
+
* flow damage-control event <type> <ctx> Check event against rules
|
|
14
|
+
* flow damage-control status Show damage control status
|
|
15
|
+
* flow damage-control rules Show all rules
|
|
16
|
+
* flow dc check "rm -rf node_modules" Shorthand
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { getProjectRoot, colors, getConfig } = require('./flow-utils');
|
|
22
|
+
|
|
23
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
24
|
+
const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
25
|
+
const PATTERNS_FILE = path.join(WORKFLOW_DIR, 'damage-control.yaml');
|
|
26
|
+
|
|
27
|
+
// ============================================================
|
|
28
|
+
// Event Types and Actions
|
|
29
|
+
// ============================================================
|
|
30
|
+
|
|
31
|
+
const EVENT_TYPES = ['bash', 'file', 'stop', 'prompt', 'all'];
|
|
32
|
+
const ACTIONS = ['block', 'warn', 'ask', 'allow'];
|
|
33
|
+
|
|
34
|
+
// Maximum allowed regex pattern length to prevent abuse
|
|
35
|
+
// Reduced from 500 to prevent ReDoS attacks with complex patterns
|
|
36
|
+
const MAX_REGEX_LENGTH = 100;
|
|
37
|
+
|
|
38
|
+
// Maximum input length to test against regex (prevents slow matching)
|
|
39
|
+
const MAX_INPUT_LENGTH = 10000;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a RegExp safely, rejecting patterns that could cause ReDoS
|
|
43
|
+
* @param {string} pattern - The regex pattern string
|
|
44
|
+
* @param {string} flags - Optional regex flags
|
|
45
|
+
* @returns {RegExp|null} - Compiled regex or null if unsafe/invalid
|
|
46
|
+
*/
|
|
47
|
+
function safeRegExp(pattern, flags = '') {
|
|
48
|
+
// Reject overly long patterns
|
|
49
|
+
if (pattern.length > MAX_REGEX_LENGTH) {
|
|
50
|
+
console.error(`Regex pattern too long (${pattern.length} > ${MAX_REGEX_LENGTH}): ${pattern.substring(0, 50)}...`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for common ReDoS patterns (nested quantifiers)
|
|
55
|
+
// These patterns can cause exponential backtracking
|
|
56
|
+
const redosPatterns = [
|
|
57
|
+
/\([^)]*\+[^)]*\)\+/, // (a+)+ nested quantifiers
|
|
58
|
+
/\([^)]*\*[^)]*\)\+/, // (a*)+
|
|
59
|
+
/\([^)]*\+[^)]*\)\*/, // (a+)*
|
|
60
|
+
/\([^)]*\*[^)]*\)\*/, // (a*)*
|
|
61
|
+
/\([^)]*\+[^)]*\)\{/, // (a+){n}
|
|
62
|
+
/\([^)]*\*[^)]*\)\{/, // (a*){n}
|
|
63
|
+
/\.\*\.\*/, // .*.* greedy wildcards
|
|
64
|
+
/\.\+\.\+/, // .+.+ greedy wildcards
|
|
65
|
+
/\([^)]*\|[^)]*\)\+/, // (a|b)+ alternation with quantifier
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const redos of redosPatterns) {
|
|
69
|
+
if (redos.test(pattern)) {
|
|
70
|
+
console.error(`Potentially unsafe regex pattern (ReDoS risk): ${pattern}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return new RegExp(pattern, flags);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`Invalid regex pattern: ${pattern} - ${err.message}`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Safely test a regex against input with length limits
|
|
85
|
+
* @param {RegExp} regex - The compiled regex
|
|
86
|
+
* @param {string} input - The input string to test
|
|
87
|
+
* @returns {boolean} - True if matches, false otherwise
|
|
88
|
+
*/
|
|
89
|
+
function safeRegexTest(regex, input) {
|
|
90
|
+
if (!regex) return false;
|
|
91
|
+
|
|
92
|
+
// Truncate overly long inputs to prevent slow matching
|
|
93
|
+
const safeInput = typeof input === 'string' && input.length > MAX_INPUT_LENGTH
|
|
94
|
+
? input.substring(0, MAX_INPUT_LENGTH)
|
|
95
|
+
: String(input);
|
|
96
|
+
|
|
97
|
+
return regex.test(safeInput);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function log(color, ...args) {
|
|
101
|
+
console.log(colors[color] + args.join(' ') + colors.reset);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Commands that are always safe (read-only)
|
|
105
|
+
const SAFE_COMMANDS = [
|
|
106
|
+
/^ls\b/,
|
|
107
|
+
/^cat\b/,
|
|
108
|
+
/^head\b/,
|
|
109
|
+
/^tail\b/,
|
|
110
|
+
/^grep\b/,
|
|
111
|
+
/^rg\b/,
|
|
112
|
+
/^find\b/,
|
|
113
|
+
/^git\s+(status|log|diff|branch|show|remote|tag)\b/,
|
|
114
|
+
/^npm\s+(test|run|list|ls|view|search|info)\b/,
|
|
115
|
+
/^node\s+--check\b/,
|
|
116
|
+
/^node\s+-c\b/,
|
|
117
|
+
/^echo\b/,
|
|
118
|
+
/^pwd\b/,
|
|
119
|
+
/^which\b/,
|
|
120
|
+
/^type\b/,
|
|
121
|
+
/^whoami\b/,
|
|
122
|
+
/^hostname\b/,
|
|
123
|
+
/^date\b/,
|
|
124
|
+
/^wc\b/,
|
|
125
|
+
/^sort\b/,
|
|
126
|
+
/^uniq\b/,
|
|
127
|
+
/^diff\b/,
|
|
128
|
+
/^file\b/,
|
|
129
|
+
/^tree\b/,
|
|
130
|
+
/^du\b/,
|
|
131
|
+
/^df\b/
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Process YAML escape sequences in double-quoted strings
|
|
136
|
+
* Handles: \\ -> \, \n -> newline, \t -> tab, \" -> "
|
|
137
|
+
*/
|
|
138
|
+
function processYamlEscapes(str) {
|
|
139
|
+
return str
|
|
140
|
+
.replace(/\\\\/g, '\x00') // Temporarily replace \\ with placeholder
|
|
141
|
+
.replace(/\\n/g, '\n')
|
|
142
|
+
.replace(/\\t/g, '\t')
|
|
143
|
+
.replace(/\\"/g, '"')
|
|
144
|
+
.replace(/\x00/g, '\\'); // Restore \\ as single \
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Simple YAML parser for our specific format
|
|
149
|
+
* Handles: comments, key-value, arrays, nested objects with conditions
|
|
150
|
+
*/
|
|
151
|
+
function parseSimpleYaml(content) {
|
|
152
|
+
const result = { rules: [], blocked: [], ask: [], paths: {} };
|
|
153
|
+
const lines = content.split('\n');
|
|
154
|
+
|
|
155
|
+
let currentSection = null;
|
|
156
|
+
let currentSubSection = null;
|
|
157
|
+
let currentObject = null;
|
|
158
|
+
let currentConditions = null;
|
|
159
|
+
let currentCondition = null;
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < lines.length; i++) {
|
|
162
|
+
const line = lines[i];
|
|
163
|
+
const trimmed = line.trim();
|
|
164
|
+
|
|
165
|
+
// Skip empty lines and comments
|
|
166
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check indentation level
|
|
171
|
+
const indent = line.search(/\S/);
|
|
172
|
+
|
|
173
|
+
// Top-level key (no indent): rules:, blocked:, ask:, paths:
|
|
174
|
+
if (indent === 0 && trimmed.endsWith(':')) {
|
|
175
|
+
// Save any pending object
|
|
176
|
+
if (currentObject && currentSection) {
|
|
177
|
+
if (currentConditions) {
|
|
178
|
+
currentObject.conditions = currentConditions;
|
|
179
|
+
currentConditions = null;
|
|
180
|
+
}
|
|
181
|
+
result[currentSection].push(currentObject);
|
|
182
|
+
currentObject = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
currentSection = trimmed.slice(0, -1);
|
|
186
|
+
currentSubSection = null;
|
|
187
|
+
|
|
188
|
+
if (currentSection === 'paths' && !result.paths) {
|
|
189
|
+
result.paths = {};
|
|
190
|
+
} else if (!result[currentSection]) {
|
|
191
|
+
result[currentSection] = [];
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sub-section under paths (indent 2): zeroAccess:, readOnly:, noDelete:
|
|
197
|
+
if (currentSection === 'paths' && indent === 2 && trimmed.endsWith(':')) {
|
|
198
|
+
currentSubSection = trimmed.slice(0, -1);
|
|
199
|
+
result.paths[currentSubSection] = result.paths[currentSubSection] || [];
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle rules section specially (supports nested conditions array)
|
|
204
|
+
if (currentSection === 'rules') {
|
|
205
|
+
// New rule object (- name: xxx)
|
|
206
|
+
if (indent === 2 && trimmed.startsWith('-')) {
|
|
207
|
+
// Save previous rule
|
|
208
|
+
if (currentObject) {
|
|
209
|
+
if (currentConditions) {
|
|
210
|
+
currentObject.conditions = currentConditions;
|
|
211
|
+
currentConditions = null;
|
|
212
|
+
}
|
|
213
|
+
result.rules.push(currentObject);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const value = trimmed.slice(1).trim();
|
|
217
|
+
if (value.includes(':')) {
|
|
218
|
+
const colonIndex = value.indexOf(':');
|
|
219
|
+
const key = value.slice(0, colonIndex).trim();
|
|
220
|
+
const val = value.slice(colonIndex + 1).trim().replace(/^["']|["']$/g, '');
|
|
221
|
+
currentObject = { [key]: val };
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Rule property (indent 4): event:, action:, message:
|
|
227
|
+
if (currentObject && indent === 4 && trimmed.includes(':') && !trimmed.startsWith('-')) {
|
|
228
|
+
const colonIndex = trimmed.indexOf(':');
|
|
229
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
230
|
+
const rawVal = trimmed.slice(colonIndex + 1).trim();
|
|
231
|
+
|
|
232
|
+
if (key === 'conditions' && rawVal === '') {
|
|
233
|
+
// Start conditions array
|
|
234
|
+
currentConditions = [];
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const val = rawVal.replace(/^["']|["']$/g, '');
|
|
239
|
+
currentObject[key] = rawVal.startsWith('"') ? processYamlEscapes(val) : val;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Condition array item (indent 6): - field: xxx
|
|
244
|
+
if (currentConditions !== null && indent === 6 && trimmed.startsWith('-')) {
|
|
245
|
+
// Save previous condition
|
|
246
|
+
if (currentCondition) {
|
|
247
|
+
currentConditions.push(currentCondition);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const value = trimmed.slice(1).trim();
|
|
251
|
+
if (value.includes(':')) {
|
|
252
|
+
const colonIndex = value.indexOf(':');
|
|
253
|
+
const key = value.slice(0, colonIndex).trim();
|
|
254
|
+
const val = value.slice(colonIndex + 1).trim().replace(/^["']|["']$/g, '');
|
|
255
|
+
currentCondition = { [key]: val };
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Condition property (indent 8): pattern: xxx
|
|
261
|
+
if (currentCondition && indent === 8 && trimmed.includes(':')) {
|
|
262
|
+
const colonIndex = trimmed.indexOf(':');
|
|
263
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
264
|
+
const rawVal = trimmed.slice(colonIndex + 1).trim();
|
|
265
|
+
const val = rawVal.replace(/^["']|["']$/g, '');
|
|
266
|
+
currentCondition[key] = rawVal.startsWith('"') ? processYamlEscapes(val) : val;
|
|
267
|
+
|
|
268
|
+
// Check if next line is still part of condition
|
|
269
|
+
const nextLine = lines[i + 1];
|
|
270
|
+
const nextIndent = nextLine ? nextLine.search(/\S/) : 0;
|
|
271
|
+
if (!nextLine || nextIndent < 8 || (nextLine.trim().startsWith('-') && nextIndent <= 6)) {
|
|
272
|
+
currentConditions.push(currentCondition);
|
|
273
|
+
currentCondition = null;
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Array item (starts with -)
|
|
282
|
+
if (trimmed.startsWith('-')) {
|
|
283
|
+
const value = trimmed.slice(1).trim();
|
|
284
|
+
|
|
285
|
+
// Check if this is a key-value pair in the array item: - pattern: "..."
|
|
286
|
+
if (value.includes(':') && !value.startsWith('"') && !value.startsWith("'")) {
|
|
287
|
+
// Start of an object in array - split only on first colon to preserve colons in values
|
|
288
|
+
const colonIndex = value.indexOf(':');
|
|
289
|
+
const key = value.slice(0, colonIndex).trim();
|
|
290
|
+
const val = value.slice(colonIndex + 1).trim();
|
|
291
|
+
const rawVal = val.replace(/^["']|["']$/g, '');
|
|
292
|
+
currentObject = { [key]: val.startsWith('"') ? processYamlEscapes(rawVal) : rawVal };
|
|
293
|
+
} else {
|
|
294
|
+
// Simple string value
|
|
295
|
+
const rawValue = value.replace(/^["']|["']$/g, '');
|
|
296
|
+
const cleanValue = value.startsWith('"') ? processYamlEscapes(rawValue) : rawValue;
|
|
297
|
+
|
|
298
|
+
if (currentSection === 'paths' && currentSubSection) {
|
|
299
|
+
result.paths[currentSubSection].push(cleanValue);
|
|
300
|
+
} else if (currentSection && currentObject === null) {
|
|
301
|
+
result[currentSection].push(cleanValue);
|
|
302
|
+
}
|
|
303
|
+
currentObject = null;
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Continuation of object in array (indent 4+): reason: "..."
|
|
309
|
+
if (currentObject && trimmed.includes(':')) {
|
|
310
|
+
const colonIndex = trimmed.indexOf(':');
|
|
311
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
312
|
+
const rawVal = trimmed.slice(colonIndex + 1).trim();
|
|
313
|
+
const strippedVal = rawVal.replace(/^["']|["']$/g, '');
|
|
314
|
+
currentObject[key] = rawVal.startsWith('"') ? processYamlEscapes(strippedVal) : strippedVal;
|
|
315
|
+
|
|
316
|
+
// Check if next line continues this object
|
|
317
|
+
const nextLine = lines[i + 1];
|
|
318
|
+
const nextIndent = nextLine ? nextLine.search(/\S/) : 0;
|
|
319
|
+
const nextTrimmed = nextLine ? nextLine.trim() : '';
|
|
320
|
+
|
|
321
|
+
// If next line is a new array item or less indented, push current object
|
|
322
|
+
if (!nextLine || nextIndent <= 2 || nextTrimmed.startsWith('-')) {
|
|
323
|
+
if (currentSection && currentSection !== 'rules') {
|
|
324
|
+
result[currentSection].push(currentObject);
|
|
325
|
+
}
|
|
326
|
+
currentObject = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Don't forget the last object
|
|
332
|
+
if (currentObject && currentSection === 'rules') {
|
|
333
|
+
if (currentConditions) {
|
|
334
|
+
currentObject.conditions = currentConditions;
|
|
335
|
+
}
|
|
336
|
+
result.rules.push(currentObject);
|
|
337
|
+
} else if (currentObject && currentSection && currentSection !== 'rules') {
|
|
338
|
+
result[currentSection].push(currentObject);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Load damage control patterns from YAML file
|
|
346
|
+
* Supports both new event-based format and legacy format
|
|
347
|
+
*/
|
|
348
|
+
function loadPatterns() {
|
|
349
|
+
const config = getConfig();
|
|
350
|
+
const dcConfig = config.damageControl || {};
|
|
351
|
+
const patternsPath = dcConfig.patternsFile
|
|
352
|
+
? path.join(PROJECT_ROOT, dcConfig.patternsFile)
|
|
353
|
+
: PATTERNS_FILE;
|
|
354
|
+
|
|
355
|
+
if (!fs.existsSync(patternsPath)) {
|
|
356
|
+
return {
|
|
357
|
+
rules: [],
|
|
358
|
+
blocked: [],
|
|
359
|
+
ask: [],
|
|
360
|
+
paths: { zeroAccess: [], readOnly: [], noDelete: [] }
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const content = fs.readFileSync(patternsPath, 'utf-8');
|
|
366
|
+
const parsed = parseSimpleYaml(content);
|
|
367
|
+
// Ensure rules array exists
|
|
368
|
+
parsed.rules = parsed.rules || [];
|
|
369
|
+
return parsed;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
console.error(console.error('Error loading damage-control.yaml:', err.message));
|
|
372
|
+
return {
|
|
373
|
+
rules: [],
|
|
374
|
+
blocked: [],
|
|
375
|
+
ask: [],
|
|
376
|
+
paths: { zeroAccess: [], readOnly: [], noDelete: [] }
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============================================================
|
|
382
|
+
// Event-Based Rule Checking
|
|
383
|
+
// ============================================================
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Check if an event matches a rule (AND logic for conditions)
|
|
387
|
+
*
|
|
388
|
+
* @param {object} rule - Rule definition with event, action, conditions
|
|
389
|
+
* @param {string} eventType - Event type (bash/file/stop/prompt)
|
|
390
|
+
* @param {object} context - Event context
|
|
391
|
+
* @returns {string|null} - Action to take or null if no match
|
|
392
|
+
*/
|
|
393
|
+
function checkEventRule(rule, eventType, context) {
|
|
394
|
+
// Check event type match
|
|
395
|
+
if (rule.event !== 'all' && rule.event !== eventType) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// If no conditions, rule matches all events of this type
|
|
400
|
+
if (!rule.conditions || rule.conditions.length === 0) {
|
|
401
|
+
return rule.action;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check all conditions (AND logic)
|
|
405
|
+
for (const condition of rule.conditions) {
|
|
406
|
+
const value = context[condition.field];
|
|
407
|
+
|
|
408
|
+
if (value === undefined) {
|
|
409
|
+
return null; // Field doesn't exist
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const regex = safeRegExp(condition.pattern, 'i');
|
|
413
|
+
if (!regex) {
|
|
414
|
+
return null; // Invalid or unsafe regex, skip this condition
|
|
415
|
+
}
|
|
416
|
+
if (!safeRegexTest(regex, value)) {
|
|
417
|
+
return null; // Condition not met
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// All conditions matched
|
|
422
|
+
return rule.action;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Main event check function - checks event against all rules
|
|
427
|
+
*
|
|
428
|
+
* @param {string} eventType - Event type (bash/file/stop/prompt)
|
|
429
|
+
* @param {object} context - Event context
|
|
430
|
+
* @returns {object} - { allowed: boolean, action: string, message: string }
|
|
431
|
+
*/
|
|
432
|
+
function checkEvent(eventType, context = {}) {
|
|
433
|
+
const config = getConfig();
|
|
434
|
+
const dcConfig = config.damageControl || {};
|
|
435
|
+
|
|
436
|
+
// Check if damage control is enabled
|
|
437
|
+
if (!dcConfig.enabled) {
|
|
438
|
+
return { allowed: true, action: 'allow', message: 'Damage control disabled' };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Check if this event type is enabled
|
|
442
|
+
const events = dcConfig.events || { bash: true };
|
|
443
|
+
if (events[eventType] === false) {
|
|
444
|
+
return { allowed: true, action: 'allow', message: `Event type '${eventType}' disabled` };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const patterns = loadPatterns();
|
|
448
|
+
|
|
449
|
+
// Check event-based rules first (new format)
|
|
450
|
+
for (const rule of patterns.rules || []) {
|
|
451
|
+
const action = checkEventRule(rule, eventType, context);
|
|
452
|
+
if (action) {
|
|
453
|
+
const result = {
|
|
454
|
+
allowed: action === 'allow',
|
|
455
|
+
action,
|
|
456
|
+
message: rule.message || `Rule '${rule.name}' matched`,
|
|
457
|
+
rule: rule.name,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Log if configured
|
|
461
|
+
if (dcConfig.logging) {
|
|
462
|
+
logDamageControl(eventType, context, result);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (action === 'block') {
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
if (action === 'ask') {
|
|
469
|
+
return { ...result, requiresConfirmation: true };
|
|
470
|
+
}
|
|
471
|
+
if (action === 'warn') {
|
|
472
|
+
console.log(colors.yellow + `Warning: ${result.message}` + colors.reset);
|
|
473
|
+
return { allowed: true, action: 'warn', message: result.message };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Fall back to legacy patterns for bash events
|
|
479
|
+
if (eventType === 'bash') {
|
|
480
|
+
const cmd = context.command || '';
|
|
481
|
+
const legacyResult = checkCommand(cmd);
|
|
482
|
+
if (legacyResult.action !== 'allow') {
|
|
483
|
+
return {
|
|
484
|
+
allowed: legacyResult.action === 'allow',
|
|
485
|
+
...legacyResult,
|
|
486
|
+
requiresConfirmation: legacyResult.action === 'ask'
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Fall back to legacy path patterns for file events
|
|
492
|
+
if (eventType === 'file') {
|
|
493
|
+
const filePath = context.file_path || context.filePath || '';
|
|
494
|
+
const operation = context.operation || 'edit';
|
|
495
|
+
const pathResult = checkPath(filePath, operation);
|
|
496
|
+
if (!pathResult.allowed) {
|
|
497
|
+
return {
|
|
498
|
+
allowed: false,
|
|
499
|
+
action: 'block',
|
|
500
|
+
message: pathResult.reason,
|
|
501
|
+
level: pathResult.level
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { allowed: true, action: 'allow', message: 'No rules matched' };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Log damage control actions
|
|
511
|
+
*/
|
|
512
|
+
function logDamageControl(eventType, context, result) {
|
|
513
|
+
const config = getConfig();
|
|
514
|
+
const dcConfig = config.damageControl || {};
|
|
515
|
+
|
|
516
|
+
if (!dcConfig.logging) return;
|
|
517
|
+
|
|
518
|
+
const logDir = path.join(PROJECT_ROOT, '.workflow', 'logs');
|
|
519
|
+
const logPath = path.join(logDir, 'damage-control.log');
|
|
520
|
+
|
|
521
|
+
// Ensure log directory exists
|
|
522
|
+
if (!fs.existsSync(logDir)) {
|
|
523
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const timestamp = new Date().toISOString();
|
|
527
|
+
const action = result.action.toUpperCase().padEnd(7);
|
|
528
|
+
const contextStr = JSON.stringify(context).substring(0, 100);
|
|
529
|
+
|
|
530
|
+
const entry = `${timestamp} | ${action} | ${eventType} | ${contextStr} | ${result.message}\n`;
|
|
531
|
+
|
|
532
|
+
fs.appendFileSync(logPath, entry);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ============================================================
|
|
536
|
+
// Convenience Functions for Event Checking
|
|
537
|
+
// ============================================================
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Check a bash command (convenience wrapper)
|
|
541
|
+
*/
|
|
542
|
+
function checkBashEvent(command) {
|
|
543
|
+
return checkEvent('bash', { command });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Check a file operation (convenience wrapper)
|
|
548
|
+
*/
|
|
549
|
+
function checkFileEvent(filePath, operation = 'edit', content = '') {
|
|
550
|
+
return checkEvent('file', { file_path: filePath, filePath, operation, content });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Check session stop (convenience wrapper)
|
|
555
|
+
*/
|
|
556
|
+
function checkStopEvent() {
|
|
557
|
+
return checkEvent('stop', {});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Check user prompt (convenience wrapper)
|
|
562
|
+
*/
|
|
563
|
+
function checkPromptEvent(prompt) {
|
|
564
|
+
return checkEvent('prompt', { user_prompt: prompt });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Check if command is safe (read-only)
|
|
569
|
+
*/
|
|
570
|
+
function isSafeCommand(cmd) {
|
|
571
|
+
const normalizedCmd = cmd.trim();
|
|
572
|
+
return SAFE_COMMANDS.some(pattern => pattern.test(normalizedCmd));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Check command against patterns
|
|
577
|
+
* Returns: { action: 'allow' | 'block' | 'ask', reason?: string }
|
|
578
|
+
*/
|
|
579
|
+
function checkCommand(cmd) {
|
|
580
|
+
const config = getConfig();
|
|
581
|
+
const dcConfig = config.damageControl || {};
|
|
582
|
+
|
|
583
|
+
if (!dcConfig.enabled) {
|
|
584
|
+
return { action: 'allow' };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Skip safe commands
|
|
588
|
+
if (isSafeCommand(cmd)) {
|
|
589
|
+
return { action: 'allow', reason: 'Safe command' };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const patterns = loadPatterns();
|
|
593
|
+
|
|
594
|
+
// Check blocked patterns
|
|
595
|
+
for (const pattern of patterns.blocked || []) {
|
|
596
|
+
const regex = safeRegExp(pattern, 'i');
|
|
597
|
+
if (!regex) continue; // Skip invalid/unsafe patterns
|
|
598
|
+
if (regex.test(cmd)) {
|
|
599
|
+
return {
|
|
600
|
+
action: 'block',
|
|
601
|
+
reason: `Matches blocked pattern: ${pattern}`,
|
|
602
|
+
pattern
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Check ask patterns
|
|
608
|
+
for (const item of patterns.ask || []) {
|
|
609
|
+
const pattern = typeof item === 'string' ? item : item.pattern;
|
|
610
|
+
const reason = typeof item === 'object' ? item.reason : 'Matches sensitive pattern';
|
|
611
|
+
|
|
612
|
+
const regex = safeRegExp(pattern, 'i');
|
|
613
|
+
if (!regex) continue; // Skip invalid/unsafe patterns
|
|
614
|
+
if (regex.test(cmd)) {
|
|
615
|
+
return {
|
|
616
|
+
action: 'ask',
|
|
617
|
+
reason,
|
|
618
|
+
pattern
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return { action: 'allow' };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Check if a path matches a pattern using proper path segment matching
|
|
628
|
+
* Prevents false positives like "node_modules" matching "node_modules_backup"
|
|
629
|
+
*/
|
|
630
|
+
function pathMatchesPattern(normalizedPath, pattern) {
|
|
631
|
+
// Normalize pattern too
|
|
632
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
633
|
+
|
|
634
|
+
// If pattern is an absolute path or contains path separators, do exact segment matching
|
|
635
|
+
if (normalizedPattern.includes('/')) {
|
|
636
|
+
// Check if the path contains the pattern as a segment sequence
|
|
637
|
+
return normalizedPath.includes(normalizedPattern) &&
|
|
638
|
+
(normalizedPath === normalizedPattern ||
|
|
639
|
+
normalizedPath.startsWith(normalizedPattern + '/') ||
|
|
640
|
+
normalizedPath.endsWith('/' + normalizedPattern) ||
|
|
641
|
+
normalizedPath.includes('/' + normalizedPattern + '/'));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// For simple names (no path separator), match as directory/file name segment
|
|
645
|
+
const segments = normalizedPath.split('/');
|
|
646
|
+
return segments.some(segment => segment === normalizedPattern);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Check if path operation is allowed
|
|
651
|
+
* Returns: { allowed: boolean, reason?: string, level?: string }
|
|
652
|
+
*/
|
|
653
|
+
function checkPath(filePath, operation) {
|
|
654
|
+
const config = getConfig();
|
|
655
|
+
const dcConfig = config.damageControl || {};
|
|
656
|
+
|
|
657
|
+
if (!dcConfig.enabled) {
|
|
658
|
+
return { allowed: true };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const patterns = loadPatterns();
|
|
662
|
+
const paths = patterns.paths || {};
|
|
663
|
+
|
|
664
|
+
// Normalize path (handle both forward and backslashes)
|
|
665
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
666
|
+
|
|
667
|
+
// Zero access - block all operations (read, write, delete)
|
|
668
|
+
for (const p of paths.zeroAccess || []) {
|
|
669
|
+
if (pathMatchesPattern(normalizedPath, p)) {
|
|
670
|
+
return {
|
|
671
|
+
allowed: false,
|
|
672
|
+
reason: `Zero access path: ${p}`,
|
|
673
|
+
level: 'zeroAccess'
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Read-only - block write/delete
|
|
679
|
+
if (operation === 'write' || operation === 'delete') {
|
|
680
|
+
for (const p of paths.readOnly || []) {
|
|
681
|
+
if (pathMatchesPattern(normalizedPath, p)) {
|
|
682
|
+
return {
|
|
683
|
+
allowed: false,
|
|
684
|
+
reason: `Read-only path: ${p}`,
|
|
685
|
+
level: 'readOnly'
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// No-delete - block delete only
|
|
692
|
+
if (operation === 'delete') {
|
|
693
|
+
for (const p of paths.noDelete || []) {
|
|
694
|
+
if (pathMatchesPattern(normalizedPath, p)) {
|
|
695
|
+
return {
|
|
696
|
+
allowed: false,
|
|
697
|
+
reason: `No-delete path: ${p}`,
|
|
698
|
+
level: 'noDelete'
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return { allowed: true };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* AI prompt hook for unknown dangerous commands
|
|
709
|
+
* Returns: { action: 'allow' | 'block' | 'ask', reason?: string }
|
|
710
|
+
*
|
|
711
|
+
* Note: Full implementation requires API integration.
|
|
712
|
+
* This is a stub that can be enhanced with actual AI call.
|
|
713
|
+
*/
|
|
714
|
+
async function promptHookCheck(cmd) {
|
|
715
|
+
const config = getConfig();
|
|
716
|
+
const dcConfig = config.damageControl || {};
|
|
717
|
+
const promptConfig = dcConfig.promptHook || {};
|
|
718
|
+
|
|
719
|
+
if (!dcConfig.enabled || !promptConfig.enabled) {
|
|
720
|
+
return { action: 'allow' };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Skip if already caught by patterns
|
|
724
|
+
const patternResult = checkCommand(cmd);
|
|
725
|
+
if (patternResult.action !== 'allow') {
|
|
726
|
+
return patternResult;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Skip safe commands
|
|
730
|
+
if (isSafeCommand(cmd)) {
|
|
731
|
+
return { action: 'allow', reason: 'Safe command' };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// TODO: Implement actual AI API call
|
|
735
|
+
// For now, return allow with a note
|
|
736
|
+
return {
|
|
737
|
+
action: 'allow',
|
|
738
|
+
reason: 'Prompt hook enabled but API not yet integrated'
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Get status of damage control system
|
|
744
|
+
*/
|
|
745
|
+
function getStatus() {
|
|
746
|
+
const config = getConfig();
|
|
747
|
+
const dcConfig = config.damageControl || {};
|
|
748
|
+
const patterns = loadPatterns();
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
enabled: dcConfig.enabled || false,
|
|
752
|
+
promptHook: {
|
|
753
|
+
enabled: dcConfig.promptHook?.enabled || false,
|
|
754
|
+
model: dcConfig.promptHook?.model || 'haiku'
|
|
755
|
+
},
|
|
756
|
+
patternsFile: dcConfig.patternsFile || '.workflow/damage-control.yaml',
|
|
757
|
+
events: dcConfig.events || { bash: true, file: false, stop: false, prompt: false },
|
|
758
|
+
patternsLoaded: {
|
|
759
|
+
rules: (patterns.rules || []).length,
|
|
760
|
+
blocked: (patterns.blocked || []).length,
|
|
761
|
+
ask: (patterns.ask || []).length,
|
|
762
|
+
paths: {
|
|
763
|
+
zeroAccess: (patterns.paths?.zeroAccess || []).length,
|
|
764
|
+
readOnly: (patterns.paths?.readOnly || []).length,
|
|
765
|
+
noDelete: (patterns.paths?.noDelete || []).length
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
safeCommandPatterns: SAFE_COMMANDS.length
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// CLI handling
|
|
773
|
+
if (require.main === module) {
|
|
774
|
+
const args = process.argv.slice(2);
|
|
775
|
+
const command = args[0];
|
|
776
|
+
|
|
777
|
+
if (args.includes('--help') || args.includes('-h') || !command) {
|
|
778
|
+
console.log(`
|
|
779
|
+
Wogi-Flow Damage Control
|
|
780
|
+
|
|
781
|
+
Event-based pattern matching for destructive command protection.
|
|
782
|
+
Supports multiple event types: bash, file, stop, prompt.
|
|
783
|
+
|
|
784
|
+
Usage:
|
|
785
|
+
flow damage-control check "<command>" Check if bash command is allowed
|
|
786
|
+
flow damage-control event <type> <ctx> Check event against all rules
|
|
787
|
+
flow damage-control path "<path>" <op> Check if path operation is allowed
|
|
788
|
+
flow damage-control status Show damage control status
|
|
789
|
+
flow damage-control rules Show all rules (event-based + legacy)
|
|
790
|
+
|
|
791
|
+
Event Types: bash, file, stop, prompt
|
|
792
|
+
Operations for path check: read, write, delete
|
|
793
|
+
|
|
794
|
+
Examples:
|
|
795
|
+
flow dc check "rm -rf node_modules"
|
|
796
|
+
flow dc check "git reset --hard"
|
|
797
|
+
flow dc event bash '{"command": "rm -rf /"}'
|
|
798
|
+
flow dc event file '{"file_path": ".env", "operation": "edit"}'
|
|
799
|
+
flow dc path "/home/user/.ssh/id_rsa" read
|
|
800
|
+
flow dc status
|
|
801
|
+
flow dc rules
|
|
802
|
+
|
|
803
|
+
Configuration (config.json):
|
|
804
|
+
"damageControl": {
|
|
805
|
+
"enabled": false,
|
|
806
|
+
"patternsFile": ".workflow/damage-control.yaml",
|
|
807
|
+
"events": {
|
|
808
|
+
"bash": true,
|
|
809
|
+
"file": true,
|
|
810
|
+
"stop": true,
|
|
811
|
+
"prompt": false
|
|
812
|
+
},
|
|
813
|
+
"promptHook": {
|
|
814
|
+
"enabled": false,
|
|
815
|
+
"model": "haiku"
|
|
816
|
+
},
|
|
817
|
+
"logging": true
|
|
818
|
+
}
|
|
819
|
+
`);
|
|
820
|
+
process.exit(0);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
switch (command) {
|
|
824
|
+
case 'check': {
|
|
825
|
+
const cmd = args.slice(1).join(' ');
|
|
826
|
+
if (!cmd) {
|
|
827
|
+
log('red', 'Error: Command to check is required');
|
|
828
|
+
log('dim', 'Usage: flow dc check "<command>"');
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
// Use event-based check (covers both new rules and legacy)
|
|
832
|
+
const result = checkEvent('bash', { command: cmd });
|
|
833
|
+
console.log(JSON.stringify(result, null, 2));
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
case 'event': {
|
|
838
|
+
const eventType = args[1];
|
|
839
|
+
const contextStr = args[2];
|
|
840
|
+
if (!eventType || !EVENT_TYPES.includes(eventType)) {
|
|
841
|
+
log('red', `Error: Event type must be one of: ${EVENT_TYPES.join(', ')}`);
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
let context = {};
|
|
845
|
+
if (contextStr) {
|
|
846
|
+
try {
|
|
847
|
+
context = JSON.parse(contextStr);
|
|
848
|
+
} catch (err) {
|
|
849
|
+
log('red', 'Error: Context must be valid JSON');
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
const result = checkEvent(eventType, context);
|
|
854
|
+
console.log(JSON.stringify(result, null, 2));
|
|
855
|
+
process.exit(result.allowed ? 0 : 1);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
case 'path': {
|
|
860
|
+
const filePath = args[1];
|
|
861
|
+
const operation = args[2] || 'read';
|
|
862
|
+
if (!filePath) {
|
|
863
|
+
log('red', 'Error: Path is required');
|
|
864
|
+
log('dim', 'Usage: flow dc path "<path>" <operation>');
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
const result = checkPath(filePath, operation);
|
|
868
|
+
console.log(JSON.stringify(result, null, 2));
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
case 'status': {
|
|
873
|
+
const status = getStatus();
|
|
874
|
+
console.log('');
|
|
875
|
+
log('cyan', 'Damage Control Status');
|
|
876
|
+
console.log('');
|
|
877
|
+
log('white', ` Enabled: ${status.enabled ? colors.green + 'Yes' : colors.yellow + 'No'}${colors.reset}`);
|
|
878
|
+
log('white', ` Prompt Hook: ${status.promptHook.enabled ? colors.green + 'Yes' : colors.dim + 'No'}${colors.reset}`);
|
|
879
|
+
log('white', ` Patterns File: ${status.patternsFile}`);
|
|
880
|
+
console.log('');
|
|
881
|
+
log('cyan', 'Event Types:');
|
|
882
|
+
log('white', ` bash: ${status.events.bash ? colors.green + 'ON' : colors.dim + 'OFF'}${colors.reset}`);
|
|
883
|
+
log('white', ` file: ${status.events.file ? colors.green + 'ON' : colors.dim + 'OFF'}${colors.reset}`);
|
|
884
|
+
log('white', ` stop: ${status.events.stop ? colors.green + 'ON' : colors.dim + 'OFF'}${colors.reset}`);
|
|
885
|
+
log('white', ` prompt: ${status.events.prompt ? colors.green + 'ON' : colors.dim + 'OFF'}${colors.reset}`);
|
|
886
|
+
console.log('');
|
|
887
|
+
log('cyan', 'Rules Loaded:');
|
|
888
|
+
log('white', ` Event Rules: ${status.patternsLoaded.rules}`);
|
|
889
|
+
log('white', ` Legacy Blocked: ${status.patternsLoaded.blocked}`);
|
|
890
|
+
log('white', ` Legacy Ask: ${status.patternsLoaded.ask}`);
|
|
891
|
+
log('white', ` Zero Access Paths: ${status.patternsLoaded.paths.zeroAccess}`);
|
|
892
|
+
log('white', ` Read-Only Paths: ${status.patternsLoaded.paths.readOnly}`);
|
|
893
|
+
log('white', ` No-Delete Paths: ${status.patternsLoaded.paths.noDelete}`);
|
|
894
|
+
log('white', ` Safe Command Patterns: ${status.safeCommandPatterns}`);
|
|
895
|
+
console.log('');
|
|
896
|
+
|
|
897
|
+
if (!status.enabled) {
|
|
898
|
+
log('dim', 'To enable: Set damageControl.enabled to true in config.json');
|
|
899
|
+
}
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
case 'patterns':
|
|
904
|
+
case 'rules': {
|
|
905
|
+
const patterns = loadPatterns();
|
|
906
|
+
console.log('');
|
|
907
|
+
|
|
908
|
+
// Show event-based rules first (new format)
|
|
909
|
+
if (patterns.rules && patterns.rules.length > 0) {
|
|
910
|
+
log('cyan', 'Event-Based Rules:');
|
|
911
|
+
for (const rule of patterns.rules) {
|
|
912
|
+
const eventColor = rule.event === 'all' ? 'cyan' : 'yellow';
|
|
913
|
+
const actionColor = rule.action === 'block' ? 'red' : rule.action === 'warn' ? 'yellow' : 'green';
|
|
914
|
+
log(eventColor, ` [${rule.event}] ${rule.name}`);
|
|
915
|
+
log(actionColor, ` Action: ${rule.action}`);
|
|
916
|
+
if (rule.conditions && rule.conditions.length > 0) {
|
|
917
|
+
log('dim', ' Conditions:');
|
|
918
|
+
for (const c of rule.conditions) {
|
|
919
|
+
log('dim', ` ${c.field}: /${c.pattern}/`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (rule.message) {
|
|
923
|
+
log('dim', ` Message: ${rule.message}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
console.log('');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Show legacy patterns
|
|
930
|
+
log('cyan', 'Legacy Blocked Patterns:');
|
|
931
|
+
(patterns.blocked || []).forEach(p => log('red', ` - ${p}`));
|
|
932
|
+
console.log('');
|
|
933
|
+
log('cyan', 'Legacy Ask Patterns:');
|
|
934
|
+
(patterns.ask || []).forEach(p => {
|
|
935
|
+
if (typeof p === 'object') {
|
|
936
|
+
log('yellow', ` - ${p.pattern}`);
|
|
937
|
+
log('dim', ` Reason: ${p.reason}`);
|
|
938
|
+
} else {
|
|
939
|
+
log('yellow', ` - ${p}`);
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
console.log('');
|
|
943
|
+
log('cyan', 'Protected Paths:');
|
|
944
|
+
log('white', ' Zero Access:');
|
|
945
|
+
(patterns.paths?.zeroAccess || []).forEach(p => log('red', ` - ${p}`));
|
|
946
|
+
log('white', ' Read-Only:');
|
|
947
|
+
(patterns.paths?.readOnly || []).forEach(p => log('yellow', ` - ${p}`));
|
|
948
|
+
log('white', ' No-Delete:');
|
|
949
|
+
(patterns.paths?.noDelete || []).forEach(p => log('yellow', ` - ${p}`));
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
default:
|
|
954
|
+
log('red', `Unknown command: ${command}`);
|
|
955
|
+
log('dim', 'Run: flow dc --help for usage');
|
|
956
|
+
process.exit(1);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Export for use by other modules
|
|
961
|
+
module.exports = {
|
|
962
|
+
// Event-based checking (new)
|
|
963
|
+
checkEvent,
|
|
964
|
+
checkEventRule,
|
|
965
|
+
checkBashEvent,
|
|
966
|
+
checkFileEvent,
|
|
967
|
+
checkStopEvent,
|
|
968
|
+
checkPromptEvent,
|
|
969
|
+
EVENT_TYPES,
|
|
970
|
+
ACTIONS,
|
|
971
|
+
// Legacy functions (still supported)
|
|
972
|
+
loadPatterns,
|
|
973
|
+
parseSimpleYaml,
|
|
974
|
+
isSafeCommand,
|
|
975
|
+
checkCommand,
|
|
976
|
+
checkPath,
|
|
977
|
+
promptHookCheck,
|
|
978
|
+
getStatus,
|
|
979
|
+
SAFE_COMMANDS,
|
|
980
|
+
// Regex safety utilities (for use by other modules)
|
|
981
|
+
safeRegExp,
|
|
982
|
+
safeRegexTest,
|
|
983
|
+
MAX_REGEX_LENGTH,
|
|
984
|
+
MAX_INPUT_LENGTH
|
|
985
|
+
};
|