chati-dev 4.0.10 → 4.1.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.md +5 -0
- package/bin/chati.js +23 -2
- package/framework/agents/build/dev.md +43 -10
- package/framework/agents/discover/brief.md +38 -8
- package/framework/agents/discover/brownfield-wu.md +44 -3
- package/framework/agents/discover/greenfield-wu.md +14 -0
- package/framework/agents/plan/architect.md +6 -0
- package/framework/agents/plan/detail.md +25 -0
- package/framework/agents/plan/tasks.md +29 -1
- package/framework/agents/plan/ux-brand-architect.md +215 -0
- package/framework/agents/plan/ux-component-engineer.md +289 -0
- package/framework/agents/plan/ux-researcher.md +166 -0
- package/framework/agents/plan/ux.md +126 -1008
- package/framework/agents/quality/qa-implementation.md +121 -22
- package/framework/agents/quality/qa-planning.md +18 -0
- package/framework/config.yaml +15 -4
- package/framework/constitution.md +8 -2
- package/framework/context/root.md +1 -1
- package/framework/data/entity-registry.yaml +59 -3
- package/framework/hooks/constitution-guard.js +67 -1
- package/framework/hooks/license-guard.js +4 -4
- package/framework/hooks/model-governance.js +2 -1
- package/framework/hooks/prism-engine.js +74 -6
- package/framework/hooks/read-protection.js +1 -1
- package/framework/hooks/session-digest.js +159 -7
- package/framework/hooks/settings.json +16 -4
- package/framework/hooks/style-guard.js +134 -0
- package/framework/hooks/undercover-guard.js +220 -0
- package/framework/intelligence/context-engine.md +21 -0
- package/framework/intelligence/hooks-performance.md +54 -0
- package/framework/intelligence/memory-layer.md +47 -0
- package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
- package/framework/orchestrator/chati.md +327 -1067
- package/framework/templates/brandbook-html-tmpl.md +107 -0
- package/framework/templates/session-memory-tmpl.yaml +68 -0
- package/package.json +1 -1
- package/src/autonomy/build-state.js +1 -1
- package/src/autonomy/worktree-manager.js +13 -13
- package/src/config/agent-customizer.js +1 -1
- package/src/config/gemini-hooks-generator.js +6 -6
- package/src/config/ide-configs.js +1 -1
- package/src/context/bracket-tracker.js +25 -4
- package/src/context/engine.js +37 -7
- package/src/context/formatter.js +45 -1
- package/src/context/index.js +3 -3
- package/src/dashboard/data-reader.js +7 -7
- package/src/decision/engine.js +2 -2
- package/src/decision/registry-healer.js +1 -1
- package/src/decision/registry-updater.js +2 -2
- package/src/extensions/loader.js +1 -1
- package/src/gates/g1-planning-complete.js +1 -1
- package/src/gates/g2-qa-planning.js +1 -1
- package/src/gates/g3-implementation.js +4 -4
- package/src/gates/g4-qa-implementation.js +2 -2
- package/src/health/auto-fix.js +3 -3
- package/src/health/engine.js +2 -2
- package/src/installer/brownfield-upgrader.js +3 -3
- package/src/installer/manifest.js +1 -1
- package/src/installer/transaction.js +1 -1
- package/src/installer/validator.js +2 -2
- package/src/intelligence/decision-engine.js +1 -1
- package/src/intelligence/memory-manager.js +1 -1
- package/src/intelligence/registry-manager.js +2 -2
- package/src/intelligence/timeline.js +1 -1
- package/src/license/client.js +20 -2
- package/src/license/commands.js +7 -7
- package/src/memory/agent-memory.js +3 -3
- package/src/memory/daily-digest.js +170 -0
- package/src/memory/dream.js +254 -0
- package/src/memory/gotchas.js +2 -2
- package/src/memory/index.js +18 -0
- package/src/memory/magic-docs.js +98 -0
- package/src/memory/memory-extractor.js +163 -0
- package/src/memory/session-digest.js +144 -6
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/cli.js +980 -0
- package/src/orchestrator/handoff-engine.js +25 -4
- package/src/orchestrator/index.js +9 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/preview/detector.js +3 -3
- package/src/preview/launcher.js +2 -2
- package/src/quality/metrics-collector.js +1 -1
- package/src/quality/test-runner.js +2 -4
- package/src/scanning/density-scanner.js +51 -0
- package/src/scanning/env-scanner.js +97 -0
- package/src/scanning/index.js +7 -0
- package/src/scanning/leakage-scanner.js +54 -0
- package/src/scanning/placeholder-scanner.js +40 -0
- package/src/scanning/security-scanner.js +94 -0
- package/src/security/bash-security.js +335 -0
- package/src/security/index.js +9 -0
- package/src/telemetry/config.js +3 -5
- package/src/telemetry/sender.js +19 -17
- package/src/terminal/cli-registry.js +1 -1
- package/src/terminal/cost-tracker.js +1 -2
- package/src/terminal/prompt-builder.js +11 -27
- package/src/terminal/rate-limiter.js +1 -2
- package/src/terminal/run-agent.js +3 -3
- package/src/terminal/spawner.js +41 -4
- package/src/upgrade/checker.js +2 -2
- package/src/upgrade/migrator.js +1 -1
- package/src/utils/config-parser.js +1 -1
- package/src/utils/file-lock.js +3 -3
- package/src/utils/provider-limits.js +35 -4
- package/src/wizard/i18n.js +2 -2
- package/src/wizard/index.js +2 -1
- package/src/wizard/questions.js +4 -2
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Security — 23-check shell injection defense system.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Claude Code's bashSecurity.ts which runs every bash command
|
|
5
|
+
* through 23 numbered security checks protecting against shell injection,
|
|
6
|
+
* unicode attacks, IFS manipulation, and prompt injection via bash.
|
|
7
|
+
*
|
|
8
|
+
* This is critical for chati.dev because autonomous agents (run-agent.js)
|
|
9
|
+
* spawn bash processes. A malicious codebase being analyzed could exploit
|
|
10
|
+
* shell injection vectors to escape the sandbox.
|
|
11
|
+
*
|
|
12
|
+
* Each check has:
|
|
13
|
+
* - id: Unique check identifier (matches Claude Code's naming)
|
|
14
|
+
* - name: Human-readable description
|
|
15
|
+
* - pattern: RegExp to detect the threat
|
|
16
|
+
* - severity: critical | high | medium
|
|
17
|
+
* - description: What the attack does
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} SecurityCheck
|
|
22
|
+
* @property {string} id - Check identifier
|
|
23
|
+
* @property {string} name - Human-readable name
|
|
24
|
+
* @property {RegExp} pattern - Detection pattern
|
|
25
|
+
* @property {string} severity - critical | high | medium
|
|
26
|
+
* @property {string} description - What this check catches
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** @type {SecurityCheck[]} */
|
|
30
|
+
export const SECURITY_CHECKS = [
|
|
31
|
+
// --- Category: Command Structure Attacks ---
|
|
32
|
+
{
|
|
33
|
+
id: 'INCOMPLETE_COMMANDS',
|
|
34
|
+
name: 'Incomplete command with dangling operators',
|
|
35
|
+
pattern: /[|&;]\s*$/,
|
|
36
|
+
severity: 'high',
|
|
37
|
+
description: 'Command ends with pipe/semicolon/ampersand, expecting injected continuation',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'JQ_SYSTEM_FUNCTION',
|
|
41
|
+
name: 'jq system() function call',
|
|
42
|
+
pattern: /jq\b.*\bsystem\s*\(/i,
|
|
43
|
+
severity: 'critical',
|
|
44
|
+
description: 'jq system() executes arbitrary shell commands',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'JQ_FILE_ARGUMENTS',
|
|
48
|
+
name: 'jq with file argument injection',
|
|
49
|
+
pattern: /jq\b.*--from-file|jq\b.*-f\s+[^|&;]+/i,
|
|
50
|
+
severity: 'high',
|
|
51
|
+
description: 'jq --from-file can read arbitrary files',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// --- Category: Flag/Argument Obfuscation ---
|
|
55
|
+
{
|
|
56
|
+
id: 'OBFUSCATED_FLAGS',
|
|
57
|
+
name: 'Obfuscated command flags',
|
|
58
|
+
pattern: /\$[({].*[)}].*-/,
|
|
59
|
+
severity: 'high',
|
|
60
|
+
description: 'Variable expansion used to construct flags, hiding dangerous options',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'SHELL_METACHARACTERS',
|
|
64
|
+
name: 'Shell metacharacter injection',
|
|
65
|
+
pattern: /[`]|(?:\$\((?!.*\becho\b))/,
|
|
66
|
+
severity: 'critical',
|
|
67
|
+
description: 'Backticks or $() command substitution (excluding safe echo patterns)',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'DANGEROUS_VARIABLES',
|
|
71
|
+
name: 'Dangerous environment variable manipulation',
|
|
72
|
+
pattern: /(?:^|\s)(?:PATH|LD_PRELOAD|LD_LIBRARY_PATH|DYLD_INSERT_LIBRARIES|PYTHONPATH|NODE_PATH|RUBYLIB|PERL5LIB)\s*=/,
|
|
73
|
+
severity: 'critical',
|
|
74
|
+
description: 'Overwriting PATH/LD_PRELOAD can hijack command resolution or inject libraries',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// --- Category: Whitespace & Encoding Attacks ---
|
|
78
|
+
{
|
|
79
|
+
id: 'NEWLINES',
|
|
80
|
+
name: 'Embedded newlines in command',
|
|
81
|
+
pattern: /(?<!\\)\n.*(?:rm|curl|wget|chmod|chown|sudo|eval|exec)/,
|
|
82
|
+
severity: 'high',
|
|
83
|
+
description: 'Newlines can hide dangerous commands after an innocent-looking first line',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'BACKSLASH_ESCAPED_WHITESPACE',
|
|
87
|
+
name: 'Backslash-escaped whitespace hiding arguments',
|
|
88
|
+
pattern: /\\\s+(?:-|\/)/,
|
|
89
|
+
severity: 'medium',
|
|
90
|
+
description: 'Escaped whitespace can hide flags or paths from visual inspection',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'CONTROL_CHARACTERS',
|
|
94
|
+
name: 'ASCII control characters',
|
|
95
|
+
// eslint-disable-next-line no-control-regex
|
|
96
|
+
pattern: /[\x00-\x08\x0e-\x1f\x7f]/,
|
|
97
|
+
severity: 'critical',
|
|
98
|
+
description: 'Control characters can manipulate terminal behavior or hide content',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'UNICODE_WHITESPACE',
|
|
102
|
+
name: 'Unicode zero-width or invisible characters',
|
|
103
|
+
pattern: /[\u200B-\u200F\u2028-\u202F\uFEFF\u00A0\u2060\u180E]/,
|
|
104
|
+
severity: 'critical',
|
|
105
|
+
description: 'Zero-width spaces and invisible unicode can hide injected commands',
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// --- Category: Shell Expansion Attacks ---
|
|
109
|
+
{
|
|
110
|
+
id: 'DANGEROUS_PATTERNS_COMMAND_SUBSTITUTION',
|
|
111
|
+
name: 'Command substitution in dangerous context',
|
|
112
|
+
pattern: /\$\(.*(?:curl|wget|nc|bash|sh|python|perl|ruby|node)\b/i,
|
|
113
|
+
severity: 'critical',
|
|
114
|
+
description: 'Command substitution executing network or interpreter commands',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'DANGEROUS_PATTERNS_INPUT_REDIRECTION',
|
|
118
|
+
name: 'Input redirection from sensitive sources',
|
|
119
|
+
pattern: /<\s*(?:\/etc\/(?:passwd|shadow|sudoers)|\/proc\/|~\/\.ssh\/|~\/\.aws\/)/,
|
|
120
|
+
severity: 'critical',
|
|
121
|
+
description: 'Reading sensitive system files via input redirection',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'DANGEROUS_PATTERNS_OUTPUT_REDIRECTION',
|
|
125
|
+
name: 'Output redirection to sensitive targets',
|
|
126
|
+
pattern: />\s*(?:\/etc\/|~\/\.ssh\/|~\/\.bashrc|~\/\.zshrc|~\/\.profile|~\/\.gitconfig)/,
|
|
127
|
+
severity: 'critical',
|
|
128
|
+
description: 'Overwriting sensitive system or user configuration files',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'IFS_INJECTION',
|
|
132
|
+
name: 'IFS variable manipulation',
|
|
133
|
+
pattern: /\bIFS\s*=/,
|
|
134
|
+
severity: 'critical',
|
|
135
|
+
description: 'Changing Internal Field Separator can alter how shell parses commands',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'BRACE_EXPANSION',
|
|
139
|
+
name: 'Dangerous brace expansion',
|
|
140
|
+
pattern: /\{.*(?:rm|curl|wget|chmod|eval|exec|sudo).*[,}]/,
|
|
141
|
+
severity: 'high',
|
|
142
|
+
description: 'Brace expansion can generate multiple dangerous commands from one expression',
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// --- Category: Git & Process Attacks ---
|
|
146
|
+
{
|
|
147
|
+
id: 'GIT_COMMIT_SUBSTITUTION',
|
|
148
|
+
name: 'Git command with embedded substitution',
|
|
149
|
+
pattern: /git\s+(?:commit|push|tag).*\$[({]/,
|
|
150
|
+
severity: 'high',
|
|
151
|
+
description: 'Variable expansion in git commands can inject arbitrary content',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'PROC_ENVIRON_ACCESS',
|
|
155
|
+
name: 'Process environment access',
|
|
156
|
+
pattern: /\/proc\/(?:self|\d+)\/(?:environ|cmdline|maps|mem)/,
|
|
157
|
+
severity: 'critical',
|
|
158
|
+
description: 'Accessing /proc can leak secrets, memory contents, or environment variables',
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// --- Category: Token & Parsing Attacks ---
|
|
162
|
+
{
|
|
163
|
+
id: 'MALFORMED_TOKEN_INJECTION',
|
|
164
|
+
name: 'Malformed token or escape sequence',
|
|
165
|
+
pattern: /\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\[0-7]{3}/i,
|
|
166
|
+
severity: 'high',
|
|
167
|
+
description: 'Hex/unicode/octal escape sequences can encode hidden characters',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'MID_WORD_HASH',
|
|
171
|
+
name: 'Hash character in non-comment position',
|
|
172
|
+
pattern: /\w#\w/,
|
|
173
|
+
severity: 'medium',
|
|
174
|
+
description: 'Hash in mid-word position may indicate parameter expansion trick',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'COMMENT_QUOTE_DESYNC',
|
|
178
|
+
name: 'Comment/quote desynchronization',
|
|
179
|
+
pattern: /#.*['"][^'"]*$/,
|
|
180
|
+
severity: 'medium',
|
|
181
|
+
description: 'Unclosed quotes after comment can desync parsing across lines',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'QUOTED_NEWLINE',
|
|
185
|
+
name: 'Newline inside quoted string hiding commands',
|
|
186
|
+
pattern: /["'][^"']*\n[^"']*["']/,
|
|
187
|
+
severity: 'high',
|
|
188
|
+
description: 'Newlines inside quotes can hide command sequences',
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// --- Category: Zsh-Specific Attacks ---
|
|
192
|
+
{
|
|
193
|
+
id: 'ZSH_DANGEROUS_COMMANDS',
|
|
194
|
+
name: 'Zsh-specific dangerous builtins',
|
|
195
|
+
pattern: /\b(?:zmodload|sysopen|sysread|syswrite|zsystem|zselect|ztcp)\b/,
|
|
196
|
+
severity: 'critical',
|
|
197
|
+
description: 'Zsh builtins that can load modules, open sockets, or access raw file descriptors',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'BACKSLASH_ESCAPED_OPERATORS',
|
|
201
|
+
name: 'Backslash-escaped shell operators',
|
|
202
|
+
pattern: /\\[|;&]/,
|
|
203
|
+
severity: 'medium',
|
|
204
|
+
description: 'Escaped operators may bypass naive pipe/semicolon filtering',
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Run all 23 security checks against a bash command.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} command - Bash command to validate
|
|
212
|
+
* @returns {{ safe: boolean, findings: Array<{id: string, name: string, severity: string, description: string}> }}
|
|
213
|
+
*/
|
|
214
|
+
export function runSecurityChecks(command) {
|
|
215
|
+
if (!command || typeof command !== 'string') {
|
|
216
|
+
return { safe: true, findings: [], checksRun: SECURITY_CHECKS.length };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const findings = [];
|
|
220
|
+
|
|
221
|
+
for (const check of SECURITY_CHECKS) {
|
|
222
|
+
check.pattern.lastIndex = 0; // Reset for global patterns
|
|
223
|
+
if (check.pattern.test(command)) {
|
|
224
|
+
findings.push({
|
|
225
|
+
id: check.id,
|
|
226
|
+
name: check.name,
|
|
227
|
+
severity: check.severity,
|
|
228
|
+
description: check.description,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
safe: findings.length === 0,
|
|
235
|
+
findings,
|
|
236
|
+
checksRun: SECURITY_CHECKS.length,
|
|
237
|
+
criticalCount: findings.filter(f => f.severity === 'critical').length,
|
|
238
|
+
highCount: findings.filter(f => f.severity === 'high').length,
|
|
239
|
+
mediumCount: findings.filter(f => f.severity === 'medium').length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Quick check — returns true if command has any critical findings.
|
|
245
|
+
* @param {string} command
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
export function hasCriticalRisk(command) {
|
|
249
|
+
if (!command) return false;
|
|
250
|
+
return SECURITY_CHECKS
|
|
251
|
+
.filter(c => c.severity === 'critical')
|
|
252
|
+
.some(c => {
|
|
253
|
+
c.pattern.lastIndex = 0;
|
|
254
|
+
return c.pattern.test(command);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get security check summary for dashboard/reporting.
|
|
260
|
+
*/
|
|
261
|
+
export function getSecurityCheckSummary() {
|
|
262
|
+
const bySeverity = { critical: 0, high: 0, medium: 0 };
|
|
263
|
+
for (const check of SECURITY_CHECKS) {
|
|
264
|
+
bySeverity[check.severity]++;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
totalChecks: SECURITY_CHECKS.length,
|
|
268
|
+
bySeverity,
|
|
269
|
+
categories: [
|
|
270
|
+
'Command Structure Attacks',
|
|
271
|
+
'Flag/Argument Obfuscation',
|
|
272
|
+
'Whitespace & Encoding Attacks',
|
|
273
|
+
'Shell Expansion Attacks',
|
|
274
|
+
'Git & Process Attacks',
|
|
275
|
+
'Token & Parsing Attacks',
|
|
276
|
+
'Zsh-Specific Attacks',
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Language-specific security checks for SAST scanning.
|
|
283
|
+
* Complements the 23 bash security checks with code-level patterns
|
|
284
|
+
* for JavaScript, TypeScript, and Python.
|
|
285
|
+
*/
|
|
286
|
+
export const LANGUAGE_SECURITY_CHECKS = {
|
|
287
|
+
javascript: [
|
|
288
|
+
{ id: 'JS_EVAL', pattern: /\beval\s*\(/, severity: 'critical', description: 'eval() enables remote code execution' },
|
|
289
|
+
{ id: 'JS_NEW_FUNCTION', pattern: /new\s+Function\s*\(/, severity: 'critical', description: 'new Function() enables RCE' },
|
|
290
|
+
{ id: 'JS_INNERHTML', pattern: /\.innerHTML\s*=/, severity: 'critical', description: 'innerHTML enables XSS attacks' },
|
|
291
|
+
{ id: 'JS_OUTERHTML', pattern: /\.outerHTML\s*=/, severity: 'critical', description: 'outerHTML enables XSS attacks' },
|
|
292
|
+
{ id: 'JS_DOCUMENT_WRITE', pattern: /document\.write\s*\(/, severity: 'critical', description: 'document.write enables XSS' },
|
|
293
|
+
{ id: 'JS_DANGEROUSLY_SET', pattern: /dangerouslySetInnerHTML/, severity: 'critical', description: 'React XSS vector' },
|
|
294
|
+
{ id: 'JS_NULL_DOM', pattern: /getElementById\s*\([^)]+\)\s*\./, severity: 'high', description: 'DOM query without null check may crash' },
|
|
295
|
+
{ id: 'JS_QUERYSELECTOR_NULL', pattern: /querySelector\s*\([^)]+\)\s*\./, severity: 'high', description: 'querySelector without null check may crash' },
|
|
296
|
+
],
|
|
297
|
+
typescript: [
|
|
298
|
+
{ id: 'TS_EVAL', pattern: /\beval\s*\(/, severity: 'critical', description: 'eval() enables RCE' },
|
|
299
|
+
{ id: 'TS_INNERHTML', pattern: /\.innerHTML\s*=/, severity: 'critical', description: 'innerHTML enables XSS' },
|
|
300
|
+
{ id: 'TS_DANGEROUSLY_SET', pattern: /dangerouslySetInnerHTML/, severity: 'critical', description: 'React XSS vector' },
|
|
301
|
+
{ id: 'TS_ANY_CAST', pattern: /as\s+any\b/, severity: 'medium', description: 'Type assertion to any bypasses type safety' },
|
|
302
|
+
{ id: 'TS_NULL_DOM', pattern: /getElementById\s*\([^)]+\)\s*\./, severity: 'high', description: 'DOM query without null check' },
|
|
303
|
+
],
|
|
304
|
+
python: [
|
|
305
|
+
{ id: 'PY_EVAL', pattern: /\beval\s*\(/, severity: 'critical', description: 'eval() enables RCE' },
|
|
306
|
+
{ id: 'PY_EXEC', pattern: /\bexec\s*\(/, severity: 'critical', description: 'exec() enables RCE' },
|
|
307
|
+
{ id: 'PY_COMPILE', pattern: /\bcompile\s*\(/, severity: 'high', description: 'compile() can enable code injection' },
|
|
308
|
+
{ id: 'PY_SHELL_TRUE', pattern: /shell\s*=\s*True/, severity: 'critical', description: 'shell=True enables command injection' },
|
|
309
|
+
{ id: 'PY_PICKLE', pattern: /pickle\.loads?\s*\(/, severity: 'critical', description: 'pickle from untrusted source is unsafe' },
|
|
310
|
+
{ id: 'PY_YAML_LOAD', pattern: /yaml\.load\s*\([^)]*\)\s*(?!.*Loader)/, severity: 'high', description: 'yaml.load without SafeLoader is unsafe' },
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Run language-specific security checks against source code content.
|
|
316
|
+
* @param {string} content - Source code to scan
|
|
317
|
+
* @param {string} language - Language identifier (javascript, typescript, python)
|
|
318
|
+
* @returns {{ safe: boolean, findings: Array, checksRun: number }}
|
|
319
|
+
*/
|
|
320
|
+
export function runLanguageSecurityChecks(content, language) {
|
|
321
|
+
const checks = LANGUAGE_SECURITY_CHECKS[language] || [];
|
|
322
|
+
if (!content || checks.length === 0) return { safe: true, findings: [], checksRun: 0 };
|
|
323
|
+
|
|
324
|
+
const findings = [];
|
|
325
|
+
for (const check of checks) {
|
|
326
|
+
if (check.pattern.test(content)) {
|
|
327
|
+
findings.push({ id: check.id, severity: check.severity, description: check.description });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
safe: findings.length === 0,
|
|
332
|
+
findings,
|
|
333
|
+
checksRun: checks.length,
|
|
334
|
+
};
|
|
335
|
+
}
|
package/src/telemetry/config.js
CHANGED
|
@@ -27,7 +27,6 @@ export function getTelemetryConfig(targetDir) {
|
|
|
27
27
|
enabled: true,
|
|
28
28
|
anonymousId: null,
|
|
29
29
|
endpoint: process.env.CHATI_TELEMETRY_ENDPOINT || 'https://chati.dev/api/telemetry',
|
|
30
|
-
apiKey: process.env.CHATI_TELEMETRY_KEY || '10b0b54ba4f392fa46379ba778062ab0af5ca61e79609a7dce4aadd660104b56',
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
if (!existsSync(configPath)) return defaults;
|
|
@@ -41,9 +40,8 @@ export function getTelemetryConfig(targetDir) {
|
|
|
41
40
|
enabled: telemetry.enabled === true,
|
|
42
41
|
anonymousId: telemetry.anonymous_id || null,
|
|
43
42
|
endpoint: telemetry.endpoint || defaults.endpoint,
|
|
44
|
-
apiKey: telemetry.api_key || defaults.apiKey,
|
|
45
43
|
};
|
|
46
|
-
} catch {
|
|
44
|
+
} catch { /* expected: file may not exist */
|
|
47
45
|
return defaults;
|
|
48
46
|
}
|
|
49
47
|
}
|
|
@@ -83,7 +81,7 @@ export function setEnabled(targetDir, enabled) {
|
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
writeFileSync(configPath, yaml.dump(config, { lineWidth: -1 }), 'utf-8');
|
|
86
|
-
} catch {
|
|
84
|
+
} catch { /* expected: operation may fail gracefully */
|
|
87
85
|
// Silently fail — telemetry config is non-critical
|
|
88
86
|
}
|
|
89
87
|
}
|
|
@@ -110,7 +108,7 @@ export function getAnonymousId(targetDir) {
|
|
|
110
108
|
if (!parsed.telemetry) parsed.telemetry = {};
|
|
111
109
|
parsed.telemetry.anonymous_id = id;
|
|
112
110
|
writeFileSync(configPath, yaml.dump(parsed, { lineWidth: -1 }), 'utf-8');
|
|
113
|
-
} catch {
|
|
111
|
+
} catch { /* expected: file may not exist */
|
|
114
112
|
// Return generated ID even if persistence fails
|
|
115
113
|
}
|
|
116
114
|
}
|
package/src/telemetry/sender.js
CHANGED
|
@@ -58,24 +58,26 @@ export async function sendEvents(events, config) {
|
|
|
58
58
|
events,
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
62
|
+
try {
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
body: JSON.stringify(payload),
|
|
72
|
-
signal: controller.signal,
|
|
73
|
-
});
|
|
66
|
+
const res = await fetch(endpoint, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify(payload),
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
});
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
clearTimeout(timeout);
|
|
74
|
+
if (res.ok || res.status < 500) return true; // 4xx = don't retry
|
|
75
|
+
} catch { /* expected: network may be unavailable */
|
|
76
|
+
// Network error or timeout — retry if attempts remain
|
|
77
|
+
if (attempt < 3) {
|
|
78
|
+
await new Promise(r => setTimeout(r, attempt * 500)); // 500ms, 1000ms
|
|
79
|
+
}
|
|
80
|
+
}
|
|
80
81
|
}
|
|
82
|
+
return false;
|
|
81
83
|
}
|
|
@@ -173,7 +173,7 @@ export async function isProviderAvailable(name) {
|
|
|
173
173
|
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
174
174
|
execFileSync(cmd, [provider.command], { stdio: 'ignore' });
|
|
175
175
|
return true;
|
|
176
|
-
} catch {
|
|
176
|
+
} catch { /* expected: git command may fail */
|
|
177
177
|
return false;
|
|
178
178
|
}
|
|
179
179
|
}
|
|
@@ -160,8 +160,8 @@ export function buildAgentPrompt(config) {
|
|
|
160
160
|
if (gotchasSection) {
|
|
161
161
|
sections.push('<!-- GOTCHAS -->\n' + gotchasSection);
|
|
162
162
|
}
|
|
163
|
-
} catch {
|
|
164
|
-
|
|
163
|
+
} catch (err) {
|
|
164
|
+
process.stderr.write(`[chati] prompt-builder gotchas: ${err.message}\n`);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// 5. Additional context (user input relay for needs_input)
|
|
@@ -516,7 +516,7 @@ export function loadPreset(stack, projectDir) {
|
|
|
516
516
|
const raw = readFileSync(presetPath, 'utf-8');
|
|
517
517
|
const preset = parsePresetYaml(raw);
|
|
518
518
|
return { loaded: true, preset, stack };
|
|
519
|
-
} catch {
|
|
519
|
+
} catch { /* expected: file may not exist */
|
|
520
520
|
continue;
|
|
521
521
|
}
|
|
522
522
|
}
|
|
@@ -549,7 +549,7 @@ export function detectPreset(projectDir) {
|
|
|
549
549
|
let presetFiles;
|
|
550
550
|
try {
|
|
551
551
|
presetFiles = readdirSync(presetsDir).filter(f => f.endsWith('.yaml'));
|
|
552
|
-
} catch {
|
|
552
|
+
} catch { /* expected: directory may not exist */
|
|
553
553
|
return null;
|
|
554
554
|
}
|
|
555
555
|
|
|
@@ -708,10 +708,7 @@ function buildOutputInstructions(config) {
|
|
|
708
708
|
const modelValue = config?.model || 'unknown';
|
|
709
709
|
|
|
710
710
|
return `<!-- OUTPUT INSTRUCTIONS -->
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
When you complete your work, you MUST include a structured handoff block at the END of your response.
|
|
714
|
-
This block is how the orchestrator reads your results. Without it, your work cannot be collected.
|
|
711
|
+
At the END of your response, include this handoff block:
|
|
715
712
|
|
|
716
713
|
\`\`\`
|
|
717
714
|
<chati-handoff>
|
|
@@ -719,29 +716,16 @@ status: complete
|
|
|
719
716
|
score: 95
|
|
720
717
|
provider: ${providerValue}
|
|
721
718
|
model: ${modelValue}
|
|
722
|
-
summary: One to three sentence summary
|
|
719
|
+
summary: One to three sentence summary.
|
|
723
720
|
outputs:
|
|
724
|
-
- path/to/
|
|
725
|
-
- path/to/artifact2.md
|
|
721
|
+
- path/to/artifact.md
|
|
726
722
|
decisions:
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
blockers:
|
|
730
|
-
- Description of any unresolved blocker
|
|
723
|
+
key: value
|
|
724
|
+
blockers: []
|
|
731
725
|
needs_input_question: null
|
|
732
726
|
</chati-handoff>
|
|
733
727
|
\`\`\`
|
|
734
728
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
- **partial**: Some work done but not all criteria met
|
|
738
|
-
- **needs_input**: You need information from the user to continue. Set \`needs_input_question\` to your question.
|
|
739
|
-
- **error**: Something went wrong that you cannot recover from
|
|
740
|
-
|
|
741
|
-
### Important:
|
|
742
|
-
- The \`<chati-handoff>\` block MUST appear in your response
|
|
743
|
-
- Score must be 0-100 (95+ to pass quality gate)
|
|
744
|
-
- List ALL artifacts you created/modified in outputs
|
|
745
|
-
- The \`provider\` and \`model\` fields MUST match: provider: ${providerValue}, model: ${modelValue}
|
|
746
|
-
- If you need user input, set status to "needs_input" and write your question in needs_input_question`;
|
|
729
|
+
Status: complete (score >= 95) | partial | needs_input (set question) | error
|
|
730
|
+
List ALL created/modified artifacts in outputs. Provider: ${providerValue}, model: ${modelValue}.`;
|
|
747
731
|
}
|
|
@@ -19,7 +19,6 @@ export const DEFAULT_LIMITS = {
|
|
|
19
19
|
claude: 20,
|
|
20
20
|
gemini: 15,
|
|
21
21
|
codex: 10,
|
|
22
|
-
copilot: 10,
|
|
23
22
|
};
|
|
24
23
|
|
|
25
24
|
/** Sliding window size in milliseconds (1 minute). */
|
|
@@ -41,7 +40,7 @@ const WINDOW_MS = 60_000;
|
|
|
41
40
|
/**
|
|
42
41
|
* Create a rate limiter instance for a specific provider.
|
|
43
42
|
*
|
|
44
|
-
* @param {string} provider - Provider name (claude, gemini, codex
|
|
43
|
+
* @param {string} provider - Provider name (claude, gemini, codex)
|
|
45
44
|
* @param {{ limit?: number }} [customLimits={}]
|
|
46
45
|
* @returns {{ canSpawn: () => boolean, recordSpawn: () => void, waitForSlot: () => Promise<void>, getStats: () => RateLimiterStats, reset: () => void }}
|
|
47
46
|
*/
|
|
@@ -78,8 +78,8 @@ async function main() {
|
|
|
78
78
|
const raw = readFileSync(sessionPath, 'utf-8');
|
|
79
79
|
sessionState = parseSimpleYaml(raw);
|
|
80
80
|
}
|
|
81
|
-
} catch {
|
|
82
|
-
|
|
81
|
+
} catch (err) {
|
|
82
|
+
process.stderr.write(`[chati] run-agent session-parse: ${err.message}\n`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Build the agent prompt
|
|
@@ -248,7 +248,7 @@ async function flushAndSend(projectDir) {
|
|
|
248
248
|
try {
|
|
249
249
|
const tConfig = getTelemetryConfig(projectDir);
|
|
250
250
|
await sendEvents(events, { ...tConfig, version: tConfig.chatiVersion || 'unknown' });
|
|
251
|
-
} catch {
|
|
251
|
+
} catch { /* expected: operation may fail gracefully */
|
|
252
252
|
// Silently fail — telemetry must never block agent execution
|
|
253
253
|
}
|
|
254
254
|
}
|
package/src/terminal/spawner.js
CHANGED
|
@@ -23,8 +23,15 @@ export const DEFAULT_CONCURRENCY = 3;
|
|
|
23
23
|
export const TRANSIENT_PATTERNS = [
|
|
24
24
|
/rate limit/i, /too many requests/i, /429/, /503/,
|
|
25
25
|
/timeout/i, /ECONNRESET/, /ECONNREFUSED/,
|
|
26
|
+
/529/, /overloaded/i, /capacity/i,
|
|
26
27
|
];
|
|
27
28
|
|
|
29
|
+
/** Model downgrade map for fallback on overload errors. */
|
|
30
|
+
const MODEL_DOWNGRADE_MAP = { opus: 'sonnet', pro: 'flash' };
|
|
31
|
+
|
|
32
|
+
/** Patterns that indicate overload (triggers model fallback). */
|
|
33
|
+
const OVERLOAD_PATTERNS = [/529/, /overloaded/i, /capacity/i, /too many requests/i];
|
|
34
|
+
|
|
28
35
|
// ---------------------------------------------------------------------------
|
|
29
36
|
// Helpers
|
|
30
37
|
// ---------------------------------------------------------------------------
|
|
@@ -156,7 +163,8 @@ export function buildSpawnCommand(config) {
|
|
|
156
163
|
if (config.contextPayload) {
|
|
157
164
|
try {
|
|
158
165
|
env.CHATI_CONTEXT = JSON.stringify(config.contextPayload);
|
|
159
|
-
} catch {
|
|
166
|
+
} catch (err) {
|
|
167
|
+
process.stderr.write(`[chati] spawner context-serialize: ${err.message}\n`);
|
|
160
168
|
env.CHATI_CONTEXT = '{}';
|
|
161
169
|
}
|
|
162
170
|
}
|
|
@@ -431,7 +439,7 @@ export function killTerminal(handle) {
|
|
|
431
439
|
const forceKillTimer = setTimeout(() => {
|
|
432
440
|
try {
|
|
433
441
|
handle.process.kill('SIGKILL');
|
|
434
|
-
} catch {
|
|
442
|
+
} catch { /* expected: process may already be dead */
|
|
435
443
|
// already dead -- ignore
|
|
436
444
|
}
|
|
437
445
|
}, 5000);
|
|
@@ -445,7 +453,7 @@ export function killTerminal(handle) {
|
|
|
445
453
|
|
|
446
454
|
try {
|
|
447
455
|
handle.process.kill('SIGTERM');
|
|
448
|
-
} catch {
|
|
456
|
+
} catch { /* expected: process may already be dead */
|
|
449
457
|
clearTimeout(forceKillTimer);
|
|
450
458
|
handle.status = 'killed';
|
|
451
459
|
resolve({ killed: false, exitCode: handle.exitCode });
|
|
@@ -509,6 +517,7 @@ export async function spawnTerminalWithRetry(config, retryOptions = {}) {
|
|
|
509
517
|
const maxRetries = retryOptions.maxRetries ?? 2;
|
|
510
518
|
const baseDelay = retryOptions.baseDelay ?? 2000;
|
|
511
519
|
const shouldRetry = retryOptions.shouldRetry || isTransientFailure;
|
|
520
|
+
const enableModelFallback = retryOptions.enableModelFallback ?? true;
|
|
512
521
|
|
|
513
522
|
let lastHandle = null;
|
|
514
523
|
|
|
@@ -536,7 +545,35 @@ export async function spawnTerminalWithRetry(config, retryOptions = {}) {
|
|
|
536
545
|
}
|
|
537
546
|
|
|
538
547
|
// Non-transient or out of retries — return as-is
|
|
539
|
-
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Model fallback: if all retries exhausted with overload errors, try with downgraded model
|
|
552
|
+
if (enableModelFallback && lastHandle && lastHandle.exitCode !== 0) {
|
|
553
|
+
const stderrStr = Array.isArray(lastHandle.stderr) ? lastHandle.stderr.join('') : (lastHandle.stderr || '');
|
|
554
|
+
const isOverload = OVERLOAD_PATTERNS.some(p => p.test(stderrStr));
|
|
555
|
+
const fallbackModel = MODEL_DOWNGRADE_MAP[config.model];
|
|
556
|
+
|
|
557
|
+
if (isOverload && fallbackModel) {
|
|
558
|
+
const fallbackConfig = { ...config, model: fallbackModel };
|
|
559
|
+
const fallbackHandle = spawnTerminal(fallbackConfig);
|
|
560
|
+
|
|
561
|
+
await new Promise((resolve) => {
|
|
562
|
+
if (!fallbackHandle.process) { resolve(); return; }
|
|
563
|
+
if (fallbackHandle.status !== 'running') { resolve(); return; }
|
|
564
|
+
fallbackHandle.process.once('exit', () => resolve());
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Record fallback metadata
|
|
568
|
+
fallbackHandle.modelFallback = {
|
|
569
|
+
original: config.model,
|
|
570
|
+
actual: fallbackModel,
|
|
571
|
+
reason: 'overload_exhausted_retries',
|
|
572
|
+
timestamp: new Date().toISOString(),
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
return fallbackHandle;
|
|
576
|
+
}
|
|
540
577
|
}
|
|
541
578
|
|
|
542
579
|
return lastHandle;
|
package/src/upgrade/checker.js
CHANGED
|
@@ -13,7 +13,7 @@ export function getCurrentVersion(targetDir) {
|
|
|
13
13
|
try {
|
|
14
14
|
const config = yaml.load(readFileSync(configPath, 'utf-8'));
|
|
15
15
|
return config?.version || null;
|
|
16
|
-
} catch {
|
|
16
|
+
} catch { /* expected: file may not exist */
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -82,7 +82,7 @@ export function updateConfigVersion(targetDir, newVersion) {
|
|
|
82
82
|
config.version = newVersion;
|
|
83
83
|
writeFileSync(configPath, yaml.dump(config, { lineWidth: -1 }), 'utf-8');
|
|
84
84
|
return true;
|
|
85
|
-
} catch {
|
|
85
|
+
} catch { /* expected: file may not exist */
|
|
86
86
|
return false;
|
|
87
87
|
}
|
|
88
88
|
}
|
package/src/upgrade/migrator.js
CHANGED