llm-cli-gateway 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/CHANGELOG.md +541 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/approval-manager.d.ts +43 -0
- package/dist/approval-manager.js +156 -0
- package/dist/async-job-manager.d.ts +57 -0
- package/dist/async-job-manager.js +334 -0
- package/dist/claude-mcp-config.d.ts +8 -0
- package/dist/claude-mcp-config.js +161 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +56 -0
- package/dist/db.d.ts +48 -0
- package/dist/db.js +170 -0
- package/dist/executor.d.ts +30 -0
- package/dist/executor.js +315 -0
- package/dist/health.d.ts +20 -0
- package/dist/health.js +32 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +1503 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +5 -0
- package/dist/metrics.d.ts +23 -0
- package/dist/metrics.js +57 -0
- package/dist/migrate-sessions.d.ts +12 -0
- package/dist/migrate-sessions.js +145 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +100 -0
- package/dist/model-registry.d.ts +10 -0
- package/dist/model-registry.js +346 -0
- package/dist/optimizer.d.ts +3 -0
- package/dist/optimizer.js +183 -0
- package/dist/process-monitor.d.ts +54 -0
- package/dist/process-monitor.js +146 -0
- package/dist/request-helpers.d.ts +25 -0
- package/dist/request-helpers.js +32 -0
- package/dist/resources.d.ts +26 -0
- package/dist/resources.js +201 -0
- package/dist/retry.d.ts +72 -0
- package/dist/retry.js +146 -0
- package/dist/review-integrity.d.ts +50 -0
- package/dist/review-integrity.js +283 -0
- package/dist/session-manager-pg.d.ts +76 -0
- package/dist/session-manager-pg.js +383 -0
- package/dist/session-manager.d.ts +62 -0
- package/dist/session-manager.js +223 -0
- package/dist/stream-json-parser.d.ts +35 -0
- package/dist/stream-json-parser.js +94 -0
- package/package.json +90 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Integrity Bypass Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects when orchestrating agents neuter the multi-LLM review process by:
|
|
5
|
+
* - Embedding tool-suppression language in review prompts
|
|
6
|
+
* - Inlining full code instead of letting reviewers read files directly
|
|
7
|
+
* - Setting allowedTools:[] to strip tool access from reviewers
|
|
8
|
+
*
|
|
9
|
+
* Two-gate design: violations only emitted when BOTH review context AND
|
|
10
|
+
* a restriction are detected. This avoids false positives on non-review
|
|
11
|
+
* prompts that happen to contain similar language.
|
|
12
|
+
*/
|
|
13
|
+
export interface ReviewIntegrityViolation {
|
|
14
|
+
type: "tool_suppression" | "inlined_code" | "empty_allowed_tools" | "critical_tools_disallowed";
|
|
15
|
+
score: number;
|
|
16
|
+
detail: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ReviewIntegrityResult {
|
|
19
|
+
isReviewContext: boolean;
|
|
20
|
+
violations: ReviewIntegrityViolation[];
|
|
21
|
+
totalScore: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Detect whether the prompt is a review/audit context.
|
|
25
|
+
* Uses two-part detection: unambiguous phrases match alone,
|
|
26
|
+
* ambiguous verbs (review, analyze, etc.) require a code anchor.
|
|
27
|
+
* Normalizes Unicode before matching to prevent confusable bypasses.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isReviewContext(prompt: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Detect tool-suppression language in a prompt.
|
|
32
|
+
* Returns the matched patterns for diagnostics.
|
|
33
|
+
*/
|
|
34
|
+
export declare function detectToolSuppression(prompt: string): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Detect inlined code blocks that look like full file dumps.
|
|
37
|
+
* Two detection strategies:
|
|
38
|
+
* 1. Any single code block with 200+ chars is flagged.
|
|
39
|
+
* 2. Fallback: if total chars across ALL code blocks (even small ones)
|
|
40
|
+
* exceeds 1000, flag to catch split-block bypass attempts.
|
|
41
|
+
*/
|
|
42
|
+
export declare function detectInlinedCode(prompt: string): {
|
|
43
|
+
count: number;
|
|
44
|
+
totalChars: number;
|
|
45
|
+
};
|
|
46
|
+
export declare function checkReviewIntegrity(params: {
|
|
47
|
+
prompt: string;
|
|
48
|
+
allowedTools?: string[];
|
|
49
|
+
disallowedTools?: string[];
|
|
50
|
+
}): ReviewIntegrityResult;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Integrity Bypass Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects when orchestrating agents neuter the multi-LLM review process by:
|
|
5
|
+
* - Embedding tool-suppression language in review prompts
|
|
6
|
+
* - Inlining full code instead of letting reviewers read files directly
|
|
7
|
+
* - Setting allowedTools:[] to strip tool access from reviewers
|
|
8
|
+
*
|
|
9
|
+
* Two-gate design: violations only emitted when BOTH review context AND
|
|
10
|
+
* a restriction are detected. This avoids false positives on non-review
|
|
11
|
+
* prompts that happen to contain similar language.
|
|
12
|
+
*/
|
|
13
|
+
// Two-part review context detection: a REVIEW_ACTION verb/phrase + a CODE_ANCHOR
|
|
14
|
+
// in the same prompt. This avoids false positives like "Analyze customer feedback"
|
|
15
|
+
// (has action but no code anchor) while catching "Analyze the implementation" (has both).
|
|
16
|
+
//
|
|
17
|
+
// Unambiguous multi-word phrases (code review, security audit, etc.) match on
|
|
18
|
+
// their own without needing a separate code anchor.
|
|
19
|
+
// Phrases that are unambiguously code-review context on their own:
|
|
20
|
+
const UNAMBIGUOUS_REVIEW = /\b(code\s*review|security\s*audit|security\s*review|security\s*(?:vulnerabilit(?:y|ies)|scan|assessment)|bug\s*finding|quality\s*analysis|code\s*quality|code\s*audit|code\s*inspection|static\s*analysis|penetration\s*test(?:ing)?|threat\s*model|owasp|pentest|red[- ]?team|backdoor|exploitab(?:le|ility)|vulnerabilit(?:y|ies)|defects?|flaws?|weakness(?:es)?)\b/i;
|
|
21
|
+
// Broad review-action verbs that need a code anchor to confirm context:
|
|
22
|
+
const REVIEW_ACTIONS = /\b(review|audit|analyze|inspect|examine|assess|evaluate|verify|validate|triage|hunt|vet(?:ting)?|probe|diagnos(?:e|tics?)|find\s*(?:bugs?|issues?|defects?|flaws?|attack\s*(?:surface|path|vector)s?)|check\s*(?:for\s+)?(?:bugs?|issues?|errors?|problems?|defects?)|look\s*over|scan\s*(?:for|the))\b/i;
|
|
23
|
+
// Code-related anchor words that confirm the prompt is about software.
|
|
24
|
+
// Excludes ambiguous words (service, session, controller, route) that appear in non-code contexts.
|
|
25
|
+
const CODE_ANCHORS = /\b(code|source|implementation|function|method|class|module|component|files?|patch|diff|commit|PR|pull\s*request|API|endpoint|auth|parser|codebase|repositor(?:y|ies)|repo|src|\.ts|\.js|\.py|\.go|\.rs|\.java|error\s*handling|middleware|handler|test\s*suite|retry|database|query|schema|config)\b/i;
|
|
26
|
+
/**
|
|
27
|
+
* Detect whether the prompt is a review/audit context.
|
|
28
|
+
* Uses two-part detection: unambiguous phrases match alone,
|
|
29
|
+
* ambiguous verbs (review, analyze, etc.) require a code anchor.
|
|
30
|
+
* Normalizes Unicode before matching to prevent confusable bypasses.
|
|
31
|
+
*/
|
|
32
|
+
export function isReviewContext(prompt) {
|
|
33
|
+
const normalized = normalizeForMatching(prompt);
|
|
34
|
+
if (UNAMBIGUOUS_REVIEW.test(normalized))
|
|
35
|
+
return true;
|
|
36
|
+
return REVIEW_ACTIONS.test(normalized) && CODE_ANCHORS.test(normalized);
|
|
37
|
+
}
|
|
38
|
+
// Normalize text for matching: NFKD decomposition to fold compatibility characters AND
|
|
39
|
+
// decompose precomposed diacritics, then strip combining marks and confusables.
|
|
40
|
+
function normalizeForMatching(text) {
|
|
41
|
+
return text
|
|
42
|
+
// NFKD: decomposes compatibility chars AND precomposed diacritics (é → e + U+0301)
|
|
43
|
+
.normalize("NFKD")
|
|
44
|
+
// Strip combining marks (diacritics): é (e + U+0301), n̸ (n + U+0338), etc.
|
|
45
|
+
// Must happen AFTER NFKD decomposition so precomposed characters are split first.
|
|
46
|
+
.replace(/[\u0300-\u036F]/g, "")
|
|
47
|
+
// Strip invisible Unicode format characters (zero-width joiners, soft hyphens, etc.)
|
|
48
|
+
.replace(/[\u200B-\u200F\u2028-\u202F\u2060-\u206F\uFEFF\u00AD]/g, "")
|
|
49
|
+
.replace(/[\u2018\u2019\u0060\u00B4]/g, "'")
|
|
50
|
+
.replace(/[\u201C\u201D]/g, '"')
|
|
51
|
+
// Fold common Cyrillic confusables that survive NFKC (visually identical to Latin)
|
|
52
|
+
.replace(/\u0430/g, "a") // а → a
|
|
53
|
+
.replace(/\u0435/g, "e") // е → e
|
|
54
|
+
.replace(/\u043E/g, "o") // о → o
|
|
55
|
+
.replace(/\u0440/g, "p") // р → p
|
|
56
|
+
.replace(/\u0441/g, "c") // с → c
|
|
57
|
+
.replace(/\u0445/g, "x") // х → x
|
|
58
|
+
.replace(/\u0456/g, "i") // і → i (Cyrillic i)
|
|
59
|
+
.replace(/\u0410/g, "A") // А → A
|
|
60
|
+
.replace(/\u0415/g, "E") // Е → E
|
|
61
|
+
.replace(/\u041E/g, "O") // О → O
|
|
62
|
+
.replace(/\u0420/g, "P") // Р → P
|
|
63
|
+
.replace(/\u0421/g, "C") // С → C
|
|
64
|
+
.replace(/\u0425/g, "X") // Х → X
|
|
65
|
+
// Fold common Greek confusables (visually identical to Latin)
|
|
66
|
+
.replace(/\u03BF/g, "o") // ο → o (Greek omicron)
|
|
67
|
+
.replace(/\u03C5/g, "u") // υ → u (Greek upsilon)
|
|
68
|
+
.replace(/\u03BD/g, "v") // ν → v (Greek nu)
|
|
69
|
+
.replace(/\u03B1/g, "a") // α → a (Greek alpha)
|
|
70
|
+
.replace(/\u03B5/g, "e") // ε → e (Greek epsilon)
|
|
71
|
+
.replace(/\u03B9/g, "i") // ι → i (Greek iota)
|
|
72
|
+
.replace(/\u03BA/g, "k") // κ → k (Greek kappa)
|
|
73
|
+
.replace(/\u03C1/g, "p") // ρ → p (Greek rho)
|
|
74
|
+
.replace(/\u039F/g, "O") // Ο → O (Greek capital omicron)
|
|
75
|
+
.replace(/\u0391/g, "A") // Α → A (Greek capital alpha)
|
|
76
|
+
.replace(/\u0395/g, "E") // Ε → E (Greek capital epsilon)
|
|
77
|
+
.replace(/\u0399/g, "I") // Ι → I (Greek capital iota)
|
|
78
|
+
.replace(/\u039A/g, "K") // Κ → K (Greek capital kappa)
|
|
79
|
+
// Fold Latin small capitals and modifier letters (used in visual spoofing)
|
|
80
|
+
.replace(/\u1D0F/g, "o") // ᴏ → o (Latin small capital O)
|
|
81
|
+
.replace(/\u1D20/g, "v") // ᴠ → v (Latin small capital V)
|
|
82
|
+
.replace(/\u1D00/g, "a") // ᴀ → a (Latin small capital A)
|
|
83
|
+
.replace(/\u1D04/g, "c") // ᴄ → c (Latin small capital C)
|
|
84
|
+
.replace(/\u1D07/g, "e") // ᴇ → e (Latin small capital E)
|
|
85
|
+
.replace(/\u026A/g, "i") // ɪ → i (Latin small capital I)
|
|
86
|
+
.replace(/\u0280/g, "r"); // ʀ → r (Latin small capital R)
|
|
87
|
+
}
|
|
88
|
+
// Patterns that combine negation with tool/command references.
|
|
89
|
+
// Each pattern requires a negation word near a tool-related action.
|
|
90
|
+
// Tolerates punctuation and intervening clauses between negation and tool noun.
|
|
91
|
+
// (?:[\w,]+\s+){0,6} allows up to 6 intervening words/commas for punctuation-separated negations.
|
|
92
|
+
const TOOL_SUPPRESSION_PATTERNS = [
|
|
93
|
+
/\b(?:do\s+not|don't|never|must\s+not|should\s+not|shouldn't|cannot|can't)\s*,?\s*(?:[\w,]+\s+){0,6}(?:run|use|execute|invoke|call|access)\s+(?:(?:\w+\s+){0,4})(?:tools?|shell\s*commands?|bash|terminal|cli|commands?)\b/i,
|
|
94
|
+
/\b(?:do\s+not|don't|never|must\s+not|should\s+not|shouldn't)\s*,?\s*(?:[\w,]+\s+){0,6}(?:read|open|access|consult)\s+(?:(?:\w+\s+){0,4})(?:files?|the\s+file\s*system|disk|repositor(?:y|ies)\s*files?)\b/i,
|
|
95
|
+
/\bwithout\s+(?:using|running|executing|accessing)\s+(?:(?:\w+\s+){0,4})(?:tools?|shell\s*commands?|external)\b/i,
|
|
96
|
+
/\b(?:respond|answer|analyze|reply)\s+(?:only|solely|exclusively)\s+(?:based\s+on|from|using)\s+(?:the\s+)?(?:code|context|information|text)\s+(?:provided|given|above|below)\b/i,
|
|
97
|
+
/\bno\s+(?:tool|shell|file|command|filesystem)\s+(?:access|usage|calls?|execution)\b/i,
|
|
98
|
+
// Specific tool-name suppression: "Do not use Read or Grep", "never call Bash"
|
|
99
|
+
// Case-sensitive for tool identifiers to avoid false positives like "read replicas"
|
|
100
|
+
/\b(?:[Dd]o\s+not|[Dd]on't|[Nn]ever|[Mm]ust\s+not|[Ss]hould\s+not|[Ss]houldn't|[Cc]annot|[Cc]an't)\s+(?:run|use|execute|invoke|call|access)\s+(?:Read|Grep|Glob|Bash|Write|Edit)\b/,
|
|
101
|
+
// "avoid/refrain from using tools" or "avoid opening files"
|
|
102
|
+
/\b(?:avoid|refrain\s+from)\s+(?:using|running|executing|accessing|calling|opening)\s+(?:(?:\w+\s+){0,4})(?:tools?|shell\s*commands?|bash|terminal|cli|commands?|external|files?|additional\s+files?)\b/i,
|
|
103
|
+
// Standalone "no tools" — bare denial of tool access
|
|
104
|
+
/\bno\s+tools\b/i,
|
|
105
|
+
// "base your answer on this diff/snippet/code only"
|
|
106
|
+
/\b(?:base|ground)\s+(?:your\s+)?(?:answer|response|analysis|review|conclusions?)\s+(?:on|upon)\s+(?:this|the)\s+(?:diff|snippet|code|patch|context|excerpt)\s+(?:only|alone|exclusively)\b/i,
|
|
107
|
+
// "use reasoning/analysis only" (from context, not tools)
|
|
108
|
+
/\buse\s+(?:only\s+)?(?:reasoning|analysis|your\s+judgment)\s+(?:only\s+)?(?:from|based\s+on)\s+(?:the\s+)?(?:snippet|diff|code|context|patch)\b/i,
|
|
109
|
+
// "work offline" / "do not call external resources"
|
|
110
|
+
/\bwork\s+offline\b/i,
|
|
111
|
+
// "self-contained" / "snippet only" / "sole source of truth"
|
|
112
|
+
/\b(?:self[- ]contained|snippet[- ]only|sole\s+source\s+of\s+truth)\b/i,
|
|
113
|
+
// "keep analysis to/within this snippet/excerpt/diff"
|
|
114
|
+
/\b(?:keep|restrict|limit|confine)\s+(?:the\s+)?(?:analysis|review|response|yourself)\s+(?:to|within)\s+(?:this|the)\s+(?:snippet|excerpt|diff|patch|code|context|text)\b/i,
|
|
115
|
+
// "tool access is unavailable/disabled/restricted"
|
|
116
|
+
/\btool\s+access\s+(?:is\s+)?(?:unavailable|disabled|restricted|not\s+available)\b/i,
|
|
117
|
+
// "use only what is shown/provided/pasted"
|
|
118
|
+
/\buse\s+only\s+(?:what\s+is\s+)?(?:shown|provided|pasted|given|included)\b/i,
|
|
119
|
+
// "no need to execute/run/access"
|
|
120
|
+
/\bno\s+need\s+to\s+(?:execute|run|access|open|read)\b/i,
|
|
121
|
+
];
|
|
122
|
+
/**
|
|
123
|
+
* Detect tool-suppression language in a prompt.
|
|
124
|
+
* Returns the matched patterns for diagnostics.
|
|
125
|
+
*/
|
|
126
|
+
export function detectToolSuppression(prompt) {
|
|
127
|
+
const normalized = normalizeForMatching(prompt);
|
|
128
|
+
const matches = [];
|
|
129
|
+
for (const pattern of TOOL_SUPPRESSION_PATTERNS) {
|
|
130
|
+
const match = normalized.match(pattern);
|
|
131
|
+
if (match) {
|
|
132
|
+
matches.push(match[0]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return matches;
|
|
136
|
+
}
|
|
137
|
+
// Note: <code[^>]*> already matches code inside <pre><code> blocks,
|
|
138
|
+
// so a separate <pre><code> pattern is not needed (would double-count).
|
|
139
|
+
// Case-insensitive for <CODE>/<PRE> tags. Fence regex uses backreference for matched opener/closer.
|
|
140
|
+
const INLINED_CODE_PATTERNS = [
|
|
141
|
+
/<code[^>]*>([\s\S]*?)<\/code>/gi,
|
|
142
|
+
// Standalone <pre> blocks that don't contain <code> (avoids double-counting <pre><code>)
|
|
143
|
+
/<pre[^>]*>(?!\s*<code)([\s\S]*?)<\/pre>/gi,
|
|
144
|
+
// Multi-line backtick fences: opener and closer must use same number of backticks
|
|
145
|
+
/(`{3,})[^\n]*\r?\n([\s\S]*?)\1/g,
|
|
146
|
+
// Multi-line tilde fences
|
|
147
|
+
/(~{3,})[^\n]*\r?\n([\s\S]*?)\1/g,
|
|
148
|
+
// Single-line backtick fences: ```<content>``` on one line
|
|
149
|
+
/`{3,}[^\n`]*`{3,}/g,
|
|
150
|
+
];
|
|
151
|
+
// Group index for captured content differs per pattern:
|
|
152
|
+
// HTML code: group 1; pre: group 1; backtick/tilde multi-line: group 2; single-line: group 0 (full match)
|
|
153
|
+
const INLINED_CODE_CONTENT_GROUPS = [1, 1, 2, 2, 0];
|
|
154
|
+
const INLINED_CODE_MIN_LENGTH = 200;
|
|
155
|
+
const INLINED_CODE_TOTAL_THRESHOLD = 1000;
|
|
156
|
+
// Heuristic for detecting raw code pasted without fences or tags.
|
|
157
|
+
// Multi-language token pattern: JS/TS + Rust + Python + Go + Java + C/C++.
|
|
158
|
+
// Word-boundary tokens use \b; symbol tokens match without \b.
|
|
159
|
+
const RAW_CODE_TOKEN_PATTERN = /(?:\b(?:import|export|from|require|function|const|let|var|class|interface|type|return|if|else|for|while|switch|case|try|catch|throw|async|await|new|this|fn|impl|pub|struct|match|mod|use|crate|mut|enum|trait|unsafe|def|elif|lambda|yield|pass|with|raise|except|func|package|defer|goroutine|chan|select|void|static|final|abstract|extends|implements|override|sizeof|template|namespace|include|typedef|printf|println)\b|=>|===|!==|[{};])/g;
|
|
160
|
+
const RAW_CODE_MIN_TOKENS = 15;
|
|
161
|
+
const RAW_CODE_DENSITY_THRESHOLD = 1.5; // tokens per 100 chars
|
|
162
|
+
/**
|
|
163
|
+
* Detect inlined code blocks that look like full file dumps.
|
|
164
|
+
* Two detection strategies:
|
|
165
|
+
* 1. Any single code block with 200+ chars is flagged.
|
|
166
|
+
* 2. Fallback: if total chars across ALL code blocks (even small ones)
|
|
167
|
+
* exceeds 1000, flag to catch split-block bypass attempts.
|
|
168
|
+
*/
|
|
169
|
+
export function detectInlinedCode(prompt) {
|
|
170
|
+
let count = 0;
|
|
171
|
+
let totalChars = 0;
|
|
172
|
+
let allBlocksTotal = 0;
|
|
173
|
+
let allBlocksCount = 0;
|
|
174
|
+
for (let i = 0; i < INLINED_CODE_PATTERNS.length; i++) {
|
|
175
|
+
const pattern = INLINED_CODE_PATTERNS[i];
|
|
176
|
+
const contentGroup = INLINED_CODE_CONTENT_GROUPS[i];
|
|
177
|
+
pattern.lastIndex = 0;
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = pattern.exec(prompt)) !== null) {
|
|
180
|
+
const rawContent = contentGroup === 0 ? match[0] : match[contentGroup];
|
|
181
|
+
const content = (rawContent || "").trim();
|
|
182
|
+
allBlocksCount++;
|
|
183
|
+
allBlocksTotal += content.length;
|
|
184
|
+
if (content.length >= INLINED_CODE_MIN_LENGTH) {
|
|
185
|
+
count++;
|
|
186
|
+
totalChars += content.length;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Fallback: catch split-block bypass (many small blocks totaling large payload)
|
|
191
|
+
if (count === 0 && allBlocksTotal >= INLINED_CODE_TOTAL_THRESHOLD) {
|
|
192
|
+
count = allBlocksCount;
|
|
193
|
+
totalChars = allBlocksTotal;
|
|
194
|
+
}
|
|
195
|
+
// Fallback: detect plain-text code dumps (no fences or tags) via code-token density.
|
|
196
|
+
// Only triggers when no fenced/tagged blocks were found and the prompt is large enough.
|
|
197
|
+
if (count === 0 && prompt.length >= INLINED_CODE_TOTAL_THRESHOLD) {
|
|
198
|
+
const codeTokens = prompt.match(RAW_CODE_TOKEN_PATTERN);
|
|
199
|
+
const tokenCount = codeTokens ? codeTokens.length : 0;
|
|
200
|
+
// Require minimum absolute token count AND density ratio (tokens per 100 chars)
|
|
201
|
+
const density = (tokenCount / prompt.length) * 100;
|
|
202
|
+
if (tokenCount >= RAW_CODE_MIN_TOKENS && density >= RAW_CODE_DENSITY_THRESHOLD) {
|
|
203
|
+
count = 1;
|
|
204
|
+
totalChars = prompt.length;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { count, totalChars };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Combined review integrity check. Only emits violations when BOTH
|
|
211
|
+
* review context is detected AND a restriction is present.
|
|
212
|
+
*/
|
|
213
|
+
// Tools that reviewers need to independently verify code claims.
|
|
214
|
+
const CRITICAL_REVIEW_TOOLS = ["Read", "Grep", "Glob", "Bash"];
|
|
215
|
+
// Extract base tool name from scoped/pattern forms like "Read(*)", "Bash(git:*)", "Grep"
|
|
216
|
+
function canonicalizeToolName(spec) {
|
|
217
|
+
const trimmed = spec.trim();
|
|
218
|
+
const parenIdx = trimmed.indexOf("(");
|
|
219
|
+
const colonIdx = trimmed.indexOf(":");
|
|
220
|
+
const cutIdx = parenIdx >= 0 && colonIdx >= 0
|
|
221
|
+
? Math.min(parenIdx, colonIdx)
|
|
222
|
+
: parenIdx >= 0 ? parenIdx : colonIdx >= 0 ? colonIdx : -1;
|
|
223
|
+
return cutIdx >= 0 ? trimmed.slice(0, cutIdx).trim() : trimmed;
|
|
224
|
+
}
|
|
225
|
+
export function checkReviewIntegrity(params) {
|
|
226
|
+
const reviewContext = isReviewContext(params.prompt);
|
|
227
|
+
const result = {
|
|
228
|
+
isReviewContext: reviewContext,
|
|
229
|
+
violations: [],
|
|
230
|
+
totalScore: 0,
|
|
231
|
+
};
|
|
232
|
+
// Gate: no violations emitted for non-review prompts
|
|
233
|
+
if (!reviewContext) {
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
// Check tool suppression language
|
|
237
|
+
const suppressionMatches = detectToolSuppression(params.prompt);
|
|
238
|
+
if (suppressionMatches.length > 0) {
|
|
239
|
+
const violation = {
|
|
240
|
+
type: "tool_suppression",
|
|
241
|
+
score: 4,
|
|
242
|
+
detail: `Prompt contains tool-suppression language in review context: ${suppressionMatches.join("; ")}`,
|
|
243
|
+
};
|
|
244
|
+
result.violations.push(violation);
|
|
245
|
+
result.totalScore += violation.score;
|
|
246
|
+
}
|
|
247
|
+
// Check inlined code
|
|
248
|
+
const inlined = detectInlinedCode(params.prompt);
|
|
249
|
+
if (inlined.count > 0) {
|
|
250
|
+
const violation = {
|
|
251
|
+
type: "inlined_code",
|
|
252
|
+
score: 2,
|
|
253
|
+
detail: `Prompt inlines ${inlined.count} code block(s) (${inlined.totalChars} chars) instead of file paths — reviewers should read files directly`,
|
|
254
|
+
};
|
|
255
|
+
result.violations.push(violation);
|
|
256
|
+
result.totalScore += violation.score;
|
|
257
|
+
}
|
|
258
|
+
// Check empty allowedTools
|
|
259
|
+
if (params.allowedTools && params.allowedTools.length === 0) {
|
|
260
|
+
const violation = {
|
|
261
|
+
type: "empty_allowed_tools",
|
|
262
|
+
score: 4,
|
|
263
|
+
detail: "allowedTools is empty in review context — reviewers need tool access to read files and verify claims",
|
|
264
|
+
};
|
|
265
|
+
result.violations.push(violation);
|
|
266
|
+
result.totalScore += violation.score;
|
|
267
|
+
}
|
|
268
|
+
// Check disallowedTools blocking critical review tools (canonicalize to handle scoped forms like "Read(*)")
|
|
269
|
+
if (params.disallowedTools && params.disallowedTools.length > 0) {
|
|
270
|
+
const canonicalized = params.disallowedTools.map(canonicalizeToolName);
|
|
271
|
+
const blocked = CRITICAL_REVIEW_TOOLS.filter(t => canonicalized.includes(t));
|
|
272
|
+
if (blocked.length > 0) {
|
|
273
|
+
const violation = {
|
|
274
|
+
type: "critical_tools_disallowed",
|
|
275
|
+
score: 4,
|
|
276
|
+
detail: `Critical review tools disallowed: ${blocked.join(", ")} — reviewers need these to verify claims`,
|
|
277
|
+
};
|
|
278
|
+
result.violations.push(violation);
|
|
279
|
+
result.totalScore += violation.score;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import type { Redis } from "ioredis";
|
|
3
|
+
import { Session, CliType } from "./session-manager.js";
|
|
4
|
+
import { CacheTtl } from "./config.js";
|
|
5
|
+
import type { Logger } from "./logger.js";
|
|
6
|
+
export type { Logger } from "./logger.js";
|
|
7
|
+
/**
|
|
8
|
+
* PostgreSQL-backed session manager with Redis caching
|
|
9
|
+
*/
|
|
10
|
+
export declare class PostgreSQLSessionManager {
|
|
11
|
+
private pool;
|
|
12
|
+
private redis;
|
|
13
|
+
private cacheTtl;
|
|
14
|
+
private logger;
|
|
15
|
+
constructor(pool: Pool, redis: Redis, cacheTtl: CacheTtl, logger: Logger);
|
|
16
|
+
/**
|
|
17
|
+
* Acquire distributed lock using Redis SET NX EX
|
|
18
|
+
* Returns [success, lockValue] tuple
|
|
19
|
+
*/
|
|
20
|
+
private acquireLock;
|
|
21
|
+
private sleep;
|
|
22
|
+
/**
|
|
23
|
+
* Acquire a distributed lock with bounded retries to smooth contention spikes.
|
|
24
|
+
*/
|
|
25
|
+
private acquireLockWithRetry;
|
|
26
|
+
/**
|
|
27
|
+
* Release distributed lock using Lua script for atomic compare-and-delete
|
|
28
|
+
* Only releases if lockValue matches (prevents releasing another process's lock)
|
|
29
|
+
*/
|
|
30
|
+
private releaseLock;
|
|
31
|
+
/**
|
|
32
|
+
* Invalidate session cache
|
|
33
|
+
*/
|
|
34
|
+
private invalidateCache;
|
|
35
|
+
/**
|
|
36
|
+
* Invalidate session list cache using SCAN (non-blocking)
|
|
37
|
+
*/
|
|
38
|
+
private invalidateListCache;
|
|
39
|
+
/**
|
|
40
|
+
* Create a new session
|
|
41
|
+
*/
|
|
42
|
+
createSession(cli: CliType, description?: string, sessionId?: string): Promise<Session>;
|
|
43
|
+
/**
|
|
44
|
+
* Get session by ID (cache-aside pattern)
|
|
45
|
+
*/
|
|
46
|
+
getSession(sessionId: string): Promise<Session | null>;
|
|
47
|
+
/**
|
|
48
|
+
* List all sessions, optionally filtered by CLI
|
|
49
|
+
*/
|
|
50
|
+
listSessions(cli?: CliType): Promise<Session[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Delete a session
|
|
53
|
+
*/
|
|
54
|
+
deleteSession(sessionId: string): Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* Set active session for a CLI (with distributed locking)
|
|
57
|
+
*/
|
|
58
|
+
setActiveSession(cli: CliType, sessionId: string | null): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Get active session for a CLI
|
|
61
|
+
*/
|
|
62
|
+
getActiveSession(cli: CliType): Promise<Session | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Update session usage timestamp
|
|
65
|
+
*/
|
|
66
|
+
updateSessionUsage(sessionId: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Update session metadata (atomic JSONB merge)
|
|
69
|
+
*/
|
|
70
|
+
updateSessionMetadata(sessionId: string, metadata: Record<string, any>): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Clear all sessions, optionally filtered by CLI
|
|
73
|
+
* Invalidates all related caches (session, active, list)
|
|
74
|
+
*/
|
|
75
|
+
clearAllSessions(cli?: CliType): Promise<number>;
|
|
76
|
+
}
|