opencode-api-security-testing 5.4.3 → 5.4.5
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 +6 -6
- package/src/index.ts +227 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-api-security-testing",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.5",
|
|
4
4
|
"description": "API Security Testing Plugin for OpenCode - Automated vulnerability scanning and penetration testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"postinstall.mjs",
|
|
14
14
|
"preuninstall.mjs"
|
|
15
15
|
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node postinstall.mjs",
|
|
18
|
+
"preuninstall": "node preuninstall.mjs",
|
|
19
|
+
"init": "node init.mjs"
|
|
20
|
+
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"opencode",
|
|
23
23
|
"opencode-plugin",
|
package/src/index.ts
CHANGED
|
@@ -334,7 +334,118 @@ const CYBER_SUPERVISOR = DEFAULT_CONFIG.cyber_supervisor;
|
|
|
334
334
|
const modelFailureCounts = new Map<string, Map<string, number>>();
|
|
335
335
|
const sessionFailures = new Map<string, number>();
|
|
336
336
|
|
|
337
|
-
//
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// 上下文注入系统 (参考 oh-my-openagent 的 ContextCollector 模式)
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
type ContextPriority = "critical" | "high" | "normal" | "low";
|
|
342
|
+
|
|
343
|
+
interface ContextEntry {
|
|
344
|
+
id: string;
|
|
345
|
+
source: string;
|
|
346
|
+
content: string;
|
|
347
|
+
priority: ContextPriority;
|
|
348
|
+
registrationOrder: number;
|
|
349
|
+
metadata?: Record<string, unknown>;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
interface PendingContext {
|
|
353
|
+
merged: string;
|
|
354
|
+
entries: ContextEntry[];
|
|
355
|
+
hasContent: boolean;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
class ContextCollector {
|
|
359
|
+
private sessions = new Map<string, Map<string, ContextEntry>>();
|
|
360
|
+
private registrationCounter = 0;
|
|
361
|
+
|
|
362
|
+
register(sessionID: string, options: {
|
|
363
|
+
id: string;
|
|
364
|
+
source: string;
|
|
365
|
+
content: string;
|
|
366
|
+
priority?: ContextPriority;
|
|
367
|
+
metadata?: Record<string, unknown>;
|
|
368
|
+
}): void {
|
|
369
|
+
if (!this.sessions.has(sessionID)) {
|
|
370
|
+
this.sessions.set(sessionID, new Map());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const sessionMap = this.sessions.get(sessionID)!;
|
|
374
|
+
const key = `${options.source}:${options.id}`;
|
|
375
|
+
|
|
376
|
+
const entry: ContextEntry = {
|
|
377
|
+
id: options.id,
|
|
378
|
+
source: options.source,
|
|
379
|
+
content: options.content,
|
|
380
|
+
priority: options.priority ?? "normal",
|
|
381
|
+
registrationOrder: ++this.registrationCounter,
|
|
382
|
+
metadata: options.metadata,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
sessionMap.set(key, entry);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
getPending(sessionID: string): PendingContext {
|
|
389
|
+
const sessionMap = this.sessions.get(sessionID);
|
|
390
|
+
|
|
391
|
+
if (!sessionMap || sessionMap.size === 0) {
|
|
392
|
+
return {
|
|
393
|
+
merged: "",
|
|
394
|
+
entries: [],
|
|
395
|
+
hasContent: false,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const entries = this.sortEntries([...sessionMap.values()]);
|
|
400
|
+
const CONTEXT_SEPARATOR = "\n\n---\n\n";
|
|
401
|
+
const merged = entries.map(e => e.content).join(CONTEXT_SEPARATOR);
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
merged,
|
|
405
|
+
entries,
|
|
406
|
+
hasContent: entries.length > 0
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
consume(sessionID: string): PendingContext {
|
|
411
|
+
const pending = this.getPending(sessionID);
|
|
412
|
+
this.clear(sessionID);
|
|
413
|
+
return pending;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
clear(sessionID: string): void {
|
|
417
|
+
this.sessions.delete(sessionID);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
hasPending(sessionID: string): boolean {
|
|
421
|
+
const sessionMap = this.sessions.get(sessionID);
|
|
422
|
+
return sessionMap !== undefined && sessionMap.size > 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private sortEntries(entries: ContextEntry[]): ContextEntry[] {
|
|
426
|
+
const PRIORITY_ORDER: Record<ContextPriority, number> = {
|
|
427
|
+
critical: 0,
|
|
428
|
+
high: 1,
|
|
429
|
+
normal: 2,
|
|
430
|
+
low: 3
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
return entries.sort((a, b) => {
|
|
434
|
+
const priorityDiff = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
|
|
435
|
+
if (priorityDiff !== 00 return priorityDiff;
|
|
436
|
+
return a.registrationOrder - b.registrationOrder;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 全局上下文收集器实例
|
|
442
|
+
const contextCollector = new ContextCollector();
|
|
443
|
+
|
|
444
|
+
// 騡型回退状态追踪
|
|
445
|
+
const modelFailureCounts = new Map<string, Map<string, number>>();
|
|
446
|
+
const sessionFailures = new Map<string, number>();
|
|
447
|
+
|
|
448
|
+
// 騡型回退状态追踪(保留向后兼容)
|
|
338
449
|
const pendingFirstMessages = new Set<string>();
|
|
339
450
|
const injectedSessions = new Set<string>();
|
|
340
451
|
|
|
@@ -472,31 +583,28 @@ function checkDeps(ctx: { directory: string }): string {
|
|
|
472
583
|
return "";
|
|
473
584
|
}
|
|
474
585
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
586
|
+
// 获取插件内部的 agents 目录(而不是 ~/.config/opencode/agents/)
|
|
587
|
+
function getPluginAgentsDir(): string {
|
|
588
|
+
// 使用插件的安装目录下的 agents 文件夹
|
|
589
|
+
const pluginDir = dirname(dirname(__filename));
|
|
590
|
+
return join(pluginDir, "agents");
|
|
478
591
|
}
|
|
479
592
|
|
|
480
593
|
function getInjectedAgentsPrompt(): string {
|
|
481
|
-
const agentsDir =
|
|
594
|
+
const agentsDir = getPluginAgentsDir();
|
|
482
595
|
const agentsPath = join(agentsDir, "api-cyber-supervisor.md");
|
|
483
596
|
|
|
484
597
|
if (!existsSync(agentsPath)) {
|
|
598
|
+
console.log(`[api-security-testing] Agent file not found: ${agentsPath}`);
|
|
485
599
|
return "";
|
|
486
600
|
}
|
|
487
601
|
|
|
488
602
|
try {
|
|
489
603
|
const content = readFileSync(agentsPath, "utf-8");
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
${content}
|
|
496
|
-
|
|
497
|
-
To activate these agents, simply mention their name in your response (e.g., "@api-cyber-supervisor" to coordinate security testing).
|
|
498
|
-
`;
|
|
499
|
-
} catch {
|
|
604
|
+
// 不添加 UI 显示的前缀,直接返回内容(将通过 synthetic part 注入)
|
|
605
|
+
return content;
|
|
606
|
+
} catch (e) {
|
|
607
|
+
console.log(`[api-security-testing] Failed to read agent file: ${e}`);
|
|
500
608
|
return "";
|
|
501
609
|
}
|
|
502
610
|
}
|
|
@@ -1299,20 +1407,24 @@ print(json.dumps(result, ensure_ascii=False))
|
|
|
1299
1407
|
}),
|
|
1300
1408
|
},
|
|
1301
1409
|
|
|
1302
|
-
// 赛博监工 Hook - chat.message
|
|
1303
|
-
// 参考 oh-my-opencode 的 FirstMessageVariantGate 模式:只在首条消息注入一次
|
|
1410
|
+
// 赛博监工 Hook - chat.message (保留用于向后兼容,但不再直接注入 agents)
|
|
1304
1411
|
"chat.message": async (input, output) => {
|
|
1305
1412
|
const sessionID = input.sessionID;
|
|
1306
1413
|
|
|
1307
|
-
//
|
|
1414
|
+
// 只在首条消息时注册上下文 (参考 oh-my-openagent 的 ContextCollector 模式)
|
|
1308
1415
|
if (isFirstMessage(sessionID)) {
|
|
1309
1416
|
const agentsPrompt = getInjectedAgentsPrompt();
|
|
1310
1417
|
if (agentsPrompt) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1418
|
+
// 注册到上下文收集器,而不是直接修改 output.parts
|
|
1419
|
+
contextCollector.register(sessionID, {
|
|
1420
|
+
id: "api-security-agents",
|
|
1421
|
+
source: "plugin",
|
|
1422
|
+
content: agentsPrompt,
|
|
1423
|
+
priority: "high",
|
|
1424
|
+
metadata: {
|
|
1425
|
+
description: "API Security Testing Agents",
|
|
1426
|
+
timestamp: Date.now().toISOString()
|
|
1427
|
+
});
|
|
1316
1428
|
}
|
|
1317
1429
|
// 标记该 session 已注入 (参考 oh-my-opencode 的 markApplied)
|
|
1318
1430
|
markFirstMessageApplied(sessionID);
|
|
@@ -1394,18 +1506,108 @@ ${LEVEL_PROMPTS[level]}
|
|
|
1394
1506
|
|
|
1395
1507
|
// 会话事件处理
|
|
1396
1508
|
event: async (input) => {
|
|
1397
|
-
const { event } = input
|
|
1509
|
+
const { event } = input
|
|
1398
1510
|
|
|
1399
1511
|
// 新会话创建 - 标记为首条消息待注入 (参考 oh-my-opencode)
|
|
1400
1512
|
if (event.type === "session.created") {
|
|
1401
1513
|
const props = event.properties as Record<string, unknown> | undefined;
|
|
1402
1514
|
const sessionInfo = props?.info as { id?: string; parentID?: string } | undefined;
|
|
1515
|
+
|
|
1403
1516
|
// 只有主会话(非 fork)才注入
|
|
1404
1517
|
if (sessionInfo?.id && !sessionInfo.parentID) {
|
|
1405
1518
|
markSessionCreated(sessionInfo.id);
|
|
1406
|
-
console.log(`[api-security-testing]
|
|
1519
|
+
console.log(`[api-security-testing] new session created: ${sessionInfo.id}`);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// 会话删除或压缩 - 清理状态
|
|
1524
|
+
if (event.type === "session.deleted" || event.type === "session.compacted") {
|
|
1525
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
1526
|
+
let sessionID: string | undefined;
|
|
1527
|
+
|
|
1528
|
+
if (event.type === "session.deleted") {
|
|
1529
|
+
sessionID = (props?.info as { id?: string })?.id;
|
|
1530
|
+
} else {
|
|
1531
|
+
sessionID = (props?.sessionID ?? (props?.info as { id?: string })?.id) as string | undefined;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (sessionID) {
|
|
1535
|
+
clearSessionState(sessionID);
|
|
1536
|
+
resetFailureCount(sessionID);
|
|
1537
|
+
resetModelFailures(sessionID);
|
|
1407
1538
|
}
|
|
1408
1539
|
}
|
|
1540
|
+
},
|
|
1541
|
+
|
|
1542
|
+
// 上下文注入 Hook - 使用 synthetic part 模式隐藏注入内容 (参考 oh-my-openagent)
|
|
1543
|
+
// 这是关键!使用 synthetic: true 标记来隐藏 UI 显示
|
|
1544
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
1545
|
+
const { messages } = output;
|
|
1546
|
+
|
|
1547
|
+
if (messages.length === 0) {
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// 找到最后一条用户消息
|
|
1552
|
+
let lastUserMessageIndex = -1;
|
|
1553
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1554
|
+
if (messages[i].info.role === "user") {
|
|
1555
|
+
lastUserMessageIndex = i;
|
|
1556
|
+
break;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (lastUserMessageIndex === -1) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const lastUserMessage = messages[lastUserMessageIndex];
|
|
1565
|
+
|
|
1566
|
+
// 获取 sessionID (参考 oh-my-openagent 的实现)
|
|
1567
|
+
const messageSessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID;
|
|
1568
|
+
const sessionID = messageSessionID ?? Array.from(pendingFirstMessages)[0];
|
|
1569
|
+
|
|
1570
|
+
if (!sessionID) {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// 检查是否有待注入的上下文
|
|
1575
|
+
if (!contextCollector.hasPending(sessionID)) {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const pending = contextCollector.consume(sessionID);
|
|
1580
|
+
if (!pending.hasContent) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// 找到文本部分
|
|
1585
|
+
const textPartIndex = lastUserMessage.parts.findIndex(
|
|
1586
|
+
(p) => p.type === "text" && (p as { text?: string }).text
|
|
1587
|
+
);
|
|
1588
|
+
|
|
1589
|
+
if (textPartIndex === -1) {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// 创建 synthetic part - 关键!synthetic: true 隐藏 UI 显示
|
|
1594
|
+
const syntheticPart = {
|
|
1595
|
+
id: `synthetic_context_${sessionID}`,
|
|
1596
|
+
messageID: lastUserMessage.info.id,
|
|
1597
|
+
sessionID: sessionID,
|
|
1598
|
+
type: "text" as const,
|
|
1599
|
+
text: pending.merged,
|
|
1600
|
+
synthetic: true, // ← 这是关键!UI 中隐藏此内容
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
// 在文本部分之前插入 synthetic part
|
|
1604
|
+
lastUserMessage.parts.splice(textPartIndex, 0, syntheticPart);
|
|
1605
|
+
|
|
1606
|
+
console.log(`[api-security-testing] Injected context via synthetic part, session=${sessionID}, length=${pending.merged.length}`);
|
|
1607
|
+
},
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1409
1611
|
|
|
1410
1612
|
// 会话删除或压缩 - 清理状态
|
|
1411
1613
|
if (event.type === "session.deleted" || event.type === "session.compacted") {
|