workflow-ai 1.0.62 → 1.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/agent-templates/CLAUDE.md.tpl +2 -0
- package/agent-templates/QWEN.md.tpl +2 -0
- package/package.json +2 -1
- package/src/init.mjs +5 -4
- package/src/lib/agent-spawner.mjs +338 -0
- package/src/runner.mjs +15 -14
- package/src/scripts/get-next-test-id.js +94 -0
- package/src/scripts/migrate-backlog-to-tests.js +406 -0
- package/src/scripts/run-skill-tests.js +1491 -0
- package/src/scripts/scan-fixtures-for-secrets.js +248 -0
- package/src/scripts/tests/timeout-cascade.test.js +28 -0
- package/templates/plan-template.md +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { findProjectRoot } from 'workflow-ai/lib/find-root.mjs';
|
|
6
|
+
import { printResult } from 'workflow-ai/lib/utils.mjs';
|
|
7
|
+
|
|
8
|
+
const PROJECT_DIR = findProjectRoot();
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ALLOWLIST = [
|
|
11
|
+
'localhost',
|
|
12
|
+
'127.0.0.1',
|
|
13
|
+
'::1',
|
|
14
|
+
'example.com',
|
|
15
|
+
'example.org',
|
|
16
|
+
'example.net'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const BASE_PATTERNS = [
|
|
20
|
+
{ name: 'api[_-]?key', regex: /api[_-]?key\s*[:=]\s*\S+/gi },
|
|
21
|
+
{ name: 'bearer', regex: /bearer\s+[A-Za-z0-9._-]+/gi },
|
|
22
|
+
{ name: 'openai-key', regex: /sk-[a-zA-Z0-9]{20,}/gi },
|
|
23
|
+
{ name: 'jwt', regex: /eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]+\./gi },
|
|
24
|
+
{ name: 'password', regex: /password\s*[:=]\s*\S+/gi },
|
|
25
|
+
{ name: 'token', regex: /token\s*[:=]\s*\S+/gi }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const STRICT_PATTERNS = [
|
|
29
|
+
{ name: 'aws-access-key', regex: /AKIA[0-9A-Z]{16}/g },
|
|
30
|
+
{ name: 'github-token', regex: /(gh[pousr]_[a-zA-Z0-9_]{36,})/g },
|
|
31
|
+
{ name: 'slack-token', regex: /xox[baprs]-[0-9a-zA-Z-]+/g },
|
|
32
|
+
{ name: 'private-key', regex: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
33
|
+
{ name: 'google-api-key', regex: /AIza[0-9A-Za-z-_]{35}/g },
|
|
34
|
+
{ name: 'stripe-key', regex: /(sk|pk)_(test|live)_[0-9a-zA-Z]{24,}/g },
|
|
35
|
+
{ name: 'sendgrid-key', regex: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g },
|
|
36
|
+
{ name: 'mailchimp-key', regex: /[a-f0-9]{32}-us[0-9]{1,2}/g },
|
|
37
|
+
{ name: 'generic-secret', regex: /secret\s*[:=]\s*["']?\S+["']?/gi },
|
|
38
|
+
{ name: 'client-secret', regex: /client[_-]?secret\s*[:=]\s*\S+/gi }
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
let PATTERNS = [...BASE_PATTERNS];
|
|
42
|
+
|
|
43
|
+
const IP_PRIVATE_REGEX = /(^127\.)|(^10\.)|(^172\.(1[6-9]|2[0-9]|3[0-1])\.)|(^192\.168\.)|(^::1$)|(^fe80:)|(^fc)|(^fd)/i;
|
|
44
|
+
const DOMAIN_PRIVATE_REGEX = /(localhost|127\.[0-9]+\.[0-9]+\.[0-9]+|::1|example\.(com|org|net))$/i;
|
|
45
|
+
|
|
46
|
+
function parseArgs() {
|
|
47
|
+
const args = process.argv.slice(2);
|
|
48
|
+
const options = {
|
|
49
|
+
path: null,
|
|
50
|
+
strict: false
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
|
+
if (args[i] === '--path' && args[i + 1]) {
|
|
55
|
+
options.path = args[i + 1];
|
|
56
|
+
i++;
|
|
57
|
+
} else if (args[i] === '--strict') {
|
|
58
|
+
options.strict = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getDefaultScanPaths() {
|
|
66
|
+
const skillsDir = path.join(PROJECT_DIR, 'src', 'skills');
|
|
67
|
+
if (!fs.existsSync(skillsDir)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const paths = [];
|
|
72
|
+
try {
|
|
73
|
+
const skills = fs.readdirSync(skillsDir);
|
|
74
|
+
for (const skill of skills) {
|
|
75
|
+
const fixturesPath = path.join(skillsDir, skill, 'tests', 'fixtures');
|
|
76
|
+
if (fs.existsSync(fixturesPath)) {
|
|
77
|
+
paths.push(fixturesPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return paths;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isPrivateIp(ip, allowlist) {
|
|
87
|
+
if (!ip) return false;
|
|
88
|
+
const normalized = ip.trim().toLowerCase();
|
|
89
|
+
if (DEFAULT_ALLOWLIST.includes(normalized) || allowlist.includes(normalized)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return IP_PRIVATE_REGEX.test(normalized);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isPrivateDomain(domain, allowlist) {
|
|
96
|
+
if (!domain) return false;
|
|
97
|
+
const normalized = domain.trim().toLowerCase();
|
|
98
|
+
if (DEFAULT_ALLOWLIST.includes(normalized) || allowlist.includes(normalized)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return DOMAIN_PRIVATE_REGEX.test(normalized);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scanFile(filePath, allowlist = []) {
|
|
105
|
+
const findings = [];
|
|
106
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
107
|
+
const lines = content.split('\n');
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
const line = lines[i];
|
|
111
|
+
const lineNum = i + 1;
|
|
112
|
+
|
|
113
|
+
for (const pattern of PATTERNS) {
|
|
114
|
+
let match;
|
|
115
|
+
pattern.regex.lastIndex = 0;
|
|
116
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
117
|
+
findings.push({
|
|
118
|
+
file: filePath,
|
|
119
|
+
line: lineNum,
|
|
120
|
+
pattern: pattern.name,
|
|
121
|
+
match: match[0]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ipRegex = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
127
|
+
let ipMatch;
|
|
128
|
+
while ((ipMatch = ipRegex.exec(line)) !== null) {
|
|
129
|
+
if (isPrivateIp(ipMatch[0], allowlist)) {
|
|
130
|
+
findings.push({
|
|
131
|
+
file: filePath,
|
|
132
|
+
line: lineNum,
|
|
133
|
+
pattern: 'private-ip',
|
|
134
|
+
match: ipMatch[0]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const domainRegex = /\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b/g;
|
|
140
|
+
let domainMatch;
|
|
141
|
+
while ((domainMatch = domainRegex.exec(line)) !== null) {
|
|
142
|
+
if (isPrivateDomain(domainMatch[0], allowlist)) {
|
|
143
|
+
findings.push({
|
|
144
|
+
file: filePath,
|
|
145
|
+
line: lineNum,
|
|
146
|
+
pattern: 'private-domain',
|
|
147
|
+
match: domainMatch[0]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return findings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function scanDirectory(dirPath, allowlist = []) {
|
|
157
|
+
const findings = [];
|
|
158
|
+
|
|
159
|
+
if (!fs.existsSync(dirPath)) {
|
|
160
|
+
return findings;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
164
|
+
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
167
|
+
|
|
168
|
+
if (entry.isDirectory()) {
|
|
169
|
+
findings.push(...scanDirectory(fullPath, allowlist));
|
|
170
|
+
} else if (entry.isFile()) {
|
|
171
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
172
|
+
const scannable = ['.js', '.json', '.yaml', '.yml', '.txt', '.log', '.md', '.env', '.sh', '.bash', '.ts', '.tsx'];
|
|
173
|
+
if (scannable.includes(ext) || ext === '') {
|
|
174
|
+
findings.push(...scanFile(fullPath, allowlist));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return findings;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function printFindings(findings) {
|
|
183
|
+
if (findings.length === 0) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log('---RESULT---');
|
|
188
|
+
console.log('status: failed');
|
|
189
|
+
console.log('findings:');
|
|
190
|
+
|
|
191
|
+
for (const f of findings) {
|
|
192
|
+
console.log(` - file: ${f.file}`);
|
|
193
|
+
console.log(` line: ${f.line}`);
|
|
194
|
+
console.log(` pattern: "${f.pattern}"`);
|
|
195
|
+
console.log(` match: "${f.match}"`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('---RESULT---');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function main() {
|
|
202
|
+
const options = parseArgs();
|
|
203
|
+
const allowlist = [...DEFAULT_ALLOWLIST];
|
|
204
|
+
|
|
205
|
+
if (options.strict) {
|
|
206
|
+
PATTERNS = [...BASE_PATTERNS, ...STRICT_PATTERNS];
|
|
207
|
+
console.log('[Scanner] Running in STRICT mode');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let scanPaths = [];
|
|
211
|
+
|
|
212
|
+
if (options.path) {
|
|
213
|
+
const absolutePath = path.isAbsolute(options.path)
|
|
214
|
+
? options.path
|
|
215
|
+
: path.join(PROJECT_DIR, options.path);
|
|
216
|
+
scanPaths.push(absolutePath);
|
|
217
|
+
} else {
|
|
218
|
+
scanPaths = getDefaultScanPaths();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (scanPaths.length === 0) {
|
|
222
|
+
console.log('---RESULT---');
|
|
223
|
+
console.log('status: passed');
|
|
224
|
+
console.log('findings:');
|
|
225
|
+
console.log('---RESULT---');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const allFindings = [];
|
|
230
|
+
|
|
231
|
+
for (const scanPath of scanPaths) {
|
|
232
|
+
const findings = scanDirectory(scanPath, allowlist);
|
|
233
|
+
allFindings.push(...findings);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
printFindings(allFindings);
|
|
237
|
+
|
|
238
|
+
if (allFindings.length > 0) {
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
printResult({ status: 'passed', findings: [] });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main().catch(e => {
|
|
246
|
+
console.error(`[ERROR] ${e.message}`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function parseTimeout(testCase, index) {
|
|
2
|
+
return testCase.execution?.timeout_s || index.execution?.default_timeout_s || 300;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function testTimeoutCascade() {
|
|
6
|
+
const index = { execution: { default_timeout_s: 300 } };
|
|
7
|
+
|
|
8
|
+
const testCases = [
|
|
9
|
+
{ name: 'TC-001', execution: { timeout_s: 600 } },
|
|
10
|
+
{ name: 'TC-002', execution: {} },
|
|
11
|
+
{ name: 'TC-003' }
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const results = testCases.map(tc => ({
|
|
15
|
+
case: tc.name,
|
|
16
|
+
timeout: parseTimeout(tc, index)
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
console.log('Timeout cascade results:', results);
|
|
20
|
+
|
|
21
|
+
const expected = [600, 300, 300];
|
|
22
|
+
const passed = results.every((r, i) => r.timeout === expected[i]);
|
|
23
|
+
|
|
24
|
+
console.log(`Test ${passed ? 'PASSED' : 'FAILED'}`);
|
|
25
|
+
process.exit(passed ? 0 : 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
testTimeoutCascade();
|