@rigour-labs/core 2.22.0 → 3.0.1
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 +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +3 -1
- package/dist/gates/base.js +3 -0
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/context-window-artifacts.d.ts +2 -1
- package/dist/gates/context-window-artifacts.js +6 -3
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +1 -0
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +2 -1
- package/dist/gates/duplication-drift.js +4 -1
- package/dist/gates/environment.js +4 -4
- package/dist/gates/hallucinated-imports.d.ts +21 -2
- package/dist/gates/hallucinated-imports.js +116 -2
- package/dist/gates/inconsistent-error-handling.d.ts +2 -1
- package/dist/gates/inconsistent-error-handling.js +21 -7
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +34 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
- package/dist/gates/security-patterns-owasp.test.js +171 -0
- package/dist/gates/security-patterns.d.ts +6 -1
- package/dist/gates/security-patterns.js +101 -0
- package/dist/gates/structure.js +1 -1
- package/dist/hooks/checker.d.ts +23 -0
- package/dist/hooks/checker.js +222 -0
- package/dist/hooks/checker.test.d.ts +1 -0
- package/dist/hooks/checker.test.js +132 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/standalone-checker.d.ts +15 -0
- package/dist/hooks/standalone-checker.js +106 -0
- package/dist/hooks/templates.d.ts +22 -0
- package/dist/hooks/templates.js +232 -0
- package/dist/hooks/types.d.ts +34 -0
- package/dist/hooks/types.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +176 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +207 -0
- package/dist/types/index.js +32 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -28
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -51
- package/src/gates/context-window-artifacts.ts +0 -277
- package/src/gates/context.ts +0 -270
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/duplication-drift.ts +0 -231
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -46
- package/src/gates/hallucinated-imports.ts +0 -361
- package/src/gates/inconsistent-error-handling.ts +0 -254
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -188
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -306
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -338
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -200
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { AgentTeamGate, registerAgent, getSession, clearSession } from './agent-team.js';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
|
|
7
|
-
describe('AgentTeamGate', () => {
|
|
8
|
-
let testDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-team-test-'));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
clearSession(testDir);
|
|
16
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('gate initialization', () => {
|
|
20
|
-
it('should create gate with default config', () => {
|
|
21
|
-
const gate = new AgentTeamGate();
|
|
22
|
-
expect(gate.id).toBe('agent-team');
|
|
23
|
-
expect(gate.title).toBe('Agent Team Governance');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should skip when not enabled', async () => {
|
|
27
|
-
const gate = new AgentTeamGate({ enabled: false });
|
|
28
|
-
const failures = await gate.run({ cwd: testDir });
|
|
29
|
-
expect(failures).toEqual([]);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('session management', () => {
|
|
34
|
-
it('should register an agent', () => {
|
|
35
|
-
const session = registerAgent(testDir, 'agent-a', ['src/api/**']);
|
|
36
|
-
expect(session.agents).toHaveLength(1);
|
|
37
|
-
expect(session.agents[0].agentId).toBe('agent-a');
|
|
38
|
-
expect(session.agents[0].taskScope).toEqual(['src/api/**']);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should update existing agent registration', () => {
|
|
42
|
-
registerAgent(testDir, 'agent-a', ['src/api/**']);
|
|
43
|
-
const session = registerAgent(testDir, 'agent-a', ['src/api/**', 'src/utils/**']);
|
|
44
|
-
expect(session.agents).toHaveLength(1);
|
|
45
|
-
expect(session.agents[0].taskScope).toEqual(['src/api/**', 'src/utils/**']);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should persist session to disk', () => {
|
|
49
|
-
registerAgent(testDir, 'agent-a', ['src/**']);
|
|
50
|
-
const sessionPath = path.join(testDir, '.rigour', 'agent-session.json');
|
|
51
|
-
expect(fs.existsSync(sessionPath)).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should load session from disk', () => {
|
|
55
|
-
registerAgent(testDir, 'agent-a', ['src/**']);
|
|
56
|
-
// Clear in-memory cache
|
|
57
|
-
clearSession(testDir);
|
|
58
|
-
// Re-register to re-create session file
|
|
59
|
-
registerAgent(testDir, 'agent-b', ['tests/**']);
|
|
60
|
-
|
|
61
|
-
const session = getSession(testDir);
|
|
62
|
-
expect(session).not.toBeNull();
|
|
63
|
-
expect(session!.agents).toHaveLength(1); // Only agent-b after clearSession
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should clear session', () => {
|
|
67
|
-
registerAgent(testDir, 'agent-a', ['src/**']);
|
|
68
|
-
clearSession(testDir);
|
|
69
|
-
const session = getSession(testDir);
|
|
70
|
-
expect(session).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('max concurrent agents check', () => {
|
|
75
|
-
it('should pass when under limit', async () => {
|
|
76
|
-
const gate = new AgentTeamGate({ enabled: true, max_concurrent_agents: 3 });
|
|
77
|
-
registerAgent(testDir, 'agent-a', ['src/a/**']);
|
|
78
|
-
registerAgent(testDir, 'agent-b', ['src/b/**']);
|
|
79
|
-
|
|
80
|
-
const failures = await gate.run({ cwd: testDir });
|
|
81
|
-
expect(failures).toEqual([]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should fail when over limit', async () => {
|
|
85
|
-
const gate = new AgentTeamGate({ enabled: true, max_concurrent_agents: 2 });
|
|
86
|
-
registerAgent(testDir, 'agent-a', ['src/a/**']);
|
|
87
|
-
registerAgent(testDir, 'agent-b', ['src/b/**']);
|
|
88
|
-
registerAgent(testDir, 'agent-c', ['src/c/**']);
|
|
89
|
-
|
|
90
|
-
const failures = await gate.run({ cwd: testDir });
|
|
91
|
-
expect(failures).toHaveLength(1);
|
|
92
|
-
expect(failures[0].title).toBe('Too Many Concurrent Agents');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('task scope conflicts (strict mode)', () => {
|
|
97
|
-
it('should pass when scopes are disjoint', async () => {
|
|
98
|
-
const gate = new AgentTeamGate({
|
|
99
|
-
enabled: true,
|
|
100
|
-
task_ownership: 'strict'
|
|
101
|
-
});
|
|
102
|
-
registerAgent(testDir, 'agent-a', ['src/api/**']);
|
|
103
|
-
registerAgent(testDir, 'agent-b', ['src/ui/**']);
|
|
104
|
-
|
|
105
|
-
const failures = await gate.run({ cwd: testDir });
|
|
106
|
-
expect(failures).toEqual([]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should fail when scopes overlap', async () => {
|
|
110
|
-
const gate = new AgentTeamGate({
|
|
111
|
-
enabled: true,
|
|
112
|
-
task_ownership: 'strict'
|
|
113
|
-
});
|
|
114
|
-
registerAgent(testDir, 'agent-a', ['src/api/**']);
|
|
115
|
-
registerAgent(testDir, 'agent-b', ['src/api/**']); // Same scope!
|
|
116
|
-
|
|
117
|
-
const failures = await gate.run({ cwd: testDir });
|
|
118
|
-
expect(failures).toHaveLength(1);
|
|
119
|
-
expect(failures[0].title).toBe('Task Scope Conflict');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should allow overlapping scopes in collaborative mode', async () => {
|
|
123
|
-
const gate = new AgentTeamGate({
|
|
124
|
-
enabled: true,
|
|
125
|
-
task_ownership: 'collaborative'
|
|
126
|
-
});
|
|
127
|
-
registerAgent(testDir, 'agent-a', ['src/api/**']);
|
|
128
|
-
registerAgent(testDir, 'agent-b', ['src/api/**']); // Same scope - OK in collaborative
|
|
129
|
-
|
|
130
|
-
const failures = await gate.run({ cwd: testDir });
|
|
131
|
-
expect(failures).toEqual([]);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
});
|
package/src/gates/agent-team.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Team Governance Gate
|
|
3
|
-
*
|
|
4
|
-
* Supervises multi-agent coordination for frontier models like
|
|
5
|
-
* Opus 4.6 (agent teams) and GPT-5.3-Codex (coworking mode).
|
|
6
|
-
*
|
|
7
|
-
* Detects:
|
|
8
|
-
* - Cross-agent pattern conflicts
|
|
9
|
-
* - Task scope violations
|
|
10
|
-
* - Handoff context loss
|
|
11
|
-
*
|
|
12
|
-
* @since v2.14.0
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { Gate, GateContext } from './base.js';
|
|
16
|
-
import { Failure } from '../types/index.js';
|
|
17
|
-
import { Logger } from '../utils/logger.js';
|
|
18
|
-
import * as fs from 'fs';
|
|
19
|
-
import * as path from 'path';
|
|
20
|
-
|
|
21
|
-
export interface AgentRegistration {
|
|
22
|
-
agentId: string;
|
|
23
|
-
taskScope: string[]; // Glob patterns for files this agent owns
|
|
24
|
-
registeredAt: Date;
|
|
25
|
-
lastActivity?: Date;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface AgentTeamSession {
|
|
29
|
-
sessionId: string;
|
|
30
|
-
agents: AgentRegistration[];
|
|
31
|
-
startedAt: Date;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface AgentTeamConfig {
|
|
35
|
-
enabled?: boolean;
|
|
36
|
-
max_concurrent_agents?: number;
|
|
37
|
-
cross_agent_pattern_check?: boolean;
|
|
38
|
-
handoff_verification?: boolean;
|
|
39
|
-
task_ownership?: 'strict' | 'collaborative';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// In-memory session store (persisted to .rigour/agent-session.json)
|
|
43
|
-
let currentSession: AgentTeamSession | null = null;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Register an agent in the current session
|
|
47
|
-
*/
|
|
48
|
-
export function registerAgent(cwd: string, agentId: string, taskScope: string[]): AgentTeamSession {
|
|
49
|
-
if (!currentSession) {
|
|
50
|
-
currentSession = {
|
|
51
|
-
sessionId: `session-${Date.now()}`,
|
|
52
|
-
agents: [],
|
|
53
|
-
startedAt: new Date(),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check if agent already registered
|
|
58
|
-
const existing = currentSession.agents.find(a => a.agentId === agentId);
|
|
59
|
-
if (existing) {
|
|
60
|
-
existing.taskScope = taskScope;
|
|
61
|
-
existing.lastActivity = new Date();
|
|
62
|
-
} else {
|
|
63
|
-
currentSession.agents.push({
|
|
64
|
-
agentId,
|
|
65
|
-
taskScope,
|
|
66
|
-
registeredAt: new Date(),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Persist session
|
|
71
|
-
persistSession(cwd);
|
|
72
|
-
return currentSession;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get current session status
|
|
77
|
-
*/
|
|
78
|
-
export function getSession(cwd: string): AgentTeamSession | null {
|
|
79
|
-
if (!currentSession) {
|
|
80
|
-
loadSession(cwd);
|
|
81
|
-
}
|
|
82
|
-
return currentSession;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Clear current session
|
|
87
|
-
*/
|
|
88
|
-
export function clearSession(cwd: string): void {
|
|
89
|
-
currentSession = null;
|
|
90
|
-
const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
|
|
91
|
-
if (fs.existsSync(sessionPath)) {
|
|
92
|
-
fs.unlinkSync(sessionPath);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function persistSession(cwd: string): void {
|
|
97
|
-
const rigourDir = path.join(cwd, '.rigour');
|
|
98
|
-
if (!fs.existsSync(rigourDir)) {
|
|
99
|
-
fs.mkdirSync(rigourDir, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
const sessionPath = path.join(rigourDir, 'agent-session.json');
|
|
102
|
-
fs.writeFileSync(sessionPath, JSON.stringify(currentSession, null, 2));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function loadSession(cwd: string): void {
|
|
106
|
-
const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
|
|
107
|
-
if (fs.existsSync(sessionPath)) {
|
|
108
|
-
try {
|
|
109
|
-
const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
110
|
-
currentSession = {
|
|
111
|
-
...data,
|
|
112
|
-
startedAt: new Date(data.startedAt),
|
|
113
|
-
agents: data.agents.map((a: any) => ({
|
|
114
|
-
...a,
|
|
115
|
-
registeredAt: new Date(a.registeredAt),
|
|
116
|
-
lastActivity: a.lastActivity ? new Date(a.lastActivity) : undefined,
|
|
117
|
-
})),
|
|
118
|
-
};
|
|
119
|
-
} catch (err) {
|
|
120
|
-
Logger.warn('Failed to load agent session, starting fresh');
|
|
121
|
-
currentSession = null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Check if two glob patterns might overlap
|
|
128
|
-
*/
|
|
129
|
-
function scopesOverlap(scope1: string[], scope2: string[]): string[] {
|
|
130
|
-
const overlapping: string[] = [];
|
|
131
|
-
for (const s1 of scope1) {
|
|
132
|
-
for (const s2 of scope2) {
|
|
133
|
-
// Simple overlap detection - same path or one is prefix of other
|
|
134
|
-
if (s1 === s2 || s1.startsWith(s2.replace('**', '')) || s2.startsWith(s1.replace('**', ''))) {
|
|
135
|
-
overlapping.push(`${s1} ↔ ${s2}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return overlapping;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export class AgentTeamGate extends Gate {
|
|
143
|
-
private config: AgentTeamConfig;
|
|
144
|
-
|
|
145
|
-
constructor(config: AgentTeamConfig = {}) {
|
|
146
|
-
super('agent-team', 'Agent Team Governance');
|
|
147
|
-
this.config = {
|
|
148
|
-
enabled: config.enabled ?? false,
|
|
149
|
-
max_concurrent_agents: config.max_concurrent_agents ?? 3,
|
|
150
|
-
cross_agent_pattern_check: config.cross_agent_pattern_check ?? true,
|
|
151
|
-
handoff_verification: config.handoff_verification ?? true,
|
|
152
|
-
task_ownership: config.task_ownership ?? 'strict',
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
157
|
-
if (!this.config.enabled) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const failures: Failure[] = [];
|
|
162
|
-
const session = getSession(context.cwd);
|
|
163
|
-
|
|
164
|
-
if (!session || session.agents.length === 0) {
|
|
165
|
-
// No multi-agent session active, skip
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
Logger.info(`Agent Team Gate: ${session.agents.length} agents in session`);
|
|
170
|
-
|
|
171
|
-
// Check 1: Max concurrent agents
|
|
172
|
-
if (session.agents.length > (this.config.max_concurrent_agents ?? 3)) {
|
|
173
|
-
failures.push(this.createFailure(
|
|
174
|
-
`Too many concurrent agents: ${session.agents.length} (max: ${this.config.max_concurrent_agents})`,
|
|
175
|
-
undefined,
|
|
176
|
-
'Reduce the number of concurrent agents or increase max_concurrent_agents in rigour.yml',
|
|
177
|
-
'Too Many Concurrent Agents'
|
|
178
|
-
));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Check 2: Task scope conflicts (strict mode only)
|
|
182
|
-
if (this.config.task_ownership === 'strict') {
|
|
183
|
-
for (let i = 0; i < session.agents.length; i++) {
|
|
184
|
-
for (let j = i + 1; j < session.agents.length; j++) {
|
|
185
|
-
const agent1 = session.agents[i];
|
|
186
|
-
const agent2 = session.agents[j];
|
|
187
|
-
const overlaps = scopesOverlap(agent1.taskScope, agent2.taskScope);
|
|
188
|
-
|
|
189
|
-
if (overlaps.length > 0) {
|
|
190
|
-
failures.push(this.createFailure(
|
|
191
|
-
`Task scope conflict between ${agent1.agentId} and ${agent2.agentId}: ${overlaps.join(', ')}`,
|
|
192
|
-
undefined,
|
|
193
|
-
'In strict mode, each agent must have exclusive task scope. Either adjust scopes or set task_ownership: collaborative',
|
|
194
|
-
'Task Scope Conflict'
|
|
195
|
-
));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check 3: Cross-agent pattern detection (if enabled)
|
|
202
|
-
if (this.config.cross_agent_pattern_check && context.record) {
|
|
203
|
-
// This would integrate with the Pattern Index to detect conflicting patterns
|
|
204
|
-
// For now, we log that we would do this check
|
|
205
|
-
Logger.debug('Cross-agent pattern check: would analyze patterns across agent scopes');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return failures;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Failure, Gates } from '../../types/index.js';
|
|
2
|
-
|
|
3
|
-
export interface ASTHandlerContext {
|
|
4
|
-
cwd: string;
|
|
5
|
-
file: string;
|
|
6
|
-
content: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export abstract class ASTHandler {
|
|
10
|
-
constructor(protected config: Gates) { }
|
|
11
|
-
abstract supports(file: string): boolean;
|
|
12
|
-
abstract run(context: ASTHandlerContext): Promise<Failure[]>;
|
|
13
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { execa } from 'execa';
|
|
2
|
-
import { ASTHandler, ASTHandlerContext } from './base.js';
|
|
3
|
-
import { Failure } from '../../types/index.js';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
-
interface SecurityIssue {
|
|
10
|
-
type: string;
|
|
11
|
-
issue: string;
|
|
12
|
-
name: string;
|
|
13
|
-
lineno: number;
|
|
14
|
-
message: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface MetricItem {
|
|
18
|
-
type: string;
|
|
19
|
-
name: string;
|
|
20
|
-
complexity?: number;
|
|
21
|
-
parameters?: number;
|
|
22
|
-
methods?: number;
|
|
23
|
-
lineno: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface PythonAnalysisResult {
|
|
27
|
-
metrics?: MetricItem[];
|
|
28
|
-
security?: SecurityIssue[];
|
|
29
|
-
error?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class PythonHandler extends ASTHandler {
|
|
33
|
-
supports(file: string): boolean {
|
|
34
|
-
return /\.py$/.test(file);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async run(context: ASTHandlerContext): Promise<Failure[]> {
|
|
38
|
-
const failures: Failure[] = [];
|
|
39
|
-
const scriptPath = path.join(__dirname, 'python_parser.py');
|
|
40
|
-
|
|
41
|
-
// Dynamic command detection for cross-platform support (Mac/Linux usually python3, Windows usually python)
|
|
42
|
-
let pythonCmd = 'python3';
|
|
43
|
-
try {
|
|
44
|
-
await execa('python3', ['--version']);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
try {
|
|
47
|
-
await execa('python', ['--version']);
|
|
48
|
-
pythonCmd = 'python';
|
|
49
|
-
} catch (e2) {
|
|
50
|
-
// Both missing - handled by main catch
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const { stdout } = await execa(pythonCmd, [scriptPath], {
|
|
56
|
-
input: context.content,
|
|
57
|
-
cwd: context.cwd
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const result: PythonAnalysisResult = JSON.parse(stdout);
|
|
61
|
-
if (result.error) return [];
|
|
62
|
-
|
|
63
|
-
const astConfig = this.config.ast || {};
|
|
64
|
-
const safetyConfig = this.config.safety || {};
|
|
65
|
-
const maxComplexity = astConfig.complexity || 10;
|
|
66
|
-
const maxParams = astConfig.max_params || 5;
|
|
67
|
-
const maxMethods = astConfig.max_methods || 10;
|
|
68
|
-
|
|
69
|
-
// Process metrics (complexity, params, methods)
|
|
70
|
-
const metrics = result.metrics || [];
|
|
71
|
-
for (const item of metrics) {
|
|
72
|
-
if (item.type === 'function') {
|
|
73
|
-
if (item.parameters && item.parameters > maxParams) {
|
|
74
|
-
failures.push({
|
|
75
|
-
id: 'AST_MAX_PARAMS',
|
|
76
|
-
title: `Function '${item.name}' has ${item.parameters} parameters (max: ${maxParams})`,
|
|
77
|
-
details: `High parameter count detected in ${context.file} at line ${item.lineno}`,
|
|
78
|
-
files: [context.file],
|
|
79
|
-
hint: `Reduce number of parameters or use an options object.`
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (item.complexity && item.complexity > maxComplexity) {
|
|
83
|
-
failures.push({
|
|
84
|
-
id: 'AST_COMPLEXITY',
|
|
85
|
-
title: `Function '${item.name}' has complexity of ${item.complexity} (max: ${maxComplexity})`,
|
|
86
|
-
details: `High complexity detected in ${context.file} at line ${item.lineno}`,
|
|
87
|
-
files: [context.file],
|
|
88
|
-
hint: `Refactor '${item.name}' into smaller, more focused functions.`
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
} else if (item.type === 'class') {
|
|
92
|
-
if (item.methods && item.methods > maxMethods) {
|
|
93
|
-
failures.push({
|
|
94
|
-
id: 'AST_MAX_METHODS',
|
|
95
|
-
title: `Class '${item.name}' has ${item.methods} methods (max: ${maxMethods})`,
|
|
96
|
-
details: `God Object pattern detected in ${context.file} at line ${item.lineno}`,
|
|
97
|
-
files: [context.file],
|
|
98
|
-
hint: `Split class '${item.name}' into smaller services.`
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Process security issues (CSRF, hardcoded secrets, SQL injection, etc.)
|
|
105
|
-
const securityIssues = result.security || [];
|
|
106
|
-
for (const issue of securityIssues) {
|
|
107
|
-
const issueIdMap: Record<string, string> = {
|
|
108
|
-
'hardcoded_secret': 'SECURITY_HARDCODED_SECRET',
|
|
109
|
-
'csrf_disabled': 'SECURITY_CSRF_DISABLED',
|
|
110
|
-
'code_injection': 'SECURITY_CODE_INJECTION',
|
|
111
|
-
'insecure_deserialization': 'SECURITY_INSECURE_DESERIALIZATION',
|
|
112
|
-
'command_injection': 'SECURITY_COMMAND_INJECTION',
|
|
113
|
-
'sql_injection': 'SECURITY_SQL_INJECTION'
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const id = issueIdMap[issue.issue] || 'SECURITY_ISSUE';
|
|
117
|
-
|
|
118
|
-
failures.push({
|
|
119
|
-
id,
|
|
120
|
-
title: issue.message,
|
|
121
|
-
details: `Security issue in ${context.file} at line ${issue.lineno}: ${issue.name}`,
|
|
122
|
-
files: [context.file],
|
|
123
|
-
hint: this.getSecurityHint(issue.issue)
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
} catch (e: any) {
|
|
128
|
-
// If python3 is missing, we skip AST but other gates still run
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return failures;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private getSecurityHint(issueType: string): string {
|
|
135
|
-
const hints: Record<string, string> = {
|
|
136
|
-
'hardcoded_secret': 'Use environment variables: os.environ.get("SECRET_KEY")',
|
|
137
|
-
'csrf_disabled': 'Enable CSRF protection for all forms handling sensitive data',
|
|
138
|
-
'code_injection': 'Avoid eval/exec. Use safer alternatives like ast.literal_eval() for data parsing',
|
|
139
|
-
'insecure_deserialization': 'Use json.loads() instead of pickle for untrusted data',
|
|
140
|
-
'command_injection': 'Use subprocess with shell=False and pass arguments as a list',
|
|
141
|
-
'sql_injection': 'Use parameterized queries: cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))'
|
|
142
|
-
};
|
|
143
|
-
return hints[issueType] || 'Review and fix the security issue.';
|
|
144
|
-
}
|
|
145
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
import sys
|
|
3
|
-
import json
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
class MetricsVisitor(ast.NodeVisitor):
|
|
7
|
-
def __init__(self):
|
|
8
|
-
self.metrics = []
|
|
9
|
-
|
|
10
|
-
def visit_FunctionDef(self, node):
|
|
11
|
-
self.analyze_function(node)
|
|
12
|
-
self.generic_visit(node)
|
|
13
|
-
|
|
14
|
-
def visit_AsyncFunctionDef(self, node):
|
|
15
|
-
self.analyze_function(node)
|
|
16
|
-
self.generic_visit(node)
|
|
17
|
-
|
|
18
|
-
def visit_ClassDef(self, node):
|
|
19
|
-
methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
20
|
-
self.metrics.append({
|
|
21
|
-
"type": "class",
|
|
22
|
-
"name": node.name,
|
|
23
|
-
"methods": len(methods),
|
|
24
|
-
"lineno": node.lineno
|
|
25
|
-
})
|
|
26
|
-
self.generic_visit(node)
|
|
27
|
-
|
|
28
|
-
def analyze_function(self, node):
|
|
29
|
-
complexity = 1
|
|
30
|
-
for n in ast.walk(node):
|
|
31
|
-
if isinstance(n, (ast.If, ast.While, ast.For, ast.AsyncFor, ast.Try, ast.ExceptHandler, ast.With, ast.AsyncWith)):
|
|
32
|
-
complexity += 1
|
|
33
|
-
elif isinstance(n, ast.BoolOp):
|
|
34
|
-
complexity += len(n.values) - 1
|
|
35
|
-
elif isinstance(n, ast.IfExp):
|
|
36
|
-
complexity += 1
|
|
37
|
-
|
|
38
|
-
params = len(node.args.args) + len(node.args.kwonlyargs)
|
|
39
|
-
if node.args.vararg: params += 1
|
|
40
|
-
if node.args.kwarg: params += 1
|
|
41
|
-
|
|
42
|
-
self.metrics.append({
|
|
43
|
-
"type": "function",
|
|
44
|
-
"name": node.name,
|
|
45
|
-
"complexity": complexity,
|
|
46
|
-
"parameters": params,
|
|
47
|
-
"lineno": node.lineno
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class SecurityVisitor(ast.NodeVisitor):
|
|
52
|
-
"""Detects security issues in Python code via AST analysis."""
|
|
53
|
-
|
|
54
|
-
def __init__(self, content):
|
|
55
|
-
self.issues = []
|
|
56
|
-
self.content = content
|
|
57
|
-
self.lines = content.split('\n')
|
|
58
|
-
|
|
59
|
-
def visit_Assign(self, node):
|
|
60
|
-
"""Check for hardcoded secrets and CSRF disabled."""
|
|
61
|
-
for target in node.targets:
|
|
62
|
-
if isinstance(target, ast.Name):
|
|
63
|
-
name = target.id
|
|
64
|
-
# Check for hardcoded SECRET_KEY
|
|
65
|
-
if name == 'SECRET_KEY':
|
|
66
|
-
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
67
|
-
self.issues.append({
|
|
68
|
-
"type": "security",
|
|
69
|
-
"issue": "hardcoded_secret",
|
|
70
|
-
"name": "SECRET_KEY",
|
|
71
|
-
"lineno": node.lineno,
|
|
72
|
-
"message": "Hardcoded SECRET_KEY detected. Use environment variables instead."
|
|
73
|
-
})
|
|
74
|
-
# Check for hardcoded passwords/tokens
|
|
75
|
-
if name.upper() in ('PASSWORD', 'API_KEY', 'TOKEN', 'AUTH_TOKEN', 'PRIVATE_KEY', 'AWS_SECRET_KEY'):
|
|
76
|
-
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
77
|
-
self.issues.append({
|
|
78
|
-
"type": "security",
|
|
79
|
-
"issue": "hardcoded_secret",
|
|
80
|
-
"name": name,
|
|
81
|
-
"lineno": node.lineno,
|
|
82
|
-
"message": f"Hardcoded {name} detected. Use environment variables instead."
|
|
83
|
-
})
|
|
84
|
-
# Check for csrf = False
|
|
85
|
-
if name.lower() in ('csrf', 'csrf_enabled', 'wtf_csrf_enabled'):
|
|
86
|
-
if isinstance(node.value, ast.Constant) and node.value.value == False:
|
|
87
|
-
self.issues.append({
|
|
88
|
-
"type": "security",
|
|
89
|
-
"issue": "csrf_disabled",
|
|
90
|
-
"name": name,
|
|
91
|
-
"lineno": node.lineno,
|
|
92
|
-
"message": "CSRF protection is disabled. This is a security vulnerability."
|
|
93
|
-
})
|
|
94
|
-
self.generic_visit(node)
|
|
95
|
-
|
|
96
|
-
def visit_Call(self, node):
|
|
97
|
-
"""Check for dangerous function calls."""
|
|
98
|
-
func_name = None
|
|
99
|
-
if isinstance(node.func, ast.Name):
|
|
100
|
-
func_name = node.func.id
|
|
101
|
-
elif isinstance(node.func, ast.Attribute):
|
|
102
|
-
func_name = node.func.attr
|
|
103
|
-
|
|
104
|
-
# Check for eval/exec usage
|
|
105
|
-
if func_name in ('eval', 'exec'):
|
|
106
|
-
self.issues.append({
|
|
107
|
-
"type": "security",
|
|
108
|
-
"issue": "code_injection",
|
|
109
|
-
"name": func_name,
|
|
110
|
-
"lineno": node.lineno,
|
|
111
|
-
"message": f"Use of {func_name}() detected. This can lead to code injection vulnerabilities."
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
# Check for pickle usage (insecure deserialization)
|
|
115
|
-
if func_name in ('loads', 'load') and isinstance(node.func, ast.Attribute):
|
|
116
|
-
if isinstance(node.func.value, ast.Name) and node.func.value.id == 'pickle':
|
|
117
|
-
self.issues.append({
|
|
118
|
-
"type": "security",
|
|
119
|
-
"issue": "insecure_deserialization",
|
|
120
|
-
"name": "pickle",
|
|
121
|
-
"lineno": node.lineno,
|
|
122
|
-
"message": "Pickle deserialization is unsafe. Use json instead for untrusted data."
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
# Check for shell=True in subprocess
|
|
126
|
-
if func_name in ('run', 'call', 'Popen', 'check_output', 'check_call'):
|
|
127
|
-
for keyword in node.keywords:
|
|
128
|
-
if keyword.arg == 'shell' and isinstance(keyword.value, ast.Constant) and keyword.value.value == True:
|
|
129
|
-
self.issues.append({
|
|
130
|
-
"type": "security",
|
|
131
|
-
"issue": "command_injection",
|
|
132
|
-
"name": func_name,
|
|
133
|
-
"lineno": node.lineno,
|
|
134
|
-
"message": "shell=True in subprocess can lead to command injection."
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
self.generic_visit(node)
|
|
138
|
-
|
|
139
|
-
def check_sql_injection(self):
|
|
140
|
-
"""Check for SQL injection patterns using regex on source."""
|
|
141
|
-
sql_patterns = [
|
|
142
|
-
(r'execute\s*\(\s*["\'].*%s', 'SQL string formatting with %s'),
|
|
143
|
-
(r'execute\s*\(\s*f["\']', 'SQL f-string'),
|
|
144
|
-
(r'execute\s*\(\s*["\'].*\+', 'SQL string concatenation'),
|
|
145
|
-
(r'cursor\.execute\s*\(\s*["\'].*\.format\s*\(', 'SQL .format()'),
|
|
146
|
-
]
|
|
147
|
-
for pattern, desc in sql_patterns:
|
|
148
|
-
for i, line in enumerate(self.lines, 1):
|
|
149
|
-
if re.search(pattern, line, re.IGNORECASE):
|
|
150
|
-
self.issues.append({
|
|
151
|
-
"type": "security",
|
|
152
|
-
"issue": "sql_injection",
|
|
153
|
-
"name": desc,
|
|
154
|
-
"lineno": i,
|
|
155
|
-
"message": f"Potential SQL injection: {desc}. Use parameterized queries."
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def analyze_code(content):
|
|
160
|
-
try:
|
|
161
|
-
tree = ast.parse(content)
|
|
162
|
-
|
|
163
|
-
# Collect metrics
|
|
164
|
-
visitor = MetricsVisitor()
|
|
165
|
-
visitor.visit(tree)
|
|
166
|
-
|
|
167
|
-
# Collect security issues
|
|
168
|
-
security_visitor = SecurityVisitor(content)
|
|
169
|
-
security_visitor.visit(tree)
|
|
170
|
-
security_visitor.check_sql_injection()
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
"metrics": visitor.metrics,
|
|
174
|
-
"security": security_visitor.issues
|
|
175
|
-
}
|
|
176
|
-
except Exception as e:
|
|
177
|
-
return {"error": str(e)}
|
|
178
|
-
|
|
179
|
-
if __name__ == "__main__":
|
|
180
|
-
content = sys.stdin.read()
|
|
181
|
-
print(json.dumps(analyze_code(content)))
|