opencode-api-security-testing 5.4.3 → 5.4.4

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 +216 -11
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.4",
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
 
@@ -1299,20 +1410,24 @@ print(json.dumps(result, ensure_ascii=False))
1299
1410
  }),
1300
1411
  },
1301
1412
 
1302
- // 赛博监工 Hook - chat.message
1303
- // 参考 oh-my-opencode 的 FirstMessageVariantGate 模式:只在首条消息注入一次
1413
+ // 赛博监工 Hook - chat.message (保留用于向后兼容,但不再直接注入 agents)
1304
1414
  "chat.message": async (input, output) => {
1305
1415
  const sessionID = input.sessionID;
1306
1416
 
1307
- // 只在首条消息时注入 agents prompt (参考 oh-my-opencodefirstMessageVariantGate)
1417
+ // 只在首条消息时注册上下文 (参考 oh-my-openagentContextCollector 模式)
1308
1418
  if (isFirstMessage(sessionID)) {
1309
1419
  const agentsPrompt = getInjectedAgentsPrompt();
1310
1420
  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
- }
1421
+ // 注册到上下文收集器,而不是直接修改 output.parts
1422
+ contextCollector.register(sessionID, {
1423
+ id: "api-security-agents",
1424
+ source: "plugin",
1425
+ content: agentsPrompt,
1426
+ priority: "high",
1427
+ metadata: {
1428
+ description: "API Security Testing Agents",
1429
+ timestamp: Date.now().toISOString()
1430
+ });
1316
1431
  }
1317
1432
  // 标记该 session 已注入 (参考 oh-my-opencode 的 markApplied)
1318
1433
  markFirstMessageApplied(sessionID);
@@ -1394,16 +1509,17 @@ ${LEVEL_PROMPTS[level]}
1394
1509
 
1395
1510
  // 会话事件处理
1396
1511
  event: async (input) => {
1397
- const { event } = input;
1512
+ const { event } = input
1398
1513
 
1399
1514
  // 新会话创建 - 标记为首条消息待注入 (参考 oh-my-opencode)
1400
1515
  if (event.type === "session.created") {
1401
1516
  const props = event.properties as Record<string, unknown> | undefined;
1402
1517
  const sessionInfo = props?.info as { id?: string; parentID?: string } | undefined;
1518
+
1403
1519
  // 只有主会话(非 fork)才注入
1404
1520
  if (sessionInfo?.id && !sessionInfo.parentID) {
1405
1521
  markSessionCreated(sessionInfo.id);
1406
- console.log(`[api-security-testing] New session created: ${sessionInfo.id}`);
1522
+ console.log(`[api-security-testing] new session created: ${sessionInfo.id}`);
1407
1523
  }
1408
1524
  }
1409
1525
 
@@ -1425,6 +1541,95 @@ ${LEVEL_PROMPTS[level]}
1425
1541
  }
1426
1542
  }
1427
1543
  },
1544
+
1545
+ // 上下文注入 Hook - 使用 synthetic part 模式隐藏注入内容 (参考 oh-my-openagent)
1546
+ // 这是关键!使用 synthetic: true 标记来隐藏 UI 显示
1547
+ "experimental.chat.messages.transform": async (_input, output) => {
1548
+ const { messages } = output;
1549
+
1550
+ if (messages.length === 0) {
1551
+ return;
1552
+ }
1553
+
1554
+ // 找到最后一条用户消息
1555
+ let lastUserMessageIndex = -1;
1556
+ for (let i = messages.length - 1; i >= 0; i--) {
1557
+ if (messages[i].info.role === "user") {
1558
+ lastUserMessageIndex = i;
1559
+ break;
1560
+ }
1561
+ }
1562
+
1563
+ if (lastUserMessageIndex === -1) {
1564
+ return;
1565
+ }
1566
+
1567
+ const lastUserMessage = messages[lastUserMessageIndex];
1568
+
1569
+ // 获取 sessionID (参考 oh-my-openagent 的实现)
1570
+ const messageSessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID;
1571
+ const sessionID = messageSessionID ?? Array.from(pendingFirstMessages)[0];
1572
+
1573
+ if (!sessionID) {
1574
+ return;
1575
+ }
1576
+
1577
+ // 检查是否有待注入的上下文
1578
+ if (!contextCollector.hasPending(sessionID)) {
1579
+ return;
1580
+ }
1581
+
1582
+ const pending = contextCollector.consume(sessionID);
1583
+ if (!pending.hasContent) {
1584
+ return;
1585
+ }
1586
+
1587
+ // 找到文本部分
1588
+ const textPartIndex = lastUserMessage.parts.findIndex(
1589
+ (p) => p.type === "text" && (p as { text?: string }).text
1590
+ );
1591
+
1592
+ if (textPartIndex === -1) {
1593
+ return;
1594
+ }
1595
+
1596
+ // 创建 synthetic part - 关键!synthetic: true 隐藏 UI 显示
1597
+ const syntheticPart = {
1598
+ id: `synthetic_context_${sessionID}`,
1599
+ messageID: lastUserMessage.info.id,
1600
+ sessionID: sessionID,
1601
+ type: "text" as const,
1602
+ text: pending.merged,
1603
+ synthetic: true, // ← 这是关键!UI 中隐藏此内容
1604
+ };
1605
+
1606
+ // 在文本部分之前插入 synthetic part
1607
+ lastUserMessage.parts.splice(textPartIndex, 0, syntheticPart);
1608
+
1609
+ console.log(`[api-security-testing] Injected context via synthetic part, session=${sessionID}, length=${pending.merged.length}`);
1610
+ },
1611
+ };
1612
+ }
1613
+ }
1614
+
1615
+ // 会话删除或压缩 - 清理状态
1616
+ if (event.type === "session.deleted" || event.type === "session.compacted") {
1617
+ const props = event.properties as Record<string, unknown> | undefined;
1618
+ let sessionID: string | undefined;
1619
+
1620
+ if (event.type === "session.deleted") {
1621
+ sessionID = (props?.info as { id?: string })?.id;
1622
+ } else {
1623
+ sessionID = (props?.sessionID ?? (props?.info as { id?: string })?.id) as string | undefined;
1624
+ }
1625
+
1626
+ if (sessionID) {
1627
+ clearSessionState(sessionID);
1628
+ resetFailureCount(sessionID);
1629
+ resetModelFailures(sessionID);
1630
+ }
1631
+ }
1632
+ },
1428
1633
  };
1429
1634
  };
1430
1635