foxclaw 0.1.2 → 0.1.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 (368) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -14,8 +14,6 @@ from hkc.hkc_kde.extractor import Extractor
14
14
  from hkc.hkc_api.ingest_jobs import IngestJobRegistry
15
15
 
16
16
 
17
- # ── D1: IngestJobRegistry 4 thread 并发 ────────────────
18
-
19
17
  def d1_ingest_4_workers():
20
18
  """D1. 4 worker 真并发 peak=4,4 任务 ~0.3s 完成"""
21
19
  reg = IngestJobRegistry(max_workers=4)
@@ -36,7 +34,6 @@ def d1_ingest_4_workers():
36
34
  t0 = time.time()
37
35
  for j in jobs:
38
36
  reg.submit(j, work)
39
- # wait done
40
37
  while True:
41
38
  statuses = [reg.get(j.job_id)["status"] for j in jobs]
42
39
  if all(s == "done" for s in statuses):
@@ -49,8 +46,6 @@ def d1_ingest_4_workers():
49
46
  print(f" D1 PASS: peak={peak[0]}, 4 jobs in {elapsed*1000:.0f}ms (串行需 1.2s)")
50
47
 
51
48
 
52
- # ── D2: Extractor 16 chunk 限流雪崩 ────────────────────
53
-
54
49
  def d2_429_snowball():
55
50
  """D2. 16 chunk × 4 worker × 100% 429 永不成功,验证不死循环 + 总调用有上界"""
56
51
  state = {"calls": 0}
@@ -62,8 +57,6 @@ def d2_429_snowball():
62
57
  raise err
63
58
 
64
59
  ext = Extractor(provider="anthropic", model="m")
65
- # 替换 _extract_chunk:走 _retry_llm 但传极小 base_delay 测试用
66
- # 同时 monkey-patch time.sleep 避免真睡
67
60
  import hkc.hkc_kde.extractor as ex_mod
68
61
  orig_sleep = ex_mod.time.sleep
69
62
  sleeps = []
@@ -88,7 +81,6 @@ def d2_429_snowball():
88
81
  result = ext.extract(chunks)
89
82
  elapsed = time.time() - t0
90
83
 
91
- # 期望:16 chunk 全 fail,总调用 = 16 * (1+3 retry) = 64,无死循环
92
84
  success = sum(1 for item in result.items if item is not None)
93
85
  assert success == 0, f"expected 0 success, got {success}"
94
86
  assert state["calls"] == 64, f"snowball: expected 64 calls, got {state['calls']}"
@@ -99,17 +91,13 @@ def d2_429_snowball():
99
91
  ex_mod.time.sleep = orig_sleep
100
92
 
101
93
 
102
- # ── D3: progress_cb 线程安全 ─────────────────────────
103
-
104
94
  def d3_progress_cb_thread_safe():
105
95
  """D3. progress_cb 在 as_completed 循环调,主线程串行;16 chunk 计数无重复无漏"""
106
96
  ext = Extractor(provider="anthropic", model="m")
107
97
 
108
- # 替换 _extract_chunk 为 fast mock:50% 概率 fail(模拟 chunk 失败)
109
98
  import random
110
99
  random.seed(42)
111
100
  def mock_extract(chunk, timeout=60.0):
112
- # 永远返 RawExtraction
113
101
  from hkc.hkc_kde.models import RawExtraction
114
102
  return RawExtraction()
115
103
  ext._extract_chunk = mock_extract
@@ -131,21 +119,16 @@ def d3_progress_cb_thread_safe():
131
119
  chunks = [MockChunk(i) for i in range(16)]
132
120
  result = ext.extract(chunks, progress_cb=cb)
133
121
 
134
- # 验证:done_calls 长度 16(每个 chunk 一次)
135
122
  assert len(done_calls) == 16, f"expected 16 callbacks, got {len(done_calls)}"
136
- # 验证:1..16 各一次(set 大小 = 16,排序后 1..16)
137
123
  assert sorted(done_calls) == list(range(1, 17)), \
138
124
  f"expected processed 1..16, got {sorted(done_calls)}"
139
125
  print(f" D3 PASS: {len(done_calls)} callbacks, all 1..16 unique")
140
126
 
141
127
 
142
- # ── D4: max_retries 可调 ──────────────────────────────
143
-
144
128
  def d4_max_retries_configurable():
145
129
  """D4. _retry_llm 的 max_retries 真的生效"""
146
130
  ext = Extractor(provider="mock", model="m")
147
131
 
148
- # 测 max_retries=2(默认 3):4 次后 raise
149
132
  call_count = [0]
150
133
  def always_fail():
151
134
  call_count[0] += 1
@@ -153,7 +136,6 @@ def d4_max_retries_configurable():
153
136
  err.status_code = 429
154
137
  raise err
155
138
 
156
- # 用 monkey-patch 把 time.sleep 替换掉,这样不会真睡
157
139
  import hkc.hkc_kde.extractor as ex_mod
158
140
  orig_sleep = ex_mod.time.sleep
159
141
  sleeps = []
@@ -164,7 +146,6 @@ def d4_max_retries_configurable():
164
146
  assert False, "expected raise"
165
147
  except Exception:
166
148
  pass
167
- # max_retries=2 + 1 = 3 次尝试
168
149
  assert call_count[0] == 3, f"max_retries=2 expected 3 calls, got {call_count[0]}"
169
150
  assert len(sleeps) == 2, f"max_retries=2 expected 2 sleeps, got {len(sleeps)}"
170
151
  print(f" D4 PASS: max_retries=2 -> 3 calls, 2 sleeps")
@@ -172,8 +153,6 @@ def d4_max_retries_configurable():
172
153
  ex_mod.time.sleep = orig_sleep
173
154
 
174
155
 
175
- # ── D5: retry + progress_cb 顺序 ─────────────────────
176
-
177
156
  def d5_retry_no_progress_leak():
178
157
  """D5. retry 期间 progress_cb 仍只调 1 次/chunk(不管 retry 几次)"""
179
158
  import time as time_mod
@@ -209,9 +188,7 @@ def d5_retry_no_progress_leak():
209
188
  cb_count[0] += 1
210
189
  chunks = [MockChunk(i) for i in range(8)]
211
190
  result = ext.extract(chunks, progress_cb=cb)
212
- # 不管 retry 几次,每个 chunk 调 1 次 progress_cb
213
191
  assert cb_count[0] == 8, f"expected 8 progress, got {cb_count[0]}"
214
- # 8 chunk 全 fail,总调用 = 8 * 4 = 32
215
192
  assert state["calls"] == 32, f"expected 32 calls, got {state['calls']}"
216
193
  success = sum(1 for item in result.items if item is not None)
217
194
  assert success == 0
@@ -21,7 +21,6 @@ from pathlib import Path
21
21
 
22
22
  sys.path.insert(0, str(Path(__file__).parent.parent))
23
23
 
24
- # 关闭联网触发(chaos 只测 LLM 咽喉,不触发搜索)
25
24
  import foxclaw.core.web_search as ws # noqa: E402
26
25
  ws.needs_search = lambda q: False
27
26
 
@@ -37,7 +36,7 @@ def _make_router(primary="deepseek", fallback=None):
37
36
  if fallback:
38
37
  os.environ[PROVIDER_ENV[fallback]] = "chaos-test-key"
39
38
  r = ModelRouter(provider=primary)
40
- r._client = object() # 使 is_stub == False
39
+ r._client = object()
41
40
  r._clients[primary] = object()
42
41
  if fallback:
43
42
  r._clients[fallback] = object()
@@ -87,11 +86,11 @@ def case4_circuit_breaker():
87
86
  """[4] 断路器:provider 失败后进入冷却,立即重试被跳过(不再紧密重试)。"""
88
87
  r = _make_router("deepseek")
89
88
  r._call_provider = _boom
90
- r.complete(_SYS, _MSG) # 第一次失败 → 进入 cooldown
89
+ r.complete(_SYS, _MSG)
91
90
  cooled = "deepseek" in r._cooldown
92
91
  t0 = time.perf_counter()
93
- r.complete(_SYS, _MSG) # 第二次:cooldown 未过期 → 直接跳过,不等超时
94
- fast = (time.perf_counter() - t0) < 0.5 # 跳过热 provider,应远快于真实超时
92
+ r.complete(_SYS, _MSG)
93
+ fast = (time.perf_counter() - t0) < 0.5
95
94
  ok = cooled and fast and r.is_degraded()
96
95
  return ok, (f"冷却中={cooled} 跳过耗时={((time.perf_counter()-t0)*1000):.0f}ms"), "断路器生效"
97
96
 
@@ -101,11 +100,10 @@ def case5_recovery():
101
100
  r = _make_router("deepseek")
102
101
  r._call_provider = _boom
103
102
  r.complete(_SYS, _MSG)
104
- assert "deepseek" in r._cooldown # 先确认在冷却
105
- r._cooldown["deepseek"] = 0 # 模拟冷却过期
103
+ assert "deepseek" in r._cooldown
104
+ r._cooldown["deepseek"] = 0
106
105
  r._call_provider = lambda *a, **k: ("恢复后的正常回复", None)
107
106
  out = r.complete(_SYS, _MSG)
108
- # 注:degraded 是「曾降级」的累积运维标志(不因恢复清零),恢复语义看「解冻 + 成功返回」。
109
107
  ok = (out == "恢复后的正常回复") and ("deepseek" not in r._cooldown)
110
108
  return ok, ("解冻=" + str("deepseek" not in r._cooldown)), out[:40]
111
109
 
@@ -26,7 +26,6 @@ from foxclaw.core.model_router import ModelRouter # noqa: E402
26
26
  _SYS = "你是 Foxclaw 助手。"
27
27
  _MSG = [{"role": "user", "content": "请只回复两个字:你好"}]
28
28
  _MAX_TOKENS = 64
29
- # stub/故障 的特征文案(出现即视为"非真实成功")
30
29
  _BAD_MARKERS = ("【未执行】", "【模型返回为空】", "【全部模型失败】", "【模型流式中断】", "STUB")
31
30
 
32
31
 
@@ -19,8 +19,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
19
19
  from hkc.hkc_kde.extractor import Extractor
20
20
 
21
21
 
22
- # ── Mock 异常类型 ────────────────────────────────────
23
-
24
22
  class FakeResponse:
25
23
  def __init__(self, headers=None):
26
24
  self.headers = headers or {}
@@ -53,7 +51,6 @@ class FakeRateLimitName(Exception):
53
51
  """模拟 openai.RateLimitError(type name 含 'RateLimit',无 status_code)"""
54
52
  def __init__(self, msg="rate"):
55
53
  super().__init__(msg)
56
- # 不设 status_code;靠 type name 'RateLimit' 识别
57
54
 
58
55
 
59
56
  class FakeValueError(Exception):
@@ -63,9 +60,6 @@ class FakeValueError(Exception):
63
60
  self.status_code = 400
64
61
 
65
62
 
66
- # ── 测试实例化 ───────────────────────────────────────
67
- # KnowledgeExtractor __init__ 不调 LLM,只是记 provider/model 配置
68
- # _retry_llm 是 instance method,任意实例都能调
69
63
  ext = Extractor(provider="mock", model="mock-model")
70
64
 
71
65
 
@@ -133,8 +127,8 @@ def t3_429_exhaust():
133
127
  assert False, "应该 raise"
134
128
  except Fake429Error:
135
129
  pass
136
- assert calls[0] == 4 # 1 + 3 retries
137
- assert len(sleeps) == 3 # 3 次 retry 才 sleep
130
+ assert calls[0] == 4
131
+ assert len(sleeps) == 3
138
132
  print(" 3. 429 4 次后 raise (1+3 retry): PASS, calls={}, sleeps={}".format(calls[0], sleeps))
139
133
  finally:
140
134
  restore_sleep(ex, original)
@@ -207,7 +201,6 @@ def t8_backoff():
207
201
  ext._retry_llm(func, max_retries=3, base_delay=1.0, max_delay=3.0)
208
202
  except Fake429Error:
209
203
  pass
210
- # 3 次 retry:sleeps = [1, 2, 3] (4 cap at max_delay=3)
211
204
  assert sleeps == [1.0, 2.0, 3.0], f"expected [1,2,3], got {sleeps}"
212
205
  print(" 8. 退避 1→2→3 (max_delay cap): PASS, sleeps={}".format(sleeps))
213
206
  finally:
@@ -23,13 +23,12 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
23
23
  PORT = int(os.environ.get("FOXCLAW_BENCH_PORT", "8231"))
24
24
  BASE = f"http://127.0.0.1:{PORT}"
25
25
 
26
- # 一批覆盖不同分诊路径的任务模板(每个请求附唯一编号,便于校验会话唯一性)
27
26
  TEMPLATES = [
28
- "分析本月理财预算与收支", # finance
29
- "总结这篇论文的核心要点", # office
30
- "安排明天上午的会议日程", # admin
31
- "整理并汇总这批资料", # assistant
32
- "你能做什么", # supervisor 自答(meta)
27
+ "分析本月理财预算与收支",
28
+ "总结这篇论文的核心要点",
29
+ "安排明天上午的会议日程",
30
+ "整理并汇总这批资料",
31
+ "你能做什么",
33
32
  ]
34
33
 
35
34
 
@@ -46,7 +45,6 @@ def _start_server():
46
45
  import uvicorn
47
46
  os.environ.setdefault("FOXCLAW_DATA_DIR", tempfile.mkdtemp())
48
47
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "50"
49
- # 不配任何 LLM key → Router 自动降级 stub(不联网、瞬时返回)
50
48
  for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
51
49
  "GOOGLE_API_KEY", "DASHSCOPE_API_KEY"):
52
50
  os.environ.pop(k, None)
@@ -132,7 +130,6 @@ async def main_async(total, concurrency):
132
130
  print("✗ 服务未就绪"); return 1
133
131
  print("服务就绪。先测单请求基线(concurrency=1)...")
134
132
 
135
- # 基线:串行 10 个请求,量"单请求真实成本"(无并发争用),用于解释并发下的延迟来源
136
133
  base = await _run_load(10, 1)
137
134
  base_lat = base["latencies"]
138
135
  base_p50 = _pct(base_lat, 50)
@@ -21,10 +21,8 @@ import time
21
21
  from pathlib import Path
22
22
  from types import SimpleNamespace
23
23
 
24
- # 必须在 import VectorIndex 之前设,否则模块级常量已固化
25
24
  os.environ["HKC_VECTOR_IVF_THRESHOLD"] = "500"
26
25
 
27
- # 让 vendor.hkc.* 可导入(脚本放在 bench/,不在 vendor 树内)
28
26
  ROOT = Path(__file__).resolve().parent.parent
29
27
  sys.path.insert(0, str(ROOT))
30
28
 
@@ -70,7 +68,6 @@ def main() -> None:
70
68
  idx = VectorIndex(backend=backend)
71
69
  print(f"index type 初始: {type(idx._index).__name__}")
72
70
 
73
- # ── Phase 1: append < threshold,保持 Flat ──────────────────
74
71
  phase1_n = threshold - 1
75
72
  print(f"\n[Phase 1] append {phase1_n} 条(< {threshold},应保持 Flat)...")
76
73
  for ku in kus[:phase1_n]:
@@ -79,7 +76,6 @@ def main() -> None:
79
76
  med, mn, mx = bench_search(idx, "find something about topic 7")
80
77
  print(f" search ×100: median={med:.2f}ms min={mn:.2f}ms max={mx:.2f}ms")
81
78
 
82
- # ── Phase 2: append 触发升级 Flat → IVF ──────────────────
83
79
  print(f"\n[Phase 2] append 第 {threshold} 条(应触发升级)...")
84
80
  t0 = time.perf_counter()
85
81
  idx.append_ku(kus[phase1_n])
@@ -91,7 +87,6 @@ def main() -> None:
91
87
  med, mn, mx = bench_search(idx, "find something about topic 7")
92
88
  print(f" search ×100: median={med:.2f}ms min={mn:.2f}ms max={mx:.2f}ms")
93
89
 
94
- # ── Phase 3: append 1000 条,验证 IVF 追加正常 ──────────────────
95
90
  rest = N - threshold
96
91
  print(f"\n[Phase 3] append {rest} 条(应直接 add 到 IVF)...")
97
92
  t0 = time.perf_counter()
@@ -103,7 +98,6 @@ def main() -> None:
103
98
  med, mn, mx = bench_search(idx, "find something about topic 7")
104
99
  print(f" search ×100: median={med:.2f}ms min={mn:.2f}ms max={mx:.2f}ms")
105
100
 
106
- # ── Phase 4: save/load roundtrip(IVF 必须正确持久化) ──────────────────
107
101
  print(f"\n[Phase 4] save/load roundtrip(IVF 持久化)...")
108
102
  with tempfile.TemporaryDirectory() as td:
109
103
  p = str(Path(td) / "vec")
@@ -120,7 +114,7 @@ def main() -> None:
120
114
  print(f" loaded ntotal={n_loaded}, type={type_loaded}")
121
115
  if n_loaded != N:
122
116
  print(f" [FAIL] 数据丢失! 期望 {N} 实际 {n_loaded}")
123
- elif "FlatIP" in type_loaded: # 严格判:IndexFlatIP 才算 Flat(IndexIVFFlat 含 "Flat" 字符串,要排除)
117
+ elif "FlatIP" in type_loaded:
124
118
  print(f" [FAIL] IVF 没保存,降回 FlatIP")
125
119
  else:
126
120
  print(f" [OK] IVF 完整保存/加载(ntotal={n_loaded}, type={type_loaded})")
@@ -0,0 +1,189 @@
1
+ # Content Gate and Lexicons
2
+
3
+ This layer does one thing: it makes a deterministic decision on **both the user request and the model output**, and cuts the turn off with a fixed line when something matches.
4
+
5
+ It does not call a model, does not do semantic understanding, and does not replace platform review or human moderation. The prompt layer keeps the model from crossing the line on its own; this layer holds the floor when either the model or the user gets around the prompt.
6
+
7
+ ## Three sources of judgement
8
+
9
+ | Source | Where | Configurable | Covers |
10
+ |---|---|---|---|
11
+ | Built-in rules | `_BUILTIN_RULES` in `foxclaw/core/content_guard.py` | No (only `off` disables them) | Six **request-shaped** categories: drug/explosive manufacturing, fraud and money laundering, intrusion into someone else's systems, forged documents, suicide methods, sexual content involving minors |
12
+ | External lexicon | Files listed in `FOXCLAW_CONTENT_LEXICON` | Yes | Whatever you decide — political, sexual, violent categories all live here |
13
+ | Prompt | `KERNEL_PROMPT` §8 (Content boundaries) | Code change | Soft constraint; owns the tone and the refusal pattern |
14
+
15
+ The built-in rules are deliberately narrow: a **verb plus a target** has to appear together. "How do I make explosives" matches; "I bought a book on the history of explosives" does not. The goal is something that works out of the box without interrupting ordinary conversation, not full coverage of every violative phrasing.
16
+
17
+ **Large category lists are not built in.** Review requirements change, the blast radius of false positives is a deployment decision, and tens of thousands of terms do not belong in source. Whether to mount them, and which categories, is your call.
18
+
19
+ ## Default behaviour
20
+
21
+ It is on out of the box, with no configuration:
22
+
23
+ - mode defaults to `enforce`
24
+ - only the built-in rules run when no lexicon is mounted
25
+ - a request that matches returns the fixed line immediately — **the request never reaches the model**
26
+ - an output that matches is replaced in full; no fragment of the original survives
27
+ - a streamed output that matches stops emitting at the match
28
+
29
+ The refusal follows the input language and exists in exactly two forms: `Let's talk about something else.` / `我们换个话题聊聊吧。`
30
+
31
+ No explanation, no elaboration, no counter-question, no alternative offered. A refusal must not become a channel for leaking information or continuing the thread.
32
+
33
+ ## Three modes
34
+
35
+ ```bash
36
+ FOXCLAW_CONTENT_GUARD_MODE=enforce # default: actually block
37
+ FOXCLAW_CONTENT_GUARD_MODE=audit # record only, for measuring false positives
38
+ FOXCLAW_CONTENT_GUARD_MODE=off # disabled
39
+ ```
40
+
41
+ An unrecognised value falls back to `enforce` — it over-blocks rather than failing silently.
42
+
43
+ `audit` hits are written to the **process stdout**:
44
+
45
+ ```
46
+ [Foxclaw] ⚠ 内容命中(未拦截): category=politics word='...'
47
+ ```
48
+
49
+ Only `audit` prints. `enforce` hits are silent. If you need an audit trail, wire `Decision.to_dict()` at the call site, or run `audit` for a while to collect a baseline first.
50
+
51
+ ## Where to put the configuration
52
+
53
+ Two paths, different behaviour:
54
+
55
+ - **`.env` (recommended)**: the API service loads `.env` before importing the package, so values placed there take effect. Empty values count as "not configured" and never override a real environment variable.
56
+ - **Process environment**: when you use `Foxclaw()` from the SDK, `.env` is not loaded for you — export the variables before the process starts.
57
+
58
+ The `.env` load can be skipped with `FOXCLAW_SKIP_DOTENV=1`.
59
+
60
+ ## Lexicon format
61
+
62
+ One term per line, UTF-8, `#` for comments:
63
+
64
+ ```
65
+ # term<TAB>category; the category is optional
66
+ some-term politics
67
+ another-term sexual
68
+ term-without-category
69
+ ```
70
+
71
+ The details below come from the loader, not from convention:
72
+
73
+ | Item | Behaviour |
74
+ |---|---|
75
+ | Separator | `term<TAB>category`; with no TAB the whole line is the term and the category becomes `platform` |
76
+ | Comments | lines starting with `#` are skipped |
77
+ | Whitespace and punctuation | every term goes through `compact()`: whitespace and CJK/ASCII punctuation stripped, NFKC-normalised, lowercased |
78
+ | Length | anything shorter than 2 characters **after** `compact()` is dropped (single characters cause too much collateral damage and rarely catch real violations) |
79
+ | Same term, several categories | allowed; scan results are deduplicated by `(category, term)` and the first hit wins |
80
+ | Bad lines, unreadable files | skipped silently — a bad lexicon must not prevent the service from starting |
81
+ | When it takes effect | the index is built **at process start**; editing a lexicon requires a restart, there is no hot reload |
82
+
83
+ Prefer **absolute paths**. Relative paths resolve against the process working directory, which under systemd is often not the project root.
84
+
85
+ After a restart, confirm how many terms actually loaded:
86
+
87
+ ```bash
88
+ python -c "from foxclaw.core.content_guard import ContentGuard as C; print(C().lexicon_size)"
89
+ ```
90
+
91
+ `0` means nothing was read.
92
+
93
+ ## Converting a third-party list
94
+
95
+ Existing lists — the ~60k-term main dictionary of `houbb/sensitive-word`, or the versions already split into political / sexual / violent files — come in incompatible shapes: single file of bare terms or one file per category, GBK encoding, and the same term written three ways (`敏感词`, `敏 感 词`, `SENSITIVE`).
96
+
97
+ `tools/build_content_lexicon.py` does the conversion. **The script lives in the repository and is not shipped in the wheel** — if you installed from PyPI, take it from the GitHub repository:
98
+
99
+ ```bash
100
+ # single-file dictionary, everything under the `platform` category
101
+ python tools/build_content_lexicon.py sensitive_word_dict.txt -o content_lexicon.txt
102
+
103
+ # per-category files: a CJK file name becomes the category (政治类.txt → 政治类)
104
+ python tools/build_content_lexicon.py dict/*.txt -o content_lexicon.txt
105
+
106
+ # keep only what you want
107
+ python tools/build_content_lexicon.py dict/*.txt --exclude 政治类,色情类 --exclude "暴力*" -o content_lexicon.txt
108
+
109
+ # single file, explicit category name
110
+ python tools/build_content_lexicon.py ads.txt --category ads -o content_lexicon.txt
111
+ ```
112
+
113
+ It does four things:
114
+
115
+ 1. **Detects the encoding**: `utf-8-sig` first, then `gb18030`. Many lists downloaded inside China are GBK, while the loader reads UTF-8 and ignores undecodable bytes — skip this step and terms go missing silently after mounting.
116
+ 2. **Normalises during conversion**, using the loader's own `compact()`, so duplicate spellings collapse now instead of bloating the automaton later.
117
+ 3. **Filters by category**: `--include` / `--exclude` accept wildcards, comma-separated and repeatable.
118
+ 4. **Verifies by reading back**: loads the output through a real `ContentGuard` and samples terms to confirm they still match; exits with code 1 if that fails.
119
+
120
+ Common options:
121
+
122
+ | Option | Default | Meaning |
123
+ |---|---|---|
124
+ | `-o, --output` | `content_lexicon.txt` | output path |
125
+ | `--category` | `platform` | category used when a file name has no CJK characters |
126
+ | `--include` / `--exclude` | none | category filters, wildcards supported |
127
+ | `--encoding` | `auto` | force a specific codec |
128
+ | `--min-len` | `2` | drop shorter terms (matches the loader) |
129
+ | `--max-len` | `0` | drop longer terms, `0` means no limit |
130
+ | `--sample` | `5` | terms sampled during verification |
131
+ | `--no-verify` | off | skip the read-back check |
132
+
133
+ At 60k terms, conversion plus load and verification takes about half a second.
134
+
135
+ Generated lexicons are ignored by git (`/content_lexicon*.txt`). Do not commit them.
136
+
137
+ ## Run `audit` before you enforce
138
+
139
+ **This is the step most likely to be skipped, and the most expensive to skip.**
140
+
141
+ A 60k-term list inevitably contains a lot of two-character terms. Mount it under `enforce` and ordinary order discussions, manuscript reviews, or medical questions will get cut off — the whole output is replaced, the user gets no explanation, only a sudden "let's talk about something else".
142
+
143
+ Suggested sequence:
144
+
145
+ 1. mount the lexicon, set `FOXCLAW_CONTENT_GUARD_MODE=audit`
146
+ 2. use the product normally for a few days, collect the hit lines from stdout
147
+ 3. review `category` and `word` for each: genuine violation or collateral damage
148
+ 4. drop the noisy categories with `--exclude`, or regenerate with longer terms
149
+ 5. switch to `enforce` once the rate is acceptable
150
+
151
+ There is no allowlist mechanism. The only levers against false positives are: mount fewer categories, use longer terms, raise `--min-len`.
152
+
153
+ ## Fidelity, stated plainly
154
+
155
+ This is a **speed bump**, not a wall. It explicitly does not handle:
156
+
157
+ - pinyin, homophones, split characters, transliteration (endless, and worse for false positives)
158
+ - images, audio, attachment bodies
159
+ - multi-turn grooming (splitting a sentence across three turns)
160
+ - books, statutes, and medical texts imported into the knowledge base
161
+
162
+ That last point is intentional: **the filter only sits on "user request" and "model output"**, and never touches the user's own material. A history book that describes an explosive formula should not fail to import.
163
+
164
+ Streaming has an inherent limit: at the moment of the match it can only guarantee that nothing further is emitted — **a prefix already streamed out cannot be recalled**. The non-streaming path replaces the whole output and has no such gap.
165
+
166
+ ## Troubleshooting
167
+
168
+ | Symptom | Check |
169
+ |---|---|
170
+ | Nothing is blocked at all | is `FOXCLAW_CONTENT_GUARD_MODE` set to `off`; did the variable actually reach the process |
171
+ | Built-in rules fire, lexicon does not | is `lexicon_size` 0; relative vs absolute path; file encoding |
172
+ | Some mounted terms never match | was the term under 2 characters after `compact()`; did a space become part of the term |
173
+ | Edited the lexicon, nothing changed | the process was not restarted; the index is built at start |
174
+ | Normal conversation gets cut | too many two-character terms; switch to `audit` and drop that category |
175
+
176
+ ## Relationship to the prompt layer
177
+
178
+ Different jobs, both required:
179
+
180
+ - the prompt layer (`KERNEL_PROMPT` §8 plus `no_harmful_content` in `behavior.yaml`) owns the semantic boundary: what counts as violative, how a refusal should sound, how to tell the user's material from the user's instructions.
181
+ - the code layer (this document) owns the deterministic floor: a match is a match, no model judgement involved.
182
+
183
+ With only the prompt, a successful jailbreak leaves nothing behind it. With only the code, every edge case lives or dies by the lexicon, and answers turn mechanical. Together, the model knows where the line is and there is a backstop when it crosses anyway.
184
+
185
+ ## Scope of responsibility
186
+
187
+ This repository ships no political, sexual, or violent term list. Whether to mount one, which categories, and which mode are deployment decisions — no policy is assumed here.
188
+
189
+ The layer is string matching after normalisation: it does not guarantee coverage and is not a content moderation or compliance service. Deployers who expose the service publicly are responsible for their own content compliance. The software is provided "as is", see [LICENSE](../LICENSE).