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.
Files changed (2) hide show
  1. package/package.json +6 -6
  2. 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",
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
- "postinstall": "node postinstall.mjs",
18
- "preuninstall": "node preuninstall.mjs",
19
- "init": "node init.mjs"
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
- // 首条消息追踪 (参考 oh-my-opencode 的 FirstMessageVariantGate 模式)
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
- function getAgentsDir(): string {
476
- const home = process.env.HOME || process.env.USERPROFILE || "/root";
477
- return join(home, AGENTS_DIR);
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 = getAgentsDir();
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
- return `
491
-
492
- [API Security Testing Agents Available]
493
- When performing security testing tasks, you can use the following specialized agents:
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
- // 只在首条消息时注入 agents prompt (参考 oh-my-opencodefirstMessageVariantGate)
1414
+ // 只在首条消息时注册上下文 (参考 oh-my-openagentContextCollector 模式)
1308
1415
  if (isFirstMessage(sessionID)) {
1309
1416
  const agentsPrompt = getInjectedAgentsPrompt();
1310
1417
  if (agentsPrompt) {
1311
- const parts = output.parts as Array<{ type: string; text?: string }>;
1312
- const textPart = parts.find(p => p.type === "text");
1313
- if (textPart && textPart.text) {
1314
- textPart.text += agentsPrompt;
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] New session created: ${sessionInfo.id}`);
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") {