opencode-mad 0.4.1 → 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 +3 -3
- package/plugins/mad-plugin.ts +404 -2
- 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
|
-
//
|
|
16
|
-
|
|
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
|
+
|
|
46
|
+
// Current version of opencode-mad
|
|
47
|
+
const CURRENT_VERSION = "1.0.0"
|
|
17
48
|
|
|
18
49
|
// Update notification state (shown only once per session)
|
|
19
50
|
let updateNotificationShown = false
|
|
@@ -1117,6 +1148,306 @@ Use this at the end of the MAD workflow to ensure code quality.`,
|
|
|
1117
1148
|
}
|
|
1118
1149
|
},
|
|
1119
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
|
+
}),
|
|
1120
1451
|
},
|
|
1121
1452
|
|
|
1122
1453
|
// Event hooks
|
|
@@ -1132,6 +1463,77 @@ Use this at the end of the MAD workflow to ensure code quality.`,
|
|
|
1132
1463
|
})
|
|
1133
1464
|
}
|
|
1134
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
|
+
},
|
|
1135
1537
|
}
|
|
1136
1538
|
}
|
|
1137
1539
|
|