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.
- package/package.json +6 -6
- 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
|
+
"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
|
-
|
|
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
|
|
|
@@ -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
|
-
//
|
|
1417
|
+
// 只在首条消息时注册上下文 (参考 oh-my-openagent 的 ContextCollector 模式)
|
|
1308
1418
|
if (isFirstMessage(sessionID)) {
|
|
1309
1419
|
const agentsPrompt = getInjectedAgentsPrompt();
|
|
1310
1420
|
if (agentsPrompt) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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]
|
|
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
|
|