argo-search 2.9.8 → 2.9.9

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 (46) hide show
  1. package/README.en.md +30 -20
  2. package/README.es.md +30 -20
  3. package/README.ja.md +30 -20
  4. package/README.ko.md +30 -20
  5. package/README.md +32 -22
  6. package/SKILL.md +1 -1
  7. package/bin/argo +4 -0
  8. package/bin/argo.js +1 -1
  9. package/config.yaml +52 -5
  10. package/docs/ARGO_INTRO.md +5 -3
  11. package/docs/RELEASE_NOTES_v2.9.9.md +78 -0
  12. package/package.json +3 -2
  13. package/packages/dsh-plugin/README.md +3 -2
  14. package/packages/dsh-plugin/dsh/client.js +98 -32
  15. package/packages/dsh-plugin/dsh/diagnostics.js +43 -0
  16. package/packages/dsh-plugin/dsh/index.js +23 -1
  17. package/packages/dsh-plugin/package.json +2 -2
  18. package/packages/dsh-plugin/test/client.test.js +2 -1
  19. package/scripts/adaptive.py +102 -42
  20. package/scripts/bench_tail_ab.py +281 -0
  21. package/scripts/bounded_run.py +91 -49
  22. package/scripts/clock_bridge.py +22 -0
  23. package/scripts/content_security.py +119 -4
  24. package/scripts/dispatch_result.py +9 -2
  25. package/scripts/engine_add.py +602 -0
  26. package/scripts/engine_dispatch.py +9 -0
  27. package/scripts/evidence_loop.py +4 -0
  28. package/scripts/fetch_quality.py +13 -0
  29. package/scripts/fetch_v3.py +3 -3
  30. package/scripts/http_client.py +24 -38
  31. package/scripts/image_vision/vision_probe +0 -0
  32. package/scripts/local_engines.py +321 -0
  33. package/scripts/matrix_search_eval.py +111 -1
  34. package/scripts/mcp_transport.py +1 -1
  35. package/scripts/net_proxy.py +27 -0
  36. package/scripts/recovery.py +7 -3
  37. package/scripts/route_combo.py +19 -5
  38. package/scripts/route_domains.py +138 -12
  39. package/scripts/safe_conn.py +52 -0
  40. package/scripts/search_entry.py +1 -0
  41. package/scripts/search_pipeline.py +20 -2
  42. package/scripts/search_rank.py +6 -0
  43. package/scripts/serp_guard.py +23 -0
  44. package/scripts/tail_completion.py +212 -0
  45. package/scripts/url_safety.py +126 -3
  46. package/scripts/usage_log.py +45 -0
@@ -35,6 +35,7 @@ THREAT_NAMES = {
35
35
  "payload_smuggling": "载荷走私",
36
36
  "xss_injection": "XSS 注入",
37
37
  "recursive_injection": "递归注入",
38
+ "credential_exposure": "凭证暴露",
38
39
  }
39
40
 
40
41
 
@@ -181,11 +182,53 @@ INJECTION_PATTERNS_EL = [
181
182
  r"υποδύσου\s+(?:τον\s+)?(?:άνθρωπο|χρήστη|διαχειριστή|σύστημα)",
182
183
  ]
183
184
 
185
+ # 西班牙语注入
186
+ INJECTION_PATTERNS_ES = [
187
+ r"(?i)ignora\s+(?:todas\s+)?(?:las\s+)?(?:instrucciones|órdenes|ordenes|reglas|indicaciones)\s+(?:anteriores|previas)",
188
+ r"(?i)olvida\s+(?:todas\s+)?(?:las\s+)?(?:instrucciones|órdenes|reglas)",
189
+ r"(?i)(?:muéstrame|muestrame|enséñame|ensename|dime|revela|imprime|exhibe)\s+(?:tu\s+)?(?:prompt\s+del\s+sistema|prompt|instrucciones|configuración|configuracion)",
190
+ r"(?i)ya\s+no\s+(?:eres|es)\s+(?:un\s+)?(?:asistente|bot|ia|agente)",
191
+ r"(?i)(?:eres|ahora\s+eres)\s+(?:una\s+)?(?:persona|humano|usuario|administrador|sistema)",
192
+ r"(?i)(?:actúa|actua|comporta|finge|simula)\s+(?:como|de\s+que)\s+(?:una\s+)?(?:persona|humano|usuario|administrador|sistema)",
193
+ r"(?i)(?:elude|evita|ignora)\s+(?:todas\s+)?(?:las\s+)?(?:restricciones|limitaciones|protecciones|reglas)",
194
+ r"(?i)te\s+llamas\s+(?:ChatGPT|Claude|GPT|asistente\s+de\s+ia)",
195
+ ]
196
+
197
+ # 法语注入
198
+ INJECTION_PATTERNS_FR = [
199
+ r"(?i)ignore\s+(?:toutes\s+)?(?:les\s+)?(?:instructions|consignes|règles|regles|ordres)\s+(?:précédentes|precedentes)",
200
+ r"(?i)oublie\s+(?:toutes\s+)?(?:les\s+)?(?:instructions|consignes|règles|regles)",
201
+ r"(?i)(?:montre|affiche|dis|révèle|revel|imprime)\s+(?:ton|votre|ta)?\s*(?:prompt\s+système|prompt|instructions|configuration)",
202
+ r"(?i)tu\s+n'es\s+plus\s+(?:un\s+)?(?:assistant|bot|ia|agent)",
203
+ r"(?i)tu\s+es\s+(?:désormais|desormais\s+)?(?:une\s+)?(?:personne|humain|utilisateur|administrateur|système|systeme)",
204
+ r"(?i)(?:fais\s+semblant|joue|agis)\s+(?:comme|en\s+tant\s+que)\s+(?:une\s+)?(?:personne|humain|utilisateur|administrateur|système|systeme)",
205
+ r"(?i)(?:contourne|évite|evite)\s+(?:toutes\s+)?(?:les\s+)?(?:restrictions|limitations|protections|règles|regles)",
206
+ r"(?i)ton\s+nom\s+est\s+(?:ChatGPT|Claude|GPT|assistant\s+ia)",
207
+ ]
208
+
209
+ # 德语注入
210
+ INJECTION_PATTERNS_DE = [
211
+ r"(?i)ignoriere\s+(?:alle\s+)?(?:vorherigen\s+)?(?:Anweisungen|Regeln|Instruktionen|Befehle)",
212
+ r"(?i)vergiss\s+(?:alle\s+)?(?:vorherigen\s+)?(?:Anweisungen|Regeln|Instruktionen)",
213
+ r"(?i)(?:zeig|zeige|erzähl|erzahl|gib\s+preis|offenbare|drucke)\s+(?:mir\s+)?(?:deinen\s+)?(?:System-?Prompt|Systemprompt|Anweisungen|Einstellungen)",
214
+ r"(?i)du\s+bist\s+(?:nicht\s+mehr|kein\s+mehr)\s+(?:ein\s+)?(?:Assistent|Bot|KI|Agent)",
215
+ r"(?i)du\s+bist\s+(?:nun\s+)?(?:ein\s+)?(?:Mensch|Benutzer|Administrator|System)",
216
+ r"(?i)(?:tu|gib\s+vor|verhalte\s+dich)\s+(?:als|wie)\s+(?:ein\s+)?(?:Mensch|Benutzer|Administrator|System)",
217
+ r"(?i)(?:umgehe|ignoriere)\s+(?:alle\s+)?(?:Einschränkungen|Einschrankungen|Beschränkungen|Beschrankungen|Schutzmaßnahmen|Schutzmassnahmen|Regeln)",
218
+ r"(?i)dein\s+Name\s+ist\s+(?:ChatGPT|Claude|GPT|KI-Assistent)",
219
+ ]
220
+
221
+ # 拉丁语系专用表(不含英语通用,避免 en/latin 两条路重复匹配)
222
+ INJECTION_PATTERNS_LATIN = (
223
+ INJECTION_PATTERNS_ES + INJECTION_PATTERNS_FR + INJECTION_PATTERNS_DE
224
+ )
225
+
184
226
  # 按语系分类的注入模式表(lang_detect 主语言标签 → 模式列表)
185
227
  LANG_INJECTION_PATTERNS: dict[str, list[str]] = {
186
228
  "zh": INJECTION_PATTERNS_ZH,
187
229
  "ja": INJECTION_PATTERNS_JA,
188
230
  "ko": INJECTION_PATTERNS_KO,
231
+ "latin": INJECTION_PATTERNS_LATIN,
189
232
  "cyrillic": INJECTION_PATTERNS_RU,
190
233
  "arabic": INJECTION_PATTERNS_AR,
191
234
  "hebrew": INJECTION_PATTERNS_HE,
@@ -259,6 +302,21 @@ XSS_PATTERNS = [
259
302
  r"(?i)window\.location",
260
303
  ]
261
304
 
305
+ # 凭证形态:出现在抓取正文里的密钥形状串——可能是页面自身的示例,也可能是
306
+ # 被注入/外泄的凭证。标 credential_exposure(权重 0.5,单独命中不触发隔离,
307
+ # 但进 threat_types 供上层判断)。
308
+ SECRET_FORM_PATTERNS = [
309
+ r"sk-[A-Za-z0-9_\-]{16,}",
310
+ r"sk-ant-[A-Za-z0-9_\-]{16,}",
311
+ r"ghp_[A-Za-z0-9]{20,}",
312
+ r"github_pat_[A-Za-z0-9_]{20,}",
313
+ r"AKIA[0-9A-Z]{16}",
314
+ r"ASIA[0-9A-Z]{16}",
315
+ r"xox[baprs]-[A-Za-z0-9\-]{10,}",
316
+ r"AIza[0-9A-Za-z_\-]{35}",
317
+ r"-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----",
318
+ ]
319
+
262
320
  # 检测但不过度脱敏的文件路径类模式
263
321
  _NO_REDACT_PATTERNS: frozenset = frozenset({
264
322
  r"(?i)/etc/passwd",
@@ -317,6 +375,7 @@ WEIGHTS = {
317
375
  "payload_smuggling": 0.9,
318
376
  "xss_injection": 0.9,
319
377
  "recursive_injection": 1.0,
378
+ "credential_exposure": 0.5,
320
379
  }
321
380
 
322
381
 
@@ -348,6 +407,7 @@ class ContentScrubber:
348
407
  threats.extend(self._detect_exfiltration(normalized))
349
408
  threats.extend(self._detect_impersonation(normalized))
350
409
  threats.extend(self._detect_xss(normalized))
410
+ threats.extend(self._detect_secret_forms(normalized))
351
411
 
352
412
  # 阶段 3:语义意图分析
353
413
  threats.extend(self._semantic_analysis(normalized))
@@ -467,9 +527,10 @@ class ContentScrubber:
467
527
  if lang == "zh" or lang == "mixed":
468
528
  patterns.extend(INJECTION_PATTERNS_ZH)
469
529
  for key, plist in LANG_INJECTION_PATTERNS.items():
470
- if lang == key:
530
+ # en 与 latin 共享拉丁模式:ES/FR/DE 注入多为 ASCII 或带扩展字符,
531
+ # 纯 ASCII 时 lang_detect 判 en,漏掉它们等于给拉丁注入留门
532
+ if lang == key or (lang == "en" and key == "latin"):
471
533
  patterns.extend(plist)
472
- # en/latin 落到英文通用(已有)
473
534
  return tuple(patterns)
474
535
 
475
536
  def _detect_injections(self, text: str, lang: str = "other") -> list[ThreatDetection]:
@@ -485,6 +546,11 @@ class ContentScrubber:
485
546
  def _detect_xss(self, text: str) -> list[ThreatDetection]:
486
547
  return self._match_patterns(text, XSS_PATTERNS, "xss_injection", 0.9)
487
548
 
549
+ def _detect_secret_forms(self, text: str) -> list[ThreatDetection]:
550
+ """凭证形态检测(输出侧防护):正文里的密钥形状串。"""
551
+ return self._match_patterns(text, SECRET_FORM_PATTERNS,
552
+ "credential_exposure", 0.7)
553
+
488
554
  @staticmethod
489
555
  def _match_patterns(text: str, patterns: list[str],
490
556
  threat_type: str, confidence: float) -> list[ThreatDetection]:
@@ -629,10 +695,51 @@ def scrub_content(content: str) -> ScrubResult:
629
695
  return get_scrubber().scrub(content)
630
696
 
631
697
 
698
+ # ── 风险门禁(v2.9.8)──────────────────────────────────────────────────────
699
+ #
700
+ # 此前 scrub_to_dict 只回传元数据(risk_score 等),**清洗后的正文被丢弃**——
701
+ # 安全引擎只「评分」不「门禁」,高风险内容照样进证据流水线与 Agent 上下文。
702
+ # 门禁把「评分」变成「动作」:risk_score ≥ 阈值时,fetch 层用清洗后正文替换
703
+ # 原始正文并标 content_quarantined,使被注入污染的正文不进证据分、不进上下文。
704
+ #
705
+ # 开关与阈值都可配(见 references/usage.md 功能开关总表):
706
+ # ARGO_CONTENT_RISK_GATE 默认开;关掉 = 放行高风险内容,仅应急用
707
+ # ARGO_CONTENT_RISK_THRESHOLD 默认 0.5,与历史 content_clean 阈值一致
708
+ RISK_GATE_THRESHOLD = 0.5
709
+
710
+
711
+ def risk_gate_enabled() -> bool:
712
+ """内容风险门禁总开关(默认开)。"""
713
+ try:
714
+ from engine_env import env_flag
715
+ return env_flag("ARGO_CONTENT_RISK_GATE", default=True)
716
+ except Exception:
717
+ return True
718
+
719
+
720
+ def risk_gate_threshold() -> float:
721
+ """门禁阈值:risk_score ≥ 此值即隔离。默认 0.5,越界回落默认。"""
722
+ try:
723
+ from engine_env import get_env
724
+ val = float(str(get_env(["ARGO_CONTENT_RISK_THRESHOLD"])).strip())
725
+ except (TypeError, ValueError):
726
+ return RISK_GATE_THRESHOLD
727
+ if val <= 0.0 or val > 1.0:
728
+ return RISK_GATE_THRESHOLD
729
+ return val
730
+
731
+
632
732
  def scrub_to_dict(content: str) -> dict:
633
- """清洗入口(dict 形态,供 fetch 层直接合并)。"""
733
+ """清洗入口(dict 形态,供 fetch 层直接合并)。
734
+
735
+ 除历史字段外,回传门禁三件套:gate_applied(门禁是否生效)、
736
+ quarantined(是否隔离)、cleaned_content(仅隔离时回传,供替换原始正文)。
737
+ """
634
738
  r = scrub_content(content)
635
- return {
739
+ gate_on = risk_gate_enabled()
740
+ threshold = risk_gate_threshold()
741
+ quarantined = gate_on and r.risk_score >= threshold
742
+ out = {
636
743
  "content_clean": r.clean,
637
744
  "risk_score": r.risk_score,
638
745
  "threat_count": len(r.threats),
@@ -640,7 +747,15 @@ def scrub_to_dict(content: str) -> dict:
640
747
  "redactions": r.redactions,
641
748
  "encoding_layers_stripped": r.encoding_layers_stripped,
642
749
  "content_lang": r.content_lang,
750
+ "gate_applied": gate_on,
751
+ "gate_threshold": threshold,
752
+ "quarantined": quarantined,
643
753
  }
754
+ # 只有被隔离时才回传清洗后正文:它替换原始正文,是门禁的「动作」部分;
755
+ # 未命中时多带一份正文副本只是白占内存。
756
+ if quarantined:
757
+ out["cleaned_content"] = r.content
758
+ return out
644
759
 
645
760
 
646
761
  # ── CLI ────────────────────────────────────────────────────────────────────
@@ -39,20 +39,26 @@ class DispatchResult:
39
39
  (deep 等无预算模式 total 为 None)。搜索输出把它挂进 timing.budget,
40
40
  让「这次离预算上限还有多远」可观测,而不必反推 process_ms。
41
41
 
42
+ `stranded`:早停弃置且仍存活的引擎 ``[(engine, holder, now_fn), ...]``,
43
+ 交 tail_completion 后台收割(学习器记真实成绩 + 合并写 L2 缓存)。主响应
44
+ 与 cancelled 记账不受它影响——它改的是「交付被丢弃」这一件事。
45
+
42
46
  记账字段的语义与历史见模块 docstring;`wasted_ms` 仅为兼容别名。
43
47
  """
44
48
 
45
49
  __slots__ = ("raw_results", "engine_outcomes", "engine_latency",
46
50
  "wasted_ms", "early_stopped", "run_one", "ingest",
47
51
  "budget_used_ms", "budget_total_ms", "useful_ms",
48
- "first_sufficient_ms", "tail_wait_ms", "abandoned_engine_ms")
52
+ "first_sufficient_ms", "tail_wait_ms", "abandoned_engine_ms",
53
+ "stranded")
49
54
 
50
55
  def __init__(self, raw_results: Any, engine_outcomes: Any,
51
56
  engine_latency: Any, wasted_ms: int, early_stopped: bool,
52
57
  run_one: Any, ingest: Any, budget_used_ms: int | None = None,
53
58
  budget_total_ms: int | None = None, useful_ms: int = 0,
54
59
  first_sufficient_ms: int = 0, tail_wait_ms: int = 0,
55
- abandoned_engine_ms: int | None = None) -> None:
60
+ abandoned_engine_ms: int | None = None,
61
+ stranded: Any = ()) -> None:
56
62
  self.raw_results = raw_results
57
63
  self.engine_outcomes = engine_outcomes
58
64
  self.engine_latency = engine_latency
@@ -68,6 +74,7 @@ class DispatchResult:
68
74
  # 旧调用方只传 wasted_ms:别名取同一值,不引入第二个真源。
69
75
  self.abandoned_engine_ms = (wasted_ms if abandoned_engine_ms is None
70
76
  else abandoned_engine_ms)
77
+ self.stranded = tuple(stranded or ())
71
78
 
72
79
 
73
80
  def summarize_dispatch(*, wall_ms: int, contributed_ms: list[float],