opencode-mad 0.4.0 → 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/README.md +155 -32
- package/agents/mad-analyste.md +356 -0
- package/agents/mad-architecte.md +348 -0
- package/agents/mad-reviewer.md +299 -0
- package/agents/mad-security.md +387 -0
- package/agents/orchestrator.md +363 -627
- package/package.json +1 -1
- package/plugins/mad-plugin.ts +415 -7
- package/skills/mad-workflow/SKILL.md +180 -87
package/package.json
CHANGED
package/plugins/mad-plugin.ts
CHANGED
|
@@ -12,8 +12,39 @@ import { execSync } from "child_process"
|
|
|
12
12
|
* running in parallel via OpenCode's Task tool.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
// Types for agent permissions (constraint enforcement)
|
|
16
|
+
interface AgentPermissions {
|
|
17
|
+
type: 'orchestrator' | 'analyste' | 'architecte' | 'developer' | 'tester' | 'reviewer' | 'fixer' | 'merger' | 'security'
|
|
18
|
+
canEdit: boolean
|
|
19
|
+
canWrite: boolean
|
|
20
|
+
canPatch: boolean
|
|
21
|
+
allowedPaths: string[] | null // null = all, [] = none, [...] = specific list
|
|
22
|
+
deniedPaths: string[] // Explicitly denied paths
|
|
23
|
+
worktree: string | null // Worktree path if applicable
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Global map to store permissions by sessionID
|
|
27
|
+
const agentPermissions = new Map<string, AgentPermissions>()
|
|
28
|
+
|
|
29
|
+
// Simple glob matching (for patterns like /backend/**)
|
|
30
|
+
function matchGlob(path: string, pattern: string): boolean {
|
|
31
|
+
// Normalize paths
|
|
32
|
+
const normalizedPath = path.replace(/\\/g, '/')
|
|
33
|
+
const normalizedPattern = pattern.replace(/\\/g, '/')
|
|
34
|
+
|
|
35
|
+
// Convert glob to regex
|
|
36
|
+
const regexPattern = normalizedPattern
|
|
37
|
+
.replace(/\*\*/g, '{{DOUBLESTAR}}')
|
|
38
|
+
.replace(/\*/g, '[^/]*')
|
|
39
|
+
.replace(/{{DOUBLESTAR}}/g, '.*')
|
|
40
|
+
.replace(/\//g, '\\/')
|
|
41
|
+
|
|
42
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
43
|
+
return regex.test(normalizedPath)
|
|
44
|
+
}
|
|
45
|
+
|
|
15
46
|
// Current version of opencode-mad
|
|
16
|
-
const CURRENT_VERSION = "0.
|
|
47
|
+
const CURRENT_VERSION = "1.0.0"
|
|
17
48
|
|
|
18
49
|
// Update notification state (shown only once per session)
|
|
19
50
|
let updateNotificationShown = false
|
|
@@ -21,6 +52,9 @@ let pendingUpdateMessage: string | null = null
|
|
|
21
52
|
|
|
22
53
|
export const MADPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
|
|
23
54
|
|
|
55
|
+
// Use the directory provided by OpenCode, fallback to process.cwd() for backwards compatibility
|
|
56
|
+
const baseDirectory = directory || process.cwd()
|
|
57
|
+
|
|
24
58
|
/**
|
|
25
59
|
* Helper to run shell commands with proper error handling (cross-platform)
|
|
26
60
|
*/
|
|
@@ -28,7 +62,7 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
|
|
|
28
62
|
try {
|
|
29
63
|
const output = execSync(cmd, {
|
|
30
64
|
encoding: "utf-8",
|
|
31
|
-
cwd: cwd ||
|
|
65
|
+
cwd: cwd || baseDirectory,
|
|
32
66
|
stdio: ["pipe", "pipe", "pipe"]
|
|
33
67
|
})
|
|
34
68
|
return { success: true, output: output.trim() }
|
|
@@ -43,9 +77,10 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
|
|
|
43
77
|
|
|
44
78
|
/**
|
|
45
79
|
* Helper to get git root with error handling
|
|
80
|
+
* @param basePath - Optional base path to start from (defaults to baseDirectory)
|
|
46
81
|
*/
|
|
47
|
-
const getGitRoot = (): string => {
|
|
48
|
-
const result = runCommand("git rev-parse --show-toplevel")
|
|
82
|
+
const getGitRoot = (basePath?: string): string => {
|
|
83
|
+
const result = runCommand("git rev-parse --show-toplevel", basePath || baseDirectory)
|
|
49
84
|
if (!result.success) {
|
|
50
85
|
throw new Error(`Not a git repository or git not found: ${result.error}`)
|
|
51
86
|
}
|
|
@@ -54,18 +89,20 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
|
|
|
54
89
|
|
|
55
90
|
/**
|
|
56
91
|
* Helper to get current branch with fallback
|
|
92
|
+
* @param basePath - Optional base path to run git command from (defaults to baseDirectory)
|
|
57
93
|
*/
|
|
58
|
-
const getCurrentBranch = (): string => {
|
|
59
|
-
const result = runCommand("git symbolic-ref --short HEAD")
|
|
94
|
+
const getCurrentBranch = (basePath?: string): string => {
|
|
95
|
+
const result = runCommand("git symbolic-ref --short HEAD", basePath || baseDirectory)
|
|
60
96
|
return result.success ? result.output : "main"
|
|
61
97
|
}
|
|
62
98
|
|
|
63
99
|
/**
|
|
64
100
|
* Helper to log MAD events
|
|
101
|
+
* Uses baseDirectory to find the git root for log file location
|
|
65
102
|
*/
|
|
66
103
|
const logEvent = (level: "info" | "warn" | "error" | "debug", message: string, context?: any) => {
|
|
67
104
|
try {
|
|
68
|
-
const gitRoot = getGitRoot()
|
|
105
|
+
const gitRoot = getGitRoot(baseDirectory)
|
|
69
106
|
const logFile = join(gitRoot, ".mad-logs.jsonl")
|
|
70
107
|
const logEntry = JSON.stringify({
|
|
71
108
|
timestamp: new Date().toISOString(),
|
|
@@ -1111,6 +1148,306 @@ Use this at the end of the MAD workflow to ensure code quality.`,
|
|
|
1111
1148
|
}
|
|
1112
1149
|
},
|
|
1113
1150
|
}),
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Register agent permissions for constraint enforcement
|
|
1154
|
+
*/
|
|
1155
|
+
mad_register_agent: tool({
|
|
1156
|
+
description: `Register an agent's permissions for constraint enforcement.
|
|
1157
|
+
Call this when spawning a subagent to define what it can and cannot do.
|
|
1158
|
+
The plugin will then BLOCK any unauthorized actions.`,
|
|
1159
|
+
args: {
|
|
1160
|
+
sessionID: tool.schema.string().describe("The session ID of the agent"),
|
|
1161
|
+
agentType: tool.schema.enum([
|
|
1162
|
+
'orchestrator', 'analyste', 'architecte', 'developer',
|
|
1163
|
+
'tester', 'reviewer', 'fixer', 'merger', 'security'
|
|
1164
|
+
]).describe("Type of agent"),
|
|
1165
|
+
worktree: tool.schema.string().optional().describe("Worktree path if applicable"),
|
|
1166
|
+
allowedPaths: tool.schema.array(tool.schema.string()).optional().describe("Paths the agent can edit (glob patterns)"),
|
|
1167
|
+
deniedPaths: tool.schema.array(tool.schema.string()).optional().describe("Paths explicitly denied"),
|
|
1168
|
+
},
|
|
1169
|
+
async execute(args, context) {
|
|
1170
|
+
const { sessionID, agentType, worktree, allowedPaths, deniedPaths } = args
|
|
1171
|
+
|
|
1172
|
+
// Define default permissions based on agent type
|
|
1173
|
+
const readOnlyAgents = ['orchestrator', 'analyste', 'architecte', 'tester', 'reviewer', 'security']
|
|
1174
|
+
const canEdit = !readOnlyAgents.includes(agentType)
|
|
1175
|
+
|
|
1176
|
+
const permissions: AgentPermissions = {
|
|
1177
|
+
type: agentType,
|
|
1178
|
+
canEdit,
|
|
1179
|
+
canWrite: canEdit,
|
|
1180
|
+
canPatch: canEdit,
|
|
1181
|
+
allowedPaths: allowedPaths || null,
|
|
1182
|
+
deniedPaths: deniedPaths || [],
|
|
1183
|
+
worktree: worktree || null,
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
agentPermissions.set(sessionID, permissions)
|
|
1187
|
+
|
|
1188
|
+
logEvent("info", `Registered agent permissions`, { sessionID, agentType, canEdit, worktree })
|
|
1189
|
+
|
|
1190
|
+
return getUpdateNotification() + `✅ Agent registered: ${agentType} (canEdit: ${canEdit}, worktree: ${worktree || 'none'})`
|
|
1191
|
+
}
|
|
1192
|
+
}),
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Analyze codebase - for Analyst agent
|
|
1196
|
+
*/
|
|
1197
|
+
mad_analyze: tool({
|
|
1198
|
+
description: `Trigger a codebase analysis. Returns a structured report.
|
|
1199
|
+
Use mode 'full' for complete project scan, 'targeted' for task-specific analysis.`,
|
|
1200
|
+
args: {
|
|
1201
|
+
mode: tool.schema.enum(['full', 'targeted']).describe("Analysis mode"),
|
|
1202
|
+
focus: tool.schema.string().optional().describe("For targeted mode: what to focus on"),
|
|
1203
|
+
paths: tool.schema.array(tool.schema.string()).optional().describe("Specific paths to analyze"),
|
|
1204
|
+
},
|
|
1205
|
+
async execute(args, context) {
|
|
1206
|
+
const { mode, focus, paths } = args
|
|
1207
|
+
const gitRoot = getGitRoot()
|
|
1208
|
+
|
|
1209
|
+
let report = `# Codebase Analysis Report\n\n`
|
|
1210
|
+
report += `**Mode:** ${mode}\n`
|
|
1211
|
+
report += `**Date:** ${new Date().toISOString()}\n\n`
|
|
1212
|
+
|
|
1213
|
+
// Collecter les informations de base
|
|
1214
|
+
const structure = runCommand('find . -type f -name "*.ts" -o -name "*.js" -o -name "*.json" | grep -v node_modules | head -50', gitRoot)
|
|
1215
|
+
const packageJsonPath = join(gitRoot, 'package.json')
|
|
1216
|
+
const packageJson = existsSync(packageJsonPath)
|
|
1217
|
+
? JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
1218
|
+
: null
|
|
1219
|
+
|
|
1220
|
+
report += `## Project Structure\n\`\`\`\n${structure.output}\n\`\`\`\n\n`
|
|
1221
|
+
|
|
1222
|
+
if (packageJson) {
|
|
1223
|
+
report += `## Dependencies\n`
|
|
1224
|
+
report += `- **Name:** ${packageJson.name}\n`
|
|
1225
|
+
report += `- **Version:** ${packageJson.version}\n`
|
|
1226
|
+
report += `- **Dependencies:** ${Object.keys(packageJson.dependencies || {}).length}\n`
|
|
1227
|
+
report += `- **DevDependencies:** ${Object.keys(packageJson.devDependencies || {}).length}\n\n`
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (mode === 'targeted' && focus) {
|
|
1231
|
+
report += `## Targeted Analysis: ${focus}\n`
|
|
1232
|
+
// Chercher les fichiers pertinents
|
|
1233
|
+
const relevantFiles = runCommand(`grep -rl "${focus}" --include="*.ts" --include="*.js" . | grep -v node_modules | head -20`, gitRoot)
|
|
1234
|
+
report += `### Relevant Files\n\`\`\`\n${relevantFiles.output || 'No files found'}\n\`\`\`\n`
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
logEvent("info", "Codebase analysis completed", { mode, focus })
|
|
1238
|
+
return getUpdateNotification() + report
|
|
1239
|
+
}
|
|
1240
|
+
}),
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Unregister agent permissions when it completes
|
|
1244
|
+
*/
|
|
1245
|
+
mad_unregister_agent: tool({
|
|
1246
|
+
description: `Unregister an agent's permissions when it completes.`,
|
|
1247
|
+
args: {
|
|
1248
|
+
sessionID: tool.schema.string().describe("The session ID to unregister"),
|
|
1249
|
+
},
|
|
1250
|
+
async execute(args) {
|
|
1251
|
+
const existed = agentPermissions.has(args.sessionID)
|
|
1252
|
+
agentPermissions.delete(args.sessionID)
|
|
1253
|
+
|
|
1254
|
+
if (existed) {
|
|
1255
|
+
logEvent("info", `Unregistered agent`, { sessionID: args.sessionID })
|
|
1256
|
+
return `✅ Agent unregistered: ${args.sessionID}`
|
|
1257
|
+
} else {
|
|
1258
|
+
return `⚠️ Agent was not registered: ${args.sessionID}`
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}),
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Create development plan - for Architect agent
|
|
1265
|
+
*/
|
|
1266
|
+
mad_create_plan: tool({
|
|
1267
|
+
description: `Store a development plan created by the Architect agent.
|
|
1268
|
+
The plan will be available for the orchestrator to present to the user.`,
|
|
1269
|
+
args: {
|
|
1270
|
+
planName: tool.schema.string().describe("Name/identifier for the plan"),
|
|
1271
|
+
plan: tool.schema.string().describe("The full development plan in markdown"),
|
|
1272
|
+
tasks: tool.schema.array(tool.schema.object({
|
|
1273
|
+
name: tool.schema.string(),
|
|
1274
|
+
branch: tool.schema.string(),
|
|
1275
|
+
ownership: tool.schema.array(tool.schema.string()),
|
|
1276
|
+
denied: tool.schema.array(tool.schema.string()).optional(),
|
|
1277
|
+
dependencies: tool.schema.array(tool.schema.string()).optional(),
|
|
1278
|
+
})).describe("Structured task list"),
|
|
1279
|
+
},
|
|
1280
|
+
async execute(args, context) {
|
|
1281
|
+
const { planName, plan, tasks } = args
|
|
1282
|
+
|
|
1283
|
+
// Stocker le plan en mémoire (pourrait être persisté plus tard)
|
|
1284
|
+
const planData = {
|
|
1285
|
+
name: planName,
|
|
1286
|
+
createdAt: new Date().toISOString(),
|
|
1287
|
+
plan,
|
|
1288
|
+
tasks,
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Log pour debugging
|
|
1292
|
+
logEvent("info", "Development plan created", { planName, taskCount: tasks.length })
|
|
1293
|
+
|
|
1294
|
+
return getUpdateNotification() + `✅ Plan '${planName}' created with ${tasks.length} tasks.\n\n${plan}`
|
|
1295
|
+
}
|
|
1296
|
+
}),
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Submit code review - for Reviewer agent
|
|
1300
|
+
*/
|
|
1301
|
+
mad_review: tool({
|
|
1302
|
+
description: `Submit a code review report for a worktree.
|
|
1303
|
+
Called by the Reviewer agent after analyzing the code.`,
|
|
1304
|
+
args: {
|
|
1305
|
+
worktree: tool.schema.string().describe("Worktree that was reviewed"),
|
|
1306
|
+
verdict: tool.schema.enum(['approved', 'changes_requested', 'rejected']).describe("Review verdict"),
|
|
1307
|
+
summary: tool.schema.string().describe("Brief summary of the review"),
|
|
1308
|
+
issues: tool.schema.array(tool.schema.object({
|
|
1309
|
+
severity: tool.schema.enum(['critical', 'major', 'minor']),
|
|
1310
|
+
file: tool.schema.string(),
|
|
1311
|
+
line: tool.schema.number().optional(),
|
|
1312
|
+
message: tool.schema.string(),
|
|
1313
|
+
suggestion: tool.schema.string().optional(),
|
|
1314
|
+
})).optional().describe("List of issues found"),
|
|
1315
|
+
positives: tool.schema.array(tool.schema.string()).optional().describe("Positive aspects of the code"),
|
|
1316
|
+
},
|
|
1317
|
+
async execute(args, context) {
|
|
1318
|
+
const { worktree, verdict, summary, issues, positives } = args
|
|
1319
|
+
const gitRoot = getGitRoot()
|
|
1320
|
+
const worktreePath = join(gitRoot, "worktrees", worktree)
|
|
1321
|
+
|
|
1322
|
+
if (!existsSync(worktreePath)) {
|
|
1323
|
+
return getUpdateNotification() + `❌ Worktree not found: ${worktreePath}`
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Créer le rapport de review
|
|
1327
|
+
let report = `# Code Review: ${worktree}\n\n`
|
|
1328
|
+
report += `**Verdict:** ${verdict === 'approved' ? '✅ APPROVED' : verdict === 'changes_requested' ? '⚠️ CHANGES REQUESTED' : '❌ REJECTED'}\n\n`
|
|
1329
|
+
report += `## Summary\n${summary}\n\n`
|
|
1330
|
+
|
|
1331
|
+
if (positives && positives.length > 0) {
|
|
1332
|
+
report += `## Positives 👍\n`
|
|
1333
|
+
positives.forEach(p => report += `- ${p}\n`)
|
|
1334
|
+
report += '\n'
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (issues && issues.length > 0) {
|
|
1338
|
+
report += `## Issues Found\n`
|
|
1339
|
+
const critical = issues.filter(i => i.severity === 'critical')
|
|
1340
|
+
const major = issues.filter(i => i.severity === 'major')
|
|
1341
|
+
const minor = issues.filter(i => i.severity === 'minor')
|
|
1342
|
+
|
|
1343
|
+
if (critical.length > 0) {
|
|
1344
|
+
report += `### 🚨 Critical (${critical.length})\n`
|
|
1345
|
+
critical.forEach(i => {
|
|
1346
|
+
report += `- **${i.file}${i.line ? `:${i.line}` : ''}** - ${i.message}\n`
|
|
1347
|
+
if (i.suggestion) report += ` → Suggestion: ${i.suggestion}\n`
|
|
1348
|
+
})
|
|
1349
|
+
}
|
|
1350
|
+
if (major.length > 0) {
|
|
1351
|
+
report += `### ⚠️ Major (${major.length})\n`
|
|
1352
|
+
major.forEach(i => {
|
|
1353
|
+
report += `- **${i.file}${i.line ? `:${i.line}` : ''}** - ${i.message}\n`
|
|
1354
|
+
if (i.suggestion) report += ` → Suggestion: ${i.suggestion}\n`
|
|
1355
|
+
})
|
|
1356
|
+
}
|
|
1357
|
+
if (minor.length > 0) {
|
|
1358
|
+
report += `### 💡 Minor (${minor.length})\n`
|
|
1359
|
+
minor.forEach(i => {
|
|
1360
|
+
report += `- **${i.file}${i.line ? `:${i.line}` : ''}** - ${i.message}\n`
|
|
1361
|
+
})
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Sauvegarder le rapport dans le worktree
|
|
1366
|
+
writeFileSync(join(worktreePath, '.agent-review'), report)
|
|
1367
|
+
|
|
1368
|
+
logEvent("info", "Code review submitted", { worktree, verdict, issueCount: issues?.length || 0 })
|
|
1369
|
+
|
|
1370
|
+
return getUpdateNotification() + report
|
|
1371
|
+
}
|
|
1372
|
+
}),
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Security scan - for Security agent
|
|
1376
|
+
*/
|
|
1377
|
+
mad_security_scan: tool({
|
|
1378
|
+
description: `Submit a security scan report for a worktree or the main project.
|
|
1379
|
+
Called by the Security agent after scanning for vulnerabilities.`,
|
|
1380
|
+
args: {
|
|
1381
|
+
target: tool.schema.string().describe("Worktree name or 'main' for main project"),
|
|
1382
|
+
riskLevel: tool.schema.enum(['low', 'medium', 'high', 'critical']).describe("Overall risk level"),
|
|
1383
|
+
summary: tool.schema.string().describe("Brief summary of findings"),
|
|
1384
|
+
vulnerabilities: tool.schema.array(tool.schema.object({
|
|
1385
|
+
id: tool.schema.string(),
|
|
1386
|
+
severity: tool.schema.enum(['low', 'medium', 'high', 'critical']),
|
|
1387
|
+
type: tool.schema.string(),
|
|
1388
|
+
file: tool.schema.string().optional(),
|
|
1389
|
+
line: tool.schema.number().optional(),
|
|
1390
|
+
description: tool.schema.string(),
|
|
1391
|
+
remediation: tool.schema.string(),
|
|
1392
|
+
})).optional().describe("List of vulnerabilities found"),
|
|
1393
|
+
dependencyIssues: tool.schema.array(tool.schema.object({
|
|
1394
|
+
package: tool.schema.string(),
|
|
1395
|
+
severity: tool.schema.string(),
|
|
1396
|
+
cve: tool.schema.string().optional(),
|
|
1397
|
+
fix: tool.schema.string(),
|
|
1398
|
+
})).optional().describe("Vulnerable dependencies"),
|
|
1399
|
+
},
|
|
1400
|
+
async execute(args, context) {
|
|
1401
|
+
const { target, riskLevel, summary, vulnerabilities, dependencyIssues } = args
|
|
1402
|
+
const gitRoot = getGitRoot()
|
|
1403
|
+
|
|
1404
|
+
let report = `# Security Scan Report: ${target}\n\n`
|
|
1405
|
+
report += `**Risk Level:** ${riskLevel === 'critical' ? '🚨 CRITICAL' : riskLevel === 'high' ? '🔴 HIGH' : riskLevel === 'medium' ? '🟡 MEDIUM' : '🟢 LOW'}\n`
|
|
1406
|
+
report += `**Date:** ${new Date().toISOString()}\n\n`
|
|
1407
|
+
report += `## Summary\n${summary}\n\n`
|
|
1408
|
+
|
|
1409
|
+
if (vulnerabilities && vulnerabilities.length > 0) {
|
|
1410
|
+
report += `## Vulnerabilities (${vulnerabilities.length})\n\n`
|
|
1411
|
+
vulnerabilities.forEach(v => {
|
|
1412
|
+
const icon = v.severity === 'critical' ? '🚨' : v.severity === 'high' ? '🔴' : v.severity === 'medium' ? '🟡' : '🟢'
|
|
1413
|
+
report += `### ${icon} [${v.id}] ${v.type}\n`
|
|
1414
|
+
report += `**Severity:** ${v.severity.toUpperCase()}\n`
|
|
1415
|
+
if (v.file) report += `**Location:** ${v.file}${v.line ? `:${v.line}` : ''}\n`
|
|
1416
|
+
report += `**Description:** ${v.description}\n`
|
|
1417
|
+
report += `**Remediation:** ${v.remediation}\n\n`
|
|
1418
|
+
})
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (dependencyIssues && dependencyIssues.length > 0) {
|
|
1422
|
+
report += `## Vulnerable Dependencies (${dependencyIssues.length})\n\n`
|
|
1423
|
+
report += `| Package | Severity | CVE | Fix |\n`
|
|
1424
|
+
report += `|---------|----------|-----|-----|\n`
|
|
1425
|
+
dependencyIssues.forEach(d => {
|
|
1426
|
+
report += `| ${d.package} | ${d.severity} | ${d.cve || 'N/A'} | ${d.fix} |\n`
|
|
1427
|
+
})
|
|
1428
|
+
report += '\n'
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Verdict
|
|
1432
|
+
const canMerge = riskLevel === 'low' || (riskLevel === 'medium' && (!vulnerabilities || vulnerabilities.filter(v => v.severity === 'critical' || v.severity === 'high').length === 0))
|
|
1433
|
+
report += `## Verdict\n`
|
|
1434
|
+
report += canMerge
|
|
1435
|
+
? `✅ **PASS** - No critical security issues blocking merge.\n`
|
|
1436
|
+
: `❌ **FAIL** - Critical security issues must be resolved before merge.\n`
|
|
1437
|
+
|
|
1438
|
+
// Sauvegarder si c'est un worktree
|
|
1439
|
+
if (target !== 'main') {
|
|
1440
|
+
const worktreePath = join(gitRoot, "worktrees", target)
|
|
1441
|
+
if (existsSync(worktreePath)) {
|
|
1442
|
+
writeFileSync(join(worktreePath, '.agent-security'), report)
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
logEvent("info", "Security scan completed", { target, riskLevel, vulnCount: vulnerabilities?.length || 0 })
|
|
1447
|
+
|
|
1448
|
+
return getUpdateNotification() + report
|
|
1449
|
+
}
|
|
1450
|
+
}),
|
|
1114
1451
|
},
|
|
1115
1452
|
|
|
1116
1453
|
// Event hooks
|
|
@@ -1126,6 +1463,77 @@ Use this at the end of the MAD workflow to ensure code quality.`,
|
|
|
1126
1463
|
})
|
|
1127
1464
|
}
|
|
1128
1465
|
},
|
|
1466
|
+
|
|
1467
|
+
// Hook to enforce agent constraints before tool execution
|
|
1468
|
+
hook: {
|
|
1469
|
+
"tool.execute.before": async (input: any, output: any) => {
|
|
1470
|
+
const perms = agentPermissions.get(input.sessionID)
|
|
1471
|
+
|
|
1472
|
+
// If no permissions registered, let it pass (backwards compatibility)
|
|
1473
|
+
if (!perms) return
|
|
1474
|
+
|
|
1475
|
+
const toolName = input.tool
|
|
1476
|
+
const args = output.args || {}
|
|
1477
|
+
|
|
1478
|
+
// 1. Block edit/write/patch for read-only agents
|
|
1479
|
+
if (['edit', 'write', 'patch', 'multiedit'].includes(toolName)) {
|
|
1480
|
+
if (!perms.canEdit) {
|
|
1481
|
+
logEvent("warn", `BLOCKED: ${perms.type} tried to use ${toolName}`, { sessionID: input.sessionID })
|
|
1482
|
+
throw new Error(`🚫 BLOCKED: Agent type '${perms.type}' cannot use '${toolName}' tool. This agent is READ-ONLY.`)
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// 2. Check path if allowedPaths is defined
|
|
1486
|
+
const targetPath = args.filePath || args.file_path || args.path
|
|
1487
|
+
if (targetPath) {
|
|
1488
|
+
// Check denied paths
|
|
1489
|
+
if (perms.deniedPaths.some((p: string) => targetPath.includes(p) || matchGlob(targetPath, p))) {
|
|
1490
|
+
logEvent("warn", `BLOCKED: ${perms.type} tried to edit denied path`, { sessionID: input.sessionID, path: targetPath })
|
|
1491
|
+
throw new Error(`🚫 BLOCKED: Cannot edit '${targetPath}' - this path is explicitly denied for this agent.`)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Check allowed paths (if defined)
|
|
1495
|
+
if (perms.allowedPaths && perms.allowedPaths.length > 0) {
|
|
1496
|
+
const isAllowed = perms.allowedPaths.some((p: string) => targetPath.includes(p) || matchGlob(targetPath, p))
|
|
1497
|
+
if (!isAllowed) {
|
|
1498
|
+
logEvent("warn", `BLOCKED: ${perms.type} tried to edit outside allowed paths`, { sessionID: input.sessionID, path: targetPath, allowedPaths: perms.allowedPaths })
|
|
1499
|
+
throw new Error(`🚫 BLOCKED: Cannot edit '${targetPath}' - outside allowed paths: ${perms.allowedPaths.join(', ')}`)
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// 3. For bash, check if trying to modify files
|
|
1506
|
+
if (toolName === 'bash' && perms && !perms.canEdit) {
|
|
1507
|
+
const cmd = args.command || ''
|
|
1508
|
+
const dangerousPatterns = [
|
|
1509
|
+
/\becho\s+.*>/, // echo > file
|
|
1510
|
+
/\bcat\s+.*>/, // cat > file
|
|
1511
|
+
/\brm\s+/, // rm
|
|
1512
|
+
/\bmv\s+/, // mv
|
|
1513
|
+
/\bcp\s+/, // cp (can create files)
|
|
1514
|
+
/\bmkdir\s+/, // mkdir
|
|
1515
|
+
/\btouch\s+/, // touch
|
|
1516
|
+
/\bnpm\s+install/, // npm install (modifies node_modules)
|
|
1517
|
+
/\bgit\s+commit/, // git commit
|
|
1518
|
+
/\bgit\s+push/, // git push
|
|
1519
|
+
]
|
|
1520
|
+
|
|
1521
|
+
for (const pattern of dangerousPatterns) {
|
|
1522
|
+
if (pattern.test(cmd)) {
|
|
1523
|
+
logEvent("warn", `BLOCKED: ${perms.type} tried dangerous bash command`, { sessionID: input.sessionID, command: cmd })
|
|
1524
|
+
throw new Error(`🚫 BLOCKED: Agent type '${perms.type}' cannot run '${cmd}' - this command modifies files and this agent is READ-ONLY.`)
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// 4. Force CWD in worktree for agents with worktree
|
|
1530
|
+
if (toolName === 'bash' && perms?.worktree && args.command) {
|
|
1531
|
+
// Prefix command with cd to worktree
|
|
1532
|
+
const worktreePath = perms.worktree.replace(/\\/g, '/')
|
|
1533
|
+
output.args.command = `cd "${worktreePath}" && ${args.command}`
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1129
1537
|
}
|
|
1130
1538
|
}
|
|
1131
1539
|
|