opencode-api-security-testing 5.4.2 → 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 +268 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-api-security-testing",
3
- "version": "5.4.2",
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,9 +334,151 @@ 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
- // 追踪已注入 agents prompt 的 session (只注入一次)
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
+ // 騡型回退状态追踪(保留向后兼容)
449
+ const pendingFirstMessages = new Set<string>();
338
450
  const injectedSessions = new Set<string>();
339
451
 
452
+ // 标记新会话创建(由 event hook 调用)
453
+ function markSessionCreated(sessionID: string | undefined): void {
454
+ if (sessionID) {
455
+ pendingFirstMessages.add(sessionID);
456
+ console.log(`[api-security-testing] Session marked for first message injection: ${sessionID}`);
457
+ }
458
+ }
459
+
460
+ // 检查是否是首条消息(由 chat.message hook 调用)
461
+ function isFirstMessage(sessionID: string | undefined): boolean {
462
+ if (!sessionID) return false;
463
+ return pendingFirstMessages.has(sessionID);
464
+ }
465
+
466
+ // 标记首条消息已处理(由 chat.message hook 调用,注入后立即调用)
467
+ function markFirstMessageApplied(sessionID: string | undefined): void {
468
+ if (sessionID) {
469
+ pendingFirstMessages.delete(sessionID);
470
+ injectedSessions.add(sessionID); // 记录已注入
471
+ }
472
+ }
473
+
474
+ // 清理会话状态(由 event hook 在 session.deleted 时调用)
475
+ function clearSessionState(sessionID: string | undefined): void {
476
+ if (sessionID) {
477
+ pendingFirstMessages.delete(sessionID);
478
+ injectedSessions.delete(sessionID);
479
+ }
480
+ }
481
+
340
482
  function getConfigPath(ctx: { directory: string }): string {
341
483
  return join(ctx.directory, SKILL_DIR, "assets", CONFIG_FILE);
342
484
  }
@@ -1268,12 +1410,29 @@ print(json.dumps(result, ensure_ascii=False))
1268
1410
  }),
1269
1411
  },
1270
1412
 
1271
- // 赛博监工 Hook - chat.message
1272
- // 注意:已禁用自动注入 agents prompt,避免重复注入问题
1273
- // 如需使用 agents,请在需要时手动调用相关工具
1413
+ // 赛博监工 Hook - chat.message (保留用于向后兼容,但不再直接注入 agents)
1274
1414
  "chat.message": async (input, output) => {
1275
1415
  const sessionID = input.sessionID;
1276
1416
 
1417
+ // 只在首条消息时注册上下文 (参考 oh-my-openagent 的 ContextCollector 模式)
1418
+ if (isFirstMessage(sessionID)) {
1419
+ const agentsPrompt = getInjectedAgentsPrompt();
1420
+ if (agentsPrompt) {
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
+ });
1431
+ }
1432
+ // 标记该 session 已注入 (参考 oh-my-opencode 的 markApplied)
1433
+ markFirstMessageApplied(sessionID);
1434
+ }
1435
+
1277
1436
  // 赛博监工压力注入(仅在失败时)
1278
1437
  if (config.cyber_supervisor.enabled && config.cyber_supervisor.auto_trigger) {
1279
1438
  const failures = getFailureCount(sessionID);
@@ -1348,10 +1507,112 @@ ${LEVEL_PROMPTS[level]}
1348
1507
  return output;
1349
1508
  },
1350
1509
 
1351
- // 会话清理
1510
+ // 会话事件处理
1352
1511
  event: async (input) => {
1353
- const { event } = input;
1512
+ const { event } = input
1513
+
1514
+ // 新会话创建 - 标记为首条消息待注入 (参考 oh-my-opencode)
1515
+ if (event.type === "session.created") {
1516
+ const props = event.properties as Record<string, unknown> | undefined;
1517
+ const sessionInfo = props?.info as { id?: string; parentID?: string } | undefined;
1518
+
1519
+ // 只有主会话(非 fork)才注入
1520
+ if (sessionInfo?.id && !sessionInfo.parentID) {
1521
+ markSessionCreated(sessionInfo.id);
1522
+ console.log(`[api-security-testing] new session created: ${sessionInfo.id}`);
1523
+ }
1524
+ }
1525
+
1526
+ // 会话删除或压缩 - 清理状态
1527
+ if (event.type === "session.deleted" || event.type === "session.compacted") {
1528
+ const props = event.properties as Record<string, unknown> | undefined;
1529
+ let sessionID: string | undefined;
1530
+
1531
+ if (event.type === "session.deleted") {
1532
+ sessionID = (props?.info as { id?: string })?.id;
1533
+ } else {
1534
+ sessionID = (props?.sessionID ?? (props?.info as { id?: string })?.id) as string | undefined;
1535
+ }
1536
+
1537
+ if (sessionID) {
1538
+ clearSessionState(sessionID);
1539
+ resetFailureCount(sessionID);
1540
+ resetModelFailures(sessionID);
1541
+ }
1542
+ }
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
+ }
1354
1614
 
1615
+ // 会话删除或压缩 - 清理状态
1355
1616
  if (event.type === "session.deleted" || event.type === "session.compacted") {
1356
1617
  const props = event.properties as Record<string, unknown> | undefined;
1357
1618
  let sessionID: string | undefined;
@@ -1363,6 +1624,7 @@ ${LEVEL_PROMPTS[level]}
1363
1624
  }
1364
1625
 
1365
1626
  if (sessionID) {
1627
+ clearSessionState(sessionID);
1366
1628
  resetFailureCount(sessionID);
1367
1629
  resetModelFailures(sessionID);
1368
1630
  }