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.
- package/package.json +6 -6
- 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.
|
|
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,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
|
-
//
|
|
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
|
}
|