opencode-api-security-testing 3.0.5 → 3.0.6
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/package.json +1 -1
- package/src/hooks/directory-agents-injector.ts +102 -0
- package/src/index.ts +103 -2
package/package.json
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const AGENTS_FILENAME = "AGENTS.md";
|
|
6
|
+
const AGENTS_DIR = ".config/opencode/agents";
|
|
7
|
+
|
|
8
|
+
export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
|
9
|
+
function resolveAgentsDir(): string | null {
|
|
10
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
11
|
+
if (!home) return null;
|
|
12
|
+
return join(home, AGENTS_DIR);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findAgentsMdUp(startDir: string, agentsDir: string): string | null {
|
|
16
|
+
let current = startDir;
|
|
17
|
+
|
|
18
|
+
while (true) {
|
|
19
|
+
const agentsPath = join(current, AGENTS_FILENAME);
|
|
20
|
+
if (existsSync(agentsPath)) {
|
|
21
|
+
return agentsPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (current === agentsDir) break;
|
|
25
|
+
const parent = dirname(current);
|
|
26
|
+
if (parent === current) break;
|
|
27
|
+
if (parent === "/" || parent === home) break;
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getSessionKey(sessionID: string): string {
|
|
35
|
+
return `api-sec-inject-${sessionID}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const injectedPaths = new Set<string>();
|
|
39
|
+
|
|
40
|
+
const toolExecuteAfter = async (
|
|
41
|
+
input: { tool: string; sessionID: string; callID: string },
|
|
42
|
+
output: { title: string; output: string; metadata: unknown }
|
|
43
|
+
) => {
|
|
44
|
+
const toolName = input.tool.toLowerCase();
|
|
45
|
+
const agentsDir = resolveAgentsDir();
|
|
46
|
+
|
|
47
|
+
if (!agentsDir || !existsSync(agentsDir)) return;
|
|
48
|
+
|
|
49
|
+
if (toolName === "read") {
|
|
50
|
+
const filePath = output.title;
|
|
51
|
+
if (!filePath) return;
|
|
52
|
+
|
|
53
|
+
const resolved = resolve(filePath);
|
|
54
|
+
const dir = dirname(resolved);
|
|
55
|
+
|
|
56
|
+
if (!dir.includes(agentsDir)) return;
|
|
57
|
+
|
|
58
|
+
const cacheKey = getSessionKey(input.sessionID);
|
|
59
|
+
if (injectedPaths.has(cacheKey + resolved)) return;
|
|
60
|
+
|
|
61
|
+
const agentsPath = findAgentsMdUp(dir, agentsDir);
|
|
62
|
+
if (!agentsPath) return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const content = readFileSync(agentsPath, "utf-8");
|
|
66
|
+
output.output += `\n\n[Auto-injected from ${AGENTS_FILENAME}]\n${content}`;
|
|
67
|
+
injectedPaths.add(cacheKey + resolved);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("[api-security-testing] Failed to inject agents:", err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const eventHandler = async (input: { event: { type: string; properties?: unknown } }) => {
|
|
75
|
+
const { event } = input;
|
|
76
|
+
|
|
77
|
+
if (event.type === "session.deleted" || event.type === "session.compacted") {
|
|
78
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
79
|
+
let sessionID: string | undefined;
|
|
80
|
+
|
|
81
|
+
if (event.type === "session.deleted") {
|
|
82
|
+
sessionID = (props?.info as { id?: string })?.id;
|
|
83
|
+
} else {
|
|
84
|
+
sessionID = (props?.sessionID ?? (props?.info as { id?: string })?.id) as string | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (sessionID) {
|
|
88
|
+
const cacheKey = getSessionKey(sessionID);
|
|
89
|
+
for (const key of injectedPaths.keys()) {
|
|
90
|
+
if (key.startsWith(cacheKey)) {
|
|
91
|
+
injectedPaths.delete(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"tool.execute.after": toolExecuteAfter,
|
|
100
|
+
event: eventHandler,
|
|
101
|
+
};
|
|
102
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { existsSync } from "fs";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
5
|
|
|
6
6
|
const SKILL_DIR = "skills/api-security-testing";
|
|
7
7
|
const CORE_DIR = `${SKILL_DIR}/core`;
|
|
8
|
+
const AGENTS_DIR = ".config/opencode/agents";
|
|
9
|
+
const AGENTS_FILENAME = "AGENTS.md";
|
|
8
10
|
|
|
9
11
|
function getSkillPath(ctx: { directory: string }): string {
|
|
10
12
|
return join(ctx.directory, SKILL_DIR);
|
|
@@ -23,9 +25,40 @@ function checkDeps(ctx: { directory: string }): string {
|
|
|
23
25
|
return "";
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function getAgentsDir(): string {
|
|
29
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
30
|
+
return join(home, AGENTS_DIR);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getInjectedAgentsPrompt(): string {
|
|
34
|
+
const agentsDir = getAgentsDir();
|
|
35
|
+
const agentsPath = join(agentsDir, "api-cyber-supervisor.md");
|
|
36
|
+
|
|
37
|
+
if (!existsSync(agentsPath)) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const content = readFileSync(agentsPath, "utf-8");
|
|
43
|
+
return `
|
|
44
|
+
|
|
45
|
+
[API Security Testing Agents Available]
|
|
46
|
+
When performing security testing tasks, you can use the following specialized agents:
|
|
47
|
+
|
|
48
|
+
${content}
|
|
49
|
+
|
|
50
|
+
To activate these agents, simply mention their name in your response (e.g., "@api-cyber-supervisor" to coordinate security testing).
|
|
51
|
+
`;
|
|
52
|
+
} catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
const ApiSecurityTestingPlugin: Plugin = async (ctx) => {
|
|
27
58
|
console.log("[api-security-testing] Plugin loaded");
|
|
28
59
|
|
|
60
|
+
const injectedSessions = new Set<string>();
|
|
61
|
+
|
|
29
62
|
return {
|
|
30
63
|
tool: {
|
|
31
64
|
api_security_scan: tool({
|
|
@@ -245,7 +278,75 @@ print(result)
|
|
|
245
278
|
},
|
|
246
279
|
}),
|
|
247
280
|
},
|
|
281
|
+
|
|
282
|
+
"chat.message": async (input, output) => {
|
|
283
|
+
const sessionID = input.sessionID;
|
|
284
|
+
|
|
285
|
+
if (!injectedSessions.has(sessionID)) {
|
|
286
|
+
injectedSessions.add(sessionID);
|
|
287
|
+
|
|
288
|
+
const agentsPrompt = getInjectedAgentsPrompt();
|
|
289
|
+
if (agentsPrompt) {
|
|
290
|
+
const parts = output.parts as Array<{ type: string; text?: string }>;
|
|
291
|
+
const textPart = parts.find(p => p.type === "text");
|
|
292
|
+
if (textPart && textPart.text) {
|
|
293
|
+
textPart.text += agentsPrompt;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
"tool.execute.after": async (input, output) => {
|
|
300
|
+
const toolName = input.tool.toLowerCase();
|
|
301
|
+
const agentsDir = getAgentsDir();
|
|
302
|
+
|
|
303
|
+
if (!existsSync(agentsDir)) return;
|
|
304
|
+
|
|
305
|
+
if (toolName === "read") {
|
|
306
|
+
const filePath = output.title;
|
|
307
|
+
if (!filePath) return;
|
|
308
|
+
|
|
309
|
+
const resolved = resolve(filePath);
|
|
310
|
+
const dir = dirname(resolved);
|
|
311
|
+
|
|
312
|
+
if (!dir.includes(agentsDir)) return;
|
|
313
|
+
|
|
314
|
+
const agentsPath = join(agentsDir, AGENTS_FILENAME);
|
|
315
|
+
if (!existsSync(agentsPath)) return;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const content = readFileSync(agentsPath, "utf-8");
|
|
319
|
+
output.output += `\n\n[Agents Definition]\n${content}`;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.error("[api-security-testing] Failed to inject agents:", err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
event: async (input) => {
|
|
327
|
+
const { event } = input;
|
|
328
|
+
|
|
329
|
+
if (event.type === "session.deleted" || event.type === "session.compacted") {
|
|
330
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
331
|
+
let sessionID: string | undefined;
|
|
332
|
+
|
|
333
|
+
if (event.type === "session.deleted") {
|
|
334
|
+
sessionID = (props?.info as { id?: string })?.id;
|
|
335
|
+
} else {
|
|
336
|
+
sessionID = (props?.sessionID ?? (props?.info as { id?: string })?.id) as string | undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (sessionID) {
|
|
340
|
+
injectedSessions.delete(sessionID);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
},
|
|
248
344
|
};
|
|
249
345
|
};
|
|
250
346
|
|
|
251
|
-
|
|
347
|
+
function resolve(filePath: string): string {
|
|
348
|
+
if (filePath.startsWith("/")) return filePath;
|
|
349
|
+
return join(process.cwd(), filePath);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export default ApiSecurityTestingPlugin;
|